Compare commits

...

3 Commits

Author SHA1 Message Date
77bae4d6fd 💄 Optimize message action area 2025-10-09 00:00:25 +08:00
0a301c4c9b 💄 Confirm when deleting message 2025-10-08 23:54:59 +08:00
27b390a51c 💄 Hovering actions 2025-10-08 23:48:06 +08:00
2 changed files with 194 additions and 45 deletions

View File

@@ -48,6 +48,8 @@
"deletePublisherHint": "Are you sure to delete this publisher? This will also deleted all the post and collections under this publisher.",
"deletePost": "Delete Post",
"deletePostHint": "Are you sure to delete this post?",
"deleteMessage": "Delete Message",
"deleteMessageConfirmation": "Are you sure you want to delete this message?",
"copyLink": "Copy Link",
"postCreateAccountTitle": "Thanks for joining!",
"postCreateAccountNext": "What's next?",

View File

@@ -26,6 +26,7 @@ import 'package:island/widgets/post/post_shared.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/alert.dart';
class MessageItemAction {
static const String edit = "edit";
@@ -160,53 +161,85 @@ class MessageItem extends HookConsumerWidget {
? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.8)
: Colors.transparent;
return InkWell(
mouseCursor: MouseCursor.defer,
focusColor: Colors.transparent,
onLongPress: showActionMenu,
onSecondaryTap: showActionMenu,
onTap: () {
// Jump to related message
if (['messages.update', 'messages.delete'].contains(message.type) &&
message.meta['message_id'] is String &&
message.meta['message_id'] != null) {
onJump(message.meta['message_id']);
}
},
child: AnimatedContainer(
curve: Curves.easeInOut,
duration: const Duration(milliseconds: kFlashDuration),
decoration: BoxDecoration(color: flashColor),
child: switch (settings.messageDisplayStyle) {
'compact' => MessageItemDisplayIRC(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
final isHovered = useState(false);
return Stack(
clipBehavior: Clip.none,
children: [
InkWell(
mouseCursor: MouseCursor.defer,
focusColor: Colors.transparent,
onLongPress: showActionMenu,
onSecondaryTap: showActionMenu,
onTap: () {
// Jump to related message
if (['messages.update', 'messages.delete'].contains(message.type) &&
message.meta['message_id'] is String &&
message.meta['message_id'] != null) {
onJump(message.meta['message_id']);
}
},
child: Container(
width: double.infinity,
child: MouseRegion(
onEnter: (_) => isHovered.value = true,
onExit: (_) => isHovered.value = false,
child: AnimatedContainer(
curve: Curves.easeInOut,
duration: const Duration(milliseconds: kFlashDuration),
decoration: BoxDecoration(color: flashColor),
child: switch (settings.messageDisplayStyle) {
'compact' => MessageItemDisplayIRC(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
'column' => MessageItemDisplayDiscord(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
_ => MessageItemDisplayBubble(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
},
),
),
),
'column' => MessageItemDisplayDiscord(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
if (isHovered.value && !isMobile)
Positioned(
top: -15,
right: 15,
child: MouseRegion(
onEnter: (_) => isHovered.value = true,
onExit: (_) => isHovered.value = false,
child: MessageHoverActionMenu(
isCurrentUser: isCurrentUser,
onAction: onAction,
translatableLanguage: translatableLanguage,
translating: translating.value,
translatedText: translatedText.value,
translate: translate,
remoteMessage: remoteMessage,
),
),
),
_ => MessageItemDisplayBubble(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
},
),
],
);
}
}
@@ -317,6 +350,120 @@ class MessageActionSheet extends StatelessWidget {
}
}
class MessageHoverActionMenu extends StatelessWidget {
final bool isCurrentUser;
final Function(String action)? onAction;
final bool translatableLanguage;
final bool translating;
final String? translatedText;
final VoidCallback translate;
final dynamic remoteMessage;
const MessageHoverActionMenu({
super.key,
required this.isCurrentUser,
required this.onAction,
required this.translatableLanguage,
required this.translating,
required this.translatedText,
required this.translate,
required this.remoteMessage,
});
Future<void> _handleDelete(BuildContext context) async {
final confirmed = await showConfirmAlert(
'deleteMessageConfirmation'.tr(),
'deleteMessage'.tr(),
);
if (confirmed) {
onAction?.call(MessageItemAction.delete);
}
}
@override
Widget build(BuildContext context) {
// General actions (available for all users)
final generalActions = [
if (!isCurrentUser) // Hide reply for message author
IconButton(
icon: Icon(Symbols.reply, size: 16),
onPressed: () => onAction?.call(MessageItemAction.reply),
tooltip: 'reply'.tr(),
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
),
IconButton(
icon: Icon(Symbols.forward, size: 16),
onPressed: () => onAction?.call(MessageItemAction.forward),
tooltip: 'forward'.tr(),
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
),
if (translatableLanguage)
IconButton(
icon: Icon(Symbols.translate, size: 16),
onPressed: translate,
tooltip:
translatedText == null ? 'translate'.tr() : 'translated'.tr(),
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
),
];
// Author-only actions (edit/delete)
final authorActions = [
if (isCurrentUser)
IconButton(
icon: Icon(Symbols.edit, size: 16),
onPressed: () => onAction?.call(MessageItemAction.edit),
tooltip: 'edit'.tr(),
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
),
if (isCurrentUser)
IconButton(
icon: Icon(Symbols.delete, size: 16, color: Colors.red),
onPressed: () => _handleDelete(context),
tooltip: 'delete'.tr(),
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
),
];
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// General actions (left side)
...generalActions,
// Separator (only if both general and author actions exist)
if (generalActions.isNotEmpty && authorActions.isNotEmpty)
Container(
width: 1,
height: 24,
color: Theme.of(context).colorScheme.outlineVariant,
margin: const EdgeInsets.symmetric(horizontal: 4),
),
// Author actions (right side)
...authorActions,
],
),
);
}
}
class MessageItemDisplayBubble extends HookConsumerWidget {
final LocalChatMessage message;
final bool isCurrentUser;