Skip to content

Commit

Permalink
refactor: Try to display image #2771
Browse files Browse the repository at this point in the history
  • Loading branch information
bibash28 committed Jul 23, 2024
1 parent eb70913 commit 4a9ab3a
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 19 deletions.
1 change: 1 addition & 0 deletions lib/chat_room/chat_room.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export 'cubit/chat_room_cubit.dart';
export 'matrix_chat/matrix_chat.dart';
export 'view/chat_room_view.dart';
export 'widget/mxc_image.dart';
22 changes: 5 additions & 17 deletions lib/chat_room/matrix_chat/matrix_chat_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -205,27 +205,23 @@ class MatrixChatImpl extends MatrixChatInterface {
final url =
(file != null && file is Map<String, dynamic>) ? file['url'] : '';

final imageUrl = getUrlFromImage(url.toString());

message = ImageMessage(
id: const Uuid().v4(),
remoteId: event.eventId,
name: event.plaintextBody,
size: size,
uri: imageUrl,
uri: url.toString(),
status: mapEventStatusToMessageStatus(event.status),
createdAt: event.originServerTs.millisecondsSinceEpoch,
author: User(
id: event.senderId,
),
author: User(id: event.senderId),
);
} else if (event.messageType == 'm.file') {
message = FileMessage(
id: const Uuid().v4(),
remoteId: event.eventId,
name: event.plaintextBody,
size: size,
uri: getUrlFromUri(url: event.content['url'] as String? ?? ''),
uri: getThumbnail(url: event.content['url'] as String? ?? ''),
status: mapEventStatusToMessageStatus(event.status),
createdAt: event.originServerTs.millisecondsSinceEpoch,
author: User(
Expand All @@ -252,7 +248,7 @@ class MatrixChatImpl extends MatrixChatInterface {
),
name: event.plaintextBody,
size: size,
uri: getUrlFromUri(url: event.content['url'] as String? ?? ''),
uri: getThumbnail(url: event.content['url'] as String? ?? ''),
status: mapEventStatusToMessageStatus(event.status),
createdAt: event.originServerTs.millisecondsSinceEpoch,
author: User(
Expand Down Expand Up @@ -470,7 +466,7 @@ class MatrixChatImpl extends MatrixChatInterface {
}

@override
String getUrlFromUri({
String getThumbnail({
required String url,
int width = 500,
int height = 500,
Expand All @@ -485,14 +481,6 @@ class MatrixChatImpl extends MatrixChatInterface {
return uri.toString();
}

@override
String getUrlFromImage(String url) {
if (url.trim().isEmpty) return '';

final Uri uri = Uri.parse(url).getDownloadLink(client!);
return uri.toString();
}

@override
Future<void> register({
required String did,
Expand Down
3 changes: 1 addition & 2 deletions lib/chat_room/matrix_chat/matrix_chat_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@ abstract class MatrixChatInterface {
});
Message mapEventToMessage(Event event);
Status mapEventStatusToMessageStatus(EventStatus status);
String getUrlFromUri({
String getThumbnail({
required String url,
int width = 500,
int height = 500,
});
String getUrlFromImage(String url);
Future<String> login({
required String username,
required String password,
Expand Down
13 changes: 13 additions & 0 deletions lib/chat_room/view/chat_room_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,19 @@ class _ChatRoomViewState<B extends ChatRoomCubit> extends State<ChatRoomView> {
width: 500,
height: 500,
);
}
if (link.startsWith('mxc')) {
return MxcImage(
client: context
.read<AltmeChatSupportCubit>()
.matrixChat
.client!,
fit: BoxFit.contain,
width: 500,
height: 500,
uri: Uri.parse(link),
isThumbnail: false,
);
} else {
return Image.file(
File(link),
Expand Down
188 changes: 188 additions & 0 deletions lib/chat_room/widget/mxc_image.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import 'dart:typed_data';

import 'package:flutter/material.dart';

import 'package:http/http.dart' as http;
import 'package:matrix/matrix.dart';

class MxcImage extends StatefulWidget {
const MxcImage({
required this.client,
this.uri,
this.event,
this.width,
this.height,
this.fit,
this.placeholder,
this.isThumbnail = true,
this.animated = false,
this.animationDuration = const Duration(milliseconds: 250),
this.retryDuration = const Duration(seconds: 2),
this.animationCurve = Curves.easeInOut,
this.thumbnailMethod = ThumbnailMethod.scale,
this.cacheKey,
super.key,
});

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;
final Client client;

@override
State<MxcImage> createState() => _MxcImageState();
}

class _MxcImageState extends State<MxcImage> {
static final Map<String, Uint8List> _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<void> _load() async {
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(
widget.client,
width: realWidth,
height: realHeight,
animated: widget.animated,
method: widget.thumbnailMethod,
)
: uri.getDownloadLink(widget.client);

final storeKey = widget.isThumbnail ? httpUri : uri;

if (_isCached == null) {
final cachedData = await widget.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 widget.client.database?.storeFile(storeKey, remoteData, 0);
}

if (event != null) {
final data = await event.downloadAndDecryptAttachment(
getThumbnail: widget.isThumbnail,
);
if (data.msgType == MessageTypes.Image) {
if (!mounted) return;
setState(() {
_imageData = data.bytes;
});
return;
}
}
}

void _tryLoad(_) async {
if (_imageData != null) {
return;
}
try {
await _load();
} catch (_) {
if (!mounted) return;
await Future<void>.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;

return AnimatedCrossFade(
crossFadeState:
hasData ? CrossFadeState.showSecond : CrossFadeState.showFirst,
duration: const Duration(milliseconds: 250),
firstChild: placeholder(context),
secondChild: hasData
? Image.memory(
data,
width: widget.width,
height: widget.height,
fit: widget.fit,
filterQuality: FilterQuality.none,
errorBuilder: (context, __, s) {
_isCached = false;
_imageData = null;
WidgetsBinding.instance.addPostFrameCallback(_tryLoad);
return placeholder(context);
},
)
: SizedBox(
width: widget.width,
height: widget.height,
),
);
}
}

0 comments on commit 4a9ab3a

Please sign in to comment.