338 lines
10 KiB
TypeScript
338 lines
10 KiB
TypeScript
"use client"
|
|
|
|
import {
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
type DragEvent,
|
|
type MouseEvent,
|
|
} from "react"
|
|
import { useEmailDrag } from "@/lib/drag-context"
|
|
import type { EmailListData } from "@/components/gmail/email-list/hooks/use-email-list-data"
|
|
import type { EmailListLabels } from "@/components/gmail/email-list/hooks/use-email-list-labels"
|
|
|
|
export function useEmailListSelection(
|
|
data: EmailListData,
|
|
labels: EmailListLabels
|
|
) {
|
|
const {
|
|
selectedFolder,
|
|
isViewMode,
|
|
isXs,
|
|
touchNav,
|
|
pageIds,
|
|
listEmails,
|
|
effectiveRead,
|
|
effectiveStarred,
|
|
readOverrides,
|
|
allEmails,
|
|
setReadOverrides,
|
|
mailActions,
|
|
} = data
|
|
|
|
const { moveEmailsToTarget } = labels
|
|
|
|
const { beginDrag, registerOnDrop } = useEmailDrag()
|
|
|
|
const [selectedEmails, setSelectedEmails] = useState<string[]>([])
|
|
const rowContextMenuOpenedAtRef = useRef(0)
|
|
const contextMenuTargetIdsRef = useRef<string[]>([])
|
|
const lastSelectionAnchorIdRef = useRef<string | null>(null)
|
|
const [bulkSelectMenuOpen, setBulkSelectMenuOpen] = useState(false)
|
|
const [mobileSelectionMode, setMobileSelectionMode] = useState(false)
|
|
const [mobileXsMoreMenuOpen, setMobileXsMoreMenuOpen] = useState(false)
|
|
const [mobileXsMoveSheetOpen, setMobileXsMoveSheetOpen] = useState(false)
|
|
const [mobileXsLabelSheetOpen, setMobileXsLabelSheetOpen] = useState(false)
|
|
const [swipeLabelEmailId, setSwipeLabelEmailId] = useState<string | null>(null)
|
|
const [openSwipeRowId, setOpenSwipeRowId] = useState<string | null>(null)
|
|
|
|
const touchListSwipeEnabled = touchNav && !mobileSelectionMode && !isViewMode
|
|
|
|
useEffect(() => {
|
|
setMobileSelectionMode(false)
|
|
setSelectedEmails([])
|
|
}, [selectedFolder, data.inboxTab])
|
|
|
|
useEffect(() => {
|
|
if (!openSwipeRowId) return
|
|
const handler = (e: globalThis.TouchEvent) => {
|
|
const target = e.target as HTMLElement | null
|
|
if (!target) return
|
|
const swipeRow = target.closest(`[data-swipe-row-id="${openSwipeRowId}"]`)
|
|
if (!swipeRow) setOpenSwipeRowId(null)
|
|
}
|
|
document.addEventListener("touchstart", handler, { passive: true })
|
|
return () => document.removeEventListener("touchstart", handler)
|
|
}, [openSwipeRowId])
|
|
|
|
const openMobileXsMoveSheet = useCallback(() => {
|
|
setMobileXsMoreMenuOpen(false)
|
|
window.setTimeout(() => setMobileXsMoveSheetOpen(true), 0)
|
|
}, [])
|
|
|
|
const handleMobileXsMoveSheetOpenChange = useCallback((open: boolean) => {
|
|
setMobileXsMoveSheetOpen(open)
|
|
if (!open) {
|
|
setMobileSelectionMode(false)
|
|
setSelectedEmails([])
|
|
}
|
|
}, [])
|
|
|
|
const openMobileXsLabelSheet = useCallback(() => {
|
|
setMobileXsMoreMenuOpen(false)
|
|
setSwipeLabelEmailId(null)
|
|
window.setTimeout(() => setMobileXsLabelSheetOpen(true), 0)
|
|
}, [])
|
|
|
|
const handleLabelSheetOpenChange = useCallback((open: boolean) => {
|
|
setMobileXsLabelSheetOpen(open)
|
|
if (!open) setSwipeLabelEmailId(null)
|
|
}, [])
|
|
|
|
const selectedOnPageCount = useMemo(
|
|
() => pageIds.filter((id) => selectedEmails.includes(id)).length,
|
|
[pageIds, selectedEmails]
|
|
)
|
|
const allPageSelected = pageIds.length > 0 && selectedOnPageCount === pageIds.length
|
|
const somePageSelected = selectedOnPageCount > 0 && !allPageSelected
|
|
const selectAllChecked: boolean | "indeterminate" = allPageSelected
|
|
? true
|
|
: somePageSelected
|
|
? "indeterminate"
|
|
: false
|
|
|
|
const toggleStar = (id: string) => {
|
|
mailActions.toggleStar(id)
|
|
}
|
|
|
|
const toggleImportant = (id: string) => {
|
|
mailActions.toggleImportant(id)
|
|
}
|
|
|
|
const toggleSelect = (id: string) => {
|
|
setSelectedEmails(prev =>
|
|
prev.includes(id) ? prev.filter(e => e !== id) : [...prev, id]
|
|
)
|
|
}
|
|
|
|
const selectRangeInclusive = (fromId: string, toId: string) => {
|
|
const ids = pageIds
|
|
const i0 = ids.indexOf(fromId)
|
|
const i1 = ids.indexOf(toId)
|
|
if (i0 === -1 || i1 === -1) return
|
|
const lo = Math.min(i0, i1)
|
|
const hi = Math.max(i0, i1)
|
|
const range = ids.slice(lo, hi + 1)
|
|
setSelectedEmails((prev) => [...new Set([...prev, ...range])])
|
|
}
|
|
|
|
const handleSelectAllChange = (checked: boolean | "indeterminate") => {
|
|
if (checked === true) {
|
|
setSelectedEmails((prev) => [...new Set([...prev, ...pageIds])])
|
|
} else {
|
|
setSelectedEmails((prev) => prev.filter((id) => !pageIds.includes(id)))
|
|
}
|
|
}
|
|
|
|
const mergePageSelection = (subsetOfPageIds: string[]) => {
|
|
setSelectedEmails((prev) => {
|
|
const outsidePage = prev.filter((id) => !pageIds.includes(id))
|
|
return [...new Set([...outsidePage, ...subsetOfPageIds])]
|
|
})
|
|
}
|
|
|
|
const selectMenuAll = () => mergePageSelection(pageIds)
|
|
const selectMenuNone = () =>
|
|
setSelectedEmails((prev) => prev.filter((id) => !pageIds.includes(id)))
|
|
const selectMenuRead = () =>
|
|
mergePageSelection(
|
|
listEmails.filter((e) => effectiveRead(e)).map((e) => e.id)
|
|
)
|
|
const selectMenuUnread = () =>
|
|
mergePageSelection(
|
|
listEmails.filter((e) => !effectiveRead(e)).map((e) => e.id)
|
|
)
|
|
const selectMenuStarred = () =>
|
|
mergePageSelection(
|
|
listEmails.filter((e) => effectiveStarred(e)).map((e) => e.id)
|
|
)
|
|
const selectMenuUnstarred = () =>
|
|
mergePageSelection(
|
|
listEmails.filter((e) => !effectiveStarred(e)).map((e) => e.id)
|
|
)
|
|
|
|
const handleRowCheckboxClickCapture = (id: string, e: MouseEvent) => {
|
|
if (e.shiftKey && lastSelectionAnchorIdRef.current != null) {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
selectRangeInclusive(lastSelectionAnchorIdRef.current, id)
|
|
lastSelectionAnchorIdRef.current = id
|
|
}
|
|
}
|
|
|
|
const bulkTargetIds = useMemo(
|
|
() => pageIds.filter((id) => selectedEmails.includes(id)),
|
|
[pageIds, selectedEmails]
|
|
)
|
|
const hasUnreadInSelection = useMemo(() => {
|
|
for (const id of bulkTargetIds) {
|
|
const email = allEmails.find((e) => e.id === id)
|
|
if (!email) continue
|
|
const isRead =
|
|
readOverrides[id] !== undefined ? readOverrides[id]! : email.read
|
|
if (!isRead) return true
|
|
}
|
|
return false
|
|
}, [bulkTargetIds, readOverrides, allEmails])
|
|
const showBulkToolbar = bulkTargetIds.length > 0
|
|
|
|
const labelSheetTargetIds = useMemo(
|
|
() => (swipeLabelEmailId ? [swipeLabelEmailId] : bulkTargetIds),
|
|
[swipeLabelEmailId, bulkTargetIds]
|
|
)
|
|
|
|
const clearBulkSelection = (ids: string[]) => {
|
|
setSelectedEmails((prev) => prev.filter((id) => !ids.includes(id)))
|
|
}
|
|
|
|
const bulkHideFromList = (ids: string[]) => {
|
|
if (ids.length === 0) return
|
|
mailActions.hideEmails(ids)
|
|
clearBulkSelection(ids)
|
|
}
|
|
|
|
const bulkArchive = () => bulkHideFromList(bulkTargetIds)
|
|
const bulkDelete = () => bulkHideFromList(bulkTargetIds)
|
|
const bulkSpam = () => bulkHideFromList(bulkTargetIds)
|
|
|
|
const handleEmailsDroppedOnTarget = useCallback(
|
|
(targetId: string, _targetLabel: string, ids: string[]) => {
|
|
if (ids.length === 0) return
|
|
moveEmailsToTarget(ids, targetId)
|
|
setSelectedEmails((prev) => prev.filter((id) => !ids.includes(id)))
|
|
},
|
|
[moveEmailsToTarget]
|
|
)
|
|
|
|
useEffect(() => {
|
|
return registerOnDrop(handleEmailsDroppedOnTarget)
|
|
}, [registerOnDrop, handleEmailsDroppedOnTarget])
|
|
|
|
const startRowDrag = useCallback(
|
|
(rowId: string, e: DragEvent<HTMLDivElement>) => {
|
|
if (isXs) return
|
|
const inSelection = selectedEmails.includes(rowId)
|
|
const ids =
|
|
inSelection && bulkTargetIds.length > 0 ? bulkTargetIds : [rowId]
|
|
if (e.dataTransfer) {
|
|
e.dataTransfer.effectAllowed = "move"
|
|
try {
|
|
e.dataTransfer.setData("text/plain", ids.join(","))
|
|
} catch {
|
|
/* some browsers throw if called outside dragstart context */
|
|
}
|
|
const ghost = document.createElement("div")
|
|
ghost.style.position = "fixed"
|
|
ghost.style.top = "-1000px"
|
|
ghost.style.left = "-1000px"
|
|
ghost.style.width = "1px"
|
|
ghost.style.height = "1px"
|
|
ghost.style.opacity = "0"
|
|
document.body.appendChild(ghost)
|
|
e.dataTransfer.setDragImage(ghost, 0, 0)
|
|
window.setTimeout(() => {
|
|
if (ghost.parentNode) ghost.parentNode.removeChild(ghost)
|
|
}, 0)
|
|
}
|
|
beginDrag(ids, selectedFolder, e.clientX, e.clientY)
|
|
},
|
|
[beginDrag, isXs, selectedEmails, bulkTargetIds, selectedFolder]
|
|
)
|
|
|
|
const bulkMarkRead = () => {
|
|
if (bulkTargetIds.length === 0) return
|
|
setReadOverrides((prev) => {
|
|
const next = { ...prev }
|
|
for (const id of bulkTargetIds) next[id] = true
|
|
return next
|
|
})
|
|
}
|
|
|
|
const bulkMarkUnread = () => {
|
|
if (bulkTargetIds.length === 0) return
|
|
setReadOverrides((prev) => {
|
|
const next = { ...prev }
|
|
for (const id of bulkTargetIds) next[id] = false
|
|
return next
|
|
})
|
|
}
|
|
|
|
const bulkMoveTo = useCallback(
|
|
(targetId: string) => {
|
|
if (bulkTargetIds.length === 0) return
|
|
moveEmailsToTarget(bulkTargetIds, targetId)
|
|
if (targetId !== "inbox") {
|
|
setSelectedEmails((prev) => prev.filter((id) => !bulkTargetIds.includes(id)))
|
|
}
|
|
},
|
|
[bulkTargetIds, moveEmailsToTarget]
|
|
)
|
|
|
|
const openSwipeRowLabelSheet = useCallback((emailId: string) => {
|
|
setSwipeLabelEmailId(emailId)
|
|
setMobileXsLabelSheetOpen(true)
|
|
}, [])
|
|
|
|
return {
|
|
selectedEmails,
|
|
setSelectedEmails,
|
|
rowContextMenuOpenedAtRef,
|
|
contextMenuTargetIdsRef,
|
|
lastSelectionAnchorIdRef,
|
|
bulkSelectMenuOpen,
|
|
setBulkSelectMenuOpen,
|
|
mobileSelectionMode,
|
|
setMobileSelectionMode,
|
|
mobileXsMoreMenuOpen,
|
|
setMobileXsMoreMenuOpen,
|
|
mobileXsMoveSheetOpen,
|
|
mobileXsLabelSheetOpen,
|
|
swipeLabelEmailId,
|
|
openSwipeRowId,
|
|
setOpenSwipeRowId,
|
|
touchListSwipeEnabled,
|
|
openMobileXsMoveSheet,
|
|
handleMobileXsMoveSheetOpenChange,
|
|
openMobileXsLabelSheet,
|
|
handleLabelSheetOpenChange,
|
|
selectAllChecked,
|
|
handleSelectAllChange,
|
|
selectMenuAll,
|
|
selectMenuNone,
|
|
selectMenuRead,
|
|
selectMenuUnread,
|
|
selectMenuStarred,
|
|
selectMenuUnstarred,
|
|
toggleStar,
|
|
toggleImportant,
|
|
toggleSelect,
|
|
handleRowCheckboxClickCapture,
|
|
bulkTargetIds,
|
|
hasUnreadInSelection,
|
|
showBulkToolbar,
|
|
labelSheetTargetIds,
|
|
bulkArchive,
|
|
bulkDelete,
|
|
bulkSpam,
|
|
bulkMarkRead,
|
|
bulkMarkUnread,
|
|
bulkMoveTo,
|
|
startRowDrag,
|
|
openSwipeRowLabelSheet,
|
|
}
|
|
}
|
|
|
|
export type EmailListSelection = ReturnType<typeof useEmailListSelection>
|