From d9ab6ad8b303619a4971277e9c19ffc8fae7e0b5 Mon Sep 17 00:00:00 2001 From: Krille Date: Fri, 10 May 2024 12:49:32 +0200 Subject: [PATCH] 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 | 189 +++--------------- pubspec.lock | 32 +++ pubspec.yaml | 1 + 6 files changed, 188 insertions(+), 192 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(_); + Widget build(BuildContext context) { + final uri = this.uri; + if (uri == null) { + return placeholder?.call(context) ?? const Placeholder(); } - } - @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 client = Matrix.of(context).client; - 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 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