fluffychat merge

This commit is contained in:
ggurdin 2025-06-27 15:46:05 -04:00
commit 46a8477172
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
13 changed files with 989 additions and 1057 deletions

View file

@ -89,6 +89,8 @@ abstract class AppConfig {
static String _privacyUrl = "https://www.pangeachat.com/privacy";
//Pangea#
static const Set<String> defaultReactions = {'👍', '❤️', '😊'};
static String get privacyUrl => _privacyUrl;
// #Pangea
// static const String website = 'https://fluffychat.im';

View file

@ -3240,6 +3240,7 @@
"commandHint_logout": "Logout your current device",
"commandHint_logoutall": "Logout all active devices",
"displayNavigationRail": "Show navigation rail on mobile",
"customReaction": "Custom reaction",
"accountInformation": "Account information",
"addGroupDescription": "Add a chat description",
"addNewFriend": "Add new friend",

View file

@ -1,9 +1,6 @@
// ignore_for_file: depend_on_referenced_packages, implementation_imports
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -251,8 +248,6 @@ class ChatController extends State<ChatPageWithRoom>
context.go('/rooms');
}
EmojiPickerType emojiPickerType = EmojiPickerType.keyboard;
// #Pangea
// void requestHistory([_]) async {
Future<void> requestHistory() async {
@ -924,7 +919,6 @@ class ChatController extends State<ChatPageWithRoom>
// editEventId: editEvent?.eventId,
// parseCommands: parseCommands,
// );
// If the message and the sendController text don't match, it's possible
// that there was a delay in tokenization before send, and the user started
// typing a new message. We don't want to erase that, so only reset the input
@ -1031,7 +1025,6 @@ class ChatController extends State<ChatPageWithRoom>
// text: pendingText,
// selection: const TextSelection.collapsed(offset: 0),
// );
// Pangea#
setState(() {
// #Pangea
@ -1226,13 +1219,11 @@ class ChatController extends State<ChatPageWithRoom>
} else {
inputFocus.unfocus();
}
emojiPickerType = EmojiPickerType.keyboard;
setState(() => showEmojiPicker = !showEmojiPicker);
}
void _inputFocusListener() {
if (showEmojiPicker && inputFocus.hasFocus) {
emojiPickerType = EmojiPickerType.keyboard;
setState(() => showEmojiPicker = false);
}
}
@ -1431,19 +1422,6 @@ class ChatController extends State<ChatPageWithRoom>
return true;
}
bool get canEditSelectedEvents {
if (isArchived ||
selectedEvents.length != 1 ||
// #Pangea
selectedEvents.single.messageType != MessageTypes.Text ||
// Pangea#
!selectedEvents.first.status.isSent) {
return false;
}
return currentRoomBundle
.any((cl) => selectedEvents.first.senderId == cl!.userID);
}
void forwardEventsAction() async {
if (selectedEvents.isEmpty) return;
await showScaffoldDialog(
@ -1567,34 +1545,8 @@ class ChatController extends State<ChatPageWithRoom>
}
void onEmojiSelected(_, Emoji? emoji) {
switch (emojiPickerType) {
case EmojiPickerType.reaction:
senEmojiReaction(emoji);
break;
case EmojiPickerType.keyboard:
typeEmoji(emoji);
onInputBarChanged(sendController.text);
break;
}
}
void senEmojiReaction(Emoji? emoji) {
setState(() => showEmojiPicker = false);
if (emoji == null) return;
// make sure we don't send the same emoji twice
if (_allReactionEvents.any(
(e) => e.content.tryGetMap('m.relates_to')?['key'] == emoji.emoji,
)) {
return;
}
// #Pangea
// return sendEmojiAction(emoji.emoji);
sendEmojiAction(emoji.emoji);
// don't need to clear these when sending while in select mode,
// but do need to clear these when reacting from the large emoji picker
setState(() => selectedEvents.clear());
// Pangea#
typeEmoji(emoji);
onInputBarChanged(sendController.text);
}
void typeEmoji(Emoji? emoji) {
@ -1609,7 +1561,6 @@ class ChatController extends State<ChatPageWithRoom>
);
}
// Pangea#
final selection = sendController.selection;
final newText = sendController.text.isEmpty
? emoji.emoji
@ -1623,54 +1574,12 @@ class ChatController extends State<ChatPageWithRoom>
);
}
late Iterable<Event> _allReactionEvents;
void emojiPickerBackspace() {
switch (emojiPickerType) {
case EmojiPickerType.reaction:
setState(() => showEmojiPicker = false);
break;
case EmojiPickerType.keyboard:
sendController
..text = sendController.text.characters.skipLast(1).toString()
..selection = TextSelection.fromPosition(
TextPosition(offset: sendController.text.length),
);
break;
}
}
void pickEmojiReactionAction(Iterable<Event> allReactionEvents) async {
// #Pangea
closeSelectionOverlay();
// Pangea#
_allReactionEvents = allReactionEvents;
emojiPickerType = EmojiPickerType.reaction;
setState(() => showEmojiPicker = true);
}
void sendEmojiAction(String? emoji) async {
final events = List<Event>.from(selectedEvents);
// #Pangea
// keep this event selected in case the user wants to send another emoji
// setState(() => selectedEvents.clear());
// Pangea#
// if reaction already exists, don't send it again
if (timeline == null ||
events.any(
(e) => e.aggregatedEvents(timeline!, RelationshipTypes.reaction).any(
(re) => re.content.tryGetMap('m.relates_to')?['key'] == emoji,
),
)) {
return;
}
for (final event in events) {
await room.sendReaction(
event.eventId,
emoji!,
sendController
..text = sendController.text.characters.skipLast(1).toString()
..selection = TextSelection.fromPosition(
TextPosition(offset: sendController.text.length),
);
}
}
// #Pangea
@ -1702,7 +1611,6 @@ class ChatController extends State<ChatPageWithRoom>
selectedEvents.add(event);
});
}
// Pangea#
void clearSingleSelectedEvent() {
if (selectedEvents.length <= 1) {
@ -1776,9 +1684,8 @@ class ChatController extends State<ChatPageWithRoom>
final matches = selectedEvents.where((e) => e.eventId == event.eventId);
if (matches.isNotEmpty) {
setState(() => selectedEvents.remove(matches.first));
}
// Pangea#
else {
// Pangea#
} else {
setState(
() => selectedEvents.add(event),
);
@ -1883,7 +1790,7 @@ class ChatController extends State<ChatPageWithRoom>
}
Timer? _storeInputTimeoutTimer;
Duration storeInputTimeout = const Duration(milliseconds: 500);
static const Duration _storeInputTimeout = Duration(milliseconds: 500);
void onInputBarChanged(String text) {
if (_inputTextIsEmpty != text.isEmpty) {
@ -1893,7 +1800,7 @@ class ChatController extends State<ChatPageWithRoom>
}
_storeInputTimeoutTimer?.cancel();
_storeInputTimeoutTimer = Timer(storeInputTimeout, () async {
_storeInputTimeoutTimer = Timer(_storeInputTimeout, () async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('draft_$roomId', text);
});
@ -1937,12 +1844,14 @@ class ChatController extends State<ChatPageWithRoom>
bool get isArchived =>
{Membership.leave, Membership.ban}.contains(room.membership);
// #Pangea
// void showEventInfo([Event? event]) =>
// (event ?? selectedEvents.single).showInfoDialog(context);
void showEventInfo([Event? event]) {
(event ?? selectedEvents.single).showInfoDialog(context);
// #Pangea
clearSelectedEvents();
// Pangea#
}
// Pangea#
void onPhoneButtonTap() async {
// VoIP required Android SDK 21
@ -2000,7 +1909,6 @@ class ChatController extends State<ChatPageWithRoom>
replyEvent = null;
editEvent = null;
});
// #Pangea
String? get buttonEventID => timeline!.events
.firstWhereOrNull(
@ -2261,5 +2169,3 @@ class ChatController extends State<ChatPageWithRoom>
);
}
}
enum EmojiPickerType { reaction, keyboard }

View file

@ -201,6 +201,10 @@ class ChatEventList extends StatelessWidget {
// Pangea#
selected: controller.selectedEvents
.any((e) => e.eventId == event.eventId),
singleSelected:
controller.selectedEvents.singleOrNull?.eventId ==
event.eventId,
onEdit: () => controller.editSelectedEventAction(),
timeline: timeline,
displayReadMarker: i > 0 &&
controller.readMarkerEventId == event.eventId,

View file

@ -21,10 +21,6 @@ class ChatInputRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (controller.showEmojiPicker &&
controller.emojiPickerType == EmojiPickerType.reaction) {
return const SizedBox.shrink();
}
const height = 48.0;
if (!controller.room.otherPartyCanReceiveMessages) {
@ -43,302 +39,238 @@ class ChatInputRow extends StatelessWidget {
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: controller.selectMode
? <Widget>[
if (controller.selectedEvents
.every((event) => event.status == EventStatus.error))
SizedBox(
height: height,
child: TextButton(
style: TextButton.styleFrom(
foregroundColor: theme.colorScheme.error,
),
onPressed: controller.deleteErrorEventsAction,
child: Row(
children: <Widget>[
const Icon(Icons.delete),
Text(L10n.of(context).delete),
],
),
),
)
else
SizedBox(
height: height,
child: TextButton(
onPressed: controller.forwardEventsAction,
child: Row(
children: <Widget>[
const Icon(Icons.keyboard_arrow_left_outlined),
Text(L10n.of(context).forward),
],
),
),
),
controller.selectedEvents.length == 1
? controller.selectedEvents.first
.getDisplayEvent(controller.timeline!)
.status
.isSent
? SizedBox(
height: height,
child: TextButton(
onPressed: controller.replyAction,
child: Row(
children: <Widget>[
Text(L10n.of(context).reply),
const Icon(Icons.keyboard_arrow_right),
],
),
),
)
: SizedBox(
height: height,
child: TextButton(
onPressed: controller.sendAgainAction,
child: Row(
children: <Widget>[
Text(L10n.of(context).tryToSendAgain),
const SizedBox(width: 4),
const Icon(Icons.send_outlined, size: 16),
],
),
),
)
: const SizedBox.shrink(),
]
: <Widget>[
const SizedBox(width: 4),
AnimatedContainer(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
width: controller.sendController.text.isNotEmpty ? 0 : height,
height: height,
alignment: Alignment.center,
decoration: const BoxDecoration(),
clipBehavior: Clip.hardEdge,
child: PopupMenuButton<String>(
useRootNavigator: true,
icon: const Icon(Icons.add_circle_outline),
iconColor: theme.colorScheme.onPrimaryContainer,
onSelected: controller.onAddPopupMenuButtonSelected,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
if (PlatformInfos.isMobile)
PopupMenuItem<String>(
value: 'location',
child: ListTile(
leading: CircleAvatar(
backgroundColor:
theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.gps_fixed_outlined),
),
title: Text(L10n.of(context).shareLocation),
contentPadding: const EdgeInsets.all(0),
),
),
PopupMenuItem<String>(
value: 'image',
child: ListTile(
leading: CircleAvatar(
backgroundColor: theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.photo_outlined),
),
title: Text(L10n.of(context).sendImage),
contentPadding: const EdgeInsets.all(0),
),
),
PopupMenuItem<String>(
value: 'video',
child: ListTile(
leading: CircleAvatar(
backgroundColor: theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.video_camera_back_outlined),
),
title: Text(L10n.of(context).sendVideo),
contentPadding: const EdgeInsets.all(0),
),
),
PopupMenuItem<String>(
value: 'file',
child: ListTile(
leading: CircleAvatar(
backgroundColor: theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.attachment_outlined),
),
title: Text(L10n.of(context).sendFile),
contentPadding: const EdgeInsets.all(0),
),
),
],
),
),
children: <Widget>[
const SizedBox(width: 4),
AnimatedContainer(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
width: controller.sendController.text.isNotEmpty ? 0 : height,
height: height,
alignment: Alignment.center,
decoration: const BoxDecoration(),
clipBehavior: Clip.hardEdge,
child: PopupMenuButton<String>(
useRootNavigator: true,
enabled: !controller.selectMode,
icon: const Icon(Icons.add_circle_outline),
iconColor: theme.colorScheme.onPrimaryContainer,
onSelected: controller.onAddPopupMenuButtonSelected,
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
if (PlatformInfos.isMobile)
AnimatedContainer(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
width: controller.sendController.text.isNotEmpty ? 0 : height,
height: height,
alignment: Alignment.center,
decoration: const BoxDecoration(),
clipBehavior: Clip.hardEdge,
child: PopupMenuButton(
useRootNavigator: true,
icon: const Icon(Icons.camera_alt_outlined),
onSelected: controller.onAddPopupMenuButtonSelected,
iconColor: theme.colorScheme.onPrimaryContainer,
itemBuilder: (context) => [
PopupMenuItem<String>(
value: 'camera-video',
child: ListTile(
leading: CircleAvatar(
backgroundColor:
theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.videocam_outlined),
),
title: Text(L10n.of(context).recordAVideo),
contentPadding: const EdgeInsets.all(0),
),
),
PopupMenuItem<String>(
value: 'camera',
child: ListTile(
leading: CircleAvatar(
backgroundColor:
theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.camera_alt_outlined),
),
title: Text(L10n.of(context).takeAPhoto),
contentPadding: const EdgeInsets.all(0),
),
),
],
PopupMenuItem<String>(
value: 'location',
child: ListTile(
leading: CircleAvatar(
backgroundColor: theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.gps_fixed_outlined),
),
title: Text(L10n.of(context).shareLocation),
contentPadding: const EdgeInsets.all(0),
),
),
Container(
height: height,
width: height,
alignment: Alignment.center,
child: IconButton(
tooltip: L10n.of(context).emojis,
color: theme.colorScheme.onPrimaryContainer,
icon: PageTransitionSwitcher(
transitionBuilder: (
Widget child,
Animation<double> primaryAnimation,
Animation<double> secondaryAnimation,
) {
return SharedAxisTransition(
animation: primaryAnimation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.scaled,
fillColor: Colors.transparent,
child: child,
);
},
child: Icon(
controller.showEmojiPicker
? Icons.keyboard
: Icons.add_reaction_outlined,
key: ValueKey(controller.showEmojiPicker),
),
PopupMenuItem<String>(
value: 'image',
child: ListTile(
leading: CircleAvatar(
backgroundColor: theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.photo_outlined),
),
onPressed: controller.emojiPickerAction,
title: Text(L10n.of(context).sendImage),
contentPadding: const EdgeInsets.all(0),
),
),
if (Matrix.of(context).isMultiAccount &&
Matrix.of(context).hasComplexBundles &&
Matrix.of(context).currentBundle!.length > 1)
Container(
width: height,
height: height,
alignment: Alignment.center,
child: _ChatAccountPicker(controller),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 0.0),
child: InputBar(
room: controller.room,
minLines: 1,
maxLines: 8,
autofocus: !PlatformInfos.isMobile,
keyboardType: TextInputType.multiline,
textInputAction:
AppConfig.sendOnEnter == true && PlatformInfos.isMobile
? TextInputAction.send
: null,
// #Pangea
// onSubmitted: controller.onInputBarSubmitted,
onSubmitted: (value) =>
controller.onInputBarSubmitted(value, context),
// Pangea#
onSubmitImage: controller.sendImageFromClipBoard,
focusNode: controller.inputFocus,
controller: controller.sendController,
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(
left: 6.0,
right: 6.0,
bottom: 6.0,
top: 3.0,
),
hintText: L10n.of(context).writeAMessage,
hintMaxLines: 1,
border: InputBorder.none,
enabledBorder: InputBorder.none,
filled: false,
),
onChanged: controller.onInputBarChanged,
// #Pangea
hintText: "",
// Pangea#
PopupMenuItem<String>(
value: 'video',
child: ListTile(
leading: CircleAvatar(
backgroundColor: theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.video_camera_back_outlined),
),
title: Text(L10n.of(context).sendVideo),
contentPadding: const EdgeInsets.all(0),
),
),
Container(
height: height,
width: height,
alignment: Alignment.center,
child: PlatformInfos.platformCanRecord &&
controller.sendController.text.isEmpty
? FloatingActionButton.small(
tooltip: L10n.of(context).voiceMessage,
onPressed: controller.voiceMessageAction,
elevation: 0,
heroTag: null,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(height),
),
backgroundColor: theme.bubbleColor,
foregroundColor: theme.onBubbleColor,
child: const Icon(Icons.mic_none_outlined),
)
: FloatingActionButton.small(
tooltip: L10n.of(context).send,
// #Pangea
// onPressed: controller.send,
onPressed: () => controller.send(
message: controller.sendController.text,
),
// Pangea#
elevation: 0,
heroTag: null,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(height),
),
backgroundColor: theme.bubbleColor,
foregroundColor: theme.onBubbleColor,
child: const Icon(Icons.send_outlined),
),
PopupMenuItem<String>(
value: 'file',
child: ListTile(
leading: CircleAvatar(
backgroundColor: theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.attachment_outlined),
),
title: Text(L10n.of(context).sendFile),
contentPadding: const EdgeInsets.all(0),
),
),
],
),
),
if (PlatformInfos.isMobile)
AnimatedContainer(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
width: controller.sendController.text.isNotEmpty ? 0 : height,
height: height,
alignment: Alignment.center,
decoration: const BoxDecoration(),
clipBehavior: Clip.hardEdge,
child: PopupMenuButton(
enabled: !controller.selectMode,
useRootNavigator: true,
icon: const Icon(Icons.camera_alt_outlined),
onSelected: controller.onAddPopupMenuButtonSelected,
iconColor: theme.colorScheme.onPrimaryContainer,
itemBuilder: (context) => [
PopupMenuItem<String>(
value: 'camera-video',
child: ListTile(
leading: CircleAvatar(
backgroundColor: theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.videocam_outlined),
),
title: Text(L10n.of(context).recordAVideo),
contentPadding: const EdgeInsets.all(0),
),
),
PopupMenuItem<String>(
value: 'camera',
child: ListTile(
leading: CircleAvatar(
backgroundColor: theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.camera_alt_outlined),
),
title: Text(L10n.of(context).takeAPhoto),
contentPadding: const EdgeInsets.all(0),
),
),
],
),
),
Container(
height: height,
width: height,
alignment: Alignment.center,
child: IconButton(
tooltip: L10n.of(context).emojis,
color: theme.colorScheme.onPrimaryContainer,
icon: PageTransitionSwitcher(
transitionBuilder: (
Widget child,
Animation<double> primaryAnimation,
Animation<double> secondaryAnimation,
) {
return SharedAxisTransition(
animation: primaryAnimation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.scaled,
fillColor: Colors.transparent,
child: child,
);
},
child: Icon(
controller.showEmojiPicker
? Icons.keyboard
: Icons.add_reaction_outlined,
key: ValueKey(controller.showEmojiPicker),
),
),
onPressed:
controller.selectMode ? null : controller.emojiPickerAction,
),
),
if (Matrix.of(context).isMultiAccount &&
Matrix.of(context).hasComplexBundles &&
Matrix.of(context).currentBundle!.length > 1)
Container(
width: height,
height: height,
alignment: Alignment.center,
child: _ChatAccountPicker(controller),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 0.0),
child: InputBar(
room: controller.room,
minLines: 1,
readOnly: controller.selectMode,
maxLines: 8,
autofocus: !PlatformInfos.isMobile,
keyboardType: TextInputType.multiline,
textInputAction:
AppConfig.sendOnEnter == true && PlatformInfos.isMobile
? TextInputAction.send
: null,
// #Pangea
// onSubmitted: controller.onInputBarSubmitted,
onSubmitted: (c) => controller.onInputBarSubmitted(c, context),
hintText: "",
// Pangea#
onSubmitImage: controller.sendImageFromClipBoard,
focusNode: controller.inputFocus,
controller: controller.sendController,
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(
left: 6.0,
right: 6.0,
bottom: 6.0,
top: 3.0,
),
hintText: L10n.of(context).writeAMessage,
hintMaxLines: 1,
border: InputBorder.none,
enabledBorder: InputBorder.none,
filled: false,
),
onChanged: controller.onInputBarChanged,
),
),
),
Opacity(
opacity: controller.selectMode ? 0.66 : 1,
child: Container(
height: height,
width: height,
alignment: Alignment.center,
child: PlatformInfos.platformCanRecord &&
controller.sendController.text.isEmpty
? FloatingActionButton.small(
tooltip: L10n.of(context).voiceMessage,
onPressed: controller.selectMode
? null
: controller.voiceMessageAction,
elevation: 0,
heroTag: null,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(height),
),
backgroundColor: theme.bubbleColor,
foregroundColor: theme.onBubbleColor,
child: const Icon(Icons.mic_none_outlined),
)
: FloatingActionButton.small(
tooltip: L10n.of(context).send,
// #Pangea
// onPressed: controller.selectMode ? null : controller.send,
onPressed: () {},
// Pangea#
elevation: 0,
heroTag: null,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(height),
),
backgroundColor: theme.bubbleColor,
foregroundColor: theme.onBubbleColor,
child: const Icon(Icons.send_outlined),
),
),
),
],
);
}
}

View file

@ -34,12 +34,6 @@ class ChatView extends StatelessWidget {
List<Widget> _appBarActions(BuildContext context) {
if (controller.selectMode) {
return [
if (controller.canEditSelectedEvents)
IconButton(
icon: const Icon(Icons.edit_outlined),
tooltip: L10n.of(context).edit,
onPressed: controller.editSelectedEventAction,
),
IconButton(
icon: const Icon(Icons.copy_outlined),
tooltip: L10n.of(context).copy,
@ -310,13 +304,14 @@ class ChatView extends StatelessWidget {
child: ChatEventList(controller: controller),
),
),
if (controller.showScrollDownButton)
Divider(
height: 1,
color: theme.dividerColor,
),
if (controller.room.isExtinct)
Container(
margin: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
margin: EdgeInsets.all(bottomSheetPadding),
width: double.infinity,
child: ElevatedButton.icon(
icon: const Icon(Icons.chevron_right),
@ -327,11 +322,7 @@ class ChatView extends StatelessWidget {
else if (controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join)
Container(
margin: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
margin: EdgeInsets.all(bottomSheetPadding),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.5,
),
@ -386,7 +377,6 @@ class ChatView extends StatelessWidget {
// : Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// ReactionsPicker(controller),
// ReplyDisplay(controller),
// ChatInputRow(controller),
// ChatEmojiPicker(controller),

File diff suppressed because it is too large Load diff

View file

@ -519,6 +519,7 @@ class InputBar extends StatelessWidget {
// builder: (context, controller, focusNode) => TextField(
// controller: controller,
// focusNode: focusNode,
// readOnly: readOnly,
// contextMenuBuilder: (c, e) => markdownContextBuilder(c, e, controller),
// contentInsertionConfiguration: ContentInsertionConfiguration(
// onContentInserted: (KeyboardInsertedContent content) {

View file

@ -1,119 +0,0 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/app_emojis.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import '../../config/themes.dart';
class ReactionsPicker extends StatelessWidget {
final ChatController controller;
const ReactionsPicker(this.controller, {super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (controller.showEmojiPicker) return const SizedBox.shrink();
final display = controller.editEvent == null &&
controller.replyEvent == null &&
controller.room.canSendDefaultMessages &&
controller.selectedEvents.isNotEmpty;
return AnimatedContainer(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
// #Pangea
// height: (display) ? 56 : 0,
height: (display) ? AppConfig.reactionsPickerHeight : 0,
// Pangea#
child: Material(
color: Colors.transparent,
child: Builder(
builder: (context) {
if (!display) {
return const SizedBox.shrink();
}
final emojis = List<String>.from(AppEmojis.emojis);
final allReactionEvents = controller.selectedEvents.first
.aggregatedEvents(
controller.timeline!,
RelationshipTypes.reaction,
)
.where(
(event) =>
event.senderId == event.room.client.userID &&
event.type == 'm.reaction',
);
for (final event in allReactionEvents) {
try {
emojis.remove(event.content.tryGetMap('m.relates_to')!['key']);
} catch (_) {}
}
return Row(
children: [
Expanded(
child: Container(
decoration: const BoxDecoration(
// #Pangea
// color: theme.colorScheme.onInverseSurface,
// Pangea#
borderRadius: BorderRadius.only(
bottomRight: Radius.circular(AppConfig.borderRadius),
),
),
padding: const EdgeInsets.only(right: 1),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: emojis.length,
itemBuilder: (c, i) => InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => controller.sendEmojiAction(emojis[i]),
child: Container(
// #Pangea
// width: 56,
// height: 56,
width: AppConfig.reactionsPickerHeight,
height: AppConfig.reactionsPickerHeight,
// Pangea#
alignment: Alignment.center,
child: Text(
emojis[i],
// #Pangea
// style: const TextStyle(fontSize: 30),
style: const TextStyle(fontSize: 20),
// Pangea#
),
),
),
),
),
),
InkWell(
borderRadius: BorderRadius.circular(8),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
width: 36,
// #Pangea
// height: 56,
height: AppConfig.reactionsPickerHeight,
// Pangea#
decoration: BoxDecoration(
color: theme.colorScheme.onInverseSurface,
shape: BoxShape.circle,
),
child: const Icon(Icons.add_outlined),
),
onTap: () =>
controller.pickEmojiReactionAction(allReactionEvents),
),
],
);
},
),
),
);
}
}

View file

@ -124,8 +124,7 @@ class PangeaChatInputRowState extends State<PangeaChatInputRow> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (_controller.showEmojiPicker &&
_controller.emojiPickerType == EmojiPickerType.reaction) {
if (_controller.showEmojiPicker) {
return const SizedBox.shrink();
}
const height = 48.0;

View file

@ -39,7 +39,8 @@ class LemmaReactionPickerState extends State<LemmaReactionPicker> {
}
}
void setEmoji(String emoji) => widget.controller.sendEmojiAction(emoji);
void setEmoji(String emoji) {}
// widget.controller.sendEmojiAction(emoji);
Future<void> _refresh() async {
setState(() {

View file

@ -6,7 +6,6 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/events/utils/report_message.dart';
class OverlayHeader extends StatefulWidget {
@ -118,14 +117,14 @@ class OverlayHeaderState extends State<OverlayHeader> {
color: theme.colorScheme.primary,
),
if (controller.canEditSelectedEvents &&
!controller.selectedEvents.first.isActivityMessage)
IconButton(
icon: const Icon(Icons.edit_outlined),
tooltip: l10n.edit,
onPressed: controller.editSelectedEventAction,
color: theme.colorScheme.primary,
),
// if (controller.canEditSelectedEvents &&
// !controller.selectedEvents.first.isActivityMessage)
// IconButton(
// icon: const Icon(Icons.edit_outlined),
// tooltip: l10n.edit,
// onPressed: controller.editSelectedEventAction,
// color: theme.colorScheme.primary,
// ),
if (controller.canRedactSelectedEvents)
IconButton(
icon: const Icon(Icons.delete_outlined),

View file

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
Future<void> showScaffoldDialog({
Future<T?> showScaffoldDialog<T>({
required BuildContext context,
Color? barrierColor,
Color? containerColor,
@ -11,7 +11,7 @@ Future<void> showScaffoldDialog({
double maxHeight = 720,
required Widget Function(BuildContext context) builder,
}) =>
showDialog(
showDialog<T>(
context: context,
useSafeArea: false,
builder: FluffyThemes.isColumnMode(context)