// Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { FC, ReactNode } from 'react'; import React, { memo, useCallback, useMemo, useState } from 'react'; import { AxoContextMenu } from '../../axo/AxoContextMenu.dom.js'; import type { LocalizerType } from '../../types/I18N.std.js'; import type { ConversationType } from '../../state/ducks/conversations.preload.js'; import { isConversationUnread } from '../../util/isConversationUnread.std.js'; import { Environment, getEnvironment, isMockEnvironment, } from '../../environment.std.js'; import { isAlpha } from '../../util/version.std.js'; import { drop } from '../../util/drop.std.js'; import { DeleteMessagesConfirmationDialog } from '../DeleteMessagesConfirmationDialog.dom.js'; import { getMuteOptions } from '../../util/getMuteOptions.std.js'; import { CHAT_FOLDER_DEFAULTS, ChatFolderType, isConversationInChatFolder, } from '../../types/ChatFolder.std.js'; import type { ChatFolderParams, ChatFolder, ChatFolderId, } from '../../types/ChatFolder.std.js'; import { CurrentChatFolders } from '../../types/CurrentChatFolders.std.js'; import { strictAssert } from '../../util/assert.std.js'; import { UserText } from '../UserText.dom.js'; import { isConversationMuted } from '../../util/isConversationMuted.std.js'; function isEnabled() { const env = getEnvironment(); if ( env === Environment.Development || env === Environment.Test || isMockEnvironment() ) { return true; } const version = window.getVersion?.(); if (version != null) { if (isAlpha(version)) { return true; } } return false; } export type ChatFolderToggleChat = ( chatFolderId: ChatFolderId, conversationId: string, toggle: boolean ) => void; export type LeftPaneConversationListItemContextMenuProps = Readonly<{ i18n: LocalizerType; conversation: ConversationType; selectedChatFolder: ChatFolder | null; currentChatFolders: CurrentChatFolders; isActivelySearching: boolean; onMarkUnread: (conversationId: string) => void; onMarkRead: (conversationId: string) => void; onPin: (conversationId: string) => void; onUnpin: (conversationId: string) => void; onUpdateMute: (conversationId: string, muteExpiresAt: number) => void; onArchive: (conversationId: string) => void; onUnarchive: (conversationId: string) => void; onDelete: (conversationId: string) => void; onChatFolderOpenCreatePage: (initChatFolderParams: ChatFolderParams) => void; onChatFolderToggleChat: ChatFolderToggleChat; localDeleteWarningShown: boolean; setLocalDeleteWarningShown: () => void; children: ReactNode; }>; export const LeftPaneConversationListItemContextMenu: FC = memo(function ConversationListItemContextMenu(props) { const { i18n, conversation, selectedChatFolder, onMarkUnread, onMarkRead, onPin, onUnpin, onUpdateMute, onArchive, onUnarchive, onDelete, onChatFolderOpenCreatePage, onChatFolderToggleChat, } = props; const { id: conversationId, muteExpiresAt } = conversation; const selectedChatFolderId = selectedChatFolder?.id ?? null; const isSelectedChatFolderAllChats = useMemo(() => { return ( selectedChatFolder == null || selectedChatFolder.folderType === ChatFolderType.ALL ); }, [selectedChatFolder]); const muteOptions = useMemo(() => { return getMuteOptions(muteExpiresAt, i18n); }, [muteExpiresAt, i18n]); const [showConfirmDeleteDialog, setShowConfirmDeleteDialog] = useState(false); const handleOpenConfirmDeleteDialog = useCallback(() => { setShowConfirmDeleteDialog(true); }, []); const handleCloseConfirmDeleteDialog = useCallback(() => { setShowConfirmDeleteDialog(false); }, []); const isUnread = useMemo(() => { return isConversationUnread(conversation); }, [conversation]); const handleMarkUnread = useCallback(() => { onMarkUnread(conversationId); }, [onMarkUnread, conversationId]); const handleMarkRead = useCallback(() => { onMarkRead(conversationId); }, [onMarkRead, conversationId]); const handlePin = useCallback(() => { onPin(conversationId); }, [onPin, conversationId]); const handleUnpin = useCallback(() => { onUnpin(conversationId); }, [onUnpin, conversationId]); const handleUpdateMute = useCallback( (value: number) => { onUpdateMute(conversationId, value); }, [onUpdateMute, conversationId] ); const handleArchive = useCallback(() => { onArchive(conversationId); }, [onArchive, conversationId]); const handleUnarchive = useCallback(() => { onUnarchive(conversationId); }, [onUnarchive, conversationId]); const handleDelete = useCallback(() => { onDelete(conversationId); }, [onDelete, conversationId]); const handleChatFolderCreateNew = useCallback(() => { onChatFolderOpenCreatePage({ ...CHAT_FOLDER_DEFAULTS, includedConversationIds: [conversationId], }); }, [onChatFolderOpenCreatePage, conversationId]); const handleChatFolderRemoveChat = useCallback(() => { strictAssert( selectedChatFolderId != null, 'Missing selectedChatFolderId' ); onChatFolderToggleChat(selectedChatFolderId, conversationId, false); }, [onChatFolderToggleChat, selectedChatFolderId, conversationId]); return ( <> {props.children} {isUnread && ( {i18n('icu:markRead')} )} {!isUnread && !conversation.markedUnread && ( {i18n('icu:markUnread')} )} {!conversation.isPinned && ( {i18n('icu:pinConversation')} )} {conversation.isPinned && ( {i18n('icu:unpinConversation')} )} {i18n('icu:muteNotificationsTitle')} {muteOptions.map(muteOption => { return ( {muteOption.name} ); })} {!props.isActivelySearching && isSelectedChatFolderAllChats && props.currentChatFolders.hasAnyCurrentCustomChatFolders && ( {i18n( 'icu:LeftPane__ConversationListItem__ContextMenu__ChatFolderToggleChatsItem' )} {i18n( 'icu:LeftPane__ConversationListItem__ContextMenu__ChatFolderToggleChatsMenu__CreateFolderItem' )} )} {!props.isActivelySearching && !isSelectedChatFolderAllChats && ( {i18n( 'icu:LeftPane__ConversationListItem__ContextMenu__ChatFolderRemoveChatItem' )} )} {!conversation.isArchived && ( {i18n('icu:archiveConversation')} )} {conversation.isArchived && ( {i18n('icu:moveConversationToInbox')} )} {i18n('icu:deleteConversation')} {isEnabled() && ( <> Internal Copy Conversation ID {conversation.serviceId != null && ( Copy Service ID )} {conversation.pni != null && ( Copy PNI )} {conversation.groupId != null && ( Copy Group ID )} {conversation.e164 != null && ( Copy E164 )} )} {showConfirmDeleteDialog && ( )} ); }); function ContextMenuMuteNotificationsItem(props: { disabled?: boolean; value: number; onSelect: (value: number) => void; children: ReactNode; }): JSX.Element { const { value, onSelect } = props; const handleSelect = useCallback(() => { onSelect(value); }, [onSelect, value]); return ( {props.children} ); } function ContextMenuCopyTextItem(props: { value: string; children: ReactNode; }): JSX.Element { const { value } = props; const handleSelect = useCallback((): void => { drop(window.navigator.clipboard.writeText(value)); }, [value]); return ( {props.children} ); } function ContextMenuChatFolderToggleChatsSubMenu(props: { currentChatFolders: CurrentChatFolders; conversation: ConversationType; onChatFolderToggleChat: ChatFolderToggleChat; }) { const { currentChatFolders } = props; const sortedAndFilteredChatFolders = useMemo(() => { return CurrentChatFolders.toSortedArray(currentChatFolders).filter( chatFolder => { return chatFolder.folderType === ChatFolderType.CUSTOM; } ); }, [currentChatFolders]); return ( <> {sortedAndFilteredChatFolders.map(chatFolder => { return ( ); })} ); } function ContextMenuChatFolderToggleChatItem(props: { chatFolder: ChatFolder; conversation: ConversationType; onChatFolderToggleChat: ChatFolderToggleChat; }) { const { chatFolder, conversation, onChatFolderToggleChat } = props; const chatFolderId = chatFolder.id; const conversationId = conversation.id; const checked = useMemo(() => { return isConversationInChatFolder(chatFolder, conversation, { ignoreShowOnlyUnread: true, ignoreShowMutedChats: true, }); }, [chatFolder, conversation]); const isExcludedByMute = useMemo(() => { return !chatFolder.showMutedChats && isConversationMuted(conversation); }, [chatFolder, conversation]); const handleCheckedChange = useCallback( (value: boolean) => { onChatFolderToggleChat(chatFolderId, conversationId, value); }, [onChatFolderToggleChat, chatFolderId, conversationId] ); return ( ); }