From fb54548e5c53a7fcbcbee0a382f1d6b6d810f161 Mon Sep 17 00:00:00 2001 From: Krille Date: Wed, 8 May 2024 12:42:40 +0200 Subject: [PATCH 01/12] build: Update matrix dart sdk --- lib/pages/chat/chat.dart | 4 ++-- .../settings_multiple_emotes_view.dart | 6 +++--- lib/pages/user_bottom_sheet/user_bottom_sheet.dart | 5 +++-- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 1f0724ce6..8b4c2b5b8 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -763,7 +763,7 @@ class ChatController extends State ); } for (final event in selectedEvents) { - await event.remove(); + await event.cancelSend(); } setState(selectedEvents.clear); } catch (e, s) { @@ -813,7 +813,7 @@ class ChatController extends State ); } } else { - await event.remove(); + await event.cancelSend(); } }, ); diff --git a/lib/pages/settings_multiple_emotes/settings_multiple_emotes_view.dart b/lib/pages/settings_multiple_emotes/settings_multiple_emotes_view.dart index 8d62549c7..939263fd3 100644 --- a/lib/pages/settings_multiple_emotes/settings_multiple_emotes_view.dart +++ b/lib/pages/settings_multiple_emotes/settings_multiple_emotes_view.dart @@ -25,9 +25,9 @@ class MultipleEmotesSettingsView extends StatelessWidget { builder: (context, snapshot) { final packStateEvents = room.states['im.ponies.room_emotes']; // we need to manually convert the map using Map.of, otherwise assigning null will throw a type error. - final Map packs = packStateEvents != null - ? Map.of(packStateEvents) - : {}; + final packs = packStateEvents != null + ? Map.of(packStateEvents) + : {}; if (!packs.containsKey('')) { packs[''] = null; } diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet.dart index 98ad0e1c8..527915072 100644 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet.dart +++ b/lib/pages/user_bottom_sheet/user_bottom_sheet.dart @@ -127,11 +127,12 @@ class UserBottomSheetController extends State { textFields: [DialogTextField(hintText: L10n.of(context)!.reason)], ); if (reason == null || reason.single.isEmpty) return; + final result = await showFutureLoadingDialog( context: context, future: () => Matrix.of(widget.outerContext).client.reportContent( - user.roomId!, - user.eventId, + user.room.id, + user.id, reason: reason.single, score: score, ), diff --git a/pubspec.lock b/pubspec.lock index bb8cc24c7..e78306537 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1210,10 +1210,10 @@ packages: dependency: "direct main" description: name: matrix - sha256: "32c21a2ac2c221ce887b00a87f965bd8df1a3a4ba8794bbe86be8b56214051fb" + sha256: ff221de5ce4cc382147216026096700e47f5902983507decdb3b36087f558dcf url: "https://pub.dev" source: hosted - version: "0.28.1" + version: "0.29.0" meta: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4befc5006..41f14436f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,7 +64,7 @@ dependencies: keyboard_shortcuts: ^0.1.4 latlong2: ^0.9.1 linkify: ^5.0.0 - matrix: ^0.28.1 + matrix: ^0.29.0 native_imaging: ^0.1.0 package_info_plus: ^6.0.0 pasteboard: ^0.2.0 From 6a220ffb4bfc06b24c4851d5435e88b445bae0dd Mon Sep 17 00:00:00 2001 From: krille-chan Date: Thu, 9 May 2024 09:37:41 +0200 Subject: [PATCH 02/12] refactor: Precache theme and directchatmatrixid to improve performance in chat list item --- lib/pages/chat_list/chat_list_item.dart | 51 +++++++++++-------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 62a75de87..1f93b07e1 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -12,7 +12,6 @@ import 'package:fluffychat/widgets/hover_builder.dart'; import '../../config/themes.dart'; import '../../utils/date_time_extension.dart'; import '../../widgets/avatar.dart'; -import '../../widgets/matrix.dart'; enum ArchivedRoomAction { delete, rejoin } @@ -65,8 +64,11 @@ class ChatListItem extends StatelessWidget { final isMuted = room.pushRuleState != PushRuleState.notify; final typingText = room.getLocalizedTypingText(context); final lastEvent = room.lastEvent; - final ownMessage = lastEvent?.senderId == Matrix.of(context).client.userID; + final ownMessage = lastEvent?.senderId == room.client.userID; final unread = room.isUnread || room.membership == Membership.invite; + final theme = Theme.of(context); + final directChatMatrixId = room.directChatMatrixID; + final isDirectChat = directChatMatrixId != null; final unreadBubbleSize = unread || room.hasNewMessages ? room.notificationCount > 0 ? 20.0 @@ -74,9 +76,9 @@ class ChatListItem extends StatelessWidget { : 0.0; final hasNotifications = room.notificationCount > 0; final backgroundColor = selected - ? Theme.of(context).colorScheme.primaryContainer + ? theme.colorScheme.primaryContainer : activeChat - ? Theme.of(context).colorScheme.secondaryContainer + ? theme.colorScheme.secondaryContainer : null; final displayname = room.getLocalizedDisplayname( MatrixLocals(L10n.of(context)!), @@ -108,7 +110,7 @@ class ChatListItem extends StatelessWidget { child: Avatar( mxContent: room.avatar, name: displayname, - presenceUserId: room.directChatMatrixID, + presenceUserId: directChatMatrixId, presenceBackgroundColor: backgroundColor, onTap: onLongPress, ), @@ -164,7 +166,7 @@ class ChatListItem extends StatelessWidget { child: Icon( Icons.push_pin, size: 16, - color: Theme.of(context).colorScheme.primary, + color: theme.colorScheme.primary, ), ), if (lastEvent != null && room.membership != Membership.invite) @@ -175,8 +177,8 @@ class ChatListItem extends StatelessWidget { style: TextStyle( fontSize: 13, color: unread - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).textTheme.bodyMedium!.color, + ? theme.colorScheme.secondary + : theme.textTheme.bodyMedium!.color, ), ), ), @@ -204,7 +206,7 @@ class ChatListItem extends StatelessWidget { padding: const EdgeInsets.only(right: 4), child: Icon( Icons.edit_outlined, - color: Theme.of(context).colorScheme.secondary, + color: theme.colorScheme.secondary, size: 14, ), ), @@ -213,7 +215,7 @@ class ChatListItem extends StatelessWidget { ? Text( typingText, style: TextStyle( - color: Theme.of(context).colorScheme.primary, + color: theme.colorScheme.primary, ), maxLines: 1, softWrap: false, @@ -225,15 +227,15 @@ class ChatListItem extends StatelessWidget { hideEdit: true, plaintextBody: true, removeMarkdown: true, - withSenderNamePrefix: !room.isDirectChat || - room.directChatMatrixID != + withSenderNamePrefix: !isDirectChat || + directChatMatrixId != room.lastEvent?.senderId, ) ?? Future.value(L10n.of(context)!.emptyChat), builder: (context, snapshot) { return Text( room.membership == Membership.invite - ? room.isDirectChat + ? isDirectChat ? L10n.of(context)!.invitePrivateChat : L10n.of(context)!.inviteGroupChat : snapshot.data ?? @@ -244,10 +246,9 @@ class ChatListItem extends StatelessWidget { hideEdit: true, plaintextBody: true, removeMarkdown: true, - withSenderNamePrefix: - !room.isDirectChat || - room.directChatMatrixID != - room.lastEvent?.senderId, + withSenderNamePrefix: !isDirectChat || + directChatMatrixId != + room.lastEvent?.senderId, ) ?? L10n.of(context)!.emptyChat, softWrap: false, @@ -257,9 +258,7 @@ class ChatListItem extends StatelessWidget { fontWeight: unread || room.hasNewMessages ? FontWeight.bold : null, - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, + color: theme.colorScheme.onSurfaceVariant, decoration: room.lastEvent?.redacted == true ? TextDecoration.lineThrough : null, @@ -284,8 +283,8 @@ class ChatListItem extends StatelessWidget { room.membership == Membership.invite ? Colors.red : hasNotifications || room.markedUnread - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.primaryContainer, + ? theme.colorScheme.primary + : theme.colorScheme.primaryContainer, borderRadius: BorderRadius.circular(AppConfig.borderRadius), ), @@ -297,12 +296,8 @@ class ChatListItem extends StatelessWidget { color: room.highlightCount > 0 ? Colors.white : hasNotifications - ? Theme.of(context) - .colorScheme - .onPrimary - : Theme.of(context) - .colorScheme - .onPrimaryContainer, + ? theme.colorScheme.onPrimary + : theme.colorScheme.onPrimaryContainer, fontSize: 13, ), ) From 89cb6734b286c84ae2fea3a1606ca02391be7f1f Mon Sep 17 00:00:00 2001 From: Milo Ivir Date: Wed, 8 May 2024 13:26:25 +0000 Subject: [PATCH 03/12] Translated using Weblate (Croatian) Currently translated at 88.3% (553 of 626 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/hr/ --- assets/l10n/intl_hr.arb | 43 +++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/assets/l10n/intl_hr.arb b/assets/l10n/intl_hr.arb index 829d19729..dcf927e7e 100644 --- a/assets/l10n/intl_hr.arb +++ b/assets/l10n/intl_hr.arb @@ -1058,10 +1058,6 @@ "type": "text", "placeholders": {} }, - "@noGoogleServicesWarning": { - "type": "text", - "placeholders": {} - }, "none": "Ništa", "@none": { "type": "text", @@ -2294,7 +2290,7 @@ "type": "text", "placeholders": {} }, - "reportErrorDescription": "Dogodila se greška. Pokušaj ponovo kasnije. Ako želiš, grešku možeš prijaviti programerima.", + "reportErrorDescription": "😭 Joj! Dogodila se greška. Pokušaj ponovo kasnije. Ako želiš, grešku možeš prijaviti programerima.", "@reportErrorDescription": {}, "signInWithPassword": "Prijavi se s lozinkom", "@signInWithPassword": {}, @@ -2392,7 +2388,7 @@ "seconds": {} } }, - "hasKnocked": "{user} je pokucao/la", + "hasKnocked": "🚪 {user} je pokucao/la", "@hasKnocked": { "placeholders": { "user": {} @@ -2400,17 +2396,10 @@ }, "pleaseEnterANumber": "Upiši broj veći od 0", "@pleaseEnterANumber": {}, - "@banUserDescription": {}, - "@removeDevicesDescription": {}, - "@unbanUserDescription": {}, "pushNotificationsNotAvailable": "Automatsko slanje obavijesti nije dostupno", "@pushNotificationsNotAvailable": {}, - "@makeAdminDescription": {}, - "@archiveRoomDescription": {}, "learnMore": "Saznaj više", "@learnMore": {}, - "@roomUpgradeDescription": {}, - "@kickUserDescription": {}, "createGroupAndInviteUsers": "Stvori grupu i pozovi korisnike", "@createGroupAndInviteUsers": {}, "startConversation": "Pokreni konverzaciju", @@ -2419,7 +2408,7 @@ "@blockedUsers": {}, "groupCanBeFoundViaSearch": "Grupa se može pronaći putem pretrage", "@groupCanBeFoundViaSearch": {}, - "block": "blokiraj", + "block": "Blokiraj", "@block": {}, "yourGlobalUserIdIs": "Tvoj globalni korisnički ID je: ", "@yourGlobalUserIdIs": {}, @@ -2436,5 +2425,29 @@ "searchChatsRooms": "Traži #chats, @users …", "@searchChatsRooms": {}, "databaseMigrationBody": "Pričekaj. Ovo može potrajati.", - "@databaseMigrationBody": {} + "@databaseMigrationBody": {}, + "transparent": "Prozirno", + "@transparent": {}, + "formattedMessages": "Formatirane poruke", + "@formattedMessages": {}, + "incomingMessages": "Dolazne poruke", + "@incomingMessages": {}, + "passwordsDoNotMatch": "Lozinke se ne poklapaju", + "@passwordsDoNotMatch": {}, + "accessAndVisibility": "Pristup i vidljivost", + "@accessAndVisibility": {}, + "calls": "Pozivi", + "@calls": {}, + "customEmojisAndStickers": "Prilagođeni emojiji i naljepnice", + "@customEmojisAndStickers": {}, + "customEmojisAndStickersBody": "Dodaj ili dijeli prilagođene emojije ili naljepnice koje se mogu koristiti u bilo kojem razgovoru.", + "@customEmojisAndStickersBody": {}, + "accessAndVisibilityDescription": "Tko se smije pridružiti ovom razgovoru i kako se razgovor može otkriti.", + "@accessAndVisibilityDescription": {}, + "stickers": "Naljepnice", + "@stickers": {}, + "discover": "Otkrij", + "@discover": {}, + "formattedMessagesDescription": "Prikaži formatirani sadržaj poruke poput podebljanog teksta koristeći markdown.", + "@formattedMessagesDescription": {} } From d4fb0ce1a721651d0f3059fa4848a48582756237 Mon Sep 17 00:00:00 2001 From: Krille Date: Fri, 10 May 2024 08:34:46 +0200 Subject: [PATCH 04/12] chore: Follow up audioplayer --- lib/pages/chat/events/audio_player.dart | 34 ++++++++++++++----------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 77eb26832..35b71b077 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -245,8 +245,9 @@ class AudioPlayerState extends State { child: Stack( children: [ if (waveform != null) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), + Container( + height: 40, + padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Row( children: [ for (var i = 0; i < waveform.length; i++) @@ -257,28 +258,31 @@ class AudioPlayerState extends State { color: widget.color.withAlpha(64), borderRadius: BorderRadius.circular(2), ), - height: 32 * (waveform[i] / 1024), + height: 56 * (waveform[i] / 1024), ), ), ), ], ), ), - SizedBox( - height: 28, - child: Slider.adaptive( - value: currentPosition, - min: 0, - max: maxPosition, - onChangeStart: (_) => audioPlayer?.pause(), - onChangeEnd: (_) => audioPlayer?.play(), - onChanged: (pos) => audioPlayer?.seek( - Duration( - milliseconds: pos.round(), + if (audioPlayer != null) + SizedBox( + height: 40, + child: Slider( + activeColor: widget.color, + thumbColor: widget.color, + value: currentPosition, + min: 0, + max: maxPosition, + onChangeStart: (_) => audioPlayer.pause(), + onChangeEnd: (_) => audioPlayer.play(), + onChanged: (pos) => audioPlayer.seek( + Duration( + milliseconds: pos.round(), + ), ), ), ), - ), ], ), ), From ce8513d2b50ea7ec2657619a301813a0196d29be Mon Sep 17 00:00:00 2001 From: Krille Date: Fri, 10 May 2024 08:35:10 +0200 Subject: [PATCH 05/12] build: Update matrix dart sdk --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index e78306537..d3c9cad78 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1210,10 +1210,10 @@ packages: dependency: "direct main" description: name: matrix - sha256: ff221de5ce4cc382147216026096700e47f5902983507decdb3b36087f558dcf + sha256: b9aa3c1bdb1ca16c2365bb3681f861eeeb86acd0ea2df9c9ba453fdbcb564076 url: "https://pub.dev" source: hosted - version: "0.29.0" + version: "0.29.1" meta: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 41f14436f..9f23b88e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,7 +64,7 @@ dependencies: keyboard_shortcuts: ^0.1.4 latlong2: ^0.9.1 linkify: ^5.0.0 - matrix: ^0.29.0 + matrix: ^0.29.1 native_imaging: ^0.1.0 package_info_plus: ^6.0.0 pasteboard: ^0.2.0 From 4dee111c73981cf2afdb21e0f790d7f00737d6bb Mon Sep 17 00:00:00 2001 From: Krille Date: Fri, 10 May 2024 09:52:55 +0200 Subject: [PATCH 06/12] chore: Revert audioplayer changes --- lib/pages/chat/events/audio_player.dart | 157 ++++++++++-------------- 1 file changed, 68 insertions(+), 89 deletions(-) diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 35b71b077..ce0285aed 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -38,8 +38,8 @@ class AudioPlayerState extends State { StreamSubscription? onPlayerError; String? statusText; - double currentPosition = 0.0; - double maxPosition = 1.0; + int currentPosition = 0; + double maxPosition = 0; MatrixFile? matrixFile; File? audioFile; @@ -113,7 +113,9 @@ class AudioPlayerState extends State { setState(() { statusText = '${state.inMinutes.toString().padLeft(2, '0')}:${(state.inSeconds % 60).toString().padLeft(2, '0')}'; - currentPosition = state.inMilliseconds.toDouble(); + currentPosition = ((state.inMilliseconds.toDouble() / maxPosition) * + AudioPlayerWidget.wavesCount) + .round(); }); if (state.inMilliseconds.toDouble() == maxPosition) { audioPlayer.stop(); @@ -149,14 +151,12 @@ class AudioPlayerState extends State { return '${duration.inMinutes.toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}'; } - List? _getWaveform() { + List _getWaveform() { final eventWaveForm = widget.event.content .tryGetMap('org.matrix.msc1767.audio') ?.tryGetList('waveform'); - if (eventWaveForm == null || - eventWaveForm.isEmpty || - eventWaveForm.length > 100) { - return null; + if (eventWaveForm == null || eventWaveForm.isEmpty) { + return List.filled(AudioPlayerWidget.wavesCount, 500); } while (eventWaveForm.length < AudioPlayerWidget.wavesCount) { for (var i = 0; i < eventWaveForm.length; i = i + 2) { @@ -172,7 +172,7 @@ class AudioPlayerState extends State { return eventWaveForm.map((i) => i > 1024 ? 1024 : i).toList(); } - late final List? waveform; + late final List waveform; void _toggleSpeed() async { final audioPlayer = this.audioPlayer; @@ -208,9 +208,8 @@ class AudioPlayerState extends State { Widget build(BuildContext context) { final statusText = this.statusText ??= _durationString ?? '00:00'; final audioPlayer = this.audioPlayer; - final waveform = this.waveform; return Padding( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(12.0), child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -241,90 +240,70 @@ class AudioPlayerState extends State { }, ), ), - Expanded( - child: Stack( - children: [ - if (waveform != null) - Container( - height: 40, - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Row( - children: [ - for (var i = 0; i < waveform.length; i++) - Expanded( - child: Center( - child: Container( - decoration: BoxDecoration( - color: widget.color.withAlpha(64), - borderRadius: BorderRadius.circular(2), - ), - height: 56 * (waveform[i] / 1024), - ), - ), - ), - ], - ), - ), - if (audioPlayer != null) - SizedBox( - height: 40, - child: Slider( - activeColor: widget.color, - thumbColor: widget.color, - value: currentPosition, - min: 0, - max: maxPosition, - onChangeStart: (_) => audioPlayer.pause(), - onChangeEnd: (_) => audioPlayer.play(), - onChanged: (pos) => audioPlayer.seek( - Duration( - milliseconds: pos.round(), - ), - ), - ), - ), - ], - ), - ), - Container( - alignment: Alignment.centerRight, - width: 42, - child: Text( - statusText, - style: TextStyle( - color: widget.color, - ), - ), - ), - const SizedBox(width: 4), - Stack( + const SizedBox(width: 8), + Row( + mainAxisSize: MainAxisSize.min, children: [ - SizedBox( - width: buttonSize, - height: buttonSize, - child: InkWell( - splashColor: widget.color.withAlpha(128), - borderRadius: BorderRadius.circular(64), - onTap: audioPlayer == null ? null : _toggleSpeed, - child: Icon(Icons.mic_none_outlined, color: widget.color), - ), - ), - if (audioPlayer != null) - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Text( - '${audioPlayer.speed.toString()}x', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 9.0, - color: widget.color, + for (var i = 0; i < AudioPlayerWidget.wavesCount; i++) + GestureDetector( + onTapDown: (_) => audioPlayer?.seek( + Duration( + milliseconds: + (maxPosition / AudioPlayerWidget.wavesCount).round() * + i, + ), + ), + child: Container( + height: 32, + color: widget.color.withAlpha(0), + alignment: Alignment.center, + child: Opacity( + opacity: currentPosition > i ? 1 : 0.5, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 1), + decoration: BoxDecoration( + color: widget.color, + borderRadius: BorderRadius.circular(2), + ), + width: 2, + height: 32 * (waveform[i] / 1024), + ), ), ), ), ], ), + const SizedBox(width: 8), + SizedBox( + width: 36, + child: Text( + statusText, + style: TextStyle( + color: widget.color, + fontSize: 12, + ), + ), + ), + const SizedBox(width: 8), + Badge( + label: audioPlayer == null + ? null + : Text( + '${audioPlayer.speed.toString()}x', + ), + backgroundColor: Theme.of(context).colorScheme.secondary, + textColor: Theme.of(context).colorScheme.onSecondary, + child: InkWell( + splashColor: widget.color.withAlpha(128), + borderRadius: BorderRadius.circular(64), + onTap: audioPlayer == null ? null : _toggleSpeed, + child: Icon( + Icons.mic_none_outlined, + color: widget.color, + ), + ), + ), + const SizedBox(width: 8), ], ), ); From 53c26019452bb1190fbfddafff7937e783e39916 Mon Sep 17 00:00:00 2001 From: Krille Date: Fri, 10 May 2024 10:03:18 +0200 Subject: [PATCH 07/12] chore: Follow up audioplayer --- lib/pages/chat/events/audio_player.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index ce0285aed..40d4cf29f 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -286,6 +286,7 @@ class AudioPlayerState extends State { ), const SizedBox(width: 8), Badge( + isLabelVisible: audioPlayer != null, label: audioPlayer == null ? null : Text( From d9ab6ad8b303619a4971277e9c19ffc8fae7e0b5 Mon Sep 17 00:00:00 2001 From: Krille Date: Fri, 10 May 2024 12:49:32 +0200 Subject: [PATCH 08/12] refactor: Move back to cached network image for better avatar performance --- ios/Runner.xcodeproj/project.pbxproj | 18 ++ lib/pages/chat/events/image_bubble.dart | 123 ++++++++--- lib/pages/image_viewer/image_viewer_view.dart | 17 +- lib/widgets/mxc_image.dart | 191 ++++-------------- pubspec.lock | 32 +++ pubspec.yaml | 1 + 6 files changed, 189 insertions(+), 193 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index a6ce66d84..60bc014af 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -197,6 +197,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, F9C8EE392B9AB471149C306E /* [CP] Embed Pods Frameworks */, + F2B5CFB67942A2498CBB8D31 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -341,6 +342,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + F2B5CFB67942A2498CBB8D31 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; F9C8EE392B9AB471149C306E /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/lib/pages/chat/events/image_bubble.dart b/lib/pages/chat/events/image_bubble.dart index f5219b054..b12d1662c 100644 --- a/lib/pages/chat/events/image_bubble.dart +++ b/lib/pages/chat/events/image_bubble.dart @@ -1,13 +1,16 @@ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/image_viewer/image_viewer.dart'; -import 'package:fluffychat/widgets/mxc_image.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; import '../../../widgets/blur_hash.dart'; -class ImageBubble extends StatelessWidget { +class ImageBubble extends StatefulWidget { final Event event; final bool tapToView; final BoxFit fit; @@ -15,11 +18,11 @@ class ImageBubble extends StatelessWidget { final Color? backgroundColor; final bool thumbnailOnly; final bool animated; - final double width; - final double height; + final double? width; + final double? height; final void Function()? onTap; final BorderRadius? borderRadius; - + final Duration retryDuration; const ImageBubble( this.event, { this.tapToView = true, @@ -32,50 +35,102 @@ class ImageBubble extends StatelessWidget { this.animated = false, this.onTap, this.borderRadius, + this.retryDuration = const Duration(seconds: 2), super.key, }); + @override + State createState() => _ImageBubbleState(); +} + +class _ImageBubbleState extends State { + Uint8List? _imageData; + + Future _load() async { + final data = await widget.event.downloadAndDecryptAttachment( + getThumbnail: widget.thumbnailOnly, + ); + if (data.detectFileType is MatrixImageFile) { + if (!mounted) return; + setState(() { + _imageData = data.bytes; + }); + return; + } + } + + void _tryLoad([_]) async { + if (_imageData != null) { + return; + } + try { + await _load(); + } catch (_) { + if (!mounted) return; + await Future.delayed(widget.retryDuration); + _tryLoad(); + } + } + + @override + void initState() { + super.initState(); + _tryLoad(); + } + Widget _buildPlaceholder(BuildContext context) { + final width = widget.width; + final height = widget.height; + if (width == null || height == null) { + return const Center( + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ); + } final String blurHashString = - event.infoMap['xyz.amorgan.blurhash'] is String - ? event.infoMap['xyz.amorgan.blurhash'] + widget.event.infoMap['xyz.amorgan.blurhash'] is String + ? widget.event.infoMap['xyz.amorgan.blurhash'] : 'LEHV6nWB2yk8pyo0adR*.7kCMdnj'; + return SizedBox( - width: width, - height: height, + width: widget.width, + height: widget.height, child: BlurHash( blurhash: blurHashString, width: width, height: height, - fit: fit, + fit: widget.fit, ), ); } void _onTap(BuildContext context) { - if (onTap != null) { - onTap!(); + if (widget.onTap != null) { + widget.onTap!(); return; } - if (!tapToView) return; + if (!widget.tapToView) return; showDialog( context: context, useRootNavigator: false, - builder: (_) => ImageViewer(event), + builder: (_) => ImageViewer(widget.event), ); } @override Widget build(BuildContext context) { final borderRadius = - this.borderRadius ?? BorderRadius.circular(AppConfig.borderRadius); + widget.borderRadius ?? BorderRadius.circular(AppConfig.borderRadius); + final data = _imageData; + final hasData = data != null; return Material( color: Colors.transparent, clipBehavior: Clip.hardEdge, shape: RoundedRectangleBorder( borderRadius: borderRadius, side: BorderSide( - color: event.messageType == MessageTypes.Sticker + color: widget.event.messageType == MessageTypes.Sticker ? Colors.transparent : Theme.of(context).dividerColor, ), @@ -84,17 +139,31 @@ class ImageBubble extends StatelessWidget { onTap: () => _onTap(context), borderRadius: borderRadius, child: Hero( - tag: event.eventId, - child: MxcImage( - event: event, - width: width, - height: height, - fit: fit, - animated: animated, - isThumbnail: thumbnailOnly, - placeholder: event.messageType == MessageTypes.Sticker - ? null - : _buildPlaceholder, + tag: widget.event.eventId, + child: AnimatedCrossFade( + crossFadeState: + hasData ? CrossFadeState.showSecond : CrossFadeState.showFirst, + duration: FluffyThemes.animationDuration, + firstChild: _buildPlaceholder(context), + secondChild: hasData + ? Image.memory( + data, + width: widget.width, + height: widget.height, + fit: widget.fit, + filterQuality: widget.thumbnailOnly + ? FilterQuality.low + : FilterQuality.medium, + errorBuilder: (context, __, ___) { + _imageData = null; + WidgetsBinding.instance.addPostFrameCallback(_tryLoad); + return _buildPlaceholder(context); + }, + ) + : SizedBox( + width: widget.width, + height: widget.height, + ), ), ), ), diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index e4352864f..2f4abe440 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:fluffychat/pages/chat/events/image_bubble.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/mxc_image.dart'; import 'image_viewer.dart'; class ImageViewerView extends StatelessWidget { @@ -55,14 +55,13 @@ class ImageViewerView extends StatelessWidget { maxScale: 10.0, onInteractionEnd: controller.onInteractionEnds, child: Center( - child: Hero( - tag: controller.widget.event.eventId, - child: MxcImage( - event: controller.widget.event, - fit: BoxFit.contain, - isThumbnail: false, - animated: true, - ), + child: ImageBubble( + controller.widget.event, + fit: BoxFit.contain, + animated: true, + thumbnailOnly: false, + width: null, + height: null, ), ), ), diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index 9290156bf..a3ae695eb 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -1,192 +1,69 @@ -import 'dart:typed_data'; - import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; -class MxcImage extends StatefulWidget { +class MxcImage extends StatelessWidget { final Uri? uri; - final Event? event; final double? width; final double? height; final BoxFit? fit; final bool isThumbnail; final bool animated; - final Duration retryDuration; - final Duration animationDuration; - final Curve animationCurve; final ThumbnailMethod thumbnailMethod; final Widget Function(BuildContext context)? placeholder; final String? cacheKey; const MxcImage({ this.uri, - this.event, this.width, this.height, this.fit, this.placeholder, this.isThumbnail = true, this.animated = false, - this.animationDuration = FluffyThemes.animationDuration, - this.retryDuration = const Duration(seconds: 2), - this.animationCurve = FluffyThemes.animationCurve, this.thumbnailMethod = ThumbnailMethod.scale, this.cacheKey, super.key, }); - @override - State createState() => _MxcImageState(); -} - -class _MxcImageState extends State { - static final Map _imageDataCache = {}; - Uint8List? _imageDataNoCache; - Uint8List? get _imageData { - final cacheKey = widget.cacheKey; - return cacheKey == null ? _imageDataNoCache : _imageDataCache[cacheKey]; - } - - set _imageData(Uint8List? data) { - if (data == null) return; - final cacheKey = widget.cacheKey; - cacheKey == null - ? _imageDataNoCache = data - : _imageDataCache[cacheKey] = data; - } - - bool? _isCached; - - Future _load() async { - final client = Matrix.of(context).client; - final uri = widget.uri; - final event = widget.event; - - if (uri != null) { - final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; - final width = widget.width; - final realWidth = width == null ? null : width * devicePixelRatio; - final height = widget.height; - final realHeight = height == null ? null : height * devicePixelRatio; - - final httpUri = widget.isThumbnail - ? uri.getThumbnail( - client, - width: realWidth, - height: realHeight, - animated: widget.animated, - method: widget.thumbnailMethod, - ) - : uri.getDownloadLink(client); - - final storeKey = widget.isThumbnail ? httpUri : uri; - - if (_isCached == null) { - final cachedData = await client.database?.getFile(storeKey); - if (cachedData != null) { - if (!mounted) return; - setState(() { - _imageData = cachedData; - _isCached = true; - }); - return; - } - _isCached = false; - } - - final response = await http.get(httpUri); - if (response.statusCode != 200) { - if (response.statusCode == 404) { - return; - } - throw Exception(); - } - final remoteData = response.bodyBytes; - - if (!mounted) return; - setState(() { - _imageData = remoteData; - }); - await client.database?.storeFile(storeKey, remoteData, 0); - } - - if (event != null) { - final data = await event.downloadAndDecryptAttachment( - getThumbnail: widget.isThumbnail, - ); - if (data.detectFileType is MatrixImageFile) { - if (!mounted) return; - setState(() { - _imageData = data.bytes; - }); - return; - } - } - } - - void _tryLoad(_) async { - if (_imageData != null) { - return; - } - try { - await _load(); - } catch (_) { - if (!mounted) return; - await Future.delayed(widget.retryDuration); - _tryLoad(_); - } - } - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback(_tryLoad); - } - - Widget placeholder(BuildContext context) => - widget.placeholder?.call(context) ?? - Container( - width: widget.width, - height: widget.height, - alignment: Alignment.center, - child: const CircularProgressIndicator.adaptive(strokeWidth: 2), - ); - @override Widget build(BuildContext context) { - final data = _imageData; - final hasData = data != null && data.isNotEmpty; + final uri = this.uri; + if (uri == null) { + return placeholder?.call(context) ?? const Placeholder(); + } - return AnimatedCrossFade( - crossFadeState: - hasData ? CrossFadeState.showSecond : CrossFadeState.showFirst, - duration: FluffyThemes.animationDuration, - firstChild: placeholder(context), - secondChild: hasData - ? Image.memory( - data, - width: widget.width, - height: widget.height, - fit: widget.fit, - filterQuality: - widget.isThumbnail ? FilterQuality.low : FilterQuality.medium, - errorBuilder: (context, __, ___) { - _isCached = false; - _imageData = null; - WidgetsBinding.instance.addPostFrameCallback(_tryLoad); - return placeholder(context); - }, - ) - : SizedBox( - width: widget.width, - height: widget.height, - ), + final client = Matrix.of(context).client; + + final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; + final width = this.width; + final realWidth = width == null ? null : width * devicePixelRatio; + final height = this.height; + final realHeight = height == null ? null : height * devicePixelRatio; + + final imageUrl = isThumbnail + ? uri.getThumbnail( + client, + width: realWidth, + height: realHeight, + animated: animated, + method: thumbnailMethod, + ) + : uri.getDownloadLink(client); + + return CachedNetworkImage( + imageUrl: imageUrl.toString(), + width: width, + height: height, + fit: fit, + cacheKey: cacheKey, + filterQuality: isThumbnail ? FilterQuality.low : FilterQuality.medium, + errorWidget: placeholder == null + ? null + : (context, __, ___) => placeholder!.call(context), ); } } diff --git a/pubspec.lock b/pubspec.lock index d3c9cad78..477b147a6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -129,6 +129,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" + url: "https://pub.dev" + source: hosted + version: "1.2.0" callkeep: dependency: "direct main" description: @@ -1270,6 +1294,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + url: "https://pub.dev" + source: hosted + version: "2.0.0" olm: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9f23b88e8..e085683b5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: async: ^2.11.0 badges: ^3.1.2 blurhash_dart: ^1.2.1 + cached_network_image: ^3.3.1 callkeep: ^0.3.2 chewie: ^1.8.1 collection: ^1.18.0 From 938e1a91aef7fee0f63a9bd5293503eef2cf29e3 Mon Sep 17 00:00:00 2001 From: Krille Date: Fri, 10 May 2024 12:49:40 +0200 Subject: [PATCH 09/12] Revert "refactor: Move back to cached network image for better avatar performance" This reverts commit d9ab6ad8b303619a4971277e9c19ffc8fae7e0b5. --- ios/Runner.xcodeproj/project.pbxproj | 18 -- lib/pages/chat/events/image_bubble.dart | 123 +++--------- lib/pages/image_viewer/image_viewer_view.dart | 17 +- lib/widgets/mxc_image.dart | 185 +++++++++++++++--- pubspec.lock | 32 --- pubspec.yaml | 1 - 6 files changed, 190 insertions(+), 186 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 60bc014af..a6ce66d84 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -197,7 +197,6 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, F9C8EE392B9AB471149C306E /* [CP] Embed Pods Frameworks */, - F2B5CFB67942A2498CBB8D31 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -342,23 +341,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - F2B5CFB67942A2498CBB8D31 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; F9C8EE392B9AB471149C306E /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/lib/pages/chat/events/image_bubble.dart b/lib/pages/chat/events/image_bubble.dart index b12d1662c..f5219b054 100644 --- a/lib/pages/chat/events/image_bubble.dart +++ b/lib/pages/chat/events/image_bubble.dart @@ -1,16 +1,13 @@ -import 'dart:typed_data'; - import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/image_viewer/image_viewer.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; import '../../../widgets/blur_hash.dart'; -class ImageBubble extends StatefulWidget { +class ImageBubble extends StatelessWidget { final Event event; final bool tapToView; final BoxFit fit; @@ -18,11 +15,11 @@ class ImageBubble extends StatefulWidget { final Color? backgroundColor; final bool thumbnailOnly; final bool animated; - final double? width; - final double? height; + final double width; + final double height; final void Function()? onTap; final BorderRadius? borderRadius; - final Duration retryDuration; + const ImageBubble( this.event, { this.tapToView = true, @@ -35,102 +32,50 @@ class ImageBubble extends StatefulWidget { this.animated = false, this.onTap, this.borderRadius, - this.retryDuration = const Duration(seconds: 2), super.key, }); - @override - State createState() => _ImageBubbleState(); -} - -class _ImageBubbleState extends State { - Uint8List? _imageData; - - Future _load() async { - final data = await widget.event.downloadAndDecryptAttachment( - getThumbnail: widget.thumbnailOnly, - ); - if (data.detectFileType is MatrixImageFile) { - if (!mounted) return; - setState(() { - _imageData = data.bytes; - }); - return; - } - } - - void _tryLoad([_]) async { - if (_imageData != null) { - return; - } - try { - await _load(); - } catch (_) { - if (!mounted) return; - await Future.delayed(widget.retryDuration); - _tryLoad(); - } - } - - @override - void initState() { - super.initState(); - _tryLoad(); - } - Widget _buildPlaceholder(BuildContext context) { - final width = widget.width; - final height = widget.height; - if (width == null || height == null) { - return const Center( - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), - ); - } final String blurHashString = - widget.event.infoMap['xyz.amorgan.blurhash'] is String - ? widget.event.infoMap['xyz.amorgan.blurhash'] + event.infoMap['xyz.amorgan.blurhash'] is String + ? event.infoMap['xyz.amorgan.blurhash'] : 'LEHV6nWB2yk8pyo0adR*.7kCMdnj'; - return SizedBox( - width: widget.width, - height: widget.height, + width: width, + height: height, child: BlurHash( blurhash: blurHashString, width: width, height: height, - fit: widget.fit, + fit: fit, ), ); } void _onTap(BuildContext context) { - if (widget.onTap != null) { - widget.onTap!(); + if (onTap != null) { + onTap!(); return; } - if (!widget.tapToView) return; + if (!tapToView) return; showDialog( context: context, useRootNavigator: false, - builder: (_) => ImageViewer(widget.event), + builder: (_) => ImageViewer(event), ); } @override Widget build(BuildContext context) { final borderRadius = - widget.borderRadius ?? BorderRadius.circular(AppConfig.borderRadius); - final data = _imageData; - final hasData = data != null; + this.borderRadius ?? BorderRadius.circular(AppConfig.borderRadius); return Material( color: Colors.transparent, clipBehavior: Clip.hardEdge, shape: RoundedRectangleBorder( borderRadius: borderRadius, side: BorderSide( - color: widget.event.messageType == MessageTypes.Sticker + color: event.messageType == MessageTypes.Sticker ? Colors.transparent : Theme.of(context).dividerColor, ), @@ -139,31 +84,17 @@ class _ImageBubbleState extends State { onTap: () => _onTap(context), borderRadius: borderRadius, child: Hero( - tag: widget.event.eventId, - child: AnimatedCrossFade( - crossFadeState: - hasData ? CrossFadeState.showSecond : CrossFadeState.showFirst, - duration: FluffyThemes.animationDuration, - firstChild: _buildPlaceholder(context), - secondChild: hasData - ? Image.memory( - data, - width: widget.width, - height: widget.height, - fit: widget.fit, - filterQuality: widget.thumbnailOnly - ? FilterQuality.low - : FilterQuality.medium, - errorBuilder: (context, __, ___) { - _imageData = null; - WidgetsBinding.instance.addPostFrameCallback(_tryLoad); - return _buildPlaceholder(context); - }, - ) - : SizedBox( - width: widget.width, - height: widget.height, - ), + tag: event.eventId, + child: MxcImage( + event: event, + width: width, + height: height, + fit: fit, + animated: animated, + isThumbnail: thumbnailOnly, + placeholder: event.messageType == MessageTypes.Sticker + ? null + : _buildPlaceholder, ), ), ), diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index 2f4abe440..e4352864f 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:fluffychat/pages/chat/events/image_bubble.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; import 'image_viewer.dart'; class ImageViewerView extends StatelessWidget { @@ -55,13 +55,14 @@ class ImageViewerView extends StatelessWidget { maxScale: 10.0, onInteractionEnd: controller.onInteractionEnds, child: Center( - child: ImageBubble( - controller.widget.event, - fit: BoxFit.contain, - animated: true, - thumbnailOnly: false, - width: null, - height: null, + child: Hero( + tag: controller.widget.event.eventId, + child: MxcImage( + event: controller.widget.event, + fit: BoxFit.contain, + isThumbnail: false, + animated: true, + ), ), ), ), diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index a3ae695eb..9290156bf 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -1,69 +1,192 @@ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:http/http.dart' as http; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; -class MxcImage extends StatelessWidget { +class MxcImage extends StatefulWidget { final Uri? uri; + final Event? event; final double? width; final double? height; final BoxFit? fit; final bool isThumbnail; final bool animated; + final Duration retryDuration; + final Duration animationDuration; + final Curve animationCurve; final ThumbnailMethod thumbnailMethod; final Widget Function(BuildContext context)? placeholder; final String? cacheKey; const MxcImage({ this.uri, + this.event, this.width, this.height, this.fit, this.placeholder, this.isThumbnail = true, this.animated = false, + this.animationDuration = FluffyThemes.animationDuration, + this.retryDuration = const Duration(seconds: 2), + this.animationCurve = FluffyThemes.animationCurve, this.thumbnailMethod = ThumbnailMethod.scale, this.cacheKey, super.key, }); @override - Widget build(BuildContext context) { - final uri = this.uri; - if (uri == null) { - return placeholder?.call(context) ?? const Placeholder(); + State createState() => _MxcImageState(); +} + +class _MxcImageState extends State { + static final Map _imageDataCache = {}; + Uint8List? _imageDataNoCache; + Uint8List? get _imageData { + final cacheKey = widget.cacheKey; + return cacheKey == null ? _imageDataNoCache : _imageDataCache[cacheKey]; + } + + set _imageData(Uint8List? data) { + if (data == null) return; + final cacheKey = widget.cacheKey; + cacheKey == null + ? _imageDataNoCache = data + : _imageDataCache[cacheKey] = data; + } + + bool? _isCached; + + Future _load() async { + final client = Matrix.of(context).client; + final uri = widget.uri; + final event = widget.event; + + if (uri != null) { + final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; + final width = widget.width; + final realWidth = width == null ? null : width * devicePixelRatio; + final height = widget.height; + final realHeight = height == null ? null : height * devicePixelRatio; + + final httpUri = widget.isThumbnail + ? uri.getThumbnail( + client, + width: realWidth, + height: realHeight, + animated: widget.animated, + method: widget.thumbnailMethod, + ) + : uri.getDownloadLink(client); + + final storeKey = widget.isThumbnail ? httpUri : uri; + + if (_isCached == null) { + final cachedData = await client.database?.getFile(storeKey); + if (cachedData != null) { + if (!mounted) return; + setState(() { + _imageData = cachedData; + _isCached = true; + }); + return; + } + _isCached = false; + } + + final response = await http.get(httpUri); + if (response.statusCode != 200) { + if (response.statusCode == 404) { + return; + } + throw Exception(); + } + final remoteData = response.bodyBytes; + + if (!mounted) return; + setState(() { + _imageData = remoteData; + }); + await client.database?.storeFile(storeKey, remoteData, 0); } - final client = Matrix.of(context).client; + if (event != null) { + final data = await event.downloadAndDecryptAttachment( + getThumbnail: widget.isThumbnail, + ); + if (data.detectFileType is MatrixImageFile) { + if (!mounted) return; + setState(() { + _imageData = data.bytes; + }); + return; + } + } + } - final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; - final width = this.width; - final realWidth = width == null ? null : width * devicePixelRatio; - final height = this.height; - final realHeight = height == null ? null : height * devicePixelRatio; + void _tryLoad(_) async { + if (_imageData != null) { + return; + } + try { + await _load(); + } catch (_) { + if (!mounted) return; + await Future.delayed(widget.retryDuration); + _tryLoad(_); + } + } - final imageUrl = isThumbnail - ? uri.getThumbnail( - client, - width: realWidth, - height: realHeight, - animated: animated, - method: thumbnailMethod, - ) - : uri.getDownloadLink(client); + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback(_tryLoad); + } - return CachedNetworkImage( - imageUrl: imageUrl.toString(), - width: width, - height: height, - fit: fit, - cacheKey: cacheKey, - filterQuality: isThumbnail ? FilterQuality.low : FilterQuality.medium, - errorWidget: placeholder == null - ? null - : (context, __, ___) => placeholder!.call(context), + Widget placeholder(BuildContext context) => + widget.placeholder?.call(context) ?? + Container( + width: widget.width, + height: widget.height, + alignment: Alignment.center, + child: const CircularProgressIndicator.adaptive(strokeWidth: 2), + ); + + @override + Widget build(BuildContext context) { + final data = _imageData; + final hasData = data != null && data.isNotEmpty; + + return AnimatedCrossFade( + crossFadeState: + hasData ? CrossFadeState.showSecond : CrossFadeState.showFirst, + duration: FluffyThemes.animationDuration, + firstChild: placeholder(context), + secondChild: hasData + ? Image.memory( + data, + width: widget.width, + height: widget.height, + fit: widget.fit, + filterQuality: + widget.isThumbnail ? FilterQuality.low : FilterQuality.medium, + errorBuilder: (context, __, ___) { + _isCached = false; + _imageData = null; + WidgetsBinding.instance.addPostFrameCallback(_tryLoad); + return placeholder(context); + }, + ) + : SizedBox( + width: widget.width, + height: widget.height, + ), ); } } diff --git a/pubspec.lock b/pubspec.lock index 477b147a6..d3c9cad78 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -129,30 +129,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - cached_network_image: - dependency: "direct main" - description: - name: cached_network_image - sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" - url: "https://pub.dev" - source: hosted - version: "3.3.1" - cached_network_image_platform_interface: - dependency: transitive - description: - name: cached_network_image_platform_interface - sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" - url: "https://pub.dev" - source: hosted - version: "4.0.0" - cached_network_image_web: - dependency: transitive - description: - name: cached_network_image_web - sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" - url: "https://pub.dev" - source: hosted - version: "1.2.0" callkeep: dependency: "direct main" description: @@ -1294,14 +1270,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" - octo_image: - dependency: transitive - description: - name: octo_image - sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" - url: "https://pub.dev" - source: hosted - version: "2.0.0" olm: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e085683b5..9f23b88e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,6 @@ dependencies: async: ^2.11.0 badges: ^3.1.2 blurhash_dart: ^1.2.1 - cached_network_image: ^3.3.1 callkeep: ^0.3.2 chewie: ^1.8.1 collection: ^1.18.0 From 3d362df80a1dd84e41441e94eef752dba63a83aa Mon Sep 17 00:00:00 2001 From: Krille Date: Fri, 10 May 2024 15:26:17 +0200 Subject: [PATCH 10/12] refactor: Remove no longer necessary mx-reply workaround --- lib/pages/chat/events/html_message.dart | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index 8726bb7f6..d6e6cbc44 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -59,22 +59,6 @@ class HtmlMessage extends StatelessWidget { @override Widget build(BuildContext context) { - // riot-web is notorious for creating bad reply fallback events from invalid messages which, if - // not handled properly, can lead to impersination. As such, we strip the entire `` tags - // here already, to prevent that from happening. - // We do *not* do this in an AST and just with simple regex here, as riot-web tends to create - // miss-matching tags, and this way we actually correctly identify what we want to strip and, well, - // strip it. - final renderHtml = html.replaceAll( - RegExp( - '.*', - caseSensitive: false, - multiLine: false, - dotAll: true, - ), - '', - ); - final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor; final linkColor = textColor.withAlpha(150); @@ -89,7 +73,7 @@ class HtmlMessage extends StatelessWidget { padding: HtmlPaddings.only(left: 6, bottom: 0), ); - final element = _linkifyHtml(HtmlParser.parseHTML(renderHtml)); + final element = _linkifyHtml(HtmlParser.parseHTML(html)); // there is no need to pre-validate the html, as we validate it while rendering return Html.fromElement( From 3c85786c1061e8c758b9598b97a595eab6ae6956 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Fri, 10 May 2024 16:32:14 +0200 Subject: [PATCH 11/12] refactor: Improve performance of chat list --- lib/pages/chat_list/chat_list_body.dart | 49 +++++++++---------------- lib/pages/chat_list/chat_list_item.dart | 6 +++ 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index 8d93febbb..eeb6bb656 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -14,7 +14,6 @@ import 'package:fluffychat/pages/chat_list/status_msg_list.dart'; import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/public_room_bottom_sheet.dart'; @@ -43,7 +42,7 @@ class ChatListViewBody extends StatelessWidget { Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(100); final subtitleColor = Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(50); - + final filter = controller.searchController.text.toLowerCase(); return PageTransitionSwitcher( transitionBuilder: ( Widget child, @@ -239,35 +238,23 @@ class ChatListViewBody extends StatelessWidget { ), ), if (client.prevBatch != null) - SliverList( - delegate: SliverChildBuilderDelegate( - (BuildContext context, int i) { - if (!rooms[i] - .getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), - ) - .toLowerCase() - .contains( - controller.searchController.text.toLowerCase(), - )) { - return const SizedBox.shrink(); - } - final activeChat = controller.activeChat == rooms[i].id; - return ChatListItem( - rooms[i], - key: Key('chat_list_item_${rooms[i].id}'), - selected: - controller.selectedRoomIds.contains(rooms[i].id), - onTap: controller.selectMode == SelectMode.select - ? () => controller.toggleSelection(rooms[i].id) - : () => onChatTap(rooms[i], context), - onLongPress: () => - controller.toggleSelection(rooms[i].id), - activeChat: activeChat, - ); - }, - childCount: rooms.length, - ), + SliverList.builder( + itemCount: rooms.length, + itemBuilder: (BuildContext context, int i) { + return ChatListItem( + rooms[i], + key: Key('chat_list_item_${rooms[i].id}'), + filter: filter, + selected: + controller.selectedRoomIds.contains(rooms[i].id), + onTap: controller.selectMode == SelectMode.select + ? () => controller.toggleSelection(rooms[i].id) + : () => onChatTap(rooms[i], context), + onLongPress: () => + controller.toggleSelection(rooms[i].id), + activeChat: controller.activeChat == rooms[i].id, + ); + }, ), ], ), diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 1f93b07e1..024acc294 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -22,6 +22,7 @@ class ChatListItem extends StatelessWidget { final void Function()? onLongPress; final void Function()? onForget; final void Function() onTap; + final String? filter; const ChatListItem( this.room, { @@ -30,6 +31,7 @@ class ChatListItem extends StatelessWidget { required this.onTap, this.onLongPress, this.onForget, + this.filter, super.key, }); @@ -83,6 +85,10 @@ class ChatListItem extends StatelessWidget { final displayname = room.getLocalizedDisplayname( MatrixLocals(L10n.of(context)!), ); + final filter = this.filter; + if (filter != null && !displayname.toLowerCase().contains(filter)) { + return const SizedBox.shrink(); + } return Padding( padding: const EdgeInsets.symmetric( horizontal: 8, From 9da309be62b949a0da96a81fbc4652a7a6717555 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Fri, 10 May 2024 17:31:32 +0200 Subject: [PATCH 12/12] chore: Make bottomnavbar labels always visible --- lib/pages/chat_list/chat_list_view.dart | 11 +++++------ lib/pages/chat_list/space_view.dart | 5 +++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 96e2a4675..a91a0ee5e 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -31,13 +31,13 @@ class ChatListView extends StatelessWidget { badgePosition: badgePosition, filter: controller.getRoomFilterByActiveFilter(ActiveFilter.messages), - child: const Icon(Icons.forum_outlined), + child: const Icon(Icons.chat_outlined), ), selectedIcon: UnreadRoomsBadge( badgePosition: badgePosition, filter: controller.getRoomFilterByActiveFilter(ActiveFilter.messages), - child: const Icon(Icons.forum), + child: const Icon(Icons.chat), ), label: L10n.of(context)!.messages, ), @@ -60,13 +60,13 @@ class ChatListView extends StatelessWidget { badgePosition: badgePosition, filter: controller.getRoomFilterByActiveFilter(ActiveFilter.allChats), - child: const Icon(Icons.forum_outlined), + child: const Icon(Icons.chat_outlined), ), selectedIcon: UnreadRoomsBadge( badgePosition: badgePosition, filter: controller.getRoomFilterByActiveFilter(ActiveFilter.allChats), - child: const Icon(Icons.forum), + child: const Icon(Icons.chat), ), label: L10n.of(context)!.chats, ), @@ -187,8 +187,7 @@ class ChatListView extends StatelessWidget { ? NavigationBar( elevation: 4, labelBehavior: - NavigationDestinationLabelBehavior.alwaysHide, - height: 64, + NavigationDestinationLabelBehavior.alwaysShow, shadowColor: Theme.of(context).colorScheme.onBackground, surfaceTintColor: diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 0db6c5fbf..692429e4f 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -49,8 +49,9 @@ class _SpaceViewState extends State { loadHierarchy(); } - Future loadHierarchy([String? prevBatch]) async { - final activeSpaceId = widget.controller.activeSpaceId!; + Future loadHierarchy([String? prevBatch]) async { + final activeSpaceId = widget.controller.activeSpaceId; + if (activeSpaceId == null) return null; final client = Matrix.of(context).client; final activeSpace = client.getRoomById(activeSpaceId);