feat: message list message menu

Signed-off-by: Sebastian <krupinski01@gmail.com>
This commit is contained in:
2026-05-30 18:30:43 -04:00
parent ec776bbb78
commit d24e6110b9
3 changed files with 256 additions and 17 deletions
+113 -17
View File
@@ -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<Props>(), {
// 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<number | null>(null)
const longPressActivated = ref(false)
const suppressNextClick = ref(false)
const contextMenuVisible = ref(false)
const contextMenuTarget = ref<[number, number] | undefined>(undefined)
const contextMenuMessage = ref<EntityObject | null>(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(() => {
</v-list-item-subtitle>
<template v-slot:append>
<div class="d-flex flex-column align-center">
<v-icon
v-if="message.properties.isFlagged"
size="small"
color="warning"
class="mb-1"
>
mdi-star
</v-icon>
<v-icon
v-if="message.properties.hasAttachments"
size="small"
color="grey"
>
mdi-paperclip
</v-icon>
<div class="message-item-append">
<div class="d-flex flex-column align-center">
<v-icon
v-if="message.properties.isFlagged"
size="small"
color="warning"
class="mb-1"
>
mdi-star
</v-icon>
<v-icon
v-if="message.properties.hasAttachments"
size="small"
color="grey"
>
mdi-paperclip
</v-icon>
</div>
<MessageListItemMenu
:message="message"
@reply="handleContextMenuReply"
@forward="handleContextMenuForward"
@move="handleContextMenuMove"
@delete="handleContextMenuDelete"
@flag="handleContextMenuFlag"
/>
</div>
</template>
</v-list-item>
<v-divider />
</template>
</v-virtual-scroll>
<MessageListItemMenu
v-if="contextMenuMessage"
v-model="contextMenuVisible"
:message="getContextMenuMessage()"
:target="contextMenuTarget"
:show-activator="false"
@reply="handleContextMenuReply"
@forward="handleContextMenuForward"
@move="handleContextMenuMove"
@delete="handleContextMenuDelete"
@flag="handleContextMenuFlag"
/>
</div>
</template>
@@ -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);
}