451 lines
		
	
	
	
		
			13 KiB
			
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			451 lines
		
	
	
	
		
			13 KiB
			
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
// 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<string, CustomColorType>;
 | 
						|
  getConversationsWithCustomColor: (
 | 
						|
    colorId: string
 | 
						|
  ) => Promise<Array<ConversationType>>;
 | 
						|
  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 function 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 = () => (
 | 
						|
    <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 {
 | 
						|
          addCustomColor(color, conversationId);
 | 
						|
        }
 | 
						|
      }}
 | 
						|
    />
 | 
						|
  );
 | 
						|
 | 
						|
  return (
 | 
						|
    <div className="ChatColorPicker__container">
 | 
						|
      {customColorToEdit ? renderCustomColorEditorWrapper() : null}
 | 
						|
      {confirmResetWhat ? (
 | 
						|
        <ConfirmationDialog
 | 
						|
          dialogName="ChatColorPicker.confirmReset"
 | 
						|
          actions={[
 | 
						|
            {
 | 
						|
              action: resetDefaultChatColor,
 | 
						|
              style: 'affirmative',
 | 
						|
              text: i18n('icu:ChatColorPicker__confirm-reset-default'),
 | 
						|
            },
 | 
						|
            {
 | 
						|
              action: () => {
 | 
						|
                resetDefaultChatColor();
 | 
						|
                resetAllChatColors();
 | 
						|
              },
 | 
						|
              style: 'affirmative',
 | 
						|
              text: i18n('icu:ChatColorPicker__resetAll'),
 | 
						|
            },
 | 
						|
          ]}
 | 
						|
          i18n={i18n}
 | 
						|
          onClose={() => {
 | 
						|
            setConfirmResetWhat(false);
 | 
						|
          }}
 | 
						|
          title={i18n('icu:ChatColorPicker__resetDefault')}
 | 
						|
        >
 | 
						|
          {i18n('icu:ChatColorPicker__confirm-reset-message')}
 | 
						|
        </ConfirmationDialog>
 | 
						|
      ) : null}
 | 
						|
      {confirmResetAll ? (
 | 
						|
        <ConfirmationDialog
 | 
						|
          dialogName="ChatColorPicker.confirmResetAll"
 | 
						|
          actions={[
 | 
						|
            {
 | 
						|
              action: resetAllChatColors,
 | 
						|
              style: 'affirmative',
 | 
						|
              text: i18n('icu:ChatColorPicker__confirm-reset'),
 | 
						|
            },
 | 
						|
          ]}
 | 
						|
          i18n={i18n}
 | 
						|
          onClose={() => {
 | 
						|
            setConfirmResetAll(false);
 | 
						|
          }}
 | 
						|
          title={i18n('icu:ChatColorPicker__resetAll')}
 | 
						|
        >
 | 
						|
          {i18n('icu:ChatColorPicker__confirm-reset-message')}
 | 
						|
        </ConfirmationDialog>
 | 
						|
      ) : null}
 | 
						|
      <SampleMessageBubbles
 | 
						|
        backgroundStyle={getCustomColorStyle(selectedCustomColor.value)}
 | 
						|
        color={selectedColor}
 | 
						|
        i18n={i18n}
 | 
						|
      />
 | 
						|
      <hr />
 | 
						|
      <div className="ChatColorPicker__bubbles">
 | 
						|
        {ConversationColors.map((color, i) => (
 | 
						|
          <div
 | 
						|
            aria-label={color}
 | 
						|
            aria-selected={color === selectedColor}
 | 
						|
            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="option"
 | 
						|
            tabIndex={0}
 | 
						|
            ref={i === 0 ? focusRef : undefined}
 | 
						|
          />
 | 
						|
        ))}
 | 
						|
        {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={() => {
 | 
						|
                addCustomColor(colorValues, conversationId);
 | 
						|
              }}
 | 
						|
              onEdit={() => {
 | 
						|
                setCustomColorToEdit({ id: colorId, value: colorValues });
 | 
						|
              }}
 | 
						|
            />
 | 
						|
          );
 | 
						|
        })}
 | 
						|
        <div
 | 
						|
          aria-label={i18n('icu:ChatColorPicker__custom-color--label')}
 | 
						|
          className="ChatColorPicker__bubble ChatColorPicker__bubble--custom"
 | 
						|
          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 />
 | 
						|
      {conversationId ? (
 | 
						|
        <PanelRow
 | 
						|
          label={i18n('icu:ChatColorPicker__reset')}
 | 
						|
          onClick={() => {
 | 
						|
            colorSelected({ conversationId });
 | 
						|
          }}
 | 
						|
        />
 | 
						|
      ) : null}
 | 
						|
      <PanelRow
 | 
						|
        label={i18n('icu:ChatColorPicker__resetAll')}
 | 
						|
        onClick={() => {
 | 
						|
          if (isGlobal) {
 | 
						|
            setConfirmResetWhat(true);
 | 
						|
          } else {
 | 
						|
            setConfirmResetAll(true);
 | 
						|
          }
 | 
						|
        }}
 | 
						|
      />
 | 
						|
    </div>
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
type CustomColorBubblePropsType = {
 | 
						|
  color: CustomColorType;
 | 
						|
  colorId: string;
 | 
						|
  getConversationsWithCustomColor: (
 | 
						|
    colorId: string
 | 
						|
  ) => Promise<Array<ConversationType>>;
 | 
						|
  i18n: LocalizerType;
 | 
						|
  isSelected: boolean;
 | 
						|
  onDelete: () => unknown;
 | 
						|
  onDupe: () => unknown;
 | 
						|
  onEdit: () => unknown;
 | 
						|
  onChoose: () => unknown;
 | 
						|
};
 | 
						|
 | 
						|
function 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}
 | 
						|
      aria-selected={isSelected}
 | 
						|
      className={classNames({
 | 
						|
        ChatColorPicker__bubble: true,
 | 
						|
        'ChatColorPicker__bubble--custom-selected': isSelected,
 | 
						|
        'ChatColorPicker__bubble--selected': isSelected,
 | 
						|
      })}
 | 
						|
      onClick={handleClick}
 | 
						|
      onKeyDown={(ev: KeyboardEvent) => {
 | 
						|
        if (ev.key === 'Enter') {
 | 
						|
          handleClick(ev);
 | 
						|
        }
 | 
						|
      }}
 | 
						|
      role="option"
 | 
						|
      tabIndex={0}
 | 
						|
      style={{
 | 
						|
        ...getCustomColorStyle(color),
 | 
						|
      }}
 | 
						|
    />
 | 
						|
  );
 | 
						|
 | 
						|
  return (
 | 
						|
    <>
 | 
						|
      {confirmDeleteCount ? (
 | 
						|
        <ConfirmationDialog
 | 
						|
          dialogName="ChatColorPicker.confirmDelete"
 | 
						|
          actions={[
 | 
						|
            {
 | 
						|
              action: onDelete,
 | 
						|
              style: 'negative',
 | 
						|
              text: i18n('icu:ChatColorPicker__context--delete'),
 | 
						|
            },
 | 
						|
          ]}
 | 
						|
          i18n={i18n}
 | 
						|
          onClose={() => {
 | 
						|
            setConfirmDeleteCount(undefined);
 | 
						|
          }}
 | 
						|
          title={i18n('icu:ChatColorPicker__delete--title')}
 | 
						|
        >
 | 
						|
          {i18n('icu:ChatColorPicker__delete--message', {
 | 
						|
            num: 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('icu:ChatColorPicker__context--edit')}
 | 
						|
        </MenuItem>
 | 
						|
        <MenuItem
 | 
						|
          attributes={{
 | 
						|
            className: 'ChatColorPicker__context--duplicate',
 | 
						|
          }}
 | 
						|
          onClick={(event: MouseEvent) => {
 | 
						|
            event.stopPropagation();
 | 
						|
            event.preventDefault();
 | 
						|
 | 
						|
            onDupe();
 | 
						|
          }}
 | 
						|
        >
 | 
						|
          {i18n('icu:ChatColorPicker__context--duplicate')}
 | 
						|
        </MenuItem>
 | 
						|
        <MenuItem
 | 
						|
          attributes={{
 | 
						|
            className: 'ChatColorPicker__context--delete',
 | 
						|
          }}
 | 
						|
          onClick={async (event: MouseEvent) => {
 | 
						|
            event.stopPropagation();
 | 
						|
            event.preventDefault();
 | 
						|
 | 
						|
            const conversations =
 | 
						|
              await getConversationsWithCustomColor(colorId);
 | 
						|
            if (!conversations.length) {
 | 
						|
              onDelete();
 | 
						|
            } else {
 | 
						|
              setConfirmDeleteCount(conversations.length);
 | 
						|
            }
 | 
						|
          }}
 | 
						|
        >
 | 
						|
          {i18n('icu:ChatColorPicker__context--delete')}
 | 
						|
        </MenuItem>
 | 
						|
      </ContextMenu>
 | 
						|
    </>
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
type CustomColorEditorWrapperPropsType = {
 | 
						|
  customColorToEdit?: CustomColorDataType;
 | 
						|
  i18n: LocalizerType;
 | 
						|
  onClose: () => unknown;
 | 
						|
  onSave: (color: CustomColorType) => unknown;
 | 
						|
};
 | 
						|
 | 
						|
function CustomColorEditorWrapper({
 | 
						|
  customColorToEdit,
 | 
						|
  i18n,
 | 
						|
  onClose,
 | 
						|
  onSave,
 | 
						|
}: CustomColorEditorWrapperPropsType): JSX.Element {
 | 
						|
  const editor = (
 | 
						|
    <CustomColorEditor
 | 
						|
      customColor={customColorToEdit?.value}
 | 
						|
      i18n={i18n}
 | 
						|
      onClose={onClose}
 | 
						|
      onSave={onSave}
 | 
						|
    />
 | 
						|
  );
 | 
						|
 | 
						|
  return (
 | 
						|
    <Modal
 | 
						|
      modalName="ChatColorPicker"
 | 
						|
      hasXButton
 | 
						|
      i18n={i18n}
 | 
						|
      moduleClassName="ChatColorPicker__modal"
 | 
						|
      noMouseClose
 | 
						|
      onClose={onClose}
 | 
						|
      title={i18n('icu:CustomColorEditor__title')}
 | 
						|
    >
 | 
						|
      {editor}
 | 
						|
    </Modal>
 | 
						|
  );
 | 
						|
}
 |