From d24e6110b96a3375f1908451d442867e6576d8a1 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 30 May 2026 18:30:43 -0400 Subject: [PATCH] feat: message list message menu Signed-off-by: Sebastian --- src/components/MessageList.vue | 130 ++++++++++++++++++++--- src/components/MessageListItemMenu.vue | 138 +++++++++++++++++++++++++ src/pages/MailPage.vue | 5 + 3 files changed, 256 insertions(+), 17 deletions(-) create mode 100644 src/components/MessageListItemMenu.vue diff --git a/src/components/MessageList.vue b/src/components/MessageList.vue index f558021..6c76702 100644 --- a/src/components/MessageList.vue +++ b/src/components/MessageList.vue @@ -4,6 +4,7 @@ import type { EntityIdentifier } from '@MailManager/types/common' import type { EntityObject } from '@MailManager/models' import type { CollectionObject } from '@MailManager/models/collection' import RecipientDetails from '@/components/common/RecipientDetails.vue' +import MessageListItemMenu from '@/components/MessageListItemMenu.vue' // Props interface Props { @@ -24,6 +25,11 @@ const props = withDefaults(defineProps(), { // Emits const emit = defineEmits<{ open: [message: EntityObject] + reply: [message: EntityObject] + forward: [message: EntityObject] + move: [message: EntityObject] + delete: [message: EntityObject] + flag: [message: EntityObject, flag: string, value: boolean] selectionMode: [message: EntityObject] selectionToggleOne: [message: EntityObject] selectionToggleAll: [value: boolean] @@ -36,6 +42,9 @@ const emit = defineEmits<{ const longPressTimer = ref(null) const longPressActivated = ref(false) const suppressNextClick = ref(false) +const contextMenuVisible = ref(false) +const contextMenuTarget = ref<[number, number] | undefined>(undefined) +const contextMenuMessage = ref(null) const LONG_PRESS_MS = 400 const selectedIdSet = computed(() => new Set(props.selectionList)) const selectionCount = computed(() => props.selectionList.length ?? 0) @@ -117,7 +126,7 @@ const formatDate = (date: Date | string | number | null | undefined): string => } const isSelectionControlClick = (event: MouseEvent | KeyboardEvent): boolean => { - return event.target instanceof Element && event.target.closest('.message-selection-checkbox') !== null + return event.target instanceof Element && event.target.closest('.message-selection-checkbox, .message-item-menu-trigger, .message-item-menu-content') !== null } const handleSelectionToggleOne = (message: EntityObject) => { @@ -173,6 +182,43 @@ const handleMouseDown = (event: MouseEvent, message: EntityObject) => { emit('selectionMode', message) } +const openContextMenu = (event: MouseEvent, message: EntityObject) => { + if (isSelectionControlClick(event)) { + return + } + + event.preventDefault() + event.stopPropagation() + + contextMenuMessage.value = message + contextMenuTarget.value = [event.clientX, event.clientY] + contextMenuVisible.value = true +} + +const handleContextMenuReply = (message: EntityObject) => { + emit('reply', message) +} + +const handleContextMenuForward = (message: EntityObject) => { + emit('forward', message) +} + +const handleContextMenuMove = (message: EntityObject) => { + emit('move', message) +} + +const handleContextMenuDelete = (message: EntityObject) => { + emit('delete', message) +} + +const handleContextMenuFlag = (message: EntityObject, flag: string, value: boolean) => { + emit('flag', message, flag, value) +} + +const getContextMenuMessage = (): EntityObject => { + return contextMenuMessage.value as EntityObject +} + const handleTouchStart = (message: EntityObject) => { clearLongPressTimer() longPressActivated.value = false @@ -346,6 +392,7 @@ onBeforeUnmount(() => { }" @mousedown="handleMouseDown($event, message)" @click="handleMouseClick($event, message)" + @contextmenu="openContextMenu($event, message)" @touchstart.passive="handleTouchStart(message)" @touchend="handleTouchEnd" @touchcancel="handleTouchEnd" @@ -396,28 +443,52 @@ onBeforeUnmount(() => { + + @@ -502,6 +573,31 @@ onBeforeUnmount(() => { min-width: 72px; } +.message-item-append { + display: flex; + align-items: center; + gap: 4px; +} + +@media (hover: hover) { + .message-item :deep(.message-item-menu-trigger) { + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 0.2s ease, visibility 0.2s ease; + } + + .message-item:hover :deep(.message-item-menu-trigger), + .message-item:focus-within :deep(.message-item-menu-trigger), + .message-item.opened :deep(.message-item-menu-trigger), + .message-item.selected :deep(.message-item-menu-trigger), + .message-item :deep(.message-item-menu-trigger[aria-expanded='true']) { + opacity: 1; + visibility: visible; + pointer-events: auto; + } +} + .message-item:hover { background-color: rgba(var(--v-theme-on-surface), 0.04); } diff --git a/src/components/MessageListItemMenu.vue b/src/components/MessageListItemMenu.vue new file mode 100644 index 0000000..ab3c6c7 --- /dev/null +++ b/src/components/MessageListItemMenu.vue @@ -0,0 +1,138 @@ + + + \ No newline at end of file diff --git a/src/pages/MailPage.vue b/src/pages/MailPage.vue index 373b510..87e6259 100644 --- a/src/pages/MailPage.vue +++ b/src/pages/MailPage.vue @@ -297,6 +297,11 @@ const handleMessageSelectionDelete = () => mailUiStore.deleteSelectedMessages() :selection-mode="selectionMode" :loading="loading" @open="handleMessageOpen" + @reply="handleMessageComposeReply" + @forward="handleMessageComposeForward" + @move="handleMessageMove" + @delete="handleMessageDelete" + @flag="handleMessageFlag" @selection-mode="handleMessageSelectionMode" @selection-toggle-one="handleMessageSelectionToggleOne" @selection-toggle-all="handleMessageSelectionToggleAll"