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.borderColor,
|
||||||
this.textColor,
|
this.textColor,
|
||||||
this.timeTextColor,
|
this.timeTextColor,
|
||||||
|
this.imageBackgroundColor,
|
||||||
this.borderRadius,
|
this.borderRadius,
|
||||||
this.messageAlignment,
|
this.messageAlignment,
|
||||||
this.messageSidePadding,
|
this.messageSidePadding,
|
||||||
|
@ -163,6 +164,7 @@ class MessageTheme {
|
||||||
borderColor: theme.colorScheme.primary,
|
borderColor: theme.colorScheme.primary,
|
||||||
textColor: theme.colorScheme.onPrimary,
|
textColor: theme.colorScheme.onPrimary,
|
||||||
timeTextColor: theme.colorScheme.onPrimary,
|
timeTextColor: theme.colorScheme.onPrimary,
|
||||||
|
imageBackgroundColor: theme.colorScheme.secondaryContainer,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
textAlignment: TextAlign.start,
|
textAlignment: TextAlign.start,
|
||||||
messageSidePadding: 144.0,
|
messageSidePadding: 144.0,
|
||||||
|
@ -201,6 +203,12 @@ class MessageTheme {
|
||||||
/// Defaults to [ThemeData.colorScheme.primaryColor]
|
/// Defaults to [ThemeData.colorScheme.primaryColor]
|
||||||
final Color? borderColor;
|
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
|
/// The border radius of the message container
|
||||||
/// Defaults to [BorderRadius.circular(12)]
|
/// Defaults to [BorderRadius.circular(12)]
|
||||||
final BorderRadius? borderRadius;
|
final BorderRadius? borderRadius;
|
||||||
|
@ -231,6 +239,7 @@ class MessageTheme {
|
||||||
Color? borderColor,
|
Color? borderColor,
|
||||||
Color? textColor,
|
Color? textColor,
|
||||||
Color? timeTextColor,
|
Color? timeTextColor,
|
||||||
|
Color? imageBackgroundColor,
|
||||||
BorderRadius? borderRadius,
|
BorderRadius? borderRadius,
|
||||||
double? messageSidePadding,
|
double? messageSidePadding,
|
||||||
TextAlign? messageAlignment,
|
TextAlign? messageAlignment,
|
||||||
|
@ -245,6 +254,7 @@ class MessageTheme {
|
||||||
borderColor: borderColor ?? this.borderColor,
|
borderColor: borderColor ?? this.borderColor,
|
||||||
textColor: textColor ?? this.textColor,
|
textColor: textColor ?? this.textColor,
|
||||||
timeTextColor: timeTextColor ?? this.timeTextColor,
|
timeTextColor: timeTextColor ?? this.timeTextColor,
|
||||||
|
imageBackgroundColor: imageBackgroundColor ?? this.imageBackgroundColor,
|
||||||
borderRadius: borderRadius ?? this.borderRadius,
|
borderRadius: borderRadius ?? this.borderRadius,
|
||||||
messageSidePadding: messageSidePadding ?? this.messageSidePadding,
|
messageSidePadding: messageSidePadding ?? this.messageSidePadding,
|
||||||
messageAlignment: messageAlignment ?? this.messageAlignment,
|
messageAlignment: messageAlignment ?? this.messageAlignment,
|
||||||
|
@ -262,6 +272,8 @@ class MessageTheme {
|
||||||
borderColor: borderColor ?? other.borderColor,
|
borderColor: borderColor ?? other.borderColor,
|
||||||
textColor: textColor ?? other.textColor,
|
textColor: textColor ?? other.textColor,
|
||||||
timeTextColor: timeTextColor ?? other.timeTextColor,
|
timeTextColor: timeTextColor ?? other.timeTextColor,
|
||||||
|
imageBackgroundColor:
|
||||||
|
imageBackgroundColor ?? other.imageBackgroundColor,
|
||||||
borderRadius: borderRadius ?? other.borderRadius,
|
borderRadius: borderRadius ?? other.borderRadius,
|
||||||
messageSidePadding: messageSidePadding ?? other.messageSidePadding,
|
messageSidePadding: messageSidePadding ?? other.messageSidePadding,
|
||||||
messageAlignment: messageAlignment ?? other.messageAlignment,
|
messageAlignment: messageAlignment ?? other.messageAlignment,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import "dart:async";
|
||||||
|
|
||||||
import "package:chat_repository_interface/chat_repository_interface.dart";
|
import "package:chat_repository_interface/chat_repository_interface.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_accessibility/flutter_accessibility.dart";
|
import "package:flutter_accessibility/flutter_accessibility.dart";
|
||||||
|
@ -278,6 +280,7 @@ class _ChatMessageBubble extends StatelessWidget {
|
||||||
_DefaultChatImage(
|
_DefaultChatImage(
|
||||||
message: message,
|
message: message,
|
||||||
messageTheme: messageTheme,
|
messageTheme: messageTheme,
|
||||||
|
options: options,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
],
|
],
|
||||||
|
@ -314,40 +317,121 @@ class _ChatMessageBubble extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DefaultChatImage extends StatelessWidget {
|
class _DefaultChatImage extends StatefulWidget {
|
||||||
const _DefaultChatImage({
|
const _DefaultChatImage({
|
||||||
required this.message,
|
required this.message,
|
||||||
required this.messageTheme,
|
required this.messageTheme,
|
||||||
|
required this.options,
|
||||||
});
|
});
|
||||||
|
|
||||||
final MessageModel message;
|
final MessageModel message;
|
||||||
|
final ChatOptions options;
|
||||||
final MessageTheme messageTheme;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var chatScope = ChatScope.of(context);
|
super.build(context);
|
||||||
var options = chatScope.options;
|
|
||||||
var textTheme = Theme.of(context).textTheme;
|
var theme = Theme.of(context);
|
||||||
var imageUrl = message.imageUrl!;
|
|
||||||
|
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(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ClipRRect(
|
child: LayoutBuilder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
builder: (context, constraints) => ConstrainedBox(
|
||||||
child: AnimatedSize(
|
constraints: BoxConstraints.tightForFinite(
|
||||||
duration: const Duration(milliseconds: 300),
|
width: constraints.maxWidth,
|
||||||
child: Image(
|
height: constraints.maxWidth,
|
||||||
image:
|
),
|
||||||
options.imageProviderResolver(context, Uri.parse(imageUrl)),
|
child: ClipRRect(
|
||||||
fit: BoxFit.fitWidth,
|
borderRadius: BorderRadius.circular(12),
|
||||||
errorBuilder: (context, error, stackTrace) => Text(
|
child: ColoredBox(
|
||||||
// TODO(Jacques): Non-replaceable text
|
color: widget.messageTheme.imageBackgroundColor ??
|
||||||
"Something went wrong with loading the image",
|
theme.colorScheme.secondaryContainer,
|
||||||
style: textTheme.bodyLarge?.copyWith(
|
child: asyncImageBuilder,
|
||||||
color: messageTheme.textColor,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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
|
/// A container for the chat message that provides a decoration around the
|
||||||
|
|
Loading…
Reference in a new issue