// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { KeyboardEvent, MouseEvent } from 'react'; import React, { useRef, useState } from 'react'; import classNames from 'classnames'; import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu'; import { ConfirmationDialog } from './ConfirmationDialog'; import { CustomColorEditor } from './CustomColorEditor'; import { Modal } from './Modal'; import type { ConversationColorType, CustomColorType } from '../types/Colors'; import { ConversationColors } from '../types/Colors'; import type { ConversationType } from '../state/ducks/conversations'; import type { LocalizerType } from '../types/Util'; import { SampleMessageBubbles } from './SampleMessageBubbles'; import { PanelRow } from './conversation/conversation-details/PanelRow'; import { getCustomColorStyle } from '../util/getCustomColorStyle'; import { useDelayedRestoreFocus } from '../hooks/useRestoreFocus'; type CustomColorDataType = { id?: string; value?: CustomColorType; }; export type PropsDataType = { conversationId?: string; customColors?: Record; getConversationsWithCustomColor: ( colorId: string ) => Promise>; i18n: LocalizerType; isGlobal?: boolean; selectedColor?: ConversationColorType; selectedCustomColor: CustomColorDataType; }; type PropsActionType = { addCustomColor: (color: CustomColorType, conversationId?: string) => unknown; colorSelected: (payload: { conversationId: string; conversationColor?: ConversationColorType; customColorData?: { id: string; value: CustomColorType; }; }) => unknown; editCustomColor: (colorId: string, color: CustomColorType) => unknown; removeCustomColor: (colorId: string) => unknown; removeCustomColorOnConversations: (colorId: string) => unknown; resetAllChatColors: () => unknown; resetDefaultChatColor: () => unknown; setGlobalDefaultConversationColor: ( color: ConversationColorType, customColorData?: { id: string; value: CustomColorType; } ) => unknown; }; export type PropsType = PropsDataType & PropsActionType; export const ChatColorPicker = ({ addCustomColor, colorSelected, conversationId, customColors = {}, editCustomColor, getConversationsWithCustomColor, i18n, isGlobal = false, removeCustomColor, removeCustomColorOnConversations, resetAllChatColors, resetDefaultChatColor, selectedColor = ConversationColors[0], selectedCustomColor, setGlobalDefaultConversationColor, }: PropsType): JSX.Element => { const [confirmResetAll, setConfirmResetAll] = useState(false); const [confirmResetWhat, setConfirmResetWhat] = useState(false); const [customColorToEdit, setCustomColorToEdit] = useState< CustomColorDataType | undefined >(undefined); const [focusRef] = useDelayedRestoreFocus(); const onSelectColor = ( conversationColor: ConversationColorType, customColorData?: { id: string; value: CustomColorType } ): void => { if (conversationId) { colorSelected({ conversationId, conversationColor, customColorData, }); } else { setGlobalDefaultConversationColor(conversationColor, customColorData); } }; const renderCustomColorEditorWrapper = () => ( setCustomColorToEdit(undefined)} onSave={(color: CustomColorType) => { if (customColorToEdit?.id) { editCustomColor(customColorToEdit.id, color); onSelectColor('custom', { id: customColorToEdit.id, value: color, }); } else { addCustomColor(color, conversationId); } }} /> ); return (
{customColorToEdit ? renderCustomColorEditorWrapper() : null} {confirmResetWhat ? ( { resetDefaultChatColor(); resetAllChatColors(); }, style: 'affirmative', text: i18n('ChatColorPicker__resetAll'), }, ]} i18n={i18n} onClose={() => { setConfirmResetWhat(false); }} title={i18n('ChatColorPicker__resetDefault')} > {i18n('ChatColorPicker__confirm-reset-message')} ) : null} {confirmResetAll ? ( { setConfirmResetAll(false); }} title={i18n('ChatColorPicker__resetAll')} > {i18n('ChatColorPicker__confirm-reset-message')} ) : null}
{ConversationColors.map((color, i) => (
onSelectColor(color)} onKeyDown={(ev: KeyboardEvent) => { if (ev.key === 'Enter') { onSelectColor(color); } }} role="button" tabIndex={0} ref={i === 0 ? focusRef : undefined} /> ))} {Object.keys(customColors).map(colorId => { const colorValues = customColors[colorId]; return ( { onSelectColor('custom', { id: colorId, value: colorValues, }); }} onDelete={() => { removeCustomColor(colorId); removeCustomColorOnConversations(colorId); }} onDupe={() => { addCustomColor(colorValues, conversationId); }} onEdit={() => { setCustomColorToEdit({ id: colorId, value: colorValues }); }} /> ); })}
setCustomColorToEdit({ id: undefined, value: undefined }) } onKeyDown={(ev: KeyboardEvent) => { if (ev.key === 'Enter') { setCustomColorToEdit({ id: undefined, value: undefined }); } }} role="button" tabIndex={0} >

{conversationId ? ( { colorSelected({ conversationId }); }} /> ) : null} { if (isGlobal) { setConfirmResetWhat(true); } else { setConfirmResetAll(true); } }} />
); }; type CustomColorBubblePropsType = { color: CustomColorType; colorId: string; getConversationsWithCustomColor: ( colorId: string ) => Promise>; i18n: LocalizerType; isSelected: boolean; onDelete: () => unknown; onDupe: () => unknown; onEdit: () => unknown; onChoose: () => unknown; }; const CustomColorBubble = ({ color, colorId, getConversationsWithCustomColor, i18n, isSelected, onDelete, onDupe, onEdit, onChoose, }: CustomColorBubblePropsType): JSX.Element => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const menuRef = useRef(null); const [confirmDeleteCount, setConfirmDeleteCount] = useState< number | undefined >(undefined); const handleClick = (ev: KeyboardEvent | MouseEvent) => { if (!isSelected) { onChoose(); return; } if (menuRef && menuRef.current) { menuRef.current.handleContextClick(ev); } }; const bubble = (
{ if (ev.key === 'Enter') { handleClick(ev); } }} role="button" tabIndex={0} style={{ ...getCustomColorStyle(color), }} /> ); return ( <> {confirmDeleteCount ? ( { setConfirmDeleteCount(undefined); }} title={i18n('ChatColorPicker__delete--title')} > {i18n('ChatColorPicker__delete--message', [ String(confirmDeleteCount), ])} ) : null} {isSelected ? ( {bubble} ) : ( bubble )} { event.stopPropagation(); event.preventDefault(); onEdit(); }} > {i18n('ChatColorPicker__context--edit')} { event.stopPropagation(); event.preventDefault(); onDupe(); }} > {i18n('ChatColorPicker__context--duplicate')} { event.stopPropagation(); event.preventDefault(); const conversations = await getConversationsWithCustomColor( colorId ); if (!conversations.length) { onDelete(); } else { setConfirmDeleteCount(conversations.length); } }} > {i18n('ChatColorPicker__context--delete')} ); }; type CustomColorEditorWrapperPropsType = { customColorToEdit?: CustomColorDataType; i18n: LocalizerType; onClose: () => unknown; onSave: (color: CustomColorType) => unknown; }; const CustomColorEditorWrapper = ({ customColorToEdit, i18n, onClose, onSave, }: CustomColorEditorWrapperPropsType): JSX.Element => { const editor = ( ); return ( {editor} ); };