From 87d3d0feed614c69e63856de6d0c4aef05a7b8b2 Mon Sep 17 00:00:00 2001 From: Krille Date: Wed, 17 Jul 2024 08:59:08 +0200 Subject: [PATCH 1/8] chore: Follow up list item click behavior --- lib/pages/chat_list/chat_list_item.dart | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index f704c41bf..40ca6631a 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -173,16 +173,19 @@ class ChatListItem extends StatelessWidget { Positioned( top: 0, right: 0, - child: AnimatedScale( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - scale: listTileHovered ? 1.0 : 0.0, - child: Material( - color: backgroundColor, - borderRadius: BorderRadius.circular(16), - child: const Icon( - Icons.arrow_drop_down_circle_outlined, - size: 18, + child: GestureDetector( + onTap: () => onLongPress?.call(context), + child: AnimatedScale( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + scale: listTileHovered ? 1.0 : 0.0, + child: Material( + color: backgroundColor, + borderRadius: BorderRadius.circular(16), + child: const Icon( + Icons.arrow_drop_down_circle_outlined, + size: 18, + ), ), ), ), From 86b1314c61f12f0ce102d98df7941d927de2653d Mon Sep 17 00:00:00 2001 From: Krille Date: Wed, 17 Jul 2024 10:27:56 +0200 Subject: [PATCH 2/8] refactor: Use cached network image for mxc image uris --- lib/pages/chat/events/html_message.dart | 1 - lib/widgets/avatar.dart | 16 ++-- lib/widgets/mxc_image.dart | 108 ++++++++---------------- pubspec.lock | 32 +++++++ pubspec.yaml | 1 + 5 files changed, 75 insertions(+), 83 deletions(-) diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index d6e6cbc44..4d5bcdc8c 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -280,7 +280,6 @@ class ImageExtension extends HtmlExtension { uri: mxcUrl, width: width ?? height ?? defaultDimension, height: height ?? width ?? defaultDimension, - cacheKey: mxcUrl.toString(), ), ), ); diff --git a/lib/widgets/avatar.dart b/lib/widgets/avatar.dart index 0c280a88b..cdb7b2e7a 100644 --- a/lib/widgets/avatar.dart +++ b/lib/widgets/avatar.dart @@ -47,29 +47,32 @@ class Avatar extends StatelessWidget { final noPic = mxContent == null || mxContent.toString().isEmpty || mxContent.toString() == 'null'; - final textWidget = Center( + final textWidget = Container( + color: name?.lightColorAvatar, + alignment: Alignment.center, child: Text( fallbackLetters, style: TextStyle( - color: noPic ? Colors.white : null, + color: Colors.white, fontSize: (size / 2.5).roundToDouble(), ), ), ); final borderRadius = this.borderRadius ?? BorderRadius.circular(size / 2); final presenceUserId = this.presenceUserId; - final color = - noPic ? name?.lightColorAvatar : Theme.of(context).secondaryHeaderColor; final container = Stack( children: [ SizedBox( width: size, height: size, child: Material( - color: color, shape: RoundedRectangleBorder( borderRadius: borderRadius, - side: border ?? BorderSide.none, + side: border ?? + BorderSide( + color: Theme.of(context).dividerColor, + width: 1, + ), ), clipBehavior: Clip.hardEdge, child: noPic @@ -81,7 +84,6 @@ class Avatar extends StatelessWidget { width: size, height: size, placeholder: (_) => textWidget, - cacheKey: mxContent.toString(), ), ), ), diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index 9290156bf..b12c9fc99 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -2,7 +2,7 @@ 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'; @@ -18,11 +18,8 @@ class MxcImage extends StatefulWidget { 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, @@ -33,11 +30,8 @@ class MxcImage extends StatefulWidget { 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, }); @@ -46,76 +40,11 @@ class MxcImage extends StatefulWidget { } 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; + Uint8List? _imageData; 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, @@ -131,7 +60,7 @@ class _MxcImageState extends State { } void _tryLoad(_) async { - if (_imageData != null) { + if (_imageData != null || widget.event == null) { return; } try { @@ -160,6 +89,36 @@ class _MxcImageState extends State { @override Widget build(BuildContext context) { + final uri = widget.uri; + + if (uri != null) { + final client = Matrix.of(context).client; + 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); + + return CachedNetworkImage( + imageUrl: httpUri.toString(), + width: width, + height: height, + fit: widget.fit, + placeholder: (context, _) => placeholder(context), + errorWidget: (context, _, __) => placeholder(context), + ); + } + final data = _imageData; final hasData = data != null && data.isNotEmpty; @@ -177,7 +136,6 @@ class _MxcImageState extends State { filterQuality: widget.isThumbnail ? FilterQuality.low : FilterQuality.medium, errorBuilder: (context, __, ___) { - _isCached = false; _imageData = null; WidgetsBinding.instance.addPostFrameCallback(_tryLoad); return placeholder(context); diff --git a/pubspec.lock b/pubspec.lock index 0ff035c1b..18e927b9f 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 36a37e8c4..4d9d028d2 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 998868dd566b3b479566963417748c6c2db1a332 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Wed, 17 Jul 2024 18:00:26 +0200 Subject: [PATCH 3/8] build: Try out flutter 3.22.2 in --- snap/snapcraft.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index a32ad5704..046b1ad66 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -66,7 +66,7 @@ parts: flutter-git: source: https://github.com/flutter/flutter.git - source-tag: 3.19.6 + source-tag: 3.22.2 source-depth: 1 plugin: nil override-build: | From 69fcb01988a58f030a1e5df5ded3c5f7fe93cab2 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Thu, 18 Jul 2024 07:33:55 +0200 Subject: [PATCH 4/8] build: Fix build snap --- pubspec.lock | 6 +++--- pubspec.yaml | 1 + snap/snapcraft.yaml | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 18e927b9f..afec622d5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2396,13 +2396,13 @@ packages: source: hosted version: "1.2.0" win32: - dependency: transitive + dependency: "direct overridden" description: name: win32 - sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" url: "https://pub.dev" source: hosted - version: "5.5.1" + version: "5.5.0" win32_registry: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4d9d028d2..d89ef7d80 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -161,3 +161,4 @@ dependency_overrides: git: url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git ref: null-safety + win32: 5.5.0 \ No newline at end of file diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 046b1ad66..a32ad5704 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -66,7 +66,7 @@ parts: flutter-git: source: https://github.com/flutter/flutter.git - source-tag: 3.22.2 + source-tag: 3.19.6 source-depth: 1 plugin: nil override-build: | From 659174b828fa5e2729456466e8b1b4e50f310075 Mon Sep 17 00:00:00 2001 From: Krille Date: Thu, 18 Jul 2024 15:26:59 +0200 Subject: [PATCH 5/8] build: Remove permissions for screensharing until it is fixed --- android/app/src/main/AndroidManifest.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c0595780a..149424e6a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -18,14 +18,12 @@ - - Date: Thu, 18 Jul 2024 16:45:10 +0200 Subject: [PATCH 6/8] chore: Follow up navrail --- lib/pages/chat_list/chat_list_item.dart | 9 +-------- lib/pages/chat_list/chat_list_view.dart | 13 +++++++++++-- lib/widgets/avatar.dart | 7 ++----- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 40ca6631a..1ba515e29 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -142,14 +142,7 @@ class ChatListItem extends StatelessWidget { right: 0, child: Avatar( border: space == null - ? room.isSpace - ? BorderSide( - width: 0, - color: Theme.of(context) - .colorScheme - .outline, - ) - : null + ? null : BorderSide( width: 2, color: backgroundColor ?? diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 5bb6687a0..4391e83b0 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -4,12 +4,14 @@ import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; +import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_list/navi_rail_item.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 '../../widgets/matrix.dart'; import 'chat_list_body.dart'; @@ -21,6 +23,7 @@ class ChatListView extends StatelessWidget { @override Widget build(BuildContext context) { + final client = Matrix.of(context).client; return StreamBuilder( stream: Matrix.of(context).onShareContentChanged.stream, builder: (_, __) { @@ -44,8 +47,14 @@ class ChatListView extends StatelessWidget { children: [ if (FluffyThemes.isColumnMode(context) && controller.widget.displayNavigationRail) ...[ - Builder( - builder: (context) { + StreamBuilder( + key: ValueKey( + client.userID.toString(), + ), + stream: client.onSync.stream + .where((s) => s.hasRoomUpdate) + .rateLimit(const Duration(seconds: 1)), + builder: (context, _) { final allSpaces = Matrix.of(context) .client .rooms diff --git a/lib/widgets/avatar.dart b/lib/widgets/avatar.dart index cdb7b2e7a..96aed1912 100644 --- a/lib/widgets/avatar.dart +++ b/lib/widgets/avatar.dart @@ -66,13 +66,10 @@ class Avatar extends StatelessWidget { width: size, height: size, child: Material( + color: Theme.of(context).colorScheme.surfaceContainerLowest, shape: RoundedRectangleBorder( borderRadius: borderRadius, - side: border ?? - BorderSide( - color: Theme.of(context).dividerColor, - width: 1, - ), + side: border ?? BorderSide.none, ), clipBehavior: Clip.hardEdge, child: noPic From 754870e3fd0d60e2cfbac3e9546ceea74a29fb7d Mon Sep 17 00:00:00 2001 From: krille-chan Date: Thu, 18 Jul 2024 16:47:58 +0200 Subject: [PATCH 7/8] chore: Follow up pop space view --- lib/pages/chat_list/chat_list_view.dart | 7 +- lib/pages/chat_list/space_view.dart | 543 ++++++++++++------------ 2 files changed, 275 insertions(+), 275 deletions(-) diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 4391e83b0..510c6c46d 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -30,9 +30,14 @@ class ChatListView extends StatelessWidget { final selectMode = controller.selectMode; return PopScope( canPop: controller.selectMode == SelectMode.normal && - !controller.isSearchMode, + !controller.isSearchMode && + controller.activeSpaceId == null, onPopInvoked: (pop) { if (pop) return; + if (controller.activeSpaceId != null) { + controller.clearActiveSpace(); + return; + } final selMode = controller.selectMode; if (controller.isSearchMode) { controller.cancelSearch(); diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 3a7fdcad0..3401aa1f1 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -165,315 +165,310 @@ class _SpaceViewState extends State { final room = Matrix.of(context).client.getRoomById(widget.spaceId); final displayname = room?.getLocalizedDisplayname() ?? L10n.of(context)!.nothingFound; - return PopScope( - canPop: false, - onPopInvoked: (_) => widget.onBack(), - child: Scaffold( - appBar: AppBar( - leading: Center( - child: CloseButton( - onPressed: widget.onBack, - ), + return Scaffold( + appBar: AppBar( + leading: Center( + child: CloseButton( + onPressed: widget.onBack, ), - titleSpacing: 0, - title: ListTile( - contentPadding: EdgeInsets.zero, - leading: Avatar( - mxContent: room?.avatar, - name: displayname, - borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), - ), - title: Text( - displayname, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: room == null - ? null - : Text( - L10n.of(context)!.countChatsAndCountParticipants( - room.spaceChildren.length, - room.summary.mJoinedMemberCount ?? 1, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + ), + titleSpacing: 0, + title: ListTile( + contentPadding: EdgeInsets.zero, + leading: Avatar( + mxContent: room?.avatar, + name: displayname, + borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), + ), + title: Text( + displayname, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - actions: [ - PopupMenuButton( - onSelected: _onSpaceAction, - itemBuilder: (context) => [ - PopupMenuItem( - value: SpaceActions.settings, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.settings_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.settings), - ], + subtitle: room == null + ? null + : Text( + L10n.of(context)!.countChatsAndCountParticipants( + room.spaceChildren.length, + room.summary.mJoinedMemberCount ?? 1, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - PopupMenuItem( - value: SpaceActions.invite, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.person_add_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.invite), - ], - ), + ), + actions: [ + PopupMenuButton( + onSelected: _onSpaceAction, + itemBuilder: (context) => [ + PopupMenuItem( + value: SpaceActions.settings, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.settings_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.settings), + ], ), - PopupMenuItem( - value: SpaceActions.leave, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.delete_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.leave), - ], - ), + ), + PopupMenuItem( + value: SpaceActions.invite, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.person_add_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.invite), + ], ), - ], - ), - ], - ), - body: room == null - ? const Center( - child: Icon( - Icons.search_outlined, - size: 80, + ), + PopupMenuItem( + value: SpaceActions.leave, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.delete_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.leave), + ], ), - ) - : StreamBuilder( - stream: room.client.onSync.stream - .where((s) => s.hasRoomUpdate) - .rateLimit(const Duration(seconds: 1)), - builder: (context, snapshot) { - final childrenIds = room.spaceChildren - .map((c) => c.roomId) - .whereType() - .toSet(); + ), + ], + ), + ], + ), + body: room == null + ? const Center( + child: Icon( + Icons.search_outlined, + size: 80, + ), + ) + : StreamBuilder( + stream: room.client.onSync.stream + .where((s) => s.hasRoomUpdate) + .rateLimit(const Duration(seconds: 1)), + builder: (context, snapshot) { + final childrenIds = room.spaceChildren + .map((c) => c.roomId) + .whereType() + .toSet(); - final joinedRooms = room.client.rooms - .where((room) => childrenIds.remove(room.id)) - .toList(); + final joinedRooms = room.client.rooms + .where((room) => childrenIds.remove(room.id)) + .toList(); - final joinedParents = room.spaceParents - .map((parent) { - final roomId = parent.roomId; - if (roomId == null) return null; - return room.client.getRoomById(roomId); - }) - .whereType() - .toList(); - final filter = _filterController.text.trim().toLowerCase(); - return CustomScrollView( - slivers: [ - SliverAppBar( - floating: true, - toolbarHeight: 72, - scrolledUnderElevation: 0, - backgroundColor: Colors.transparent, - automaticallyImplyLeading: false, - title: TextField( - controller: _filterController, - onChanged: (_) => setState(() {}), - textInputAction: TextInputAction.search, - decoration: InputDecoration( - fillColor: Theme.of(context) + final joinedParents = room.spaceParents + .map((parent) { + final roomId = parent.roomId; + if (roomId == null) return null; + return room.client.getRoomById(roomId); + }) + .whereType() + .toList(); + final filter = _filterController.text.trim().toLowerCase(); + return CustomScrollView( + slivers: [ + SliverAppBar( + floating: true, + toolbarHeight: 72, + scrolledUnderElevation: 0, + backgroundColor: Colors.transparent, + automaticallyImplyLeading: false, + title: TextField( + controller: _filterController, + onChanged: (_) => setState(() {}), + textInputAction: TextInputAction.search, + decoration: InputDecoration( + fillColor: + Theme.of(context).colorScheme.secondaryContainer, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(99), + ), + contentPadding: EdgeInsets.zero, + hintText: L10n.of(context)!.search, + hintStyle: TextStyle( + color: Theme.of(context) .colorScheme - .secondaryContainer, - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(99), - ), - contentPadding: EdgeInsets.zero, - hintText: L10n.of(context)!.search, - hintStyle: TextStyle( + .onPrimaryContainer, + fontWeight: FontWeight.normal, + ), + floatingLabelBehavior: FloatingLabelBehavior.never, + prefixIcon: IconButton( + onPressed: () {}, + icon: Icon( + Icons.search_outlined, color: Theme.of(context) .colorScheme .onPrimaryContainer, - fontWeight: FontWeight.normal, - ), - floatingLabelBehavior: FloatingLabelBehavior.never, - prefixIcon: IconButton( - onPressed: () {}, - icon: Icon( - Icons.search_outlined, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), ), ), ), ), - SliverList.builder( - itemCount: joinedParents.length, - itemBuilder: (context, i) { - final displayname = - joinedParents[i].getLocalizedDisplayname(); - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 1, - ), - child: Material( - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - clipBehavior: Clip.hardEdge, - child: ListTile( - minVerticalPadding: 0, - leading: Icon( - Icons.adaptive.arrow_back_outlined, - size: 16, - ), - title: Row( - children: [ - Avatar( - mxContent: joinedParents[i].avatar, - name: displayname, - size: Avatar.defaultSize / 2, - borderRadius: BorderRadius.circular( - AppConfig.borderRadius / 4, - ), + ), + SliverList.builder( + itemCount: joinedParents.length, + itemBuilder: (context, i) { + final displayname = + joinedParents[i].getLocalizedDisplayname(); + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 1, + ), + child: Material( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + clipBehavior: Clip.hardEdge, + child: ListTile( + minVerticalPadding: 0, + leading: Icon( + Icons.adaptive.arrow_back_outlined, + size: 16, + ), + title: Row( + children: [ + Avatar( + mxContent: joinedParents[i].avatar, + name: displayname, + size: Avatar.defaultSize / 2, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 4, ), - const SizedBox(width: 8), - Expanded(child: Text(displayname)), - ], - ), - onTap: () => - widget.toParentSpace(joinedParents[i].id), + ), + const SizedBox(width: 8), + Expanded(child: Text(displayname)), + ], ), + onTap: () => + widget.toParentSpace(joinedParents[i].id), ), + ), + ); + }, + ), + SliverList.builder( + itemCount: joinedRooms.length + 1, + itemBuilder: (context, i) { + if (i == 0) { + return SearchTitle( + title: L10n.of(context)!.joinedChats, + icon: const Icon(Icons.chat_outlined), ); - }, - ), - SliverList.builder( - itemCount: joinedRooms.length + 1, - itemBuilder: (context, i) { - if (i == 0) { - return SearchTitle( - title: L10n.of(context)!.joinedChats, - icon: const Icon(Icons.chat_outlined), - ); - } - i--; - final room = joinedRooms[i]; - return ChatListItem( + } + i--; + final room = joinedRooms[i]; + return ChatListItem( + room, + filter: filter, + onTap: () => widget.onChatTab(room), + onLongPress: (context) => widget.onChatContext( room, - filter: filter, - onTap: () => widget.onChatTab(room), - onLongPress: (context) => widget.onChatContext( - room, - context, - ), - activeChat: widget.activeChat == room.id, + context, + ), + activeChat: widget.activeChat == room.id, + ); + }, + ), + SliverList.builder( + itemCount: _discoveredChildren.length + 2, + itemBuilder: (context, i) { + if (i == 0) { + return SearchTitle( + title: L10n.of(context)!.discover, + icon: const Icon(Icons.explore_outlined), ); - }, - ), - SliverList.builder( - itemCount: _discoveredChildren.length + 2, - itemBuilder: (context, i) { - if (i == 0) { - return SearchTitle( - title: L10n.of(context)!.discover, - icon: const Icon(Icons.explore_outlined), - ); - } - i--; - if (i == _discoveredChildren.length) { - if (_noMoreRooms) { - return Padding( - padding: const EdgeInsets.all(12.0), - child: Center( - child: Text( - L10n.of(context)!.noMoreChatsFound, - style: const TextStyle(fontSize: 13), - ), - ), - ); - } + } + i--; + if (i == _discoveredChildren.length) { + if (_noMoreRooms) { return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 2.0, - ), - child: TextButton( - onPressed: _isLoading ? null : _loadHierarchy, - child: _isLoading - ? LinearProgressIndicator( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, - ), - ) - : Text(L10n.of(context)!.loadMore), + padding: const EdgeInsets.all(12.0), + child: Center( + child: Text( + L10n.of(context)!.noMoreChatsFound, + style: const TextStyle(fontSize: 13), + ), ), ); } - final item = _discoveredChildren[i]; - final displayname = item.name ?? - item.canonicalAlias ?? - L10n.of(context)!.emptyChat; - if (!displayname.toLowerCase().contains(filter)) { - return const SizedBox.shrink(); - } return Padding( padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 1, + horizontal: 12.0, + vertical: 2.0, ), - child: Material( - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - clipBehavior: Clip.hardEdge, - child: ListTile( - onTap: () => _joinChildRoom(item), - leading: Avatar( - mxContent: item.avatarUrl, - name: displayname, - borderRadius: item.roomType == 'm.space' - ? BorderRadius.circular( - AppConfig.borderRadius / 2, - ) - : null, - ), - title: Row( - children: [ - Expanded( - child: Text( - displayname, - maxLines: 1, - overflow: TextOverflow.ellipsis, + child: TextButton( + onPressed: _isLoading ? null : _loadHierarchy, + child: _isLoading + ? LinearProgressIndicator( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, ), + ) + : Text(L10n.of(context)!.loadMore), + ), + ); + } + final item = _discoveredChildren[i]; + final displayname = item.name ?? + item.canonicalAlias ?? + L10n.of(context)!.emptyChat; + if (!displayname.toLowerCase().contains(filter)) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 1, + ), + child: Material( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + clipBehavior: Clip.hardEdge, + child: ListTile( + onTap: () => _joinChildRoom(item), + leading: Avatar( + mxContent: item.avatarUrl, + name: displayname, + borderRadius: item.roomType == 'm.space' + ? BorderRadius.circular( + AppConfig.borderRadius / 2, + ) + : null, + ), + title: Row( + children: [ + Expanded( + child: Text( + displayname, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - const SizedBox(width: 8), - const Icon( - Icons.add_circle_outline_outlined, + ), + const SizedBox(width: 8), + const Icon( + Icons.add_circle_outline_outlined, + ), + ], + ), + subtitle: Text( + item.topic ?? + L10n.of(context)!.countParticipants( + item.numJoinedMembers, ), - ], - ), - subtitle: Text( - item.topic ?? - L10n.of(context)!.countParticipants( - item.numJoinedMembers, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), - ); - }, - ), - ], - ); - }, - ), - ), + ), + ); + }, + ), + ], + ); + }, + ), ); } } From b8345e2ca6d9ceef2136cb6ccaf4f8384801936b Mon Sep 17 00:00:00 2001 From: krille-chan Date: Thu, 18 Jul 2024 17:41:45 +0200 Subject: [PATCH 8/8] chore: Follow up avatars --- lib/widgets/avatar.dart | 3 +- lib/widgets/mxc_image.dart | 108 +++++++++++++++++++++++++------------ pubspec.lock | 32 ----------- pubspec.yaml | 1 - 4 files changed, 77 insertions(+), 67 deletions(-) diff --git a/lib/widgets/avatar.dart b/lib/widgets/avatar.dart index 96aed1912..28524d917 100644 --- a/lib/widgets/avatar.dart +++ b/lib/widgets/avatar.dart @@ -75,7 +75,8 @@ class Avatar extends StatelessWidget { child: noPic ? textWidget : MxcImage( - key: Key(mxContent.toString()), + key: ValueKey(mxContent.toString()), + cacheKey: '${mxContent}_$size', uri: mxContent, fit: BoxFit.cover, width: size, diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index b12c9fc99..34c53cb57 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -2,7 +2,7 @@ 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'; @@ -18,8 +18,11 @@ class MxcImage extends StatefulWidget { 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, @@ -30,8 +33,11 @@ class MxcImage extends StatefulWidget { 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, }); @@ -40,11 +46,76 @@ class MxcImage extends StatefulWidget { } class _MxcImageState extends State { - Uint8List? _imageData; + static final Map _imageDataCache = {}; + Uint8List? _imageDataNoCache; + + Uint8List? get _imageData => widget.cacheKey == null + ? _imageDataNoCache + : _imageDataCache[widget.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, @@ -60,7 +131,7 @@ class _MxcImageState extends State { } void _tryLoad(_) async { - if (_imageData != null || widget.event == null) { + if (_imageData != null) { return; } try { @@ -89,36 +160,6 @@ class _MxcImageState extends State { @override Widget build(BuildContext context) { - final uri = widget.uri; - - if (uri != null) { - final client = Matrix.of(context).client; - 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); - - return CachedNetworkImage( - imageUrl: httpUri.toString(), - width: width, - height: height, - fit: widget.fit, - placeholder: (context, _) => placeholder(context), - errorWidget: (context, _, __) => placeholder(context), - ); - } - final data = _imageData; final hasData = data != null && data.isNotEmpty; @@ -136,6 +177,7 @@ class _MxcImageState extends State { filterQuality: widget.isThumbnail ? FilterQuality.low : FilterQuality.medium, errorBuilder: (context, __, ___) { + _isCached = false; _imageData = null; WidgetsBinding.instance.addPostFrameCallback(_tryLoad); return placeholder(context); diff --git a/pubspec.lock b/pubspec.lock index afec622d5..7e3909e54 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 d89ef7d80..a013aeea3 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