2021-05-28 16:15:17 +00:00
|
|
|
// Copyright 2021 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { KeyboardEvent, MouseEvent } from 'react';
|
|
|
|
import React, { useRef, useState } from 'react';
|
2021-05-28 16:15:17 +00:00
|
|
|
import classNames from 'classnames';
|
|
|
|
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
|
|
|
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
|
|
|
import { CustomColorEditor } from './CustomColorEditor';
|
|
|
|
import { Modal } from './Modal';
|
2021-10-26 19:15:33 +00:00
|
|
|
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';
|
2021-05-28 16:15:17 +00:00
|
|
|
import { SampleMessageBubbles } from './SampleMessageBubbles';
|
|
|
|
import { PanelRow } from './conversation/conversation-details/PanelRow';
|
|
|
|
import { getCustomColorStyle } from '../util/getCustomColorStyle';
|
|
|
|
|
2021-10-25 19:11:19 +00:00
|
|
|
import { useDelayedRestoreFocus } from '../hooks/useRestoreFocus';
|
|
|
|
|
2021-05-28 16:15:17 +00:00
|
|
|
type CustomColorDataType = {
|
|
|
|
id?: string;
|
|
|
|
value?: CustomColorType;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type PropsDataType = {
|
2021-06-03 21:34:36 +00:00
|
|
|
conversationId?: string;
|
2021-05-28 16:15:17 +00:00
|
|
|
customColors?: Record<string, CustomColorType>;
|
2021-08-18 20:08:14 +00:00
|
|
|
getConversationsWithCustomColor: (
|
|
|
|
colorId: string
|
|
|
|
) => Promise<Array<ConversationType>>;
|
2021-05-28 16:15:17 +00:00
|
|
|
i18n: LocalizerType;
|
2021-06-02 21:05:09 +00:00
|
|
|
isGlobal?: boolean;
|
2021-05-28 16:15:17 +00:00
|
|
|
selectedColor?: ConversationColorType;
|
|
|
|
selectedCustomColor: CustomColorDataType;
|
|
|
|
};
|
|
|
|
|
|
|
|
type PropsActionType = {
|
2021-08-18 20:08:14 +00:00
|
|
|
addCustomColor: (color: CustomColorType, conversationId?: string) => unknown;
|
2021-06-03 21:34:36 +00:00
|
|
|
colorSelected: (payload: {
|
|
|
|
conversationId: string;
|
|
|
|
conversationColor?: ConversationColorType;
|
|
|
|
customColorData?: {
|
|
|
|
id: string;
|
|
|
|
value: CustomColorType;
|
|
|
|
};
|
|
|
|
}) => unknown;
|
2021-05-28 16:15:17 +00:00
|
|
|
editCustomColor: (colorId: string, color: CustomColorType) => unknown;
|
|
|
|
removeCustomColor: (colorId: string) => unknown;
|
|
|
|
removeCustomColorOnConversations: (colorId: string) => unknown;
|
|
|
|
resetAllChatColors: () => unknown;
|
2021-06-02 21:05:09 +00:00
|
|
|
resetDefaultChatColor: () => unknown;
|
2021-06-03 21:34:36 +00:00
|
|
|
setGlobalDefaultConversationColor: (
|
|
|
|
color: ConversationColorType,
|
|
|
|
customColorData?: {
|
|
|
|
id: string;
|
|
|
|
value: CustomColorType;
|
|
|
|
}
|
|
|
|
) => unknown;
|
2021-05-28 16:15:17 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
export type PropsType = PropsDataType & PropsActionType;
|
|
|
|
|
|
|
|
export const ChatColorPicker = ({
|
|
|
|
addCustomColor,
|
2021-06-03 21:34:36 +00:00
|
|
|
colorSelected,
|
|
|
|
conversationId,
|
2021-05-28 16:15:17 +00:00
|
|
|
customColors = {},
|
|
|
|
editCustomColor,
|
|
|
|
getConversationsWithCustomColor,
|
|
|
|
i18n,
|
2021-06-02 21:05:09 +00:00
|
|
|
isGlobal = false,
|
2021-05-28 16:15:17 +00:00
|
|
|
removeCustomColor,
|
|
|
|
removeCustomColorOnConversations,
|
|
|
|
resetAllChatColors,
|
2021-06-02 21:05:09 +00:00
|
|
|
resetDefaultChatColor,
|
2021-05-28 16:15:17 +00:00
|
|
|
selectedColor = ConversationColors[0],
|
|
|
|
selectedCustomColor,
|
2021-06-03 21:34:36 +00:00
|
|
|
setGlobalDefaultConversationColor,
|
2021-05-28 16:15:17 +00:00
|
|
|
}: PropsType): JSX.Element => {
|
|
|
|
const [confirmResetAll, setConfirmResetAll] = useState(false);
|
2021-06-02 21:05:09 +00:00
|
|
|
const [confirmResetWhat, setConfirmResetWhat] = useState(false);
|
2021-05-28 16:15:17 +00:00
|
|
|
const [customColorToEdit, setCustomColorToEdit] = useState<
|
|
|
|
CustomColorDataType | undefined
|
|
|
|
>(undefined);
|
|
|
|
|
2021-10-25 19:11:19 +00:00
|
|
|
const [focusRef] = useDelayedRestoreFocus();
|
|
|
|
|
2021-06-03 21:34:36 +00:00
|
|
|
const onSelectColor = (
|
|
|
|
conversationColor: ConversationColorType,
|
|
|
|
customColorData?: { id: string; value: CustomColorType }
|
|
|
|
): void => {
|
|
|
|
if (conversationId) {
|
|
|
|
colorSelected({
|
|
|
|
conversationId,
|
|
|
|
conversationColor,
|
|
|
|
customColorData,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
setGlobalDefaultConversationColor(conversationColor, customColorData);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-05-28 16:15:17 +00:00
|
|
|
const renderCustomColorEditorWrapper = () => (
|
|
|
|
<CustomColorEditorWrapper
|
|
|
|
customColorToEdit={customColorToEdit}
|
|
|
|
i18n={i18n}
|
|
|
|
onClose={() => setCustomColorToEdit(undefined)}
|
|
|
|
onSave={(color: CustomColorType) => {
|
|
|
|
if (customColorToEdit?.id) {
|
|
|
|
editCustomColor(customColorToEdit.id, color);
|
|
|
|
onSelectColor('custom', {
|
|
|
|
id: customColorToEdit.id,
|
|
|
|
value: color,
|
|
|
|
});
|
|
|
|
} else {
|
2021-08-18 20:08:14 +00:00
|
|
|
addCustomColor(color, conversationId);
|
2021-05-28 16:15:17 +00:00
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
|
|
|
|
return (
|
2021-06-01 23:37:12 +00:00
|
|
|
<div className="ChatColorPicker__container">
|
2021-05-28 16:15:17 +00:00
|
|
|
{customColorToEdit ? renderCustomColorEditorWrapper() : null}
|
2021-06-02 21:05:09 +00:00
|
|
|
{confirmResetWhat ? (
|
|
|
|
<ConfirmationDialog
|
2022-09-27 20:24:21 +00:00
|
|
|
dialogName="ChatColorPicker.confirmReset"
|
2021-06-02 21:05:09 +00:00
|
|
|
actions={[
|
|
|
|
{
|
|
|
|
action: resetDefaultChatColor,
|
|
|
|
style: 'affirmative',
|
|
|
|
text: i18n('ChatColorPicker__confirm-reset-default'),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
action: () => {
|
|
|
|
resetDefaultChatColor();
|
|
|
|
resetAllChatColors();
|
|
|
|
},
|
|
|
|
style: 'affirmative',
|
|
|
|
text: i18n('ChatColorPicker__resetAll'),
|
|
|
|
},
|
|
|
|
]}
|
|
|
|
i18n={i18n}
|
|
|
|
onClose={() => {
|
|
|
|
setConfirmResetWhat(false);
|
|
|
|
}}
|
|
|
|
title={i18n('ChatColorPicker__resetDefault')}
|
|
|
|
>
|
|
|
|
{i18n('ChatColorPicker__confirm-reset-message')}
|
|
|
|
</ConfirmationDialog>
|
|
|
|
) : null}
|
2021-05-28 16:15:17 +00:00
|
|
|
{confirmResetAll ? (
|
|
|
|
<ConfirmationDialog
|
2022-09-27 20:24:21 +00:00
|
|
|
dialogName="ChatColorPicker.confirmResetAll"
|
2021-05-28 16:15:17 +00:00
|
|
|
actions={[
|
|
|
|
{
|
|
|
|
action: resetAllChatColors,
|
|
|
|
style: 'affirmative',
|
|
|
|
text: i18n('ChatColorPicker__confirm-reset'),
|
|
|
|
},
|
|
|
|
]}
|
|
|
|
i18n={i18n}
|
|
|
|
onClose={() => {
|
|
|
|
setConfirmResetAll(false);
|
|
|
|
}}
|
|
|
|
title={i18n('ChatColorPicker__resetAll')}
|
|
|
|
>
|
|
|
|
{i18n('ChatColorPicker__confirm-reset-message')}
|
|
|
|
</ConfirmationDialog>
|
|
|
|
) : null}
|
|
|
|
<SampleMessageBubbles
|
|
|
|
backgroundStyle={getCustomColorStyle(selectedCustomColor.value)}
|
|
|
|
color={selectedColor}
|
|
|
|
i18n={i18n}
|
|
|
|
/>
|
|
|
|
<hr />
|
|
|
|
<div className="ChatColorPicker__bubbles">
|
2021-10-25 19:11:19 +00:00
|
|
|
{ConversationColors.map((color, i) => (
|
2021-05-28 16:15:17 +00:00
|
|
|
<div
|
|
|
|
aria-label={color}
|
|
|
|
className={classNames(
|
|
|
|
`ChatColorPicker__bubble ChatColorPicker__bubble--${color}`,
|
|
|
|
{
|
|
|
|
'ChatColorPicker__bubble--selected': color === selectedColor,
|
|
|
|
}
|
|
|
|
)}
|
|
|
|
key={color}
|
|
|
|
onClick={() => onSelectColor(color)}
|
|
|
|
onKeyDown={(ev: KeyboardEvent) => {
|
|
|
|
if (ev.key === 'Enter') {
|
|
|
|
onSelectColor(color);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
role="button"
|
|
|
|
tabIndex={0}
|
2021-10-25 19:11:19 +00:00
|
|
|
ref={i === 0 ? focusRef : undefined}
|
2021-05-28 16:15:17 +00:00
|
|
|
/>
|
|
|
|
))}
|
|
|
|
{Object.keys(customColors).map(colorId => {
|
|
|
|
const colorValues = customColors[colorId];
|
|
|
|
return (
|
|
|
|
<CustomColorBubble
|
|
|
|
color={colorValues}
|
|
|
|
colorId={colorId}
|
|
|
|
getConversationsWithCustomColor={getConversationsWithCustomColor}
|
|
|
|
key={colorId}
|
|
|
|
i18n={i18n}
|
|
|
|
isSelected={colorId === selectedCustomColor.id}
|
|
|
|
onChoose={() => {
|
|
|
|
onSelectColor('custom', {
|
|
|
|
id: colorId,
|
|
|
|
value: colorValues,
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
onDelete={() => {
|
|
|
|
removeCustomColor(colorId);
|
|
|
|
removeCustomColorOnConversations(colorId);
|
|
|
|
}}
|
|
|
|
onDupe={() => {
|
2021-08-18 20:08:14 +00:00
|
|
|
addCustomColor(colorValues, conversationId);
|
2021-05-28 16:15:17 +00:00
|
|
|
}}
|
|
|
|
onEdit={() => {
|
|
|
|
setCustomColorToEdit({ id: colorId, value: colorValues });
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
<div
|
|
|
|
aria-label={i18n('ChatColorPicker__custom-color--label')}
|
2021-10-01 16:16:44 +00:00
|
|
|
className="ChatColorPicker__bubble ChatColorPicker__bubble--custom"
|
2021-05-28 16:15:17 +00:00
|
|
|
onClick={() =>
|
|
|
|
setCustomColorToEdit({ id: undefined, value: undefined })
|
|
|
|
}
|
|
|
|
onKeyDown={(ev: KeyboardEvent) => {
|
|
|
|
if (ev.key === 'Enter') {
|
|
|
|
setCustomColorToEdit({ id: undefined, value: undefined });
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
role="button"
|
|
|
|
tabIndex={0}
|
|
|
|
>
|
|
|
|
<i className="ChatColorPicker__add-icon" />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<hr />
|
2021-06-03 21:34:36 +00:00
|
|
|
{conversationId ? (
|
2021-05-28 16:15:17 +00:00
|
|
|
<PanelRow
|
|
|
|
label={i18n('ChatColorPicker__reset')}
|
2021-06-03 21:34:36 +00:00
|
|
|
onClick={() => {
|
|
|
|
colorSelected({ conversationId });
|
|
|
|
}}
|
2021-05-28 16:15:17 +00:00
|
|
|
/>
|
|
|
|
) : null}
|
|
|
|
<PanelRow
|
|
|
|
label={i18n('ChatColorPicker__resetAll')}
|
|
|
|
onClick={() => {
|
2021-06-02 21:05:09 +00:00
|
|
|
if (isGlobal) {
|
|
|
|
setConfirmResetWhat(true);
|
|
|
|
} else {
|
|
|
|
setConfirmResetAll(true);
|
|
|
|
}
|
2021-05-28 16:15:17 +00:00
|
|
|
}}
|
|
|
|
/>
|
2021-06-01 23:37:12 +00:00
|
|
|
</div>
|
2021-05-28 16:15:17 +00:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
type CustomColorBubblePropsType = {
|
|
|
|
color: CustomColorType;
|
|
|
|
colorId: string;
|
2021-08-18 20:08:14 +00:00
|
|
|
getConversationsWithCustomColor: (
|
|
|
|
colorId: string
|
|
|
|
) => Promise<Array<ConversationType>>;
|
2021-05-28 16:15:17 +00:00
|
|
|
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<any | null>(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 = (
|
|
|
|
<div
|
|
|
|
aria-label={colorId}
|
2021-06-01 23:37:12 +00:00
|
|
|
className={classNames({
|
|
|
|
ChatColorPicker__bubble: true,
|
|
|
|
'ChatColorPicker__bubble--custom-selected': isSelected,
|
2021-05-28 16:15:17 +00:00
|
|
|
'ChatColorPicker__bubble--selected': isSelected,
|
|
|
|
})}
|
|
|
|
onClick={handleClick}
|
|
|
|
onKeyDown={(ev: KeyboardEvent) => {
|
|
|
|
if (ev.key === 'Enter') {
|
|
|
|
handleClick(ev);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
role="button"
|
|
|
|
tabIndex={0}
|
|
|
|
style={{
|
|
|
|
...getCustomColorStyle(color),
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{confirmDeleteCount ? (
|
|
|
|
<ConfirmationDialog
|
2022-09-27 20:24:21 +00:00
|
|
|
dialogName="ChatColorPicker.confirmDelete"
|
2021-05-28 16:15:17 +00:00
|
|
|
actions={[
|
|
|
|
{
|
|
|
|
action: onDelete,
|
|
|
|
style: 'negative',
|
|
|
|
text: i18n('ChatColorPicker__context--delete'),
|
|
|
|
},
|
|
|
|
]}
|
|
|
|
i18n={i18n}
|
|
|
|
onClose={() => {
|
|
|
|
setConfirmDeleteCount(undefined);
|
|
|
|
}}
|
|
|
|
title={i18n('ChatColorPicker__delete--title')}
|
|
|
|
>
|
|
|
|
{i18n('ChatColorPicker__delete--message', [
|
|
|
|
String(confirmDeleteCount),
|
|
|
|
])}
|
|
|
|
</ConfirmationDialog>
|
|
|
|
) : null}
|
|
|
|
{isSelected ? (
|
|
|
|
<ContextMenuTrigger id={colorId} ref={menuRef}>
|
|
|
|
{bubble}
|
|
|
|
</ContextMenuTrigger>
|
|
|
|
) : (
|
|
|
|
bubble
|
|
|
|
)}
|
|
|
|
<ContextMenu id={colorId}>
|
|
|
|
<MenuItem
|
|
|
|
attributes={{
|
|
|
|
className: 'ChatColorPicker__context--edit',
|
|
|
|
}}
|
|
|
|
onClick={(event: MouseEvent) => {
|
|
|
|
event.stopPropagation();
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
onEdit();
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{i18n('ChatColorPicker__context--edit')}
|
|
|
|
</MenuItem>
|
|
|
|
<MenuItem
|
|
|
|
attributes={{
|
|
|
|
className: 'ChatColorPicker__context--duplicate',
|
|
|
|
}}
|
|
|
|
onClick={(event: MouseEvent) => {
|
|
|
|
event.stopPropagation();
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
onDupe();
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{i18n('ChatColorPicker__context--duplicate')}
|
|
|
|
</MenuItem>
|
|
|
|
<MenuItem
|
|
|
|
attributes={{
|
|
|
|
className: 'ChatColorPicker__context--delete',
|
|
|
|
}}
|
2021-08-18 20:08:14 +00:00
|
|
|
onClick={async (event: MouseEvent) => {
|
2021-05-28 16:15:17 +00:00
|
|
|
event.stopPropagation();
|
|
|
|
event.preventDefault();
|
|
|
|
|
2021-08-18 20:08:14 +00:00
|
|
|
const conversations = await getConversationsWithCustomColor(
|
|
|
|
colorId
|
|
|
|
);
|
2021-05-28 16:15:17 +00:00
|
|
|
if (!conversations.length) {
|
|
|
|
onDelete();
|
|
|
|
} else {
|
|
|
|
setConfirmDeleteCount(conversations.length);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{i18n('ChatColorPicker__context--delete')}
|
|
|
|
</MenuItem>
|
|
|
|
</ContextMenu>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
type CustomColorEditorWrapperPropsType = {
|
|
|
|
customColorToEdit?: CustomColorDataType;
|
|
|
|
i18n: LocalizerType;
|
|
|
|
onClose: () => unknown;
|
|
|
|
onSave: (color: CustomColorType) => unknown;
|
|
|
|
};
|
|
|
|
|
|
|
|
const CustomColorEditorWrapper = ({
|
|
|
|
customColorToEdit,
|
|
|
|
i18n,
|
|
|
|
onClose,
|
|
|
|
onSave,
|
|
|
|
}: CustomColorEditorWrapperPropsType): JSX.Element => {
|
|
|
|
const editor = (
|
|
|
|
<CustomColorEditor
|
|
|
|
customColor={customColorToEdit?.value}
|
|
|
|
i18n={i18n}
|
|
|
|
onClose={onClose}
|
|
|
|
onSave={onSave}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
|
2021-08-18 20:08:14 +00:00
|
|
|
return (
|
|
|
|
<Modal
|
2022-09-27 20:24:21 +00:00
|
|
|
modalName="ChatColorPicker"
|
2021-08-18 20:08:14 +00:00
|
|
|
hasXButton
|
|
|
|
i18n={i18n}
|
|
|
|
moduleClassName="ChatColorPicker__modal"
|
|
|
|
noMouseClose
|
|
|
|
onClose={onClose}
|
|
|
|
title={i18n('CustomColorEditor__title')}
|
|
|
|
>
|
|
|
|
{editor}
|
|
|
|
</Modal>
|
|
|
|
);
|
2021-05-28 16:15:17 +00:00
|
|
|
};
|