Files
stream-chat-flutter/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart
T
xsahil03x 7720bf677d Merge remote-tracking branch 'origin/develop' into feat/localization
# Conflicts:
#	packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart
#	packages/stream_chat_flutter/lib/src/channel_name.dart
#	packages/stream_chat_flutter/lib/src/message_search_item.dart
#	packages/stream_chat_flutter/lib/src/message_widget.dart
2021-07-28 15:18:09 +05:30

332 lines
12 KiB
Dart

import 'dart:ui';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Callback to download an attachment asset
typedef AttachmentDownloader = Future<String> Function(
Attachment attachment, {
ProgressCallback? progressCallback,
});
/// Widget that shows the options in the gallery view
class AttachmentActionsModal extends StatelessWidget {
/// Returns a new [AttachmentActionsModal]
const AttachmentActionsModal({
Key? key,
required this.currentIndex,
required this.message,
this.onShowMessage,
this.imageDownloader,
this.fileDownloader,
}) : super(key: key);
/// The message containing the attachments
final Message message;
/// Current page index
final int currentIndex;
/// Callback to show the message
final VoidCallback? onShowMessage;
/// Callback to download images
final AttachmentDownloader? imageDownloader;
/// Callback to provide download files
final AttachmentDownloader? fileDownloader;
@override
Widget build(BuildContext context) => GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => Navigator.maybePop(context),
child: _buildPage(context),
);
Widget _buildPage(BuildContext context) {
final theme = StreamChatTheme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const SizedBox(height: kToolbarHeight),
Padding(
padding: const EdgeInsets.only(right: 8),
child: Container(
width: MediaQuery.of(context).size.width * 0.5,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
child: SizedBox(
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
_buildButton(
context,
context.translations.replyLabel,
StreamSvgIcon.iconCurveLineLeftUp(
size: 24,
color: theme.colorTheme.textLowEmphasis,
),
() {
Navigator.pop(context, ReturnActionType.reply);
},
),
_buildButton(
context,
context.translations.showInChatLabel,
StreamSvgIcon.eye(
size: 24,
color: theme.colorTheme.textHighEmphasis,
),
onShowMessage,
),
_buildButton(
context,
message.attachments[currentIndex].type == 'video'
? context.translations.saveVideoLabel
: context.translations.saveImageLabel,
StreamSvgIcon.iconSave(
size: 24,
color: theme.colorTheme.textLowEmphasis,
),
() {
final attachment = message.attachments[currentIndex];
final isImage = attachment.type == 'image';
final Future<String?> Function(Attachment,
{void Function(int, int) progressCallback})
saveFile = fileDownloader ?? _downloadAttachment;
final Future<String?> Function(Attachment,
{void Function(int, int) progressCallback})
saveImage = imageDownloader ?? _downloadAttachment;
final downloader = isImage ? saveImage : saveFile;
final progressNotifier =
ValueNotifier<_DownloadProgress?>(
_DownloadProgress.initial(),
);
downloader(
attachment,
progressCallback: (received, total) {
progressNotifier.value = _DownloadProgress(
total,
received,
);
},
).catchError((e, stk) {
progressNotifier.value = null;
});
// Closing attachment actions modal before opening
// attachment download dialog
Navigator.pop(context);
showDialog(
barrierDismissible: false,
context: context,
barrierColor: theme.colorTheme.overlay,
builder: (context) => _buildDownloadProgressDialog(
context,
progressNotifier,
),
);
},
),
if (StreamChat.of(context).currentUser?.id ==
message.user?.id)
_buildButton(
context,
context.translations.deleteLabel.capitalize(),
StreamSvgIcon.delete(
size: 24,
color: theme.colorTheme.accentError,
),
() {
final channel = StreamChannel.of(context).channel;
if (message.attachments.length > 1 ||
message.text?.isNotEmpty == true) {
final remainingAttachments = [...message.attachments]
..removeAt(currentIndex);
channel.updateMessage(message.copyWith(
attachments: remainingAttachments,
));
Navigator.of(context)
..pop()
..maybePop();
} else {
channel.deleteMessage(message);
Navigator.of(context)
..pop()
..maybePop();
}
},
color: theme.colorTheme.accentError,
),
]
.map<Widget>((e) => Align(
alignment: Alignment.centerRight,
child: e,
))
.insertBetween(
Container(
height: 1,
color: theme.colorTheme.borders,
),
),
),
),
),
)
],
);
}
Widget _buildButton(
context,
String title,
StreamSvgIcon icon,
VoidCallback? onTap, {
Color? color,
Key? key,
}) =>
Material(
key: key,
color: StreamChatTheme.of(context).colorTheme.barsBg,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Row(
children: [
icon,
const SizedBox(width: 16),
Text(
title,
style: StreamChatTheme.of(context)
.textTheme
.body
.copyWith(color: color),
),
],
),
),
),
);
Widget _buildDownloadProgressDialog(
BuildContext context,
ValueNotifier<_DownloadProgress?> progressNotifier,
) {
final theme = StreamChatTheme.of(context);
return ValueListenableBuilder(
valueListenable: progressNotifier,
builder: (_, _DownloadProgress? progress, __) {
// Pop the dialog in case the progress is null or it's completed.
if (progress == null || progress.toProgressIndicatorValue == 1.0) {
Future.delayed(
const Duration(milliseconds: 500),
() => Navigator.of(context).maybePop(),
);
}
return Material(
type: MaterialType.transparency,
child: Center(
child: Container(
height: 182,
width: 182,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: theme.colorTheme.barsBg,
),
child: Center(
child: progress == null
? SizedBox(
height: 100,
width: 100,
child: StreamSvgIcon.error(
color: theme.colorTheme.disabled,
),
)
: progress.toProgressIndicatorValue == 1.0
? SizedBox(
key: const Key('completedIcon'),
height: 160,
width: 160,
child: StreamSvgIcon.check(
color: theme.colorTheme.disabled,
),
)
: SizedBox(
height: 100,
width: 100,
child: Stack(
fit: StackFit.expand,
children: [
CircularProgressIndicator(
value: progress.toProgressIndicatorValue,
strokeWidth: 8,
valueColor: AlwaysStoppedAnimation(
theme.colorTheme.accentPrimary,
),
),
Center(
child: Text(
'${progress.toPercentage}%',
style: theme.textTheme.headline.copyWith(
color: theme.colorTheme.textLowEmphasis,
),
),
),
],
),
),
),
),
),
);
},
);
}
Future<String?> _downloadAttachment(
Attachment attachment, {
ProgressCallback? progressCallback,
}) async {
String? filePath;
final appDocDir = await getTemporaryDirectory();
await Dio().download(
attachment.assetUrl ?? attachment.imageUrl ?? attachment.thumbUrl!,
(Headers responseHeaders) {
final contentType = responseHeaders[Headers.contentTypeHeader]!;
final mimeType = contentType.first.split('/').last;
filePath ??= '${appDocDir.path}/${attachment.id}.$mimeType';
return filePath!;
},
onReceiveProgress: progressCallback,
);
final result = await ImageGallerySaver.saveFile(filePath!);
return (result as Map)['filePath'];
}
}
class _DownloadProgress {
const _DownloadProgress(this.total, this.received);
factory _DownloadProgress.initial() =>
_DownloadProgress(double.maxFinite.toInt(), 0);
final int total;
final int received;
double get toProgressIndicatorValue => received / total;
int get toPercentage => (received * 100) ~/ total;
}