diff --git a/assets/l10n/intl_de.arb b/assets/l10n/intl_de.arb index c2fc1e14..935cefec 100644 --- a/assets/l10n/intl_de.arb +++ b/assets/l10n/intl_de.arb @@ -2723,5 +2723,7 @@ "@bubbleSize": { "type": "text", "placeholders": {} - } + }, + "pinMessage": "An Raum anheften", + "pinnedEventsError": "Angeheftete Nachrichten nicht gefunden" } diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index a3c076ca..3d36c2e8 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2722,5 +2722,8 @@ "sender": {}, "reaction": {} } - } + }, + "pinMessage": "Pin to room", + "pinnedEventsError": "Error loading pinned messages", + "confirmEventUnpin": "Are you sure to permanently unpin the event?" } diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 2d2ba3c0..d41e8dc0 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -823,6 +823,30 @@ class ChatController extends State { } } + unpinEvent(String eventId) async { + final response = await showOkCancelAlertDialog( + context: context, + title: L10n.of(context)!.unpin, + message: L10n.of(context)!.confirmEventUnpin, + okLabel: L10n.of(context)!.unpin, + cancelLabel: L10n.of(context)!.cancel, + ); + if (response == OkCancelResult.ok) { + final events = room!.pinnedEventIds + ..removeWhere((oldEvent) => oldEvent == eventId); + room!.setPinnedEvents(events); + } + } + + void pinEvent() { + room!.setPinnedEvents( + { + ...room!.pinnedEventIds, + ...selectedEvents.map((e) => e.eventId), + }.toList(), + ); + } + void onInputBarChanged(String text) { if (text.endsWith(' ') && matrix!.hasComplexBundles) { final clients = currentRoomBundle; diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 74880e50..e26fe7e0 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -12,6 +12,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat_app_bar_title.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/pages/chat/seen_by_row.dart'; @@ -62,6 +63,11 @@ class ChatView extends StatelessWidget { tooltip: L10n.of(context)!.redactMessage, onPressed: controller.redactEventsAction, ), + IconButton( + icon: const Icon(Icons.push_pin), + onPressed: controller.pinEvent, + tooltip: L10n.of(context)!.pinMessage, + ), if (controller.selectedEvents.length == 1) PopupMenuButton<_EventContextAction>( onSelected: (action) { @@ -208,6 +214,7 @@ class ChatView extends StatelessWidget { child: Column( children: [ TombstoneDisplay(controller), + PinnedEvents(controller), Expanded( child: GestureDetector( onTap: controller.clearSingleSelectedEvent, diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 03cb6664..e14098ca 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -22,19 +22,19 @@ class Message extends StatelessWidget { final void Function(Event)? onInfoTab; final void Function(String)? scrollToEventId; final void Function(String) unfold; - final bool? longPressSelect; - final bool? selected; + final bool longPressSelect; + final bool selected; final Timeline timeline; const Message(this.event, {this.nextEvent, - this.longPressSelect, + this.longPressSelect = false, this.onSelect, this.onInfoTab, this.onAvatarTab, this.scrollToEventId, required this.unfold, - this.selected, + this.selected = false, required this.timeline, Key? key}) : super(key: key); @@ -152,11 +152,10 @@ class Message extends StatelessWidget { clipBehavior: Clip.antiAlias, child: InkWell( onHover: (b) => useMouse = true, - onTap: !useMouse && longPressSelect! + onTap: !useMouse && longPressSelect ? () {} : () => onSelect!(event), - onLongPress: - !longPressSelect! ? null : () => onSelect!(event), + onLongPress: !longPressSelect ? null : () => onSelect!(event), borderRadius: borderRadius, child: Container( decoration: BoxDecoration( @@ -301,7 +300,7 @@ class Message extends StatelessWidget { return Center( child: Container( - color: selected! + color: selected ? Theme.of(context).primaryColor.withAlpha(100) : Theme.of(context).primaryColor.withAlpha(0), constraints: diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 29de72fa..fcb911a5 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -22,10 +22,11 @@ import 'sticker.dart'; class MessageContent extends StatelessWidget { final Event event; - final Color? textColor; + final Color textColor; final void Function(Event)? onInfoTab; - const MessageContent(this.event, {this.onInfoTab, Key? key, this.textColor}) + const MessageContent(this.event, + {this.onInfoTab, Key? key, required this.textColor}) : super(key: key); void _verifyOrRequestKey(BuildContext context) async { @@ -83,17 +84,17 @@ class MessageContent extends StatelessWidget { if (PlatformInfos.isMobile) { return AudioPlayerWidget( event, - color: textColor!, + color: textColor, ); } - return MessageDownloadContent(event, textColor!); + return MessageDownloadContent(event, textColor); case MessageTypes.Video: if (PlatformInfos.isMobile || PlatformInfos.isWeb) { return EventVideoPlayer(event); } - return MessageDownloadContent(event, textColor!); + return MessageDownloadContent(event, textColor); case MessageTypes.File: - return MessageDownloadContent(event, textColor!); + return MessageDownloadContent(event, textColor); case MessageTypes.Text: case MessageTypes.Notice: @@ -115,7 +116,7 @@ class MessageContent extends StatelessWidget { fontSize: bigEmotes ? fontSize * 3 : fontSize, ), linkStyle: TextStyle( - color: textColor!.withAlpha(150), + color: textColor.withAlpha(150), fontSize: bigEmotes ? fontSize * 3 : fontSize, decoration: TextDecoration.underline, ), @@ -200,7 +201,7 @@ class MessageContent extends StatelessWidget { decoration: event.redacted ? TextDecoration.lineThrough : null, ), linkStyle: TextStyle( - color: textColor!.withAlpha(150), + color: textColor.withAlpha(150), fontSize: bigEmotes ? fontSize * 3 : fontSize, decoration: TextDecoration.underline, ), diff --git a/lib/pages/chat/pinned_events.dart b/lib/pages/chat/pinned_events.dart new file mode 100644 index 00000000..cf61646f --- /dev/null +++ b/lib/pages/chat/pinned_events.dart @@ -0,0 +1,87 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pages/chat/events/message_content.dart'; + +class PinnedEvents extends StatelessWidget { + final ChatController controller; + + const PinnedEvents(this.controller, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final pinnedEventIds = controller.room!.pinnedEventIds; + + if (pinnedEventIds.isEmpty) { + return Container(); + } + final completers = pinnedEventIds.map>((e) { + final completer = Completer(); + controller.room! + .getEventById(e) + .then((value) => completer.complete(value)); + return completer; + }); + return FutureBuilder>( + future: Future.wait(completers.map((e) => e.future).toList()), + builder: (context, snapshot) { + if (snapshot.hasData && + snapshot.data != null && + snapshot.data!.isNotEmpty && + snapshot.data!.first != null) { + return Material( + color: Theme.of(context).secondaryHeaderColor, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 96, + ), + child: ListView.builder( + shrinkWrap: true, + reverse: true, + itemBuilder: (c, i) { + final event = snapshot.data![i]!; + return ListTile( + tileColor: Colors.transparent, + onTap: () => controller.scrollToEventId(event.eventId), + leading: IconButton( + icon: const Icon(Icons.push_pin_outlined), + tooltip: L10n.of(context)!.unpin, + onPressed: () => controller.unpinEvent(event.eventId), + ), + title: MessageContent( + snapshot.data![i]!, + textColor: + Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black, + ), + ); + }, + itemCount: snapshot.data!.length, + ), + ), + ); + } else if (snapshot.hasError) { + Logs().e('Error loading pinned events.', snapshot.error); + return ListTile( + tileColor: Theme.of(context).secondaryHeaderColor, + title: Text(L10n.of(context)!.pinnedEventsError)); + } else { + return ListTile( + tileColor: Theme.of(context).secondaryHeaderColor, + title: const Center( + child: SizedBox.square( + dimension: 24, + child: CircularProgressIndicator(), + ), + ), + ); + } + }); + } +} diff --git a/pubspec.lock b/pubspec.lock index 38812fc6..c4a4e829 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -755,7 +755,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" latlong2: dependency: transitive description: @@ -818,13 +818,15 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" matrix: dependency: "direct main" description: - name: matrix - url: "https://pub.dartlang.org" - source: hosted + path: "." + ref: main + resolved-ref: "788f8ea2a1fc6c3e417e74750c7200504275700f" + url: "https://gitlab.com/famedly/company/frontend/famedlysdk.git" + source: git version: "0.8.7" matrix_api_lite: dependency: transitive @@ -965,7 +967,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" path_drawing: dependency: transitive description: @@ -1355,7 +1357,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" sqflite: dependency: transitive description: @@ -1418,21 +1420,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.19.5" + version: "1.20.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "0.4.9" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.11" timezone: dependency: transitive description: @@ -1572,7 +1574,7 @@ packages: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.0.6" url_launcher_windows: dependency: transitive description: @@ -1600,7 +1602,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" video_compress: dependency: "direct main" description: @@ -1756,5 +1758,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.15.1 <3.0.0" - flutter: ">=2.10.0" + dart: ">=2.16.0-100.0.dev <3.0.0" + flutter: ">=2.8.0" diff --git a/pubspec.yaml b/pubspec.yaml index ef38edcc..08d4087a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -118,4 +118,8 @@ dependency_overrides: hosted: name: geolocator_android url: https://hanntech-gmbh.gitlab.io/free2pass/flutter-geolocator-floss + matrix: + git: + url: https://gitlab.com/famedly/company/frontend/famedlysdk.git + ref: main provider: 5.0.0 diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index acf95a15..f4b94444 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -9,6 +9,9 @@ list(APPEND FLUTTER_PLUGIN_LIST url_launcher_windows ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -17,3 +20,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin)