Some improvements
This commit is contained in:
		
					parent
					
						
							
								14a2714c1e
							
						
					
				
			
			
				commit
				
					
						c53eefaf6d
					
				
			
		
					 19 changed files with 205 additions and 70 deletions
				
			
		| 
						 | 
				
			
			@ -5727,6 +5727,14 @@
 | 
			
		|||
    "messageformat": "To change this setting, open the Signal app on your mobile device and navigate to Settings > Chats",
 | 
			
		||||
    "description": "Description for the generate link previews setting"
 | 
			
		||||
  },
 | 
			
		||||
  "icu:Preferences__auto-convert-emoji--title": {
 | 
			
		||||
    "messageformat": "Convert typed emoticons to emoji",
 | 
			
		||||
    "description": "Title for the auto convert emoji setting"
 | 
			
		||||
  },
 | 
			
		||||
  "icu:Preferences__auto-convert-emoji--description": {
 | 
			
		||||
    "messageformat": "For example, :-) will be converted to 🙂",
 | 
			
		||||
    "description": "Description for the auto convert emoji setting"
 | 
			
		||||
  },
 | 
			
		||||
  "icu:Preferences--advanced": {
 | 
			
		||||
    "messageformat": "Advanced",
 | 
			
		||||
    "description": "Title for advanced settings"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -972,6 +972,14 @@ export async function startApp(): Promise<void> {
 | 
			
		|||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      window.storage.get('autoConvertEmoji') === undefined &&
 | 
			
		||||
      newVersion &&
 | 
			
		||||
      !lastVersion
 | 
			
		||||
    ) {
 | 
			
		||||
      await window.storage.put('autoConvertEmoji', true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setAppLoadingScreenMessage(
 | 
			
		||||
      window.i18n('icu:optimizingApplication'),
 | 
			
		||||
      window.i18n
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ import React, { forwardRef, useMemo } from 'react';
 | 
			
		|||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
 | 
			
		||||
import { getClassNamesFor } from '../util/getClassNamesFor';
 | 
			
		||||
import { Emojify } from './conversation/Emojify';
 | 
			
		||||
 | 
			
		||||
export type PropsType = {
 | 
			
		||||
  checked?: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -61,7 +62,9 @@ export const Checkbox = forwardRef(function CheckboxInner(
 | 
			
		|||
    <div>
 | 
			
		||||
      <label htmlFor={id}>
 | 
			
		||||
        <div>{label}</div>
 | 
			
		||||
        <div className={getClassName('__description')}>{description}</div>
 | 
			
		||||
        <div className={getClassName('__description')}>
 | 
			
		||||
          <Emojify text={description ?? ''} />
 | 
			
		||||
        </div>
 | 
			
		||||
      </label>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,7 +39,9 @@ import {
 | 
			
		|||
  getDeltaToRemoveStaleMentions,
 | 
			
		||||
  getTextAndRangesFromOps,
 | 
			
		||||
  isMentionBlot,
 | 
			
		||||
  isEmojiBlot,
 | 
			
		||||
  getDeltaToRestartMention,
 | 
			
		||||
  getDeltaToRestartEmoji,
 | 
			
		||||
  insertEmojiOps,
 | 
			
		||||
  insertFormattingAndMentionsOps,
 | 
			
		||||
} from '../quill/util';
 | 
			
		||||
| 
						 | 
				
			
			@ -284,7 +286,7 @@ export function CompositionInput(props: Props): React.ReactElement {
 | 
			
		|||
    const delta = new Delta()
 | 
			
		||||
      .retain(insertionRange.index)
 | 
			
		||||
      .delete(insertionRange.length)
 | 
			
		||||
      .insert({ emoji });
 | 
			
		||||
      .insert({ emoji: { value: emoji } });
 | 
			
		||||
 | 
			
		||||
    quill.updateContents(delta, 'user');
 | 
			
		||||
    quill.setSelection(insertionRange.index + 1, 0, 'user');
 | 
			
		||||
| 
						 | 
				
			
			@ -512,17 +514,24 @@ export function CompositionInput(props: Props): React.ReactElement {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    const [blotToDelete] = quill.getLeaf(selection.index);
 | 
			
		||||
    if (!isMentionBlot(blotToDelete)) {
 | 
			
		||||
      return true;
 | 
			
		||||
    if (isMentionBlot(blotToDelete)) {
 | 
			
		||||
      const contents = quill.getContents(0, selection.index - 1);
 | 
			
		||||
      const restartDelta = getDeltaToRestartMention(contents.ops);
 | 
			
		||||
 | 
			
		||||
      quill.updateContents(restartDelta);
 | 
			
		||||
      quill.setSelection(selection.index, 0);
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const contents = quill.getContents(0, selection.index - 1);
 | 
			
		||||
    const restartDelta = getDeltaToRestartMention(contents.ops);
 | 
			
		||||
    if (isEmojiBlot(blotToDelete)) {
 | 
			
		||||
      const contents = quill.getContents(0, selection.index);
 | 
			
		||||
      const restartDelta = getDeltaToRestartEmoji(contents.ops);
 | 
			
		||||
 | 
			
		||||
    quill.updateContents(restartDelta);
 | 
			
		||||
    quill.setSelection(selection.index, 0);
 | 
			
		||||
      quill.updateContents(restartDelta);
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
    return true;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onChange = (): void => {
 | 
			
		||||
| 
						 | 
				
			
			@ -731,7 +740,9 @@ export function CompositionInput(props: Props): React.ReactElement {
 | 
			
		|||
                callbacksRef.current.onPickEmoji(emoji),
 | 
			
		||||
              skinTone,
 | 
			
		||||
            },
 | 
			
		||||
            autoSubstituteAsciiEmojis: true,
 | 
			
		||||
            autoSubstituteAsciiEmojis: {
 | 
			
		||||
              skinTone,
 | 
			
		||||
            },
 | 
			
		||||
            formattingMenu: {
 | 
			
		||||
              i18n,
 | 
			
		||||
              isMenuEnabled: isFormattingEnabled,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -76,6 +76,7 @@ export default {
 | 
			
		|||
    defaultConversationColor: DEFAULT_CONVERSATION_COLOR,
 | 
			
		||||
    deviceName: 'Work Windows ME',
 | 
			
		||||
    hasAudioNotifications: true,
 | 
			
		||||
    hasAutoConvertEmoji: true,
 | 
			
		||||
    hasAutoDownloadUpdate: true,
 | 
			
		||||
    hasAutoLaunch: true,
 | 
			
		||||
    hasCallNotifications: true,
 | 
			
		||||
| 
						 | 
				
			
			@ -133,6 +134,7 @@ export default {
 | 
			
		|||
    executeMenuRole: action('executeMenuRole'),
 | 
			
		||||
    makeSyncRequest: action('makeSyncRequest'),
 | 
			
		||||
    onAudioNotificationsChange: action('onAudioNotificationsChange'),
 | 
			
		||||
    onAutoConvertEmojiChange: action('onAutoConvertEmojiChange'),
 | 
			
		||||
    onAutoDownloadUpdateChange: action('onAutoDownloadUpdateChange'),
 | 
			
		||||
    onAutoLaunchChange: action('onAutoLaunchChange'),
 | 
			
		||||
    onCallNotificationsChange: action('onCallNotificationsChange'),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -75,6 +75,7 @@ export type PropsDataType = {
 | 
			
		|||
  defaultConversationColor: DefaultConversationColorType;
 | 
			
		||||
  deviceName?: string;
 | 
			
		||||
  hasAudioNotifications?: boolean;
 | 
			
		||||
  hasAutoConvertEmoji: boolean;
 | 
			
		||||
  hasAutoDownloadUpdate: boolean;
 | 
			
		||||
  hasAutoLaunch: boolean;
 | 
			
		||||
  hasCallNotifications: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -159,6 +160,7 @@ type PropsFunctionType = {
 | 
			
		|||
 | 
			
		||||
  // Change handlers
 | 
			
		||||
  onAudioNotificationsChange: CheckboxChangeHandlerType;
 | 
			
		||||
  onAutoConvertEmojiChange: CheckboxChangeHandlerType;
 | 
			
		||||
  onAutoDownloadUpdateChange: CheckboxChangeHandlerType;
 | 
			
		||||
  onAutoLaunchChange: CheckboxChangeHandlerType;
 | 
			
		||||
  onCallNotificationsChange: CheckboxChangeHandlerType;
 | 
			
		||||
| 
						 | 
				
			
			@ -257,6 +259,7 @@ export function Preferences({
 | 
			
		|||
  executeMenuRole,
 | 
			
		||||
  getConversationsWithCustomColor,
 | 
			
		||||
  hasAudioNotifications,
 | 
			
		||||
  hasAutoConvertEmoji,
 | 
			
		||||
  hasAutoDownloadUpdate,
 | 
			
		||||
  hasAutoLaunch,
 | 
			
		||||
  hasCallNotifications,
 | 
			
		||||
| 
						 | 
				
			
			@ -293,6 +296,7 @@ export function Preferences({
 | 
			
		|||
  makeSyncRequest,
 | 
			
		||||
  notificationContent,
 | 
			
		||||
  onAudioNotificationsChange,
 | 
			
		||||
  onAutoConvertEmojiChange,
 | 
			
		||||
  onAutoDownloadUpdateChange,
 | 
			
		||||
  onAutoLaunchChange,
 | 
			
		||||
  onCallNotificationsChange,
 | 
			
		||||
| 
						 | 
				
			
			@ -856,6 +860,16 @@ export function Preferences({
 | 
			
		|||
            name="linkPreviews"
 | 
			
		||||
            onChange={noop}
 | 
			
		||||
          />
 | 
			
		||||
          <Checkbox
 | 
			
		||||
            checked={hasAutoConvertEmoji}
 | 
			
		||||
            description={i18n(
 | 
			
		||||
              'icu:Preferences__auto-convert-emoji--description'
 | 
			
		||||
            )}
 | 
			
		||||
            label={i18n('icu:Preferences__auto-convert-emoji--title')}
 | 
			
		||||
            moduleClassName="Preferences__checkbox"
 | 
			
		||||
            name="autoConvertEmoji"
 | 
			
		||||
            onChange={onAutoConvertEmojiChange}
 | 
			
		||||
          />
 | 
			
		||||
          <Control
 | 
			
		||||
            left={i18n('icu:Preferences__sent-media-quality')}
 | 
			
		||||
            right={
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -93,6 +93,7 @@ export class SettingsChannel extends EventEmitter {
 | 
			
		|||
    });
 | 
			
		||||
    this.installSetting('textFormatting');
 | 
			
		||||
 | 
			
		||||
    this.installSetting('autoConvertEmoji');
 | 
			
		||||
    this.installSetting('autoDownloadUpdate');
 | 
			
		||||
    this.installSetting('autoLaunch');
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,6 @@
 | 
			
		|||
 | 
			
		||||
import type Quill from 'quill';
 | 
			
		||||
import Delta from 'quill-delta';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import type { EmojiData } from '../../components/emoji/lib';
 | 
			
		||||
import {
 | 
			
		||||
  convertShortName,
 | 
			
		||||
| 
						 | 
				
			
			@ -15,32 +14,34 @@ type AutoSubstituteAsciiEmojisOptions = {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
const emojiMap: Record<string, string> = {
 | 
			
		||||
  ':)': 'slightly_smiling_face',
 | 
			
		||||
  ':-)': 'slightly_smiling_face',
 | 
			
		||||
  ':(': 'slightly_frowning_face',
 | 
			
		||||
  ':-(': 'slightly_frowning_face',
 | 
			
		||||
  ':D': 'smiley',
 | 
			
		||||
  ':-D': 'smiley',
 | 
			
		||||
  ':*': 'kissing',
 | 
			
		||||
  ':-*': 'kissing',
 | 
			
		||||
  ':P': 'stuck_out_tongue',
 | 
			
		||||
  ':-D': 'grinning',
 | 
			
		||||
  ':-*': 'kissing_heart',
 | 
			
		||||
  ':-P': 'stuck_out_tongue',
 | 
			
		||||
  ';P': 'stuck_out_tongue_winking_eye',
 | 
			
		||||
  ';-P': 'stuck_out_tongue_winking_eye',
 | 
			
		||||
  'D:': 'anguished',
 | 
			
		||||
  "D-':": 'anguished',
 | 
			
		||||
  ':O': 'open_mouth',
 | 
			
		||||
  ':-O': 'open_mouth',
 | 
			
		||||
  ':-p': 'stuck_out_tongue',
 | 
			
		||||
  ":'(": 'cry',
 | 
			
		||||
  ":'-(": 'cry',
 | 
			
		||||
  ':/': 'confused',
 | 
			
		||||
  ':-/': 'confused',
 | 
			
		||||
  ';)': 'wink',
 | 
			
		||||
  ':-\\': 'confused',
 | 
			
		||||
  ';-)': 'wink',
 | 
			
		||||
  '(Y)': '+1',
 | 
			
		||||
  '(N)': '-1',
 | 
			
		||||
  '(y)': '+1',
 | 
			
		||||
  '(n)': '-1',
 | 
			
		||||
  '<3': 'heart',
 | 
			
		||||
  '^_^': 'grin',
 | 
			
		||||
  '>_<': 'laughing',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function buildRegexp(obj: Record<string, string>): RegExp {
 | 
			
		||||
  const sanitizedKeys = Object.keys(obj).map(x =>
 | 
			
		||||
    x.replace(/([^a-zA-Z0-9])/g, '\\$1')
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return new RegExp(`(${sanitizedKeys.join('|')})$`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const EMOJI_REGEXP = buildRegexp(emojiMap);
 | 
			
		||||
 | 
			
		||||
export class AutoSubstituteAsciiEmojis {
 | 
			
		||||
  options: AutoSubstituteAsciiEmojisOptions;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -50,13 +51,24 @@ export class AutoSubstituteAsciiEmojis {
 | 
			
		|||
    this.options = options;
 | 
			
		||||
    this.quill = quill;
 | 
			
		||||
 | 
			
		||||
    this.quill.on(
 | 
			
		||||
      'text-change',
 | 
			
		||||
      _.debounce(() => this.onTextChange(), 100)
 | 
			
		||||
    );
 | 
			
		||||
    this.quill.on('text-change', (_now, _before, source) => {
 | 
			
		||||
      if (source !== 'user') {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // When pasting - Quill first updates contents with "user" source and only
 | 
			
		||||
      // then updates the selection with "silent" source. This means that unless
 | 
			
		||||
      // we wrap `onTextChange` with setTimeout - we are not going to see the
 | 
			
		||||
      // updated cursor position.
 | 
			
		||||
      setTimeout(() => this.onTextChange(), 0);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onTextChange(): void {
 | 
			
		||||
    if (!window.storage.get('autoConvertEmoji', false)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const range = this.quill.getSelection();
 | 
			
		||||
 | 
			
		||||
    if (!range) {
 | 
			
		||||
| 
						 | 
				
			
			@ -65,32 +77,44 @@ export class AutoSubstituteAsciiEmojis {
 | 
			
		|||
 | 
			
		||||
    const [blot, index] = this.quill.getLeaf(range.index);
 | 
			
		||||
 | 
			
		||||
    if (blot !== undefined && blot.text !== undefined) {
 | 
			
		||||
      const blotText: string = blot.text;
 | 
			
		||||
      Object.entries(emojiMap).some(([textEmoji, emojiName]) => {
 | 
			
		||||
        if (blotText.substring(0, index).endsWith(textEmoji)) {
 | 
			
		||||
          const emojiData = convertShortNameToData(
 | 
			
		||||
            emojiName,
 | 
			
		||||
            this.options.skinTone
 | 
			
		||||
          );
 | 
			
		||||
          if (emojiData) {
 | 
			
		||||
            this.insertEmoji(
 | 
			
		||||
              emojiData,
 | 
			
		||||
              range.index - textEmoji.length,
 | 
			
		||||
              textEmoji.length
 | 
			
		||||
            );
 | 
			
		||||
            return true;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
      });
 | 
			
		||||
    if (blot?.text == null) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const textBeforeCursor = blot.text.slice(0, index);
 | 
			
		||||
    const match = textBeforeCursor.match(EMOJI_REGEXP);
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const [, textEmoji] = match;
 | 
			
		||||
    const emojiName = emojiMap[textEmoji];
 | 
			
		||||
 | 
			
		||||
    const emojiData = convertShortNameToData(emojiName, this.options.skinTone);
 | 
			
		||||
    if (emojiData) {
 | 
			
		||||
      this.insertEmoji(
 | 
			
		||||
        emojiData,
 | 
			
		||||
        range.index - textEmoji.length,
 | 
			
		||||
        textEmoji.length,
 | 
			
		||||
        textEmoji
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  insertEmoji(emojiData: EmojiData, index: number, range: number): void {
 | 
			
		||||
  insertEmoji(
 | 
			
		||||
    emojiData: EmojiData,
 | 
			
		||||
    index: number,
 | 
			
		||||
    range: number,
 | 
			
		||||
    source: string
 | 
			
		||||
  ): void {
 | 
			
		||||
    const emoji = convertShortName(emojiData.short_name, this.options.skinTone);
 | 
			
		||||
    const delta = new Delta().retain(index).delete(range).insert({ emoji });
 | 
			
		||||
    this.quill.updateContents(delta, 'user');
 | 
			
		||||
    const delta = new Delta()
 | 
			
		||||
      .retain(index)
 | 
			
		||||
      .delete(range)
 | 
			
		||||
      .insert({
 | 
			
		||||
        emoji: { value: emoji, source },
 | 
			
		||||
      });
 | 
			
		||||
    this.quill.updateContents(delta, 'api');
 | 
			
		||||
    this.quill.setSelection(index + 1, 0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,11 @@ const Embed: typeof Parchment.Embed = Quill.import('blots/embed');
 | 
			
		|||
// ts/components/conversation/Emojify.tsx
 | 
			
		||||
// ts/components/emoji/Emoji.tsx
 | 
			
		||||
 | 
			
		||||
export type EmojiBlotValue = Readonly<{
 | 
			
		||||
  value: string;
 | 
			
		||||
  source?: string;
 | 
			
		||||
}>;
 | 
			
		||||
 | 
			
		||||
export class EmojiBlot extends Embed {
 | 
			
		||||
  static override blotName = 'emoji';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -19,21 +24,30 @@ export class EmojiBlot extends Embed {
 | 
			
		|||
 | 
			
		||||
  static override className = 'emoji-blot';
 | 
			
		||||
 | 
			
		||||
  static override create(emoji: string): Node {
 | 
			
		||||
  static override create({ value: emoji, source }: EmojiBlotValue): Node {
 | 
			
		||||
    const node = super.create(undefined) as HTMLElement;
 | 
			
		||||
    node.dataset.emoji = emoji;
 | 
			
		||||
    node.dataset.source = source;
 | 
			
		||||
 | 
			
		||||
    const image = emojiToImage(emoji);
 | 
			
		||||
 | 
			
		||||
    node.setAttribute('src', image || '');
 | 
			
		||||
    node.setAttribute('data-emoji', emoji);
 | 
			
		||||
    node.setAttribute('data-source', source || '');
 | 
			
		||||
    node.setAttribute('title', emoji);
 | 
			
		||||
    node.setAttribute('aria-label', emoji);
 | 
			
		||||
 | 
			
		||||
    return node;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static override value(node: HTMLElement): string | undefined {
 | 
			
		||||
    return node.dataset.emoji;
 | 
			
		||||
  static override value(node: HTMLElement): EmojiBlotValue | undefined {
 | 
			
		||||
    const { emoji, source } = node.dataset;
 | 
			
		||||
    if (emoji === undefined) {
 | 
			
		||||
      throw new Error(
 | 
			
		||||
        `Failed to make EmojiBlot with emoji: ${emoji}, source: ${source}`
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return { value: emoji, source };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -247,7 +247,12 @@ export class EmojiCompletion {
 | 
			
		|||
  ): void {
 | 
			
		||||
    const emoji = convertShortName(emojiData.short_name, this.options.skinTone);
 | 
			
		||||
 | 
			
		||||
    const delta = new Delta().retain(index).delete(range).insert({ emoji });
 | 
			
		||||
    const delta = new Delta()
 | 
			
		||||
      .retain(index)
 | 
			
		||||
      .delete(range)
 | 
			
		||||
      .insert({
 | 
			
		||||
        emoji: { value: emoji },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    if (withTrailingSpace) {
 | 
			
		||||
      // The extra space we add won't be formatted unless we manually provide attributes
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,8 +15,8 @@ export const matchEmojiImage: Matcher = (
 | 
			
		|||
    node.classList.contains('emoji') ||
 | 
			
		||||
    node.classList.contains('module-emoji__image--16px')
 | 
			
		||||
  ) {
 | 
			
		||||
    const emoji = node.getAttribute('aria-label');
 | 
			
		||||
    return new Delta().insert({ emoji }, attributes);
 | 
			
		||||
    const value = node.getAttribute('aria-label');
 | 
			
		||||
    return new Delta().insert({ emoji: { value } }, attributes);
 | 
			
		||||
  }
 | 
			
		||||
  return delta;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -27,8 +27,8 @@ export const matchEmojiBlot: Matcher = (
 | 
			
		|||
  attributes: AttributeMap
 | 
			
		||||
): Delta => {
 | 
			
		||||
  if (node.classList.contains('emoji-blot')) {
 | 
			
		||||
    const { emoji } = node.dataset;
 | 
			
		||||
    return new Delta().insert({ emoji }, attributes);
 | 
			
		||||
    const { emoji: value, source } = node.dataset;
 | 
			
		||||
    return new Delta().insert({ emoji: { value, source } }, attributes);
 | 
			
		||||
  }
 | 
			
		||||
  return delta;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ import type {
 | 
			
		|||
} from '../types/BodyRange';
 | 
			
		||||
import { BodyRange } from '../types/BodyRange';
 | 
			
		||||
import type { MentionBlot } from './mentions/blot';
 | 
			
		||||
import type { EmojiBlot } from './emoji/blot';
 | 
			
		||||
import { isNewlineOnlyOp, QuillFormattingStyle } from './formatting/menu';
 | 
			
		||||
import { isNotNil } from '../util/isNotNil';
 | 
			
		||||
import type { AciString } from '../types/ServiceId';
 | 
			
		||||
| 
						 | 
				
			
			@ -27,6 +28,9 @@ export type FormattingBlotValue = {
 | 
			
		|||
  style: BodyRange.Style;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const isEmojiBlot = (blot: LeafBlot): blot is EmojiBlot =>
 | 
			
		||||
  blot.value() && blot.value().emoji;
 | 
			
		||||
 | 
			
		||||
export const isMentionBlot = (blot: LeafBlot): blot is MentionBlot =>
 | 
			
		||||
  blot.value() && blot.value().mention;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -37,7 +41,10 @@ export type RetainOp = Op & { retain: number };
 | 
			
		|||
export type InsertOp<K extends string, T> = Op & { insert: { [V in K]: T } };
 | 
			
		||||
 | 
			
		||||
export type InsertMentionOp = InsertOp<'mention', MentionBlotValue>;
 | 
			
		||||
export type InsertEmojiOp = InsertOp<'emoji', string>;
 | 
			
		||||
export type InsertEmojiOp = InsertOp<
 | 
			
		||||
  'emoji',
 | 
			
		||||
  { value: string; source?: string }
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
export const isRetainOp = (op?: Op): op is RetainOp =>
 | 
			
		||||
  op !== undefined && op.retain !== undefined;
 | 
			
		||||
| 
						 | 
				
			
			@ -64,7 +71,7 @@ export const getTextFromOps = (ops: Array<DeltaOperation>): string =>
 | 
			
		|||
      }
 | 
			
		||||
 | 
			
		||||
      if (isInsertEmojiOp(op)) {
 | 
			
		||||
        return acc + op.insert.emoji;
 | 
			
		||||
        return acc + op.insert.emoji.value;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (isInsertMentionOp(op)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -187,7 +194,7 @@ export const getTextAndRangesFromOps = (
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    if (isInsertEmojiOp(op)) {
 | 
			
		||||
      return acc + op.insert.emoji;
 | 
			
		||||
      return acc + op.insert.emoji.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isInsertMentionOp(op)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -304,6 +311,27 @@ export const getDeltaToRestartMention = (ops: Array<Op>): Delta => {
 | 
			
		|||
  return new Delta(changes);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getDeltaToRestartEmoji = (ops: Array<Op>): Delta => {
 | 
			
		||||
  const changes = new Array<Op>();
 | 
			
		||||
  for (const op of ops.slice(0, -1)) {
 | 
			
		||||
    if (op.insert && typeof op.insert === 'string') {
 | 
			
		||||
      changes.push({ retain: op.insert.length });
 | 
			
		||||
    } else {
 | 
			
		||||
      changes.push({ retain: 1 });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const last = ops.at(-1);
 | 
			
		||||
  if (!last || !last.insert) {
 | 
			
		||||
    throw new Error('No emoji to delete');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  changes.push({ delete: 1 });
 | 
			
		||||
  if ((last as InsertEmojiOp).insert.emoji?.source) {
 | 
			
		||||
    changes.push({ insert: (last as InsertEmojiOp).insert.emoji?.source });
 | 
			
		||||
  }
 | 
			
		||||
  return new Delta(changes);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getDeltaToRemoveStaleMentions = (
 | 
			
		||||
  ops: Array<Op>,
 | 
			
		||||
  memberAcis: Array<AciString>
 | 
			
		||||
| 
						 | 
				
			
			@ -422,7 +450,7 @@ export const insertEmojiOps = (
 | 
			
		|||
        if (emojiData) {
 | 
			
		||||
          ops.push({ insert: text.slice(index, match.index), attributes });
 | 
			
		||||
          ops.push({
 | 
			
		||||
            insert: { emoji },
 | 
			
		||||
            insert: { emoji: { value: emoji } },
 | 
			
		||||
            attributes: { ...existingAttributes, ...attributes },
 | 
			
		||||
          });
 | 
			
		||||
          index = match.index + emoji.length;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,12 +59,12 @@ describe('getDeltaToRemoveStaleMentions', () => {
 | 
			
		|||
      const originalOps = [
 | 
			
		||||
        {
 | 
			
		||||
          insert: {
 | 
			
		||||
            emoji: '😂',
 | 
			
		||||
            emoji: { value: '😂' },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          insert: {
 | 
			
		||||
            emoji: '🍋',
 | 
			
		||||
            emoji: { value: '🍋' },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ];
 | 
			
		||||
| 
						 | 
				
			
			@ -312,7 +312,7 @@ describe('getTextAndRangesFromOps', () => {
 | 
			
		|||
      const ops = [
 | 
			
		||||
        {
 | 
			
		||||
          insert: {
 | 
			
		||||
            emoji: '😂',
 | 
			
		||||
            emoji: { value: '😂' },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
| 
						 | 
				
			
			@ -579,7 +579,7 @@ describe('getDeltaToRestartMention', () => {
 | 
			
		|||
      const originalOps = [
 | 
			
		||||
        {
 | 
			
		||||
          insert: {
 | 
			
		||||
            emoji: '😂',
 | 
			
		||||
            emoji: { value: '😂' },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								ts/types/Storage.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								ts/types/Storage.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -49,6 +49,7 @@ export type StorageAccessType = {
 | 
			
		|||
  'always-relay-calls': boolean;
 | 
			
		||||
  'audio-notification': boolean;
 | 
			
		||||
  'auto-download-update': boolean;
 | 
			
		||||
  autoConvertEmoji: boolean;
 | 
			
		||||
  'badge-count-muted-conversations': boolean;
 | 
			
		||||
  'blocked-groups': ReadonlyArray<string>;
 | 
			
		||||
  'blocked-uuids': ReadonlyArray<ServiceIdString>;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ export const STORAGE_UI_KEYS: ReadonlyArray<keyof StorageAccessType> = [
 | 
			
		|||
  'audio-notification',
 | 
			
		||||
  'audioMessage',
 | 
			
		||||
  'auto-download-update',
 | 
			
		||||
  'autoConvertEmoji',
 | 
			
		||||
  'badge-count-muted-conversations',
 | 
			
		||||
  'call-ringtone-notification',
 | 
			
		||||
  'call-system-notification',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,6 +50,7 @@ export type IPCEventsValuesType = {
 | 
			
		|||
  alwaysRelayCalls: boolean | undefined;
 | 
			
		||||
  audioNotification: boolean | undefined;
 | 
			
		||||
  audioMessage: boolean;
 | 
			
		||||
  autoConvertEmoji: boolean;
 | 
			
		||||
  autoDownloadUpdate: boolean;
 | 
			
		||||
  autoLaunch: boolean;
 | 
			
		||||
  callRingtoneNotification: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -344,6 +345,8 @@ export function createIPCEvents(
 | 
			
		|||
      window.storage.get('auto-download-update', true),
 | 
			
		||||
    setAutoDownloadUpdate: value =>
 | 
			
		||||
      window.storage.put('auto-download-update', value),
 | 
			
		||||
    getAutoConvertEmoji: () => window.storage.get('autoConvertEmoji', false),
 | 
			
		||||
    setAutoConvertEmoji: value => window.storage.put('autoConvertEmoji', value),
 | 
			
		||||
    getSentMediaQualitySetting: () =>
 | 
			
		||||
      window.storage.get('sent-media-quality', 'standard'),
 | 
			
		||||
    setSentMediaQualitySetting: value =>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,6 +38,7 @@ installCallback('syncRequest');
 | 
			
		|||
installSetting('alwaysRelayCalls');
 | 
			
		||||
installSetting('audioMessage');
 | 
			
		||||
installSetting('audioNotification');
 | 
			
		||||
installSetting('autoConvertEmoji');
 | 
			
		||||
installSetting('autoDownloadUpdate');
 | 
			
		||||
installSetting('autoLaunch');
 | 
			
		||||
installSetting('callRingtoneNotification');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,6 +34,7 @@ SettingsWindowProps.onRender(
 | 
			
		|||
    executeMenuRole,
 | 
			
		||||
    getConversationsWithCustomColor,
 | 
			
		||||
    hasAudioNotifications,
 | 
			
		||||
    hasAutoConvertEmoji,
 | 
			
		||||
    hasAutoDownloadUpdate,
 | 
			
		||||
    hasAutoLaunch,
 | 
			
		||||
    hasCallNotifications,
 | 
			
		||||
| 
						 | 
				
			
			@ -69,6 +70,7 @@ SettingsWindowProps.onRender(
 | 
			
		|||
    makeSyncRequest,
 | 
			
		||||
    notificationContent,
 | 
			
		||||
    onAudioNotificationsChange,
 | 
			
		||||
    onAutoConvertEmojiChange,
 | 
			
		||||
    onAutoDownloadUpdateChange,
 | 
			
		||||
    onAutoLaunchChange,
 | 
			
		||||
    onCallNotificationsChange,
 | 
			
		||||
| 
						 | 
				
			
			@ -135,6 +137,7 @@ SettingsWindowProps.onRender(
 | 
			
		|||
        executeMenuRole={executeMenuRole}
 | 
			
		||||
        getConversationsWithCustomColor={getConversationsWithCustomColor}
 | 
			
		||||
        hasAudioNotifications={hasAudioNotifications}
 | 
			
		||||
        hasAutoConvertEmoji={hasAutoConvertEmoji}
 | 
			
		||||
        hasAutoDownloadUpdate={hasAutoDownloadUpdate}
 | 
			
		||||
        hasAutoLaunch={hasAutoLaunch}
 | 
			
		||||
        hasCallNotifications={hasCallNotifications}
 | 
			
		||||
| 
						 | 
				
			
			@ -174,6 +177,7 @@ SettingsWindowProps.onRender(
 | 
			
		|||
        makeSyncRequest={makeSyncRequest}
 | 
			
		||||
        notificationContent={notificationContent}
 | 
			
		||||
        onAudioNotificationsChange={onAudioNotificationsChange}
 | 
			
		||||
        onAutoConvertEmojiChange={onAutoConvertEmojiChange}
 | 
			
		||||
        onAutoDownloadUpdateChange={onAutoDownloadUpdateChange}
 | 
			
		||||
        onAutoLaunchChange={onAutoLaunchChange}
 | 
			
		||||
        onCallNotificationsChange={onCallNotificationsChange}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,6 +22,7 @@ function doneRendering() {
 | 
			
		|||
 | 
			
		||||
const settingMessageAudio = createSetting('audioMessage');
 | 
			
		||||
const settingAudioNotification = createSetting('audioNotification');
 | 
			
		||||
const settingAutoConvertEmoji = createSetting('autoConvertEmoji');
 | 
			
		||||
const settingAutoDownloadUpdate = createSetting('autoDownloadUpdate');
 | 
			
		||||
const settingAutoLaunch = createSetting('autoLaunch');
 | 
			
		||||
const settingCallRingtoneNotification = createSetting(
 | 
			
		||||
| 
						 | 
				
			
			@ -140,6 +141,7 @@ async function renderPreferences() {
 | 
			
		|||
    blockedCount,
 | 
			
		||||
    deviceName,
 | 
			
		||||
    hasAudioNotifications,
 | 
			
		||||
    hasAutoConvertEmoji,
 | 
			
		||||
    hasAutoDownloadUpdate,
 | 
			
		||||
    hasAutoLaunch,
 | 
			
		||||
    hasCallNotifications,
 | 
			
		||||
| 
						 | 
				
			
			@ -181,6 +183,7 @@ async function renderPreferences() {
 | 
			
		|||
    blockedCount: settingBlockedCount.getValue(),
 | 
			
		||||
    deviceName: settingDeviceName.getValue(),
 | 
			
		||||
    hasAudioNotifications: settingAudioNotification.getValue(),
 | 
			
		||||
    hasAutoConvertEmoji: settingAutoConvertEmoji.getValue(),
 | 
			
		||||
    hasAutoDownloadUpdate: settingAutoDownloadUpdate.getValue(),
 | 
			
		||||
    hasAutoLaunch: settingAutoLaunch.getValue(),
 | 
			
		||||
    hasCallNotifications: settingCallSystemNotification.getValue(),
 | 
			
		||||
| 
						 | 
				
			
			@ -247,6 +250,7 @@ async function renderPreferences() {
 | 
			
		|||
    defaultConversationColor,
 | 
			
		||||
    deviceName,
 | 
			
		||||
    hasAudioNotifications,
 | 
			
		||||
    hasAutoConvertEmoji,
 | 
			
		||||
    hasAutoDownloadUpdate,
 | 
			
		||||
    hasAutoLaunch,
 | 
			
		||||
    hasCallNotifications,
 | 
			
		||||
| 
						 | 
				
			
			@ -320,6 +324,9 @@ async function renderPreferences() {
 | 
			
		|||
    onAudioNotificationsChange: attachRenderCallback(
 | 
			
		||||
      settingAudioNotification.setValue
 | 
			
		||||
    ),
 | 
			
		||||
    onAutoConvertEmojiChange: attachRenderCallback(
 | 
			
		||||
      settingAutoConvertEmoji.setValue
 | 
			
		||||
    ),
 | 
			
		||||
    onAutoDownloadUpdateChange: attachRenderCallback(
 | 
			
		||||
      settingAutoDownloadUpdate.setValue
 | 
			
		||||
    ),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue