mirror of
https://github.com/Iconica-Development/flutter_chat.git
synced 2025-05-18 18:33:49 +02:00
feat: add loading and refresh handling for images
This commit is contained in:
parent
3f1caa912b
commit
f286e7fb79
2 changed files with 140 additions and 20 deletions
|
@ -147,6 +147,7 @@ class MessageTheme {
|
|||
this.borderColor,
|
||||
this.textColor,
|
||||
this.timeTextColor,
|
||||
this.imageBackgroundColor,
|
||||
this.borderRadius,
|
||||
this.messageAlignment,
|
||||
this.messageSidePadding,
|
||||
|
@ -163,6 +164,7 @@ class MessageTheme {
|
|||
borderColor: theme.colorScheme.primary,
|
||||
textColor: theme.colorScheme.onPrimary,
|
||||
timeTextColor: theme.colorScheme.onPrimary,
|
||||
imageBackgroundColor: theme.colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
textAlignment: TextAlign.start,
|
||||
messageSidePadding: 144.0,
|
||||
|
@ -201,6 +203,12 @@ class MessageTheme {
|
|||
/// Defaults to [ThemeData.colorScheme.primaryColor]
|
||||
final Color? borderColor;
|
||||
|
||||
/// The color of the background when an image is loading, the image is
|
||||
/// transparent or there is an error.
|
||||
///
|
||||
/// Defaults to [ThemeData.colorScheme.secondaryContainer]
|
||||
final Color? imageBackgroundColor;
|
||||
|
||||
/// The border radius of the message container
|
||||
/// Defaults to [BorderRadius.circular(12)]
|
||||
final BorderRadius? borderRadius;
|
||||
|
@ -231,6 +239,7 @@ class MessageTheme {
|
|||
Color? borderColor,
|
||||
Color? textColor,
|
||||
Color? timeTextColor,
|
||||
Color? imageBackgroundColor,
|
||||
BorderRadius? borderRadius,
|
||||
double? messageSidePadding,
|
||||
TextAlign? messageAlignment,
|
||||
|
@ -245,6 +254,7 @@ class MessageTheme {
|
|||
borderColor: borderColor ?? this.borderColor,
|
||||
textColor: textColor ?? this.textColor,
|
||||
timeTextColor: timeTextColor ?? this.timeTextColor,
|
||||
imageBackgroundColor: imageBackgroundColor ?? this.imageBackgroundColor,
|
||||
borderRadius: borderRadius ?? this.borderRadius,
|
||||
messageSidePadding: messageSidePadding ?? this.messageSidePadding,
|
||||
messageAlignment: messageAlignment ?? this.messageAlignment,
|
||||
|
@ -262,6 +272,8 @@ class MessageTheme {
|
|||
borderColor: borderColor ?? other.borderColor,
|
||||
textColor: textColor ?? other.textColor,
|
||||
timeTextColor: timeTextColor ?? other.timeTextColor,
|
||||
imageBackgroundColor:
|
||||
imageBackgroundColor ?? other.imageBackgroundColor,
|
||||
borderRadius: borderRadius ?? other.borderRadius,
|
||||
messageSidePadding: messageSidePadding ?? other.messageSidePadding,
|
||||
messageAlignment: messageAlignment ?? other.messageAlignment,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:chat_repository_interface/chat_repository_interface.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_accessibility/flutter_accessibility.dart";
|
||||
|
@ -278,6 +280,7 @@ class _ChatMessageBubble extends StatelessWidget {
|
|||
_DefaultChatImage(
|
||||
message: message,
|
||||
messageTheme: messageTheme,
|
||||
options: options,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
],
|
||||
|
@ -314,40 +317,121 @@ class _ChatMessageBubble extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _DefaultChatImage extends StatelessWidget {
|
||||
class _DefaultChatImage extends StatefulWidget {
|
||||
const _DefaultChatImage({
|
||||
required this.message,
|
||||
required this.messageTheme,
|
||||
required this.options,
|
||||
});
|
||||
|
||||
final MessageModel message;
|
||||
|
||||
final ChatOptions options;
|
||||
final MessageTheme messageTheme;
|
||||
|
||||
@override
|
||||
State<_DefaultChatImage> createState() => _DefaultChatImageState();
|
||||
}
|
||||
|
||||
/// Exception thrown when the image builder fails to recognize the image
|
||||
class InvalidImageUrlException implements Exception {}
|
||||
|
||||
class _DefaultChatImageState extends State<_DefaultChatImage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
late ImageProvider provider;
|
||||
late Completer imageLoadingCompleter;
|
||||
|
||||
void _preloadImage() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
var uri = Uri.tryParse(widget.message.imageUrl ?? "");
|
||||
if (uri == null) {
|
||||
imageLoadingCompleter.completeError(InvalidImageUrlException());
|
||||
return;
|
||||
}
|
||||
|
||||
provider = widget.options.imageProviderResolver(
|
||||
context,
|
||||
uri,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
await precacheImage(
|
||||
provider,
|
||||
context,
|
||||
onError: imageLoadingCompleter.completeError,
|
||||
);
|
||||
|
||||
imageLoadingCompleter.complete();
|
||||
});
|
||||
}
|
||||
|
||||
void _refreshImage() {
|
||||
setState(() {
|
||||
imageLoadingCompleter = Completer();
|
||||
});
|
||||
_preloadImage();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
imageLoadingCompleter = Completer();
|
||||
_preloadImage();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _DefaultChatImage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.message.imageUrl != widget.message.imageUrl) {
|
||||
_refreshImage();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var chatScope = ChatScope.of(context);
|
||||
var options = chatScope.options;
|
||||
var textTheme = Theme.of(context).textTheme;
|
||||
var imageUrl = message.imageUrl!;
|
||||
super.build(context);
|
||||
|
||||
var theme = Theme.of(context);
|
||||
|
||||
var asyncImageBuilder = FutureBuilder<void>(
|
||||
future: imageLoadingCompleter.future,
|
||||
builder: (context, snapshot) => switch (snapshot.connectionState) {
|
||||
ConnectionState.waiting => Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: widget.messageTheme.textColor,
|
||||
),
|
||||
),
|
||||
ConnectionState.done when !snapshot.hasError => Image(
|
||||
image: provider,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) =>
|
||||
_DefaultMessageImageError(
|
||||
messageTheme: widget.messageTheme,
|
||||
onRefresh: _refreshImage,
|
||||
),
|
||||
),
|
||||
_ => _DefaultMessageImageError(
|
||||
messageTheme: widget.messageTheme,
|
||||
onRefresh: _refreshImage,
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Image(
|
||||
image:
|
||||
options.imageProviderResolver(context, Uri.parse(imageUrl)),
|
||||
fit: BoxFit.fitWidth,
|
||||
errorBuilder: (context, error, stackTrace) => Text(
|
||||
// TODO(Jacques): Non-replaceable text
|
||||
"Something went wrong with loading the image",
|
||||
style: textTheme.bodyLarge?.copyWith(
|
||||
color: messageTheme.textColor,
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => ConstrainedBox(
|
||||
constraints: BoxConstraints.tightForFinite(
|
||||
width: constraints.maxWidth,
|
||||
height: constraints.maxWidth,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: ColoredBox(
|
||||
color: widget.messageTheme.imageBackgroundColor ??
|
||||
theme.colorScheme.secondaryContainer,
|
||||
child: asyncImageBuilder,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -355,6 +439,30 @@ class _DefaultChatImage extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
||||
class _DefaultMessageImageError extends StatelessWidget {
|
||||
const _DefaultMessageImageError({
|
||||
required this.messageTheme,
|
||||
required this.onRefresh,
|
||||
});
|
||||
|
||||
final MessageTheme messageTheme;
|
||||
final VoidCallback onRefresh;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: IconButton(
|
||||
onPressed: onRefresh,
|
||||
icon: Icon(
|
||||
Icons.refresh,
|
||||
color: messageTheme.textColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// A container for the chat message that provides a decoration around the
|
||||
|
|
Loading…
Reference in a new issue