Skip to content

Commit

Permalink
Some improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
indutny-signal committed Dec 20, 2023
1 parent 14a2714 commit c53eefa
Show file tree
Hide file tree
Showing 19 changed files with 205 additions and 70 deletions.
8 changes: 8 additions & 0 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 8 additions & 0 deletions ts/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion ts/components/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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>
);
Expand Down
29 changes: 20 additions & 9 deletions ts/components/CompositionInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ import {
getDeltaToRemoveStaleMentions,
getTextAndRangesFromOps,
isMentionBlot,
isEmojiBlot,
getDeltaToRestartMention,
getDeltaToRestartEmoji,
insertEmojiOps,
insertFormattingAndMentionsOps,
} from '../quill/util';
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -731,7 +740,9 @@ export function CompositionInput(props: Props): React.ReactElement {
callbacksRef.current.onPickEmoji(emoji),
skinTone,
},
autoSubstituteAsciiEmojis: true,
autoSubstituteAsciiEmojis: {
skinTone,
},
formattingMenu: {
i18n,
isMenuEnabled: isFormattingEnabled,
Expand Down
2 changes: 2 additions & 0 deletions ts/components/Preferences.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export default {
defaultConversationColor: DEFAULT_CONVERSATION_COLOR,
deviceName: 'Work Windows ME',
hasAudioNotifications: true,
hasAutoConvertEmoji: true,
hasAutoDownloadUpdate: true,
hasAutoLaunch: true,
hasCallNotifications: true,
Expand Down Expand Up @@ -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'),
Expand Down
14 changes: 14 additions & 0 deletions ts/components/Preferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export type PropsDataType = {
defaultConversationColor: DefaultConversationColorType;
deviceName?: string;
hasAudioNotifications?: boolean;
hasAutoConvertEmoji: boolean;
hasAutoDownloadUpdate: boolean;
hasAutoLaunch: boolean;
hasCallNotifications: boolean;
Expand Down Expand Up @@ -159,6 +160,7 @@ type PropsFunctionType = {

// Change handlers
onAudioNotificationsChange: CheckboxChangeHandlerType;
onAutoConvertEmojiChange: CheckboxChangeHandlerType;
onAutoDownloadUpdateChange: CheckboxChangeHandlerType;
onAutoLaunchChange: CheckboxChangeHandlerType;
onCallNotificationsChange: CheckboxChangeHandlerType;
Expand Down Expand Up @@ -257,6 +259,7 @@ export function Preferences({
executeMenuRole,
getConversationsWithCustomColor,
hasAudioNotifications,
hasAutoConvertEmoji,
hasAutoDownloadUpdate,
hasAutoLaunch,
hasCallNotifications,
Expand Down Expand Up @@ -293,6 +296,7 @@ export function Preferences({
makeSyncRequest,
notificationContent,
onAudioNotificationsChange,
onAutoConvertEmojiChange,
onAutoDownloadUpdateChange,
onAutoLaunchChange,
onCallNotificationsChange,
Expand Down Expand Up @@ -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={
Expand Down
1 change: 1 addition & 0 deletions ts/main/settingsChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export class SettingsChannel extends EventEmitter {
});
this.installSetting('textFormatting');

this.installSetting('autoConvertEmoji');
this.installSetting('autoDownloadUpdate');
this.installSetting('autoLaunch');

Expand Down
112 changes: 68 additions & 44 deletions ts/quill/auto-substitute-ascii-emojis/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;

Expand All @@ -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) {
Expand All @@ -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);
}
}
20 changes: 17 additions & 3 deletions ts/quill/emoji/blot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,42 @@ 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';

static override tagName = 'img';

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 };
}
}
7 changes: 6 additions & 1 deletion ts/quill/emoji/completion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit c53eefa

Please sign in to comment.