diff --git a/assets/images/ellipsis.svg b/assets/images/ellipsis.svg
new file mode 100644
index 0000000000..eef149715d
--- /dev/null
+++ b/assets/images/ellipsis.svg
@@ -0,0 +1,3 @@
+
diff --git a/lib/constants/resources.dart b/lib/constants/resources.dart
index 449187d764..083efd4ead 100644
--- a/lib/constants/resources.dart
+++ b/lib/constants/resources.dart
@@ -2,6 +2,8 @@
// **************************************************************************
// Auto generated by https://github.com/fluttercandies/assets_generator
// **************************************************************************
+// ignore_for_file: constant_identifier_names
+
class Resources {
const Resources._();
static const String assetsCaptchaHtml = 'assets/captcha.html';
@@ -156,6 +158,8 @@ class Resources {
static const String assetsImagesEditImageUndoSvg =
'assets/images/edit_image_undo.svg';
+ static const String assetsImagesEllipsisSvg = 'assets/images/ellipsis.svg';
+
static const String assetsImagesEmojiAnimalSvg =
'assets/images/emoji_animal.svg';
diff --git a/lib/widgets/message/item/image/image_preview_page.dart b/lib/widgets/message/item/image/image_preview_page.dart
index 53b571c27b..6a7980e3ab 100644
--- a/lib/widgets/message/item/image/image_preview_page.dart
+++ b/lib/widgets/message/item/image/image_preview_page.dart
@@ -18,6 +18,7 @@ import '../../../action_button.dart';
import '../../../avatar_view/avatar_view.dart';
import '../../../image.dart';
import '../../../interactive_decorated_box.dart';
+import '../../../menu.dart';
import '../../../toast.dart';
import '../../../user_selector/conversation_selector.dart';
import '../../message.dart';
@@ -356,83 +357,160 @@ class _Bar extends StatelessWidget {
),
],
),
- const Spacer(),
- ActionButton(
- name: Resources.assetsImagesZoomInSvg,
- color: context.theme.icon,
- size: 20,
- onTap: controller.zoomIn,
- ),
- const SizedBox(width: 14),
- ActionButton(
- name: Resources.assetsImagesZoomOutSvg,
- size: 20,
- color: context.theme.icon,
- onTap: controller.zoomOut,
- ),
- const SizedBox(width: 14),
- ActionButton(
- name: Resources.assetsImagesRotatoSvg,
- color: context.theme.icon,
- size: 20,
- onTap: controller.rotate,
- ),
- const SizedBox(width: 14),
- if (!isTranscriptPage)
- ActionButton(
- name: Resources.assetsImagesShareSvg,
- size: 20,
- color: context.theme.icon,
- onTap: () async {
- final accountServer = context.accountServer;
- final result = await showConversationSelector(
- context: context,
- singleSelect: true,
- title: context.l10n.forward,
- onlyContact: false,
- );
- if (result == null || result.isEmpty) return;
- await accountServer.forwardMessage(
- message.messageId,
- result.first.encryptCategory!,
- conversationId: result.first.conversationId,
- recipientId: result.first.userId,
- );
- },
- ),
- if (!isTranscriptPage) const SizedBox(width: 14),
- ActionButton(
- name: Resources.assetsImagesCopySvg,
- color: context.theme.icon,
- size: 20,
- onTap: () => _copyUrl(
- context,
- context.accountServer
- .convertMessageAbsolutePath(message, isTranscriptPage)),
- ),
- const SizedBox(width: 14),
- ActionButton(
- name: Resources.assetsImagesAttachmentDownloadSvg,
- color: context.theme.icon,
- size: 20,
- onTap: () async {
- if (message.mediaUrl?.isEmpty ?? true) return;
- await saveAs(
- context, context.accountServer, message, isTranscriptPage);
- },
- ),
const SizedBox(width: 14),
- ActionButton(
- name: Resources.assetsImagesIcCloseBigSvg,
- color: context.theme.icon,
- size: 20,
- onTap: () => Navigator.pop(context),
+ _Action(
+ controller: controller,
+ isTranscriptPage: isTranscriptPage,
+ message: message,
),
const SizedBox(width: 24),
],
);
}
+class _Action extends StatelessWidget {
+ const _Action({
+ required this.controller,
+ required this.isTranscriptPage,
+ required this.message,
+ });
+
+ final TransformImageController controller;
+ final bool isTranscriptPage;
+ final MessageItem message;
+
+ static const _dividerWidth = 14.0;
+ static const _divider = SizedBox(width: _dividerWidth);
+ static const _width = 36;
+
+ @override
+ Widget build(BuildContext context) {
+ Future share() async {
+ final accountServer = context.accountServer;
+ final result = await showConversationSelector(
+ context: context,
+ singleSelect: true,
+ title: context.l10n.forward,
+ onlyContact: false,
+ );
+ if (result == null || result.isEmpty) return;
+ await accountServer.forwardMessage(
+ message.messageId,
+ result.first.encryptCategory!,
+ conversationId: result.first.conversationId,
+ recipientId: result.first.userId,
+ );
+ }
+
+ Future copy() => _copyUrl(
+ context,
+ context.accountServer
+ .convertMessageAbsolutePath(message, isTranscriptPage));
+
+ Future download() async {
+ if (message.mediaUrl?.isEmpty ?? true) return;
+ await saveAs(context, context.accountServer, message, isTranscriptPage);
+ }
+
+ final collapsible = [
+ if (!isTranscriptPage)
+ ActionButton(
+ name: Resources.assetsImagesShareSvg,
+ size: 20,
+ color: context.theme.icon,
+ onTap: share,
+ ),
+ ActionButton(
+ name: Resources.assetsImagesCopySvg,
+ color: context.theme.icon,
+ size: 20,
+ onTap: copy,
+ ),
+ ActionButton(
+ name: Resources.assetsImagesAttachmentDownloadSvg,
+ color: context.theme.icon,
+ size: 20,
+ onTap: download,
+ )
+ ];
+
+ final close = ActionButton(
+ name: Resources.assetsImagesIcCloseBigSvg,
+ color: context.theme.icon,
+ size: 20,
+ onTap: () => Navigator.pop(context),
+ );
+
+ final common = [
+ ActionButton(
+ name: Resources.assetsImagesZoomInSvg,
+ color: context.theme.icon,
+ size: 20,
+ onTap: controller.zoomIn,
+ ),
+ ActionButton(
+ name: Resources.assetsImagesZoomOutSvg,
+ size: 20,
+ color: context.theme.icon,
+ onTap: controller.zoomOut,
+ ),
+ ActionButton(
+ name: Resources.assetsImagesRotatoSvg,
+ color: context.theme.icon,
+ size: 20,
+ onTap: controller.rotate,
+ ),
+ ];
+
+ final menu = ContextMenuPortalEntry(
+ buildMenus: () => [
+ ContextMenu(
+ icon: Resources.assetsImagesShareSvg,
+ title: context.l10n.forward,
+ onTap: share,
+ ),
+ ContextMenu(
+ icon: Resources.assetsImagesCopySvg,
+ title: context.l10n.copy,
+ onTap: copy,
+ ),
+ ContextMenu(
+ icon: Resources.assetsImagesAttachmentDownloadSvg,
+ title: context.l10n.download,
+ onTap: download,
+ ),
+ ],
+ child: Builder(
+ builder: (context) => ActionButton(
+ name: Resources.assetsImagesEllipsisSvg,
+ color: context.theme.icon,
+ size: 20,
+ onTapUp: (event) => context.sendMenuPosition(event.globalPosition),
+ ),
+ ),
+ );
+
+ return Expanded(
+ child: LayoutBuilder(builder: (context, constraints) {
+ final count = common.length + collapsible.length + 1;
+ final collapsed = (count * _width + (count - 1) * _dividerWidth) >=
+ constraints.maxWidth;
+
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ ...common,
+ if (collapsed) menu,
+ close,
+ if (!collapsed) ...collapsible,
+ ].joinList(_divider),
+ );
+ }),
+ );
+ }
+}
+
class _Item extends HookWidget {
const _Item({
required this.message,