simplified positioning of toolbar
This commit is contained in:
parent
868e83709d
commit
0373d01f1b
16 changed files with 1041 additions and 1425 deletions
|
|
@ -3,14 +3,12 @@ import 'dart:developer';
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/setting_keys.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_emoji_picker.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_view.dart';
|
||||
import 'package:fluffychat/pages/chat/event_info_dialog.dart';
|
||||
import 'package:fluffychat/pages/chat/recording_dialog.dart';
|
||||
|
|
@ -18,6 +16,7 @@ import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/choreo_record.dart';
|
||||
|
|
@ -29,8 +28,10 @@ import 'package:fluffychat/pangea/utils/error_handler.dart';
|
|||
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
|
||||
import 'package:fluffychat/pangea/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/utils/report_message.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart';
|
||||
import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart';
|
||||
import 'package:fluffychat/utils/error_reporter.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart';
|
||||
|
|
@ -928,20 +929,17 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
|
||||
void copyEventsAction() {
|
||||
// #Pangea
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
// Pangea#
|
||||
Clipboard.setData(ClipboardData(text: _getSelectedEventString()));
|
||||
setState(() {
|
||||
showEmojiPicker = false;
|
||||
selectedEvents.clear();
|
||||
// #Pangea
|
||||
// selectedEvents.clear();
|
||||
clearSelectedEvents();
|
||||
// Pangea#
|
||||
});
|
||||
}
|
||||
|
||||
void reportEventAction() async {
|
||||
// #Pangea
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
// Pangea#
|
||||
final event = selectedEvents.single;
|
||||
// #Pangea
|
||||
clearSelectedEvents();
|
||||
|
|
@ -1035,9 +1033,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
|
||||
void redactEventsAction() async {
|
||||
// #Pangea
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
// Pangea#
|
||||
final reasonInput = selectedEvents.any((event) => event.status.isSent)
|
||||
? await showTextInputDialog(
|
||||
context: context,
|
||||
|
|
@ -1086,6 +1081,9 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
},
|
||||
);
|
||||
}
|
||||
// #Pangea
|
||||
clearSelectedEvents();
|
||||
// Pangea#
|
||||
setState(() {
|
||||
showEmojiPicker = false;
|
||||
selectedEvents.clear();
|
||||
|
|
@ -1133,9 +1131,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
|
||||
void forwardEventsAction() async {
|
||||
// #Pangea
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
// Pangea#
|
||||
if (selectedEvents.length == 1) {
|
||||
Matrix.of(context).shareContent =
|
||||
selectedEvents.first.getDisplayEvent(timeline!).content;
|
||||
|
|
@ -1169,7 +1164,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
selectedEvents.clear();
|
||||
});
|
||||
// #Pangea
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
clearSelectedEvents();
|
||||
// Pangea
|
||||
inputFocus.requestFocus();
|
||||
}
|
||||
|
|
@ -1283,39 +1278,32 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
|
||||
void pickEmojiReactionAction(Iterable<Event> allReactionEvents) async {
|
||||
// #Pangea
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
// Pangea#
|
||||
_allReactionEvents = allReactionEvents;
|
||||
emojiPickerType = EmojiPickerType.reaction;
|
||||
setState(() => showEmojiPicker = true);
|
||||
// #Pangea
|
||||
OverlayUtil.showOverlay(
|
||||
context: context,
|
||||
child: ChatEmojiPicker(this),
|
||||
transformTargetId: selectedEvents.first.eventId,
|
||||
targetAnchor: Alignment.center,
|
||||
followerAnchor: Alignment.center,
|
||||
backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(100),
|
||||
closePrevOverlay: false,
|
||||
onDismiss: hideEmojiPicker,
|
||||
position: OverlayEnum.bottom,
|
||||
);
|
||||
// Pangea#
|
||||
}
|
||||
|
||||
void sendEmojiAction(String? emoji) async {
|
||||
final events = List<Event>.from(selectedEvents);
|
||||
setState(() => selectedEvents.clear());
|
||||
// #Pangea
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
// Pangea#
|
||||
for (final event in events) {
|
||||
await room.sendReaction(
|
||||
event.eventId,
|
||||
emoji!,
|
||||
);
|
||||
}
|
||||
// #Pangea
|
||||
clearSelectedEvents();
|
||||
// Pangea#
|
||||
}
|
||||
|
||||
void clearSelectedEvents() => setState(() {
|
||||
// #Pangea
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
// Pangea#
|
||||
selectedEvents.clear();
|
||||
showEmojiPicker = false;
|
||||
});
|
||||
|
|
@ -1552,12 +1540,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
bool get isArchived =>
|
||||
{Membership.leave, Membership.ban}.contains(room.membership);
|
||||
|
||||
void showEventInfo([Event? event])
|
||||
// #Pangea
|
||||
// =>
|
||||
{
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
// Pangea#
|
||||
void showEventInfo([Event? event]) {
|
||||
(event ?? selectedEvents.single).showInfoDialog(context);
|
||||
// #Pangea
|
||||
clearSelectedEvents();
|
||||
|
|
@ -1618,80 +1601,51 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
editEvent = null;
|
||||
});
|
||||
|
||||
// #Pangea
|
||||
final Map<String, PangeaMessageEvent> _pangeaMessageEvents = {};
|
||||
final Map<String, ToolbarDisplayController> _toolbarDisplayControllers = {};
|
||||
// #Pangea
|
||||
MessageTextSelection textSelection = MessageTextSelection();
|
||||
|
||||
void setPangeaMessageEvent(String eventId) {
|
||||
final Event? event = timeline!.events.firstWhereOrNull(
|
||||
(e) => e.eventId == eventId,
|
||||
);
|
||||
if (event == null || timeline == null) return;
|
||||
_pangeaMessageEvents[eventId] = PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: timeline!,
|
||||
ownMessage: event.senderId == room.client.userID,
|
||||
);
|
||||
}
|
||||
|
||||
void setToolbarDisplayController(
|
||||
String eventId, {
|
||||
Event? nextEvent,
|
||||
Event? previousEvent,
|
||||
void showToolbar(
|
||||
PangeaMessageEvent pangeaMessageEvent, {
|
||||
MessageMode? mode,
|
||||
}) {
|
||||
final Event? event = timeline!.events.firstWhereOrNull(
|
||||
(e) => e.eventId == eventId,
|
||||
);
|
||||
if (event == null || timeline == null) return;
|
||||
if (_pangeaMessageEvents[eventId] == null) {
|
||||
setPangeaMessageEvent(eventId);
|
||||
if (_pangeaMessageEvents[eventId] == null) return;
|
||||
// Close keyboard, if open
|
||||
if (inputFocus.hasFocus && PlatformInfos.isMobile) {
|
||||
inputFocus.unfocus();
|
||||
return;
|
||||
}
|
||||
// Close emoji picker, if open
|
||||
showEmojiPicker = false;
|
||||
|
||||
// Check if the user has set their languages. If not, prompt them to do so.
|
||||
if (!MatrixState.pangeaController.languageController.languagesSet) {
|
||||
pLanguageDialog(context, () {});
|
||||
return;
|
||||
}
|
||||
|
||||
Widget? overlayEntry;
|
||||
try {
|
||||
_toolbarDisplayControllers[eventId] = ToolbarDisplayController(
|
||||
targetId: event.eventId,
|
||||
pangeaMessageEvent: _pangeaMessageEvents[eventId]!,
|
||||
immersionMode: choreographer.immersionMode,
|
||||
overlayEntry = MessageSelectionOverlay(
|
||||
controller: this,
|
||||
nextEvent: nextEvent,
|
||||
previousEvent: previousEvent,
|
||||
);
|
||||
_toolbarDisplayControllers[eventId]!.setToolbar();
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
m: "Failed to set toolbar display controller",
|
||||
data: {
|
||||
"eventId": eventId,
|
||||
"event": event.toJson(),
|
||||
"pangeaMessageEvent": _pangeaMessageEvents[eventId]?.toString(),
|
||||
},
|
||||
event: pangeaMessageEvent.event,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
textSelection: textSelection,
|
||||
);
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: StackTrace.current);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
PangeaMessageEvent? getPangeaMessageEvent(String eventId) {
|
||||
if (_pangeaMessageEvents[eventId] == null) {
|
||||
setPangeaMessageEvent(eventId);
|
||||
}
|
||||
return _pangeaMessageEvents[eventId];
|
||||
}
|
||||
|
||||
ToolbarDisplayController? getToolbarDisplayController(
|
||||
String eventId, {
|
||||
Event? nextEvent,
|
||||
Event? previousEvent,
|
||||
}) {
|
||||
if (_toolbarDisplayControllers[eventId] == null) {
|
||||
setToolbarDisplayController(
|
||||
eventId,
|
||||
nextEvent: nextEvent,
|
||||
previousEvent: previousEvent,
|
||||
);
|
||||
}
|
||||
return _toolbarDisplayControllers[eventId];
|
||||
OverlayUtil.showOverlay(
|
||||
context: context,
|
||||
child: overlayEntry,
|
||||
transformTargetId: "",
|
||||
backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(200),
|
||||
closePrevOverlay:
|
||||
MatrixState.pangeaController.subscriptionController.isSubscribed,
|
||||
position: OverlayEnum.centered,
|
||||
onDismiss: clearSelectedEvents,
|
||||
);
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
|
|
|
|||
|
|
@ -14,90 +14,95 @@ class ChatEmojiPicker extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(),
|
||||
height: controller.showEmojiPicker
|
||||
? MediaQuery.of(context).size.height / 2
|
||||
: 0,
|
||||
child: controller.showEmojiPicker
|
||||
? DefaultTabController(
|
||||
length: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
tabs: [
|
||||
Tab(text: L10n.of(context)!.emojis),
|
||||
Tab(text: L10n.of(context)!.stickers),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
EmojiPicker(
|
||||
onEmojiSelected: controller.onEmojiSelected,
|
||||
onBackspacePressed: controller.emojiPickerBackspace,
|
||||
config: Config(
|
||||
emojiViewConfig: EmojiViewConfig(
|
||||
noRecents: const NoRecent(),
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
),
|
||||
bottomActionBarConfig: const BottomActionBarConfig(
|
||||
enabled: false,
|
||||
),
|
||||
categoryViewConfig: CategoryViewConfig(
|
||||
backspaceColor: theme.colorScheme.primary,
|
||||
iconColor:
|
||||
theme.colorScheme.primary.withOpacity(0.5),
|
||||
iconColorSelected: theme.colorScheme.primary,
|
||||
indicatorColor: theme.colorScheme.primary,
|
||||
),
|
||||
skinToneConfig: SkinToneConfig(
|
||||
dialogBackgroundColor: Color.lerp(
|
||||
theme.colorScheme.surface,
|
||||
theme.colorScheme.primaryContainer,
|
||||
0.75,
|
||||
)!,
|
||||
indicatorColor: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
StickerPickerDialog(
|
||||
room: controller.room,
|
||||
onSelected: (sticker) {
|
||||
controller.room.sendEvent(
|
||||
{
|
||||
'body': sticker.body,
|
||||
'info': sticker.info ?? {},
|
||||
'url': sticker.url.toString(),
|
||||
},
|
||||
type: EventTypes.Sticker,
|
||||
);
|
||||
controller.hideEmojiPicker();
|
||||
},
|
||||
),
|
||||
// #Pangea
|
||||
return Material(
|
||||
// Pangea#
|
||||
child: AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(),
|
||||
height: controller.showEmojiPicker
|
||||
? MediaQuery.of(context).size.height / 2
|
||||
: 0,
|
||||
child: controller.showEmojiPicker
|
||||
? DefaultTabController(
|
||||
length: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
tabs: [
|
||||
Tab(text: L10n.of(context)!.emojis),
|
||||
Tab(text: L10n.of(context)!.stickers),
|
||||
],
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: controller.hideEmojiPicker,
|
||||
shape: const CircleBorder(),
|
||||
mini: true,
|
||||
child: const Icon(Icons.close),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
EmojiPicker(
|
||||
onEmojiSelected: controller.onEmojiSelected,
|
||||
onBackspacePressed: controller.emojiPickerBackspace,
|
||||
config: Config(
|
||||
emojiViewConfig: EmojiViewConfig(
|
||||
noRecents: const NoRecent(),
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
),
|
||||
bottomActionBarConfig:
|
||||
const BottomActionBarConfig(
|
||||
enabled: false,
|
||||
),
|
||||
categoryViewConfig: CategoryViewConfig(
|
||||
backspaceColor: theme.colorScheme.primary,
|
||||
iconColor:
|
||||
theme.colorScheme.primary.withOpacity(0.5),
|
||||
iconColorSelected: theme.colorScheme.primary,
|
||||
indicatorColor: theme.colorScheme.primary,
|
||||
),
|
||||
skinToneConfig: SkinToneConfig(
|
||||
dialogBackgroundColor: Color.lerp(
|
||||
theme.colorScheme.surface,
|
||||
theme.colorScheme.primaryContainer,
|
||||
0.75,
|
||||
)!,
|
||||
indicatorColor: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
StickerPickerDialog(
|
||||
room: controller.room,
|
||||
onSelected: (sticker) {
|
||||
controller.room.sendEvent(
|
||||
{
|
||||
'body': sticker.body,
|
||||
'info': sticker.info ?? {},
|
||||
'url': sticker.url.toString(),
|
||||
},
|
||||
type: EventTypes.Sticker,
|
||||
);
|
||||
controller.hideEmojiPicker();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Pangea#
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
// #Pangea
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: controller.hideEmojiPicker,
|
||||
shape: const CircleBorder(),
|
||||
mini: true,
|
||||
child: const Icon(Icons.close),
|
||||
),
|
||||
),
|
||||
// Pangea#
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -188,7 +188,6 @@ class ChatEventList extends StatelessWidget {
|
|||
longPressSelect: controller.selectedEvents.isNotEmpty,
|
||||
// #Pangea
|
||||
immersionMode: controller.choreographer.immersionMode,
|
||||
definitions: controller.choreographer.definitionsEnabled,
|
||||
controller: controller,
|
||||
// Pangea#
|
||||
selected: controller.selectedEvents
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import 'package:fluffychat/pages/chat/chat_emoji_picker.dart';
|
|||
import 'package:fluffychat/pages/chat/chat_event_list.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_input_row.dart';
|
||||
import 'package:fluffychat/pages/chat/pinned_events.dart';
|
||||
import 'package:fluffychat/pages/chat/reactions_picker.dart';
|
||||
import 'package:fluffychat/pages/chat/reply_display.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart';
|
||||
|
|
@ -36,100 +35,98 @@ class ChatView extends StatelessWidget {
|
|||
const ChatView(this.controller, {super.key});
|
||||
|
||||
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,
|
||||
),
|
||||
// #Pangea
|
||||
if (controller.selectedEvents.length == 1 &&
|
||||
controller.selectedEvents.single.messageType == MessageTypes.Text)
|
||||
// Pangea#
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy_outlined),
|
||||
tooltip: L10n.of(context)!.copy,
|
||||
onPressed: controller.copyEventsAction,
|
||||
),
|
||||
if (controller.canSaveSelectedEvent)
|
||||
// Use builder context to correctly position the share dialog on iPad
|
||||
Builder(
|
||||
builder: (context) => IconButton(
|
||||
icon: Icon(Icons.adaptive.share),
|
||||
tooltip: L10n.of(context)!.share,
|
||||
onPressed: () => controller.saveSelectedEvent(context),
|
||||
),
|
||||
),
|
||||
if (controller.canPinSelectedEvents)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.push_pin_outlined),
|
||||
onPressed: controller.pinEvent,
|
||||
tooltip: L10n.of(context)!.pinMessage,
|
||||
),
|
||||
if (controller.canRedactSelectedEvents)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outlined),
|
||||
tooltip: L10n.of(context)!.redactMessage,
|
||||
onPressed: controller.redactEventsAction,
|
||||
),
|
||||
if (controller.selectedEvents.length == 1)
|
||||
PopupMenuButton<_EventContextAction>(
|
||||
onSelected: (action) {
|
||||
switch (action) {
|
||||
case _EventContextAction.info:
|
||||
controller.showEventInfo();
|
||||
controller.clearSelectedEvents();
|
||||
break;
|
||||
case _EventContextAction.report:
|
||||
controller.reportEventAction();
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: _EventContextAction.info,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.info_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.messageInfo),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (controller.selectedEvents.single.status.isSent)
|
||||
PopupMenuItem(
|
||||
value: _EventContextAction.report,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.shield_outlined,
|
||||
color: Colors.red,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.reportMessage),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
// #Pangea
|
||||
} else {
|
||||
return [
|
||||
RoundTimer(controller: controller),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
ChatSettingsPopupMenu(
|
||||
controller.room,
|
||||
(!controller.room.isDirectChat && !controller.room.isArchived),
|
||||
),
|
||||
];
|
||||
}
|
||||
// #Pangea
|
||||
// if (controller.selectMode) {
|
||||
// return [
|
||||
// if (controller.canEditSelectedEvents)
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.edit_outlined),
|
||||
// tooltip: L10n.of(context)!.edit,
|
||||
// onPressed: controller.editSelectedEventAction,
|
||||
// ),
|
||||
// // #Pangea
|
||||
// if (controller.selectedEvents.length == 1 &&
|
||||
// controller.selectedEvents.single.messageType == MessageTypes.Text)
|
||||
// // Pangea#
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.copy_outlined),
|
||||
// tooltip: L10n.of(context)!.copy,
|
||||
// onPressed: controller.copyEventsAction,
|
||||
// ),
|
||||
// if (controller.canSaveSelectedEvent)
|
||||
// // Use builder context to correctly position the share dialog on iPad
|
||||
// Builder(
|
||||
// builder: (context) => IconButton(
|
||||
// icon: Icon(Icons.adaptive.share),
|
||||
// tooltip: L10n.of(context)!.share,
|
||||
// onPressed: () => controller.saveSelectedEvent(context),
|
||||
// ),
|
||||
// ),
|
||||
// if (controller.canPinSelectedEvents)
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.push_pin_outlined),
|
||||
// onPressed: controller.pinEvent,
|
||||
// tooltip: L10n.of(context)!.pinMessage,
|
||||
// ),
|
||||
// if (controller.canRedactSelectedEvents)
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.delete_outlined),
|
||||
// tooltip: L10n.of(context)!.redactMessage,
|
||||
// onPressed: controller.redactEventsAction,
|
||||
// ),
|
||||
// if (controller.selectedEvents.length == 1)
|
||||
// PopupMenuButton<_EventContextAction>(
|
||||
// onSelected: (action) {
|
||||
// switch (action) {
|
||||
// case _EventContextAction.info:
|
||||
// controller.showEventInfo();
|
||||
// controller.clearSelectedEvents();
|
||||
// break;
|
||||
// case _EventContextAction.report:
|
||||
// controller.reportEventAction();
|
||||
// break;
|
||||
// }
|
||||
// },
|
||||
// itemBuilder: (context) => [
|
||||
// PopupMenuItem(
|
||||
// value: _EventContextAction.info,
|
||||
// child: Row(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// const Icon(Icons.info_outlined),
|
||||
// const SizedBox(width: 12),
|
||||
// Text(L10n.of(context)!.messageInfo),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// if (controller.selectedEvents.single.status.isSent)
|
||||
// PopupMenuItem(
|
||||
// value: _EventContextAction.report,
|
||||
// child: Row(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// const Icon(
|
||||
// Icons.shield_outlined,
|
||||
// color: Colors.red,
|
||||
// ),
|
||||
// const SizedBox(width: 12),
|
||||
// Text(L10n.of(context)!.reportMessage),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ];
|
||||
return [
|
||||
RoundTimer(controller: controller),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
ChatSettingsPopupMenu(
|
||||
controller.room,
|
||||
(!controller.room.isDirectChat && !controller.room.isArchived),
|
||||
),
|
||||
];
|
||||
// else if (!controller.room.isArchived) {
|
||||
// return [
|
||||
// if (Matrix.of(context).voipPlugin != null &&
|
||||
|
|
@ -196,28 +193,34 @@ class ChatView extends StatelessWidget {
|
|||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
actionsIconTheme: IconThemeData(
|
||||
color: controller.selectedEvents.isEmpty
|
||||
? null
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
actionsIconTheme: const IconThemeData(
|
||||
// #Pangea
|
||||
// color: controller.selectedEvents.isEmpty
|
||||
// ? null
|
||||
// : Theme.of(context).colorScheme.primary,
|
||||
// Pangea#
|
||||
),
|
||||
leading:
|
||||
// #Pangea
|
||||
// controller.selectMode
|
||||
// ? IconButton(
|
||||
// icon: const Icon(Icons.close),
|
||||
// onPressed: controller.clearSelectedEvents,
|
||||
// tooltip: L10n.of(context)!.close,
|
||||
// color: Theme.of(context).colorScheme.primary,
|
||||
// )
|
||||
// :
|
||||
// Pangea#
|
||||
UnreadRoomsBadge(
|
||||
filter: (r) =>
|
||||
r.id != controller.roomId
|
||||
// #Pangea
|
||||
&&
|
||||
!r.isAnalyticsRoom,
|
||||
// Pangea#
|
||||
badgePosition: BadgePosition.topEnd(end: 8, top: 4),
|
||||
child: const Center(child: BackButton()),
|
||||
),
|
||||
leading: controller.selectMode
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: controller.clearSelectedEvents,
|
||||
tooltip: L10n.of(context)!.close,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: UnreadRoomsBadge(
|
||||
filter: (r) =>
|
||||
r.id != controller.roomId
|
||||
// #Pangea
|
||||
&&
|
||||
!r.isAnalyticsRoom,
|
||||
// Pangea#
|
||||
badgePosition: BadgePosition.topEnd(end: 8, top: 4),
|
||||
child: const Center(child: BackButton()),
|
||||
),
|
||||
titleSpacing: 0,
|
||||
title: ChatAppBarTitle(controller),
|
||||
actions: _appBarActions(context),
|
||||
|
|
@ -501,7 +504,6 @@ class ChatView extends StatelessWidget {
|
|||
ITBar(
|
||||
choreographer: controller.choreographer,
|
||||
),
|
||||
ReactionsPicker(controller),
|
||||
ReplyDisplay(controller),
|
||||
ChatInputRow(controller),
|
||||
ChatEmojiPicker(controller),
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ import 'package:fluffychat/config/themes.dart';
|
|||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/enum/use_type.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_buttons.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/string_color.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:swipe_to_action/swipe_to_action.dart';
|
||||
|
|
@ -38,8 +39,8 @@ class Message extends StatelessWidget {
|
|||
// #Pangea
|
||||
// final void Function(Event) onSelect;
|
||||
final bool immersionMode;
|
||||
final bool definitions;
|
||||
final ChatController controller;
|
||||
final bool isOverlay;
|
||||
// Pangea#
|
||||
final Color? avatarPresenceBackgroundColor;
|
||||
|
||||
|
|
@ -64,21 +65,33 @@ class Message extends StatelessWidget {
|
|||
this.avatarPresenceBackgroundColor,
|
||||
// #Pangea
|
||||
required this.immersionMode,
|
||||
required this.definitions,
|
||||
required this.controller,
|
||||
this.isOverlay = false,
|
||||
// Pangea#
|
||||
super.key,
|
||||
});
|
||||
|
||||
// #Pangea
|
||||
PangeaMessageEvent? get pangeaMessageEvent =>
|
||||
controller.getPangeaMessageEvent(event.eventId);
|
||||
void showToolbar(PangeaMessageEvent? pangeaMessageEvent) {
|
||||
if (pangeaMessageEvent != null && !isOverlay) {
|
||||
HapticFeedback.mediumImpact();
|
||||
controller.showToolbar(pangeaMessageEvent);
|
||||
}
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// #Pangea
|
||||
debugPrint('Message.build()');
|
||||
PangeaMessageEvent? pangeaMessageEvent;
|
||||
if (event.type == EventTypes.Message) {
|
||||
pangeaMessageEvent = PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: timeline,
|
||||
ownMessage: event.senderId == Matrix.of(context).client.userID,
|
||||
);
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (controller.pangeaEditingEvent?.eventId == event.eventId) {
|
||||
pangeaMessageEvent?.updateLatestEdit();
|
||||
|
|
@ -109,8 +122,13 @@ class Message extends StatelessWidget {
|
|||
// ignore: deprecated_member_use
|
||||
var color = Theme.of(context).colorScheme.surfaceVariant;
|
||||
final displayTime = event.type == EventTypes.RoomCreate ||
|
||||
nextEvent == null ||
|
||||
!event.originServerTs.sameEnvironment(nextEvent!.originServerTs);
|
||||
nextEvent == null ||
|
||||
!event.originServerTs.sameEnvironment(nextEvent!.originServerTs)
|
||||
// #Pangea
|
||||
&&
|
||||
!isOverlay
|
||||
// Pangea#
|
||||
;
|
||||
final nextEventSameSender = nextEvent != null &&
|
||||
{
|
||||
EventTypes.Message,
|
||||
|
|
@ -163,370 +181,405 @@ class Message extends StatelessWidget {
|
|||
: Theme.of(context).colorScheme.primary;
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
ToolbarDisplayController? toolbarController;
|
||||
if (event.type == EventTypes.Message &&
|
||||
!event.redacted &&
|
||||
(event.messageType == MessageTypes.Text ||
|
||||
event.messageType == MessageTypes.Notice ||
|
||||
event.messageType == MessageTypes.Audio)) {
|
||||
toolbarController = controller.getToolbarDisplayController(
|
||||
event.eventId,
|
||||
nextEvent: nextEvent,
|
||||
previousEvent: previousEvent,
|
||||
);
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
final resetAnimateIn = this.resetAnimateIn;
|
||||
var animateIn = this.animateIn;
|
||||
|
||||
final row = StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
if (animateIn && resetAnimateIn != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
animateIn = false;
|
||||
setState(resetAnimateIn);
|
||||
});
|
||||
}
|
||||
return AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
clipBehavior: Clip.none,
|
||||
alignment: ownMessage ? Alignment.bottomRight : Alignment.bottomLeft,
|
||||
child: animateIn
|
||||
? const SizedBox(height: 0, width: double.infinity)
|
||||
: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: InkWell(
|
||||
// #Pangea
|
||||
// onTap: () => onSelect(event),
|
||||
// onLongPress: () => onSelect(event),
|
||||
// Pangea#
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
child: Material(
|
||||
final row =
|
||||
// #Pangea
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child:
|
||||
// Pangea#
|
||||
StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
if (animateIn && resetAnimateIn != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
animateIn = false;
|
||||
setState(resetAnimateIn);
|
||||
});
|
||||
}
|
||||
return AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
clipBehavior: Clip.none,
|
||||
alignment:
|
||||
ownMessage ? Alignment.bottomRight : Alignment.bottomLeft,
|
||||
child: animateIn
|
||||
? const SizedBox(height: 0, width: double.infinity)
|
||||
: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: InkWell(
|
||||
// #Pangea
|
||||
onTap: () => MatrixState.pAnyState.closeOverlay(),
|
||||
// onTap: () => onSelect(event),
|
||||
// onLongPress: () => onSelect(event),
|
||||
// Pangea#
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
color: selected
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer
|
||||
.withAlpha(100)
|
||||
: highlightMarker
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.tertiaryContainer
|
||||
.withAlpha(100)
|
||||
: Colors.transparent,
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius / 2,
|
||||
),
|
||||
color: selected
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer
|
||||
.withAlpha(100)
|
||||
: highlightMarker
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.tertiaryContainer
|
||||
.withAlpha(100)
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: rowMainAxisAlignment,
|
||||
children: [
|
||||
// #Pangea
|
||||
// if (longPressSelect)
|
||||
// SizedBox(
|
||||
// height: 32,
|
||||
// width: Avatar.defaultSize,
|
||||
// child: Checkbox.adaptive(
|
||||
// value: selected,
|
||||
// shape: const CircleBorder(),
|
||||
// onChanged: (_) => onSelect(event),
|
||||
// ),
|
||||
// )
|
||||
// else
|
||||
// Pangea#
|
||||
if (nextEventSameSender || ownMessage)
|
||||
SizedBox(
|
||||
width: Avatar.defaultSize,
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: event.status == EventStatus.error
|
||||
? const Icon(Icons.error, color: Colors.red)
|
||||
: event.fileSendingStatus != null
|
||||
? const CircularProgressIndicator
|
||||
.adaptive(
|
||||
strokeWidth: 1,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
FutureBuilder<User?>(
|
||||
future: event.fetchSenderUser(),
|
||||
builder: (context, snapshot) {
|
||||
final user = snapshot.data ??
|
||||
event.senderFromMemoryOrFallback;
|
||||
return Avatar(
|
||||
// mxContent: user.avatarUrl,
|
||||
// name: user.calcDisplayname(),
|
||||
// presenceUserId: user.stateKey,
|
||||
name: "?",
|
||||
presenceBackgroundColor:
|
||||
avatarPresenceBackgroundColor,
|
||||
onTap: () => onAvatarTab(event),
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!nextEventSameSender)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8.0,
|
||||
bottom: 4,
|
||||
),
|
||||
child: ownMessage || event.room.isDirectChat
|
||||
? const SizedBox(height: 12)
|
||||
: FutureBuilder<User?>(
|
||||
future: event.fetchSenderUser(),
|
||||
builder: (context, snapshot) {
|
||||
// final displayname = snapshot.data
|
||||
// ?.calcDisplayname() ??
|
||||
// event.senderFromMemoryOrFallback
|
||||
// .calcDisplayname();
|
||||
const displayname = "?";
|
||||
return Text(
|
||||
displayname,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: (Theme.of(context)
|
||||
.brightness ==
|
||||
Brightness.light
|
||||
? displayname.color
|
||||
: displayname
|
||||
.lightColorText),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Container(
|
||||
alignment: alignment,
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: GestureDetector(
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: rowMainAxisAlignment,
|
||||
children: [
|
||||
// #Pangea
|
||||
// if (longPressSelect)
|
||||
// SizedBox(
|
||||
// height: 32,
|
||||
// width: Avatar.defaultSize,
|
||||
// child: Checkbox.adaptive(
|
||||
// value: selected,
|
||||
// shape: const CircleBorder(),
|
||||
// onChanged: (_) => onSelect(event),
|
||||
// ),
|
||||
// )
|
||||
// else
|
||||
// Pangea#
|
||||
if (nextEventSameSender ||
|
||||
ownMessage
|
||||
// #Pangea
|
||||
onTap: () => toolbarController?.showToolbar(
|
||||
context,
|
||||
),
|
||||
onDoubleTap: () =>
|
||||
toolbarController?.showToolbar(context),
|
||||
// onLongPress: longPressSelect
|
||||
// ? null
|
||||
// : () {
|
||||
// HapticFeedback.heavyImpact();
|
||||
// onSelect(event);
|
||||
// },
|
||||
// Pangea#
|
||||
child: AnimatedOpacity(
|
||||
opacity: animateIn
|
||||
? 0
|
||||
: event.redacted ||
|
||||
event.messageType ==
|
||||
MessageTypes.BadEncrypted ||
|
||||
event.status.isSending
|
||||
? 0.5
|
||||
: 1,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
child: Material(
|
||||
color:
|
||||
noBubble ? Colors.transparent : color,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
// #Pangea
|
||||
child: CompositedTransformTarget(
|
||||
link: MatrixState.pAnyState
|
||||
.layerLinkAndKey(event.eventId)
|
||||
.link,
|
||||
child: Container(
|
||||
key: MatrixState.pAnyState
|
||||
.layerLinkAndKey(event.eventId)
|
||||
.key,
|
||||
// Pangea#
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
padding: noBubble || noPadding
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
||
|
||||
isOverlay
|
||||
// Pangea#
|
||||
)
|
||||
SizedBox(
|
||||
width: Avatar.defaultSize,
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: event.status == EventStatus.error
|
||||
? const Icon(
|
||||
Icons.error,
|
||||
color: Colors.red,
|
||||
)
|
||||
: event.fileSendingStatus != null
|
||||
? const CircularProgressIndicator
|
||||
.adaptive(
|
||||
strokeWidth: 1,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
FutureBuilder<User?>(
|
||||
future: event.fetchSenderUser(),
|
||||
builder: (context, snapshot) {
|
||||
final user = snapshot.data ??
|
||||
event.senderFromMemoryOrFallback;
|
||||
return Avatar(
|
||||
// mxContent: user.avatarUrl,
|
||||
// name: user.calcDisplayname(),
|
||||
// presenceUserId: user.stateKey,
|
||||
name: "?",
|
||||
presenceBackgroundColor:
|
||||
avatarPresenceBackgroundColor,
|
||||
onTap: () => onAvatarTab(event),
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!nextEventSameSender
|
||||
// #Pangea
|
||||
&&
|
||||
!isOverlay
|
||||
// Pangea#
|
||||
)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8.0,
|
||||
bottom: 4,
|
||||
),
|
||||
child: ownMessage || event.room.isDirectChat
|
||||
? const SizedBox(height: 12)
|
||||
: FutureBuilder<User?>(
|
||||
future: event.fetchSenderUser(),
|
||||
builder: (context, snapshot) {
|
||||
// final displayname = snapshot.data
|
||||
// ?.calcDisplayname() ??
|
||||
// event.senderFromMemoryOrFallback
|
||||
// .calcDisplayname();
|
||||
const displayname = "?";
|
||||
return Text(
|
||||
displayname,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: (Theme.of(context)
|
||||
.brightness ==
|
||||
Brightness.light
|
||||
? displayname.color
|
||||
: displayname
|
||||
.lightColorText),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth:
|
||||
FluffyThemes.columnWidth * 1.5,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
if (event.relationshipType ==
|
||||
RelationshipTypes.reply)
|
||||
FutureBuilder<Event?>(
|
||||
future: event
|
||||
.getReplyEvent(timeline),
|
||||
builder: (
|
||||
BuildContext context,
|
||||
snapshot,
|
||||
) {
|
||||
final replyEvent = snapshot
|
||||
.hasData
|
||||
? snapshot.data!
|
||||
: Event(
|
||||
eventId: event
|
||||
.relationshipEventId!,
|
||||
content: {
|
||||
'msgtype':
|
||||
'm.text',
|
||||
'body': '...',
|
||||
},
|
||||
senderId:
|
||||
event.senderId,
|
||||
type:
|
||||
'm.room.message',
|
||||
room: event.room,
|
||||
status: EventStatus
|
||||
.sent,
|
||||
originServerTs:
|
||||
DateTime.now(),
|
||||
);
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(
|
||||
bottom: 4.0,
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius:
|
||||
ReplyContent
|
||||
.borderRadius,
|
||||
onTap: () =>
|
||||
scrollToEventId(
|
||||
replyEvent.eventId,
|
||||
),
|
||||
Container(
|
||||
alignment: alignment,
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: GestureDetector(
|
||||
// #Pangea
|
||||
onTap: () =>
|
||||
showToolbar(pangeaMessageEvent),
|
||||
onDoubleTap: () =>
|
||||
showToolbar(pangeaMessageEvent),
|
||||
onLongPress: () =>
|
||||
showToolbar(pangeaMessageEvent),
|
||||
// onLongPress: longPressSelect
|
||||
// ? null
|
||||
// : () {
|
||||
// HapticFeedback.heavyImpact();
|
||||
// onSelect(event);
|
||||
// },
|
||||
// Pangea#
|
||||
child: AnimatedOpacity(
|
||||
opacity: animateIn
|
||||
? 0
|
||||
: event.redacted ||
|
||||
event.messageType ==
|
||||
MessageTypes
|
||||
.BadEncrypted ||
|
||||
event.status.isSending
|
||||
? 0.5
|
||||
: 1,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
child: Material(
|
||||
color: noBubble
|
||||
? Colors.transparent
|
||||
: color,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
// #Pangea
|
||||
child: CompositedTransformTarget(
|
||||
link: isOverlay
|
||||
? LayerLinkAndKey('overlay_msg')
|
||||
.link
|
||||
: MatrixState.pAnyState
|
||||
.layerLinkAndKey(
|
||||
event.eventId,
|
||||
)
|
||||
.link,
|
||||
child: Container(
|
||||
key: isOverlay
|
||||
? LayerLinkAndKey('overlay_msg')
|
||||
.key
|
||||
: MatrixState.pAnyState
|
||||
.layerLinkAndKey(
|
||||
event.eventId,
|
||||
)
|
||||
.key,
|
||||
// Pangea#
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
padding: noBubble || noPadding
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth:
|
||||
FluffyThemes.columnWidth *
|
||||
1.5,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
if (event.relationshipType ==
|
||||
RelationshipTypes.reply)
|
||||
FutureBuilder<Event?>(
|
||||
future: event.getReplyEvent(
|
||||
timeline,
|
||||
),
|
||||
builder: (
|
||||
BuildContext context,
|
||||
snapshot,
|
||||
) {
|
||||
final replyEvent =
|
||||
snapshot.hasData
|
||||
? snapshot.data!
|
||||
: Event(
|
||||
eventId: event
|
||||
.relationshipEventId!,
|
||||
content: {
|
||||
'msgtype':
|
||||
'm.text',
|
||||
'body':
|
||||
'...',
|
||||
},
|
||||
senderId: event
|
||||
.senderId,
|
||||
type:
|
||||
'm.room.message',
|
||||
room: event
|
||||
.room,
|
||||
status:
|
||||
EventStatus
|
||||
.sent,
|
||||
originServerTs:
|
||||
DateTime
|
||||
.now(),
|
||||
);
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets
|
||||
.only(
|
||||
bottom: 4.0,
|
||||
),
|
||||
child: AbsorbPointer(
|
||||
child: ReplyContent(
|
||||
replyEvent,
|
||||
ownMessage:
|
||||
ownMessage,
|
||||
timeline: timeline,
|
||||
child: InkWell(
|
||||
borderRadius:
|
||||
ReplyContent
|
||||
.borderRadius,
|
||||
onTap: () =>
|
||||
scrollToEventId(
|
||||
replyEvent.eventId,
|
||||
),
|
||||
child: AbsorbPointer(
|
||||
child: ReplyContent(
|
||||
replyEvent,
|
||||
ownMessage:
|
||||
ownMessage,
|
||||
timeline:
|
||||
timeline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
MessageContent(
|
||||
displayEvent,
|
||||
textColor: textColor,
|
||||
onInfoTab: onInfoTab,
|
||||
borderRadius: borderRadius,
|
||||
// #Pangea
|
||||
selected: selected,
|
||||
pangeaMessageEvent:
|
||||
pangeaMessageEvent,
|
||||
immersionMode: immersionMode,
|
||||
toolbarController:
|
||||
toolbarController,
|
||||
// Pangea#
|
||||
),
|
||||
if (event.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
)
|
||||
// #Pangea
|
||||
||
|
||||
(pangeaMessageEvent
|
||||
?.showUseType ??
|
||||
false)
|
||||
// Pangea#
|
||||
)
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(
|
||||
top: 4.0,
|
||||
);
|
||||
},
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize:
|
||||
MainAxisSize.min,
|
||||
children: [
|
||||
// #Pangea
|
||||
if (pangeaMessageEvent
|
||||
?.showUseType ??
|
||||
false) ...[
|
||||
pangeaMessageEvent!
|
||||
.msgUseType
|
||||
.iconView(
|
||||
context,
|
||||
textColor
|
||||
.withAlpha(164),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
],
|
||||
if (event
|
||||
.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
)) ...[
|
||||
// Pangea#
|
||||
Icon(
|
||||
Icons.edit_outlined,
|
||||
color: textColor
|
||||
.withAlpha(164),
|
||||
size: 14,
|
||||
),
|
||||
Text(
|
||||
' - ${displayEvent.originServerTs.localizedTimeShort(context)}',
|
||||
style: TextStyle(
|
||||
MessageContent(
|
||||
displayEvent,
|
||||
textColor: textColor,
|
||||
onInfoTab: onInfoTab,
|
||||
borderRadius: borderRadius,
|
||||
// #Pangea
|
||||
selected: selected,
|
||||
pangeaMessageEvent:
|
||||
pangeaMessageEvent,
|
||||
immersionMode: immersionMode,
|
||||
isOverlay: isOverlay,
|
||||
controller: controller,
|
||||
// Pangea#
|
||||
),
|
||||
if (event.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes
|
||||
.edit,
|
||||
)
|
||||
// #Pangea
|
||||
||
|
||||
(pangeaMessageEvent
|
||||
?.showUseType ??
|
||||
false)
|
||||
// Pangea#
|
||||
)
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(
|
||||
top: 4.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize:
|
||||
MainAxisSize.min,
|
||||
children: [
|
||||
// #Pangea
|
||||
if (pangeaMessageEvent
|
||||
?.showUseType ??
|
||||
false) ...[
|
||||
pangeaMessageEvent!
|
||||
.msgUseType
|
||||
.iconView(
|
||||
context,
|
||||
textColor
|
||||
.withAlpha(164),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
],
|
||||
if (event
|
||||
.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes
|
||||
.edit,
|
||||
)) ...[
|
||||
// Pangea#
|
||||
Icon(
|
||||
Icons.edit_outlined,
|
||||
color: textColor
|
||||
.withAlpha(164),
|
||||
fontSize: 12,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
' - ${displayEvent.originServerTs.localizedTimeShort(context)}',
|
||||
style: TextStyle(
|
||||
color: textColor
|
||||
.withAlpha(
|
||||
164,
|
||||
),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
Widget container;
|
||||
final showReceiptsRow =
|
||||
|
|
@ -544,7 +597,10 @@ class Message extends StatelessWidget {
|
|||
crossAxisAlignment:
|
||||
ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
if (displayTime || selected)
|
||||
// #Pangea
|
||||
// if (displayTime || selected)
|
||||
if ((displayTime || selected) && !isOverlay)
|
||||
// Pangea#
|
||||
Padding(
|
||||
padding: displayTime
|
||||
? const EdgeInsets.symmetric(vertical: 8.0)
|
||||
|
|
@ -595,7 +651,8 @@ class Message extends StatelessWidget {
|
|||
children: [
|
||||
if (pangeaMessageEvent?.showMessageButtons ?? false)
|
||||
MessageButtons(
|
||||
toolbarController: toolbarController,
|
||||
controller: controller,
|
||||
pangeaMessageEvent: pangeaMessageEvent!,
|
||||
),
|
||||
MessageReactions(event, timeline),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/video_player.dart';
|
||||
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_context_menu.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/pangea_rich_text.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
|
@ -37,8 +36,8 @@ class MessageContent extends StatelessWidget {
|
|||
//here rather than passing the choreographer? pangea rich text, a widget
|
||||
//further down in the chain is also using pangeaController so its not constant
|
||||
final bool immersionMode;
|
||||
final ToolbarDisplayController? toolbarController;
|
||||
final bool isOverlay;
|
||||
final ChatController controller;
|
||||
// Pangea#
|
||||
|
||||
const MessageContent(
|
||||
|
|
@ -50,8 +49,8 @@ class MessageContent extends StatelessWidget {
|
|||
required this.selected,
|
||||
this.pangeaMessageEvent,
|
||||
required this.immersionMode,
|
||||
required this.toolbarController,
|
||||
this.isOverlay = false,
|
||||
required this.controller,
|
||||
// Pangea#
|
||||
required this.borderRadius,
|
||||
});
|
||||
|
|
@ -306,45 +305,34 @@ class MessageContent extends StatelessWidget {
|
|||
style: messageTextStyle,
|
||||
pangeaMessageEvent: pangeaMessageEvent!,
|
||||
immersionMode: immersionMode,
|
||||
toolbarController: toolbarController,
|
||||
isOverlay: isOverlay,
|
||||
controller: controller,
|
||||
),
|
||||
);
|
||||
} else if (pangeaMessageEvent != null) {
|
||||
toolbarController?.toolbar?.textSelection.setMessageText(
|
||||
(event.getDisplayEvent(pangeaMessageEvent!.timeline).body),
|
||||
}
|
||||
|
||||
if (isOverlay) {
|
||||
controller.textSelection.setMessageText(
|
||||
event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
hideReply: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SelectableLinkify(
|
||||
onSelectionChanged: (selection, cause) {
|
||||
if (cause == SelectionChangedCause.longPress &&
|
||||
toolbarController != null &&
|
||||
pangeaMessageEvent != null &&
|
||||
!(toolbarController!.highlighted) &&
|
||||
!selected) {
|
||||
return;
|
||||
if (isOverlay) {
|
||||
controller.textSelection.onTextSelection(selection);
|
||||
}
|
||||
toolbarController?.toolbar?.textSelection
|
||||
.onTextSelection(selection);
|
||||
},
|
||||
onTap: () => toolbarController?.showToolbar(context),
|
||||
contextMenuBuilder: (context, state) =>
|
||||
(toolbarController?.highlighted ?? false)
|
||||
? const SizedBox.shrink()
|
||||
: MessageContextMenu.contextMenuOverride(
|
||||
context: context,
|
||||
textSelection: state,
|
||||
onDefine: () => toolbarController?.showToolbar(
|
||||
context,
|
||||
mode: MessageMode.definition,
|
||||
),
|
||||
onListen: () => toolbarController?.showToolbar(
|
||||
context,
|
||||
mode: MessageMode.textToSpeech,
|
||||
),
|
||||
),
|
||||
enableInteractiveSelection:
|
||||
toolbarController?.highlighted ?? false,
|
||||
onTap: () {
|
||||
if (pangeaMessageEvent != null && !isOverlay) {
|
||||
HapticFeedback.mediumImpact();
|
||||
controller.showToolbar(pangeaMessageEvent!);
|
||||
}
|
||||
},
|
||||
enableInteractiveSelection: isOverlay,
|
||||
// Pangea#
|
||||
text: event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
|
|
|
|||
|
|
@ -83,6 +83,9 @@ class PangeaAnyState {
|
|||
// String chatViewTargetKey(String? roomId) => "chatViewKey$roomId";
|
||||
// LayerLinkAndKey chatViewLinkAndKey(String? roomId) =>
|
||||
// layerLinkAndKey(chatViewTargetKey(roomId));
|
||||
|
||||
RenderBox? getRenderBox(String key) =>
|
||||
layerLinkAndKey(key).key.currentContext?.findRenderObject() as RenderBox?;
|
||||
}
|
||||
|
||||
class LayerLinkAndKey {
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MessageButtons extends StatelessWidget {
|
||||
final ToolbarDisplayController? toolbarController;
|
||||
final ChatController controller;
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
|
||||
const MessageButtons({
|
||||
super.key,
|
||||
this.toolbarController,
|
||||
required this.controller,
|
||||
required this.pangeaMessageEvent,
|
||||
});
|
||||
|
||||
void showActivity(BuildContext context) {
|
||||
toolbarController?.showToolbar(
|
||||
context,
|
||||
controller.showToolbar(
|
||||
pangeaMessageEvent,
|
||||
mode: MessageMode.practiceActivity,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (toolbarController == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Row(
|
||||
|
|
|
|||
|
|
@ -1,195 +1,220 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/message.dart';
|
||||
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_footer.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_header.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class MessageSelectionOverlay extends StatelessWidget {
|
||||
class MessageSelectionOverlay extends StatefulWidget {
|
||||
final ChatController controller;
|
||||
final ToolbarDisplayController toolbarController;
|
||||
final Function closeToolbar;
|
||||
final Widget toolbar;
|
||||
final Event event;
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final bool ownMessage;
|
||||
final bool immersionMode;
|
||||
final String targetId;
|
||||
final MessageMode? initialMode;
|
||||
final MessageTextSelection textSelection;
|
||||
|
||||
const MessageSelectionOverlay({
|
||||
required this.controller,
|
||||
required this.closeToolbar,
|
||||
required this.toolbar,
|
||||
required this.event,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.immersionMode,
|
||||
required this.ownMessage,
|
||||
required this.targetId,
|
||||
required this.toolbarController,
|
||||
required this.textSelection,
|
||||
this.initialMode,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final LayerLinkAndKey layerLinkAndKey =
|
||||
MatrixState.pAnyState.layerLinkAndKey(targetId);
|
||||
final targetRenderBox =
|
||||
layerLinkAndKey.key.currentContext?.findRenderObject();
|
||||
MessageSelectionOverlayState createState() => MessageSelectionOverlayState();
|
||||
}
|
||||
|
||||
double center = 290;
|
||||
double? left;
|
||||
double? right;
|
||||
bool showDown = false;
|
||||
final double footerSize = PlatformInfos.isMobile
|
||||
? PlatformInfos.isIOS
|
||||
? 128
|
||||
: 108
|
||||
: 143;
|
||||
final double headerSize = PlatformInfos.isMobile
|
||||
? PlatformInfos.isIOS
|
||||
? 121
|
||||
: 84
|
||||
: 77;
|
||||
final double stackSize =
|
||||
MediaQuery.of(context).size.height - footerSize - headerSize;
|
||||
class MessageSelectionOverlayState extends State<MessageSelectionOverlay> {
|
||||
double overlayBottomOffset = -1;
|
||||
double adjustedOverlayBottomOffset = -1;
|
||||
Size? messageSize;
|
||||
Offset? messageOffset;
|
||||
|
||||
final StreamController _completeAnimationStream =
|
||||
StreamController.broadcast();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
// position the overlay directly over the underlying message
|
||||
setOverlayBottomOffset();
|
||||
|
||||
// wait for the toolbar to animate to full height
|
||||
_completeAnimationStream.stream.first.then((_) {
|
||||
if (toolbarHeight == null ||
|
||||
messageSize == null ||
|
||||
messageOffset == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Once the toolbar has fully expanded, adjust
|
||||
// the overlay's position if there's an overflow
|
||||
final overlayTopOffset = messageOffset!.dy - toolbarHeight!;
|
||||
|
||||
final bool hasHeaderOverflow = overlayTopOffset < headerHeight;
|
||||
final bool hasFooterOverflow = overlayBottomOffset < footerHeight;
|
||||
|
||||
if (hasHeaderOverflow) {
|
||||
final overlayHeight = toolbarHeight! + messageSize!.height;
|
||||
adjustedOverlayBottomOffset = screenHeight -
|
||||
overlayHeight -
|
||||
footerHeight -
|
||||
MediaQuery.of(context).padding.bottom;
|
||||
} else if (hasFooterOverflow) {
|
||||
adjustedOverlayBottomOffset = footerHeight;
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_completeAnimationStream.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void setOverlayBottomOffset() {
|
||||
// Try to get the offset and size of the original message bubble.
|
||||
// If it fails, return an empty SizedBox. For instance, this can fail if
|
||||
// you change the screen size while the overlay is open.
|
||||
try {
|
||||
if (targetRenderBox != null) {
|
||||
final Size transformTargetSize = (targetRenderBox as RenderBox).size;
|
||||
final Offset targetOffset =
|
||||
(targetRenderBox).localToGlobal(Offset.zero);
|
||||
if (ownMessage) {
|
||||
right = MediaQuery.of(context).size.width -
|
||||
targetOffset.dx -
|
||||
transformTargetSize.width;
|
||||
} else {
|
||||
left =
|
||||
targetOffset.dx - (FluffyThemes.isColumnMode(context) ? 425 : 1);
|
||||
}
|
||||
|
||||
showDown = targetOffset.dy + transformTargetSize.height / 2 <=
|
||||
headerSize + stackSize / 2;
|
||||
|
||||
center = targetOffset.dy -
|
||||
headerSize +
|
||||
(showDown ? transformTargetSize.height + 3 : (-3));
|
||||
// If top of selected message extends below header
|
||||
if (targetOffset.dy <= headerSize) {
|
||||
center = transformTargetSize.height + 3;
|
||||
showDown = true;
|
||||
}
|
||||
// If bottom of selected message extends below footer
|
||||
else if (targetOffset.dy + transformTargetSize.height >=
|
||||
headerSize + stackSize) {
|
||||
center = stackSize - transformTargetSize.height - 3;
|
||||
}
|
||||
final double midpoint = headerSize + stackSize / 2;
|
||||
// If message is too long,
|
||||
// use default location to make full use of screen
|
||||
if (transformTargetSize.height >= stackSize / 2 - 30) {
|
||||
center = stackSize / 2 + (showDown ? -30 : 30);
|
||||
}
|
||||
// If message is not too long, but too close
|
||||
// to center of screen, scroll closer to edges
|
||||
else if (targetOffset.dy + transformTargetSize.height > midpoint - 30 &&
|
||||
targetOffset.dy < midpoint + 30) {
|
||||
final double scrollUp = midpoint + 30 - targetOffset.dy;
|
||||
final double scrollDown =
|
||||
targetOffset.dy + transformTargetSize.height - (midpoint - 30);
|
||||
final double minScroll =
|
||||
controller.scrollController.position.minScrollExtent;
|
||||
final double maxScroll =
|
||||
controller.scrollController.position.maxScrollExtent;
|
||||
final double currentOffset = controller.scrollController.offset;
|
||||
|
||||
// If can scroll up, scroll up
|
||||
if (currentOffset + scrollUp < maxScroll) {
|
||||
controller.scrollController.animateTo(
|
||||
currentOffset + scrollUp,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
);
|
||||
showDown = false;
|
||||
center = stackSize / 2 + 27;
|
||||
}
|
||||
|
||||
// Else if can scroll down, scroll down
|
||||
else if (currentOffset - scrollDown > minScroll) {
|
||||
controller.scrollController.animateTo(
|
||||
currentOffset - scrollDown,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
);
|
||||
showDown = true;
|
||||
center = stackSize / 2 - 27;
|
||||
}
|
||||
|
||||
// Neither scrolling works; leave message as-is,
|
||||
// and use centered toolbar location
|
||||
else {
|
||||
center = stackSize / 2 + (showDown ? -30 : 30);
|
||||
}
|
||||
}
|
||||
final messageRenderBox = MatrixState.pAnyState.getRenderBox(
|
||||
widget.event.eventId,
|
||||
);
|
||||
if (messageRenderBox != null && messageRenderBox.hasSize) {
|
||||
messageSize = messageRenderBox.size;
|
||||
messageOffset = messageRenderBox.localToGlobal(Offset.zero);
|
||||
final messageTopOffset = messageOffset!.dy;
|
||||
overlayBottomOffset =
|
||||
screenHeight - messageTopOffset - messageSize!.height;
|
||||
}
|
||||
} catch (err) {
|
||||
controller.showEmojiPicker = false;
|
||||
controller.selectedEvents.clear();
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
ErrorHandler.logError(e: err, s: StackTrace.current);
|
||||
// throw L10n.of(context)!.toolbarError;
|
||||
return const SizedBox();
|
||||
overlayBottomOffset = adjustedOverlayBottomOffset = -1;
|
||||
} finally {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
// height of the reply/forward bar + the reaction picker + contextual padding
|
||||
double get footerHeight =>
|
||||
48 + 56 + (FluffyThemes.isColumnMode(context) ? 16.0 : 8.0);
|
||||
|
||||
double get headerHeight =>
|
||||
(Theme.of(context).appBarTheme.toolbarHeight ?? 56) +
|
||||
MediaQuery.of(context).padding.top;
|
||||
|
||||
double get screenHeight => MediaQuery.of(context).size.height;
|
||||
|
||||
double? get toolbarHeight {
|
||||
try {
|
||||
final toolbarRenderBox = MatrixState.pAnyState.getRenderBox(
|
||||
'${widget.pangeaMessageEvent.eventId}-toolbar',
|
||||
);
|
||||
|
||||
return toolbarRenderBox?.size.height;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (overlayBottomOffset == -1) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final Widget overlayMessage = OverlayMessage(
|
||||
pangeaMessageEvent.event,
|
||||
timeline: pangeaMessageEvent.timeline,
|
||||
immersionMode: immersionMode,
|
||||
ownMessage: pangeaMessageEvent.ownMessage,
|
||||
toolbarController: toolbarController,
|
||||
width: 290,
|
||||
showDown: showDown,
|
||||
);
|
||||
|
||||
return Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment:
|
||||
ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
children: [
|
||||
OverlayHeader(
|
||||
controller: controller,
|
||||
closeToolbar: closeToolbar,
|
||||
),
|
||||
SizedBox(
|
||||
height: PlatformInfos.isAndroid ? 3 : 6,
|
||||
),
|
||||
Flexible(
|
||||
child: Stack(
|
||||
final overlayMessage = ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: FluffyThemes.columnWidth * 2.5,
|
||||
),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: widget.pangeaMessageEvent.ownMessage
|
||||
? MainAxisAlignment.end
|
||||
: MainAxisAlignment.start,
|
||||
children: [
|
||||
Positioned(
|
||||
left: left,
|
||||
right: right,
|
||||
bottom: stackSize - center + 3,
|
||||
child: showDown ? overlayMessage : toolbar,
|
||||
),
|
||||
Positioned(
|
||||
left: left,
|
||||
right: right,
|
||||
top: center + 3,
|
||||
child: showDown ? toolbar : overlayMessage,
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: widget.pangeaMessageEvent.ownMessage
|
||||
? 0
|
||||
: Avatar.defaultSize + 16,
|
||||
right: widget.pangeaMessageEvent.ownMessage ? 8 : 0,
|
||||
),
|
||||
child: MessageToolbar(
|
||||
pangeaMessageEvent: widget.pangeaMessageEvent,
|
||||
controller: widget.controller,
|
||||
textSelection: widget.textSelection,
|
||||
completeAnimationStream: _completeAnimationStream,
|
||||
initialMode: widget.initialMode,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Message(
|
||||
widget.event,
|
||||
onSwipe: () => {},
|
||||
onInfoTab: (_) => {},
|
||||
onAvatarTab: (_) => {},
|
||||
scrollToEventId: (_) => {},
|
||||
immersionMode: widget.controller.choreographer.immersionMode,
|
||||
controller: widget.controller,
|
||||
timeline: widget.controller.timeline!,
|
||||
isOverlay: true,
|
||||
animateIn: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
AnimatedPositioned(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: adjustedOverlayBottomOffset == -1
|
||||
? overlayBottomOffset
|
||||
: adjustedOverlayBottomOffset,
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: overlayMessage,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: PlatformInfos.isAndroid ? 3 : 6,
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
OverlayFooter(controller: widget.controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
Material(
|
||||
child: OverlayHeader(controller: widget.controller),
|
||||
),
|
||||
OverlayFooter(controller: controller),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,158 +1,35 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_speech_to_text_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class ToolbarDisplayController {
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final String targetId;
|
||||
final bool immersionMode;
|
||||
final ChatController controller;
|
||||
final FocusNode focusNode = FocusNode();
|
||||
Event? nextEvent;
|
||||
Event? previousEvent;
|
||||
|
||||
MessageToolbar? toolbar;
|
||||
String? overlayId;
|
||||
double? messageWidth;
|
||||
|
||||
final toolbarModeStream = StreamController<MessageMode>.broadcast();
|
||||
|
||||
ToolbarDisplayController({
|
||||
required this.pangeaMessageEvent,
|
||||
required this.targetId,
|
||||
required this.immersionMode,
|
||||
required this.controller,
|
||||
this.nextEvent,
|
||||
this.previousEvent,
|
||||
});
|
||||
|
||||
void closeToolbar() {
|
||||
controller.clearSelectedEvents();
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
}
|
||||
|
||||
void setToolbar() {
|
||||
toolbar ??= MessageToolbar(
|
||||
textSelection: MessageTextSelection(),
|
||||
room: pangeaMessageEvent.room,
|
||||
toolbarModeStream: toolbarModeStream,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
immersionMode: immersionMode,
|
||||
controller: controller,
|
||||
);
|
||||
}
|
||||
|
||||
void showToolbar(BuildContext context, {MessageMode? mode}) {
|
||||
// Close keyboard, if open
|
||||
if (controller.inputFocus.hasFocus) {
|
||||
controller.inputFocus.unfocus();
|
||||
return;
|
||||
}
|
||||
// Close emoji picker, if open
|
||||
controller.showEmojiPicker = false;
|
||||
if (highlighted) return;
|
||||
if (!MatrixState.pangeaController.languageController.languagesSet) {
|
||||
pLanguageDialog(context, () {});
|
||||
return;
|
||||
}
|
||||
focusNode.requestFocus();
|
||||
|
||||
// I'm not sure why I put this here, but it causes the toolbar
|
||||
// not to open immediately after clicking (user has to scroll or move their cursor)
|
||||
// so I'm commenting it out for now
|
||||
// WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
Widget? overlayEntry;
|
||||
if (toolbar == null) return;
|
||||
try {
|
||||
overlayEntry = MessageSelectionOverlay(
|
||||
controller: controller,
|
||||
closeToolbar: closeToolbar,
|
||||
toolbar: toolbar!,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
immersionMode: immersionMode,
|
||||
ownMessage: pangeaMessageEvent.ownMessage,
|
||||
targetId: targetId,
|
||||
toolbarController: this,
|
||||
);
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: StackTrace.current);
|
||||
return;
|
||||
}
|
||||
|
||||
OverlayUtil.showOverlay(
|
||||
context: context,
|
||||
child: overlayEntry,
|
||||
transformTargetId: targetId,
|
||||
targetAnchor: Alignment.center,
|
||||
followerAnchor: Alignment.center,
|
||||
backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(200),
|
||||
closePrevOverlay:
|
||||
MatrixState.pangeaController.subscriptionController.isSubscribed,
|
||||
position: OverlayEnum.centered,
|
||||
onDismiss: controller.clearSelectedEvents,
|
||||
);
|
||||
|
||||
controller.onSelectMessage(pangeaMessageEvent.event);
|
||||
|
||||
if (MatrixState.pAnyState.entries.isNotEmpty) {
|
||||
overlayId = MatrixState.pAnyState.entries.last.hashCode.toString();
|
||||
}
|
||||
|
||||
if (mode != null) {
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
() => toolbarModeStream.add(mode),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool get highlighted {
|
||||
if (overlayId == null) return false;
|
||||
if (MatrixState.pAnyState.entries.isEmpty) {
|
||||
overlayId = null;
|
||||
return false;
|
||||
}
|
||||
return MatrixState.pAnyState.entries.last.hashCode.toString() == overlayId;
|
||||
}
|
||||
}
|
||||
|
||||
class MessageToolbar extends StatefulWidget {
|
||||
final MessageTextSelection textSelection;
|
||||
final Room room;
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final StreamController<MessageMode> toolbarModeStream;
|
||||
final bool immersionMode;
|
||||
final ChatController controller;
|
||||
final MessageMode? initialMode;
|
||||
|
||||
final StreamController completeAnimationStream;
|
||||
|
||||
const MessageToolbar({
|
||||
super.key,
|
||||
required this.textSelection,
|
||||
required this.room,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.toolbarModeStream,
|
||||
required this.immersionMode,
|
||||
required this.controller,
|
||||
required this.completeAnimationStream,
|
||||
this.initialMode,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -164,7 +41,6 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
MessageMode? currentMode;
|
||||
bool updatingMode = false;
|
||||
late StreamSubscription<String?> selectionStream;
|
||||
late StreamSubscription<MessageMode> toolbarModeStream;
|
||||
|
||||
void updateMode(MessageMode newMode) {
|
||||
//Early exit from the function if the widget has been unmounted to prevent updates on an inactive widget.
|
||||
|
|
@ -203,7 +79,7 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
toolbarContent = MessageUnsubscribedCard(
|
||||
languageTool: newMode.title(context),
|
||||
mode: newMode,
|
||||
toolbarModeStream: widget.toolbarModeStream,
|
||||
controller: this,
|
||||
);
|
||||
} else {
|
||||
switch (currentMode) {
|
||||
|
|
@ -242,7 +118,7 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
debugPrint("show translation");
|
||||
toolbarContent = MessageTranslationCard(
|
||||
messageEvent: widget.pangeaMessageEvent,
|
||||
immersionMode: widget.immersionMode,
|
||||
immersionMode: widget.controller.choreographer.immersionMode,
|
||||
selection: widget.textSelection,
|
||||
);
|
||||
}
|
||||
|
|
@ -275,7 +151,7 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
fullText: widget.textSelection.messageText,
|
||||
fullTextLang: widget.pangeaMessageEvent.messageDisplayLangCode,
|
||||
hasInfo: true,
|
||||
room: widget.room,
|
||||
room: widget.controller.room,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -294,20 +170,20 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
super.initState();
|
||||
widget.textSelection.selectedText = null;
|
||||
|
||||
toolbarModeStream = widget.toolbarModeStream.stream.listen((mode) {
|
||||
updateMode(mode);
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
if (widget.pangeaMessageEvent.isAudioMessage) {
|
||||
updateMode(MessageMode.speechToText);
|
||||
return;
|
||||
}
|
||||
|
||||
MatrixState.pangeaController.userController.profile.userSettings
|
||||
.autoPlayMessages
|
||||
? updateMode(MessageMode.textToSpeech)
|
||||
: updateMode(MessageMode.translation);
|
||||
if (widget.initialMode != null) {
|
||||
updateMode(widget.initialMode!);
|
||||
} else {
|
||||
MatrixState.pangeaController.userController.profile.userSettings
|
||||
.autoPlayMessages
|
||||
? updateMode(MessageMode.textToSpeech)
|
||||
: updateMode(MessageMode.translation);
|
||||
}
|
||||
});
|
||||
|
||||
Timer? timer;
|
||||
|
|
@ -330,22 +206,37 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
@override
|
||||
void dispose() {
|
||||
selectionStream.cancel();
|
||||
toolbarModeStream.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double maxHeight = (MediaQuery.of(context).size.height -
|
||||
(PlatformInfos.isWeb
|
||||
? 217
|
||||
: PlatformInfos.isIOS
|
||||
? 262
|
||||
: 198)) /
|
||||
2 +
|
||||
30;
|
||||
final buttonRow = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: MessageMode.values
|
||||
.map(
|
||||
(mode) => mode.isValidMode(widget.pangeaMessageEvent.event)
|
||||
? Tooltip(
|
||||
message: mode.tooltip(context),
|
||||
child: IconButton(
|
||||
icon: Icon(mode.icon),
|
||||
color: mode.iconColor(
|
||||
widget.pangeaMessageEvent,
|
||||
currentMode,
|
||||
context,
|
||||
),
|
||||
onPressed: () => updateMode(mode),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
return Material(
|
||||
key: MatrixState.pAnyState
|
||||
.layerLinkAndKey('${widget.pangeaMessageEvent.eventId}-toolbar')
|
||||
.key,
|
||||
type: MaterialType.transparency,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
|
|
@ -359,63 +250,26 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
Radius.circular(25),
|
||||
),
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 290,
|
||||
minWidth: 290,
|
||||
maxHeight: maxHeight,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: 290,
|
||||
maxHeight: maxHeight - 72,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: toolbarContent ?? const SizedBox(),
|
||||
),
|
||||
SizedBox(height: toolbarContent == null ? 0 : 20),
|
||||
],
|
||||
if (toolbarContent != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 16),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 275,
|
||||
minWidth: 275,
|
||||
maxHeight: 250,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: toolbarContent,
|
||||
onEnd: () => widget.completeAnimationStream.add(null),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: MessageMode.values.map((mode) {
|
||||
if ([
|
||||
MessageMode.definition,
|
||||
MessageMode.textToSpeech,
|
||||
MessageMode.translation,
|
||||
].contains(mode) &&
|
||||
widget.pangeaMessageEvent.isAudioMessage) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (mode == MessageMode.speechToText &&
|
||||
!widget.pangeaMessageEvent.isAudioMessage) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Tooltip(
|
||||
message: mode.tooltip(context),
|
||||
child: IconButton(
|
||||
icon: Icon(mode.icon),
|
||||
color: mode.iconColor(
|
||||
widget.pangeaMessageEvent,
|
||||
currentMode,
|
||||
context,
|
||||
),
|
||||
onPressed: () => updateMode(mode),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
buttonRow,
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
|
@ -11,13 +10,13 @@ import '../../enum/message_mode_enum.dart';
|
|||
class MessageUnsubscribedCard extends StatelessWidget {
|
||||
final String languageTool;
|
||||
final MessageMode mode;
|
||||
final StreamController<MessageMode> toolbarModeStream;
|
||||
final MessageToolbarState controller;
|
||||
|
||||
const MessageUnsubscribedCard({
|
||||
super.key,
|
||||
required this.languageTool,
|
||||
required this.mode,
|
||||
required this.toolbarModeStream,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -29,7 +28,7 @@ class MessageUnsubscribedCard extends StatelessWidget {
|
|||
if (inTrialWindow) {
|
||||
MatrixState.pangeaController.subscriptionController
|
||||
.activateNewUserTrial();
|
||||
toolbarModeStream.add(mode);
|
||||
controller.updateMode(mode);
|
||||
} else {
|
||||
MatrixState.pangeaController.subscriptionController
|
||||
.showPaywall(context);
|
||||
|
|
@ -49,7 +48,7 @@ class MessageUnsubscribedCard extends StatelessWidget {
|
|||
child: TextButton(
|
||||
onPressed: onButtonPress,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all<Color>(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
(AppConfig.primaryColor).withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,24 +2,23 @@ import 'package:fluffychat/config/themes.dart';
|
|||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_input_row.dart';
|
||||
import 'package:fluffychat/pages/chat/reactions_picker.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OverlayFooter extends StatelessWidget {
|
||||
ChatController controller;
|
||||
final ChatController controller;
|
||||
|
||||
OverlayFooter({
|
||||
const OverlayFooter({
|
||||
required this.controller,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 18.0 : 10.0;
|
||||
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.only(
|
||||
bottom: PlatformInfos.isAndroid ? 0 : bottomSheetPadding,
|
||||
bottom: bottomSheetPadding,
|
||||
left: bottomSheetPadding,
|
||||
right: bottomSheetPadding,
|
||||
),
|
||||
|
|
@ -42,13 +41,6 @@ class OverlayFooter extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: FluffyThemes.isColumnMode(context)
|
||||
? 15.0
|
||||
: PlatformInfos.isAndroid
|
||||
? 0
|
||||
: 8.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,148 +1,92 @@
|
|||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_app_bar_title.dart';
|
||||
import 'package:fluffychat/pangea/utils/overlay.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class OverlayHeader extends StatelessWidget {
|
||||
ChatController controller;
|
||||
Function closeToolbar;
|
||||
final ChatController controller;
|
||||
|
||||
OverlayHeader({
|
||||
const OverlayHeader({
|
||||
required this.controller,
|
||||
required this.closeToolbar,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Event selectedEvent = controller.selectedEvents.single;
|
||||
|
||||
return AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
actionsIconTheme: IconThemeData(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => closeToolbar(),
|
||||
tooltip: L10n.of(context)!.close,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
titleSpacing: 0,
|
||||
title: ChatAppBarTitle(controller),
|
||||
actions: [
|
||||
if (controller.canEditSelectedEvents)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
tooltip: L10n.of(context)!.edit,
|
||||
onPressed: controller.editSelectedEventAction,
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AppBar(
|
||||
actionsIconTheme: IconThemeData(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
if (selectedEvent.messageType == MessageTypes.Text)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy_outlined),
|
||||
tooltip: L10n.of(context)!.copy,
|
||||
onPressed: controller.copyEventsAction,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
controller.clearSelectedEvents();
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
},
|
||||
tooltip: L10n.of(context)!.close,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
if (controller.canSaveSelectedEvent)
|
||||
// Use builder context to correctly position the share dialog on iPad
|
||||
Builder(
|
||||
builder: (context) => IconButton(
|
||||
icon: Icon(Icons.adaptive.share),
|
||||
tooltip: L10n.of(context)!.share,
|
||||
onPressed: () => controller.saveSelectedEvent(context),
|
||||
),
|
||||
),
|
||||
if (controller.canPinSelectedEvents)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.push_pin_outlined),
|
||||
onPressed: controller.pinEvent,
|
||||
tooltip: L10n.of(context)!.pinMessage,
|
||||
),
|
||||
if (controller.canRedactSelectedEvents)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outlined),
|
||||
tooltip: L10n.of(context)!.redactMessage,
|
||||
onPressed: controller.redactEventsAction,
|
||||
),
|
||||
IconButton(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
icon: Icon(
|
||||
Icons.more_horiz,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
onPressed: () => showPopup(context),
|
||||
titleSpacing: 0,
|
||||
title: ChatAppBarTitle(controller),
|
||||
actions: [
|
||||
if (controller.canEditSelectedEvents)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
tooltip: L10n.of(context)!.edit,
|
||||
onPressed: controller.editSelectedEventAction,
|
||||
),
|
||||
if (controller.selectedEvents.length == 1 &&
|
||||
controller.selectedEvents.single.messageType ==
|
||||
MessageTypes.Text)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy_outlined),
|
||||
tooltip: L10n.of(context)!.copy,
|
||||
onPressed: controller.copyEventsAction,
|
||||
),
|
||||
if (controller.canSaveSelectedEvent)
|
||||
// Use builder context to correctly position the share dialog on iPad
|
||||
Builder(
|
||||
builder: (context) => IconButton(
|
||||
icon: Icon(Icons.adaptive.share),
|
||||
tooltip: L10n.of(context)!.share,
|
||||
onPressed: () => controller.saveSelectedEvent(context),
|
||||
),
|
||||
),
|
||||
if (controller.canPinSelectedEvents)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.push_pin_outlined),
|
||||
onPressed: controller.pinEvent,
|
||||
tooltip: L10n.of(context)!.pinMessage,
|
||||
),
|
||||
if (controller.canRedactSelectedEvents)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outlined),
|
||||
tooltip: L10n.of(context)!.redactMessage,
|
||||
onPressed: controller.redactEventsAction,
|
||||
),
|
||||
if (controller.selectedEvents.length == 1)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.info_outlined),
|
||||
tooltip: L10n.of(context)!.messageInfo,
|
||||
onPressed: () {
|
||||
controller.showEventInfo();
|
||||
controller.clearSelectedEvents();
|
||||
},
|
||||
),
|
||||
if (controller.selectedEvents.length == 1)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.shield_outlined),
|
||||
tooltip: L10n.of(context)!.reportMessage,
|
||||
onPressed: controller.reportEventAction,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void showPopup(BuildContext context) {
|
||||
OverlayUtil.showOverlay(
|
||||
context: context,
|
||||
child: SelectionPopup(controller: controller),
|
||||
transformTargetId: "",
|
||||
targetAnchor: Alignment.center,
|
||||
followerAnchor: Alignment.center,
|
||||
closePrevOverlay: false,
|
||||
position: OverlayEnum.topRight,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectionPopup extends StatelessWidget {
|
||||
ChatController controller;
|
||||
|
||||
SelectionPopup({
|
||||
required this.controller,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
TextButton(
|
||||
onPressed: controller.showEventInfo,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.info_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.messageInfo),
|
||||
],
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: controller.reportEventAction,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.shield_outlined,
|
||||
color: Colors.red,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.reportMessage),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,188 +0,0 @@
|
|||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/events/message_content.dart';
|
||||
import 'package:fluffychat/pangea/enum/use_type.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../config/app_config.dart';
|
||||
|
||||
class OverlayMessage extends StatelessWidget {
|
||||
final Event event;
|
||||
final bool selected;
|
||||
final Timeline timeline;
|
||||
// final LanguageModel? selectedDisplayLang;
|
||||
final bool immersionMode;
|
||||
// final bool definitions;
|
||||
final bool ownMessage;
|
||||
final ToolbarDisplayController toolbarController;
|
||||
final double? width;
|
||||
final bool showDown;
|
||||
|
||||
const OverlayMessage(
|
||||
this.event, {
|
||||
this.selected = false,
|
||||
required this.timeline,
|
||||
required this.immersionMode,
|
||||
required this.ownMessage,
|
||||
required this.toolbarController,
|
||||
required this.showDown,
|
||||
this.width,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (event.type != EventTypes.Message ||
|
||||
event.messageType == EventTypes.KeyVerificationRequest) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
var color = Theme.of(context).colorScheme.surfaceContainer;
|
||||
final isLight = Theme.of(context).brightness == Brightness.light;
|
||||
var lightness = isLight ? .05 : .2;
|
||||
final textColor = ownMessage
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onSurface;
|
||||
|
||||
const hardCorner = Radius.circular(4);
|
||||
const roundedCorner = Radius.circular(AppConfig.borderRadius);
|
||||
final borderRadius = BorderRadius.only(
|
||||
topLeft: !showDown && !ownMessage ? hardCorner : roundedCorner,
|
||||
topRight: !showDown && ownMessage ? hardCorner : roundedCorner,
|
||||
bottomLeft: showDown && !ownMessage ? hardCorner : roundedCorner,
|
||||
bottomRight: showDown && ownMessage ? hardCorner : roundedCorner,
|
||||
);
|
||||
|
||||
final noBubble = {
|
||||
MessageTypes.Video,
|
||||
MessageTypes.Image,
|
||||
MessageTypes.Sticker,
|
||||
}.contains(event.messageType) &&
|
||||
!event.redacted;
|
||||
final noPadding = {
|
||||
MessageTypes.File,
|
||||
MessageTypes.Audio,
|
||||
}.contains(event.messageType);
|
||||
|
||||
if (ownMessage) {
|
||||
color = Theme.of(context).colorScheme.primary;
|
||||
lightness = isLight ? .15 : .85;
|
||||
}
|
||||
// Make overlay a little darker/lighter than the message
|
||||
color = Color.fromARGB(
|
||||
color.alpha,
|
||||
isLight || !ownMessage
|
||||
? (color.red + lightness * (255 - color.red)).round()
|
||||
: (color.red * lightness).round(),
|
||||
isLight || !ownMessage
|
||||
? (color.green + lightness * (255 - color.green)).round()
|
||||
: (color.green * lightness).round(),
|
||||
isLight || !ownMessage
|
||||
? (color.blue + lightness * (255 - color.blue)).round()
|
||||
: (color.blue * lightness).round(),
|
||||
);
|
||||
|
||||
final double maxHeight = (MediaQuery.of(context).size.height -
|
||||
(PlatformInfos.isWeb
|
||||
? 228
|
||||
: PlatformInfos.isIOS
|
||||
? 258
|
||||
: 198)) /
|
||||
2 -
|
||||
30;
|
||||
|
||||
final pangeaMessageEvent = PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: timeline,
|
||||
ownMessage: ownMessage,
|
||||
);
|
||||
|
||||
return Material(
|
||||
color: noBubble ? Colors.transparent : color,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
padding: noBubble || noPadding
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: width ?? FluffyThemes.columnWidth * 1.25,
|
||||
maxHeight: maxHeight,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: MessageContent(
|
||||
event.getDisplayEvent(timeline),
|
||||
textColor: textColor,
|
||||
borderRadius: borderRadius,
|
||||
selected: selected,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
immersionMode: immersionMode,
|
||||
toolbarController: toolbarController,
|
||||
isOverlay: true,
|
||||
),
|
||||
),
|
||||
if (event.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
) ||
|
||||
(pangeaMessageEvent.showUseType))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (pangeaMessageEvent.showUseType) ...[
|
||||
pangeaMessageEvent.msgUseType.iconView(
|
||||
context,
|
||||
textColor.withAlpha(164),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
if (event.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
)) ...[
|
||||
Icon(
|
||||
Icons.edit_outlined,
|
||||
color: textColor.withAlpha(164),
|
||||
size: 14,
|
||||
),
|
||||
Text(
|
||||
' - ${event.getDisplayEvent(timeline).originServerTs.localizedTimeShort(context)}',
|
||||
style: TextStyle(
|
||||
color: textColor.withAlpha(164),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,13 +7,15 @@ class ToolbarContentLoadingIndicator extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
height: 14,
|
||||
width: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,31 +2,31 @@ import 'dart:developer';
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_context_menu.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../enum/message_mode_enum.dart';
|
||||
import '../../models/pangea_match_model.dart';
|
||||
|
||||
class PangeaRichText extends StatefulWidget {
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final bool immersionMode;
|
||||
final ToolbarDisplayController? toolbarController;
|
||||
final TextStyle? style;
|
||||
final bool isOverlay;
|
||||
final ChatController controller;
|
||||
|
||||
const PangeaRichText({
|
||||
super.key,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.immersionMode,
|
||||
required this.toolbarController,
|
||||
required this.isOverlay,
|
||||
required this.controller,
|
||||
this.style,
|
||||
});
|
||||
|
||||
|
|
@ -59,12 +59,11 @@ class PangeaRichTextState extends State<PangeaRichText> {
|
|||
void _setTextSpan(String newTextSpan) {
|
||||
try {
|
||||
if (!mounted) return; // Early exit if the widget is no longer in the tree
|
||||
|
||||
widget.toolbarController?.toolbar?.textSelection.setMessageText(
|
||||
newTextSpan,
|
||||
);
|
||||
setState(() {
|
||||
textSpan = newTextSpan;
|
||||
if (widget.isOverlay) {
|
||||
widget.controller.textSelection.setMessageText(textSpan);
|
||||
}
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
ErrorHandler.logError(
|
||||
|
|
@ -137,35 +136,16 @@ class PangeaRichTextState extends State<PangeaRichText> {
|
|||
//TODO - take out of build function of every message
|
||||
final Widget richText = SelectableText.rich(
|
||||
onSelectionChanged: (selection, cause) {
|
||||
if (cause == SelectionChangedCause.longPress &&
|
||||
!(widget.toolbarController?.highlighted ?? false) &&
|
||||
!(widget.toolbarController?.controller.selectedEvents.any(
|
||||
(e) => e.eventId == widget.pangeaMessageEvent.eventId,
|
||||
) ??
|
||||
false)) {
|
||||
return;
|
||||
if (widget.isOverlay) {
|
||||
widget.controller.textSelection.onTextSelection(selection);
|
||||
}
|
||||
widget.toolbarController?.toolbar?.textSelection
|
||||
.onTextSelection(selection);
|
||||
},
|
||||
onTap: () => widget.toolbarController?.showToolbar(context),
|
||||
enableInteractiveSelection:
|
||||
widget.toolbarController?.highlighted ?? false,
|
||||
contextMenuBuilder: (context, state) =>
|
||||
widget.toolbarController?.highlighted ?? true
|
||||
? const SizedBox.shrink()
|
||||
: MessageContextMenu.contextMenuOverride(
|
||||
context: context,
|
||||
textSelection: state,
|
||||
onDefine: () => widget.toolbarController?.showToolbar(
|
||||
context,
|
||||
mode: MessageMode.definition,
|
||||
),
|
||||
onListen: () => widget.toolbarController?.showToolbar(
|
||||
context,
|
||||
mode: MessageMode.textToSpeech,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (!widget.isOverlay) {
|
||||
widget.controller.showToolbar(widget.pangeaMessageEvent);
|
||||
}
|
||||
},
|
||||
enableInteractiveSelection: widget.isOverlay,
|
||||
TextSpan(
|
||||
text: textSpan,
|
||||
style: widget.style,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue