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",
|
"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"
|
"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": {
|
"icu:Preferences--advanced": {
|
||||||
"messageformat": "Advanced",
|
"messageformat": "Advanced",
|
||||||
"description": "Title for advanced settings"
|
"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(
|
setAppLoadingScreenMessage(
|
||||||
window.i18n('icu:optimizingApplication'),
|
window.i18n('icu:optimizingApplication'),
|
||||||
window.i18n
|
window.i18n
|
||||||
|
|
|
@ -5,6 +5,7 @@ import React, { forwardRef, useMemo } from 'react';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { getClassNamesFor } from '../util/getClassNamesFor';
|
import { getClassNamesFor } from '../util/getClassNamesFor';
|
||||||
|
import { Emojify } from './conversation/Emojify';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
|
@ -61,7 +62,9 @@ export const Checkbox = forwardRef(function CheckboxInner(
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={id}>
|
<label htmlFor={id}>
|
||||||
<div>{label}</div>
|
<div>{label}</div>
|
||||||
<div className={getClassName('__description')}>{description}</div>
|
<div className={getClassName('__description')}>
|
||||||
|
<Emojify text={description ?? ''} />
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -39,7 +39,9 @@ import {
|
||||||
getDeltaToRemoveStaleMentions,
|
getDeltaToRemoveStaleMentions,
|
||||||
getTextAndRangesFromOps,
|
getTextAndRangesFromOps,
|
||||||
isMentionBlot,
|
isMentionBlot,
|
||||||
|
isEmojiBlot,
|
||||||
getDeltaToRestartMention,
|
getDeltaToRestartMention,
|
||||||
|
getDeltaToRestartEmoji,
|
||||||
insertEmojiOps,
|
insertEmojiOps,
|
||||||
insertFormattingAndMentionsOps,
|
insertFormattingAndMentionsOps,
|
||||||
} from '../quill/util';
|
} from '../quill/util';
|
||||||
|
@ -284,7 +286,7 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||||
const delta = new Delta()
|
const delta = new Delta()
|
||||||
.retain(insertionRange.index)
|
.retain(insertionRange.index)
|
||||||
.delete(insertionRange.length)
|
.delete(insertionRange.length)
|
||||||
.insert({ emoji });
|
.insert({ emoji: { value: emoji } });
|
||||||
|
|
||||||
quill.updateContents(delta, 'user');
|
quill.updateContents(delta, 'user');
|
||||||
quill.setSelection(insertionRange.index + 1, 0, '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);
|
const [blotToDelete] = quill.getLeaf(selection.index);
|
||||||
if (!isMentionBlot(blotToDelete)) {
|
if (isMentionBlot(blotToDelete)) {
|
||||||
return true;
|
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);
|
if (isEmojiBlot(blotToDelete)) {
|
||||||
const restartDelta = getDeltaToRestartMention(contents.ops);
|
const contents = quill.getContents(0, selection.index);
|
||||||
|
const restartDelta = getDeltaToRestartEmoji(contents.ops);
|
||||||
|
|
||||||
quill.updateContents(restartDelta);
|
quill.updateContents(restartDelta);
|
||||||
quill.setSelection(selection.index, 0);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChange = (): void => {
|
const onChange = (): void => {
|
||||||
|
@ -731,7 +740,9 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||||
callbacksRef.current.onPickEmoji(emoji),
|
callbacksRef.current.onPickEmoji(emoji),
|
||||||
skinTone,
|
skinTone,
|
||||||
},
|
},
|
||||||
autoSubstituteAsciiEmojis: true,
|
autoSubstituteAsciiEmojis: {
|
||||||
|
skinTone,
|
||||||
|
},
|
||||||
formattingMenu: {
|
formattingMenu: {
|
||||||
i18n,
|
i18n,
|
||||||
isMenuEnabled: isFormattingEnabled,
|
isMenuEnabled: isFormattingEnabled,
|
||||||
|
|
|
@ -76,6 +76,7 @@ export default {
|
||||||
defaultConversationColor: DEFAULT_CONVERSATION_COLOR,
|
defaultConversationColor: DEFAULT_CONVERSATION_COLOR,
|
||||||
deviceName: 'Work Windows ME',
|
deviceName: 'Work Windows ME',
|
||||||
hasAudioNotifications: true,
|
hasAudioNotifications: true,
|
||||||
|
hasAutoConvertEmoji: true,
|
||||||
hasAutoDownloadUpdate: true,
|
hasAutoDownloadUpdate: true,
|
||||||
hasAutoLaunch: true,
|
hasAutoLaunch: true,
|
||||||
hasCallNotifications: true,
|
hasCallNotifications: true,
|
||||||
|
@ -133,6 +134,7 @@ export default {
|
||||||
executeMenuRole: action('executeMenuRole'),
|
executeMenuRole: action('executeMenuRole'),
|
||||||
makeSyncRequest: action('makeSyncRequest'),
|
makeSyncRequest: action('makeSyncRequest'),
|
||||||
onAudioNotificationsChange: action('onAudioNotificationsChange'),
|
onAudioNotificationsChange: action('onAudioNotificationsChange'),
|
||||||
|
onAutoConvertEmojiChange: action('onAutoConvertEmojiChange'),
|
||||||
onAutoDownloadUpdateChange: action('onAutoDownloadUpdateChange'),
|
onAutoDownloadUpdateChange: action('onAutoDownloadUpdateChange'),
|
||||||
onAutoLaunchChange: action('onAutoLaunchChange'),
|
onAutoLaunchChange: action('onAutoLaunchChange'),
|
||||||
onCallNotificationsChange: action('onCallNotificationsChange'),
|
onCallNotificationsChange: action('onCallNotificationsChange'),
|
||||||
|
|
|
@ -75,6 +75,7 @@ export type PropsDataType = {
|
||||||
defaultConversationColor: DefaultConversationColorType;
|
defaultConversationColor: DefaultConversationColorType;
|
||||||
deviceName?: string;
|
deviceName?: string;
|
||||||
hasAudioNotifications?: boolean;
|
hasAudioNotifications?: boolean;
|
||||||
|
hasAutoConvertEmoji: boolean;
|
||||||
hasAutoDownloadUpdate: boolean;
|
hasAutoDownloadUpdate: boolean;
|
||||||
hasAutoLaunch: boolean;
|
hasAutoLaunch: boolean;
|
||||||
hasCallNotifications: boolean;
|
hasCallNotifications: boolean;
|
||||||
|
@ -159,6 +160,7 @@ type PropsFunctionType = {
|
||||||
|
|
||||||
// Change handlers
|
// Change handlers
|
||||||
onAudioNotificationsChange: CheckboxChangeHandlerType;
|
onAudioNotificationsChange: CheckboxChangeHandlerType;
|
||||||
|
onAutoConvertEmojiChange: CheckboxChangeHandlerType;
|
||||||
onAutoDownloadUpdateChange: CheckboxChangeHandlerType;
|
onAutoDownloadUpdateChange: CheckboxChangeHandlerType;
|
||||||
onAutoLaunchChange: CheckboxChangeHandlerType;
|
onAutoLaunchChange: CheckboxChangeHandlerType;
|
||||||
onCallNotificationsChange: CheckboxChangeHandlerType;
|
onCallNotificationsChange: CheckboxChangeHandlerType;
|
||||||
|
@ -257,6 +259,7 @@ export function Preferences({
|
||||||
executeMenuRole,
|
executeMenuRole,
|
||||||
getConversationsWithCustomColor,
|
getConversationsWithCustomColor,
|
||||||
hasAudioNotifications,
|
hasAudioNotifications,
|
||||||
|
hasAutoConvertEmoji,
|
||||||
hasAutoDownloadUpdate,
|
hasAutoDownloadUpdate,
|
||||||
hasAutoLaunch,
|
hasAutoLaunch,
|
||||||
hasCallNotifications,
|
hasCallNotifications,
|
||||||
|
@ -293,6 +296,7 @@ export function Preferences({
|
||||||
makeSyncRequest,
|
makeSyncRequest,
|
||||||
notificationContent,
|
notificationContent,
|
||||||
onAudioNotificationsChange,
|
onAudioNotificationsChange,
|
||||||
|
onAutoConvertEmojiChange,
|
||||||
onAutoDownloadUpdateChange,
|
onAutoDownloadUpdateChange,
|
||||||
onAutoLaunchChange,
|
onAutoLaunchChange,
|
||||||
onCallNotificationsChange,
|
onCallNotificationsChange,
|
||||||
|
@ -856,6 +860,16 @@ export function Preferences({
|
||||||
name="linkPreviews"
|
name="linkPreviews"
|
||||||
onChange={noop}
|
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
|
<Control
|
||||||
left={i18n('icu:Preferences__sent-media-quality')}
|
left={i18n('icu:Preferences__sent-media-quality')}
|
||||||
right={
|
right={
|
||||||
|
|
|
@ -93,6 +93,7 @@ export class SettingsChannel extends EventEmitter {
|
||||||
});
|
});
|
||||||
this.installSetting('textFormatting');
|
this.installSetting('textFormatting');
|
||||||
|
|
||||||
|
this.installSetting('autoConvertEmoji');
|
||||||
this.installSetting('autoDownloadUpdate');
|
this.installSetting('autoDownloadUpdate');
|
||||||
this.installSetting('autoLaunch');
|
this.installSetting('autoLaunch');
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
import type Quill from 'quill';
|
import type Quill from 'quill';
|
||||||
import Delta from 'quill-delta';
|
import Delta from 'quill-delta';
|
||||||
import _ from 'lodash';
|
|
||||||
import type { EmojiData } from '../../components/emoji/lib';
|
import type { EmojiData } from '../../components/emoji/lib';
|
||||||
import {
|
import {
|
||||||
convertShortName,
|
convertShortName,
|
||||||
|
@ -15,32 +14,34 @@ type AutoSubstituteAsciiEmojisOptions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const emojiMap: Record<string, string> = {
|
const emojiMap: Record<string, string> = {
|
||||||
':)': 'slightly_smiling_face',
|
|
||||||
':-)': 'slightly_smiling_face',
|
':-)': 'slightly_smiling_face',
|
||||||
':(': 'slightly_frowning_face',
|
|
||||||
':-(': 'slightly_frowning_face',
|
':-(': 'slightly_frowning_face',
|
||||||
':D': 'smiley',
|
':-D': 'grinning',
|
||||||
':-D': 'smiley',
|
':-*': 'kissing_heart',
|
||||||
':*': 'kissing',
|
|
||||||
':-*': 'kissing',
|
|
||||||
':P': 'stuck_out_tongue',
|
|
||||||
':-P': 'stuck_out_tongue',
|
':-P': 'stuck_out_tongue',
|
||||||
';P': 'stuck_out_tongue_winking_eye',
|
':-p': 'stuck_out_tongue',
|
||||||
';-P': 'stuck_out_tongue_winking_eye',
|
|
||||||
'D:': 'anguished',
|
|
||||||
"D-':": 'anguished',
|
|
||||||
':O': 'open_mouth',
|
|
||||||
':-O': 'open_mouth',
|
|
||||||
":'(": 'cry',
|
":'(": 'cry',
|
||||||
":'-(": 'cry',
|
':-\\': 'confused',
|
||||||
':/': 'confused',
|
|
||||||
':-/': 'confused',
|
|
||||||
';)': 'wink',
|
|
||||||
';-)': 'wink',
|
';-)': 'wink',
|
||||||
'(Y)': '+1',
|
'(Y)': '+1',
|
||||||
'(N)': '-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 {
|
export class AutoSubstituteAsciiEmojis {
|
||||||
options: AutoSubstituteAsciiEmojisOptions;
|
options: AutoSubstituteAsciiEmojisOptions;
|
||||||
|
|
||||||
|
@ -50,13 +51,24 @@ export class AutoSubstituteAsciiEmojis {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.quill = quill;
|
this.quill = quill;
|
||||||
|
|
||||||
this.quill.on(
|
this.quill.on('text-change', (_now, _before, source) => {
|
||||||
'text-change',
|
if (source !== 'user') {
|
||||||
_.debounce(() => this.onTextChange(), 100)
|
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 {
|
onTextChange(): void {
|
||||||
|
if (!window.storage.get('autoConvertEmoji', false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const range = this.quill.getSelection();
|
const range = this.quill.getSelection();
|
||||||
|
|
||||||
if (!range) {
|
if (!range) {
|
||||||
|
@ -65,32 +77,44 @@ export class AutoSubstituteAsciiEmojis {
|
||||||
|
|
||||||
const [blot, index] = this.quill.getLeaf(range.index);
|
const [blot, index] = this.quill.getLeaf(range.index);
|
||||||
|
|
||||||
if (blot !== undefined && blot.text !== undefined) {
|
if (blot?.text == null) {
|
||||||
const blotText: string = blot.text;
|
return;
|
||||||
Object.entries(emojiMap).some(([textEmoji, emojiName]) => {
|
}
|
||||||
if (blotText.substring(0, index).endsWith(textEmoji)) {
|
|
||||||
const emojiData = convertShortNameToData(
|
const textBeforeCursor = blot.text.slice(0, index);
|
||||||
emojiName,
|
const match = textBeforeCursor.match(EMOJI_REGEXP);
|
||||||
this.options.skinTone
|
if (match == null) {
|
||||||
);
|
return;
|
||||||
if (emojiData) {
|
}
|
||||||
this.insertEmoji(
|
|
||||||
emojiData,
|
const [, textEmoji] = match;
|
||||||
range.index - textEmoji.length,
|
const emojiName = emojiMap[textEmoji];
|
||||||
textEmoji.length
|
|
||||||
);
|
const emojiData = convertShortNameToData(emojiName, this.options.skinTone);
|
||||||
return true;
|
if (emojiData) {
|
||||||
}
|
this.insertEmoji(
|
||||||
}
|
emojiData,
|
||||||
return false;
|
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 emoji = convertShortName(emojiData.short_name, this.options.skinTone);
|
||||||
const delta = new Delta().retain(index).delete(range).insert({ emoji });
|
const delta = new Delta()
|
||||||
this.quill.updateContents(delta, 'user');
|
.retain(index)
|
||||||
|
.delete(range)
|
||||||
|
.insert({
|
||||||
|
emoji: { value: emoji, source },
|
||||||
|
});
|
||||||
|
this.quill.updateContents(delta, 'api');
|
||||||
this.quill.setSelection(index + 1, 0);
|
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/conversation/Emojify.tsx
|
||||||
// ts/components/emoji/Emoji.tsx
|
// ts/components/emoji/Emoji.tsx
|
||||||
|
|
||||||
|
export type EmojiBlotValue = Readonly<{
|
||||||
|
value: string;
|
||||||
|
source?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
export class EmojiBlot extends Embed {
|
export class EmojiBlot extends Embed {
|
||||||
static override blotName = 'emoji';
|
static override blotName = 'emoji';
|
||||||
|
|
||||||
|
@ -19,21 +24,30 @@ export class EmojiBlot extends Embed {
|
||||||
|
|
||||||
static override className = 'emoji-blot';
|
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;
|
const node = super.create(undefined) as HTMLElement;
|
||||||
node.dataset.emoji = emoji;
|
node.dataset.emoji = emoji;
|
||||||
|
node.dataset.source = source;
|
||||||
|
|
||||||
const image = emojiToImage(emoji);
|
const image = emojiToImage(emoji);
|
||||||
|
|
||||||
node.setAttribute('src', image || '');
|
node.setAttribute('src', image || '');
|
||||||
node.setAttribute('data-emoji', emoji);
|
node.setAttribute('data-emoji', emoji);
|
||||||
|
node.setAttribute('data-source', source || '');
|
||||||
node.setAttribute('title', emoji);
|
node.setAttribute('title', emoji);
|
||||||
node.setAttribute('aria-label', emoji);
|
node.setAttribute('aria-label', emoji);
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
static override value(node: HTMLElement): string | undefined {
|
static override value(node: HTMLElement): EmojiBlotValue | undefined {
|
||||||
return node.dataset.emoji;
|
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 {
|
): void {
|
||||||
const emoji = convertShortName(emojiData.short_name, this.options.skinTone);
|
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) {
|
if (withTrailingSpace) {
|
||||||
// The extra space we add won't be formatted unless we manually provide attributes
|
// 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('emoji') ||
|
||||||
node.classList.contains('module-emoji__image--16px')
|
node.classList.contains('module-emoji__image--16px')
|
||||||
) {
|
) {
|
||||||
const emoji = node.getAttribute('aria-label');
|
const value = node.getAttribute('aria-label');
|
||||||
return new Delta().insert({ emoji }, attributes);
|
return new Delta().insert({ emoji: { value } }, attributes);
|
||||||
}
|
}
|
||||||
return delta;
|
return delta;
|
||||||
};
|
};
|
||||||
|
@ -27,8 +27,8 @@ export const matchEmojiBlot: Matcher = (
|
||||||
attributes: AttributeMap
|
attributes: AttributeMap
|
||||||
): Delta => {
|
): Delta => {
|
||||||
if (node.classList.contains('emoji-blot')) {
|
if (node.classList.contains('emoji-blot')) {
|
||||||
const { emoji } = node.dataset;
|
const { emoji: value, source } = node.dataset;
|
||||||
return new Delta().insert({ emoji }, attributes);
|
return new Delta().insert({ emoji: { value, source } }, attributes);
|
||||||
}
|
}
|
||||||
return delta;
|
return delta;
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,6 +13,7 @@ import type {
|
||||||
} from '../types/BodyRange';
|
} from '../types/BodyRange';
|
||||||
import { BodyRange } from '../types/BodyRange';
|
import { BodyRange } from '../types/BodyRange';
|
||||||
import type { MentionBlot } from './mentions/blot';
|
import type { MentionBlot } from './mentions/blot';
|
||||||
|
import type { EmojiBlot } from './emoji/blot';
|
||||||
import { isNewlineOnlyOp, QuillFormattingStyle } from './formatting/menu';
|
import { isNewlineOnlyOp, QuillFormattingStyle } from './formatting/menu';
|
||||||
import { isNotNil } from '../util/isNotNil';
|
import { isNotNil } from '../util/isNotNil';
|
||||||
import type { AciString } from '../types/ServiceId';
|
import type { AciString } from '../types/ServiceId';
|
||||||
|
@ -27,6 +28,9 @@ export type FormattingBlotValue = {
|
||||||
style: BodyRange.Style;
|
style: BodyRange.Style;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isEmojiBlot = (blot: LeafBlot): blot is EmojiBlot =>
|
||||||
|
blot.value() && blot.value().emoji;
|
||||||
|
|
||||||
export const isMentionBlot = (blot: LeafBlot): blot is MentionBlot =>
|
export const isMentionBlot = (blot: LeafBlot): blot is MentionBlot =>
|
||||||
blot.value() && blot.value().mention;
|
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 InsertOp<K extends string, T> = Op & { insert: { [V in K]: T } };
|
||||||
|
|
||||||
export type InsertMentionOp = InsertOp<'mention', MentionBlotValue>;
|
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 =>
|
export const isRetainOp = (op?: Op): op is RetainOp =>
|
||||||
op !== undefined && op.retain !== undefined;
|
op !== undefined && op.retain !== undefined;
|
||||||
|
@ -64,7 +71,7 @@ export const getTextFromOps = (ops: Array<DeltaOperation>): string =>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInsertEmojiOp(op)) {
|
if (isInsertEmojiOp(op)) {
|
||||||
return acc + op.insert.emoji;
|
return acc + op.insert.emoji.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInsertMentionOp(op)) {
|
if (isInsertMentionOp(op)) {
|
||||||
|
@ -187,7 +194,7 @@ export const getTextAndRangesFromOps = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInsertEmojiOp(op)) {
|
if (isInsertEmojiOp(op)) {
|
||||||
return acc + op.insert.emoji;
|
return acc + op.insert.emoji.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInsertMentionOp(op)) {
|
if (isInsertMentionOp(op)) {
|
||||||
|
@ -304,6 +311,27 @@ export const getDeltaToRestartMention = (ops: Array<Op>): Delta => {
|
||||||
return new Delta(changes);
|
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 = (
|
export const getDeltaToRemoveStaleMentions = (
|
||||||
ops: Array<Op>,
|
ops: Array<Op>,
|
||||||
memberAcis: Array<AciString>
|
memberAcis: Array<AciString>
|
||||||
|
@ -422,7 +450,7 @@ export const insertEmojiOps = (
|
||||||
if (emojiData) {
|
if (emojiData) {
|
||||||
ops.push({ insert: text.slice(index, match.index), attributes });
|
ops.push({ insert: text.slice(index, match.index), attributes });
|
||||||
ops.push({
|
ops.push({
|
||||||
insert: { emoji },
|
insert: { emoji: { value: emoji } },
|
||||||
attributes: { ...existingAttributes, ...attributes },
|
attributes: { ...existingAttributes, ...attributes },
|
||||||
});
|
});
|
||||||
index = match.index + emoji.length;
|
index = match.index + emoji.length;
|
||||||
|
|
|
@ -59,12 +59,12 @@ describe('getDeltaToRemoveStaleMentions', () => {
|
||||||
const originalOps = [
|
const originalOps = [
|
||||||
{
|
{
|
||||||
insert: {
|
insert: {
|
||||||
emoji: '😂',
|
emoji: { value: '😂' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
insert: {
|
insert: {
|
||||||
emoji: '🍋',
|
emoji: { value: '🍋' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -312,7 +312,7 @@ describe('getTextAndRangesFromOps', () => {
|
||||||
const ops = [
|
const ops = [
|
||||||
{
|
{
|
||||||
insert: {
|
insert: {
|
||||||
emoji: '😂',
|
emoji: { value: '😂' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -579,7 +579,7 @@ describe('getDeltaToRestartMention', () => {
|
||||||
const originalOps = [
|
const originalOps = [
|
||||||
{
|
{
|
||||||
insert: {
|
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;
|
'always-relay-calls': boolean;
|
||||||
'audio-notification': boolean;
|
'audio-notification': boolean;
|
||||||
'auto-download-update': boolean;
|
'auto-download-update': boolean;
|
||||||
|
autoConvertEmoji: boolean;
|
||||||
'badge-count-muted-conversations': boolean;
|
'badge-count-muted-conversations': boolean;
|
||||||
'blocked-groups': ReadonlyArray<string>;
|
'blocked-groups': ReadonlyArray<string>;
|
||||||
'blocked-uuids': ReadonlyArray<ServiceIdString>;
|
'blocked-uuids': ReadonlyArray<ServiceIdString>;
|
||||||
|
|
|
@ -13,6 +13,7 @@ export const STORAGE_UI_KEYS: ReadonlyArray<keyof StorageAccessType> = [
|
||||||
'audio-notification',
|
'audio-notification',
|
||||||
'audioMessage',
|
'audioMessage',
|
||||||
'auto-download-update',
|
'auto-download-update',
|
||||||
|
'autoConvertEmoji',
|
||||||
'badge-count-muted-conversations',
|
'badge-count-muted-conversations',
|
||||||
'call-ringtone-notification',
|
'call-ringtone-notification',
|
||||||
'call-system-notification',
|
'call-system-notification',
|
||||||
|
|
|
@ -50,6 +50,7 @@ export type IPCEventsValuesType = {
|
||||||
alwaysRelayCalls: boolean | undefined;
|
alwaysRelayCalls: boolean | undefined;
|
||||||
audioNotification: boolean | undefined;
|
audioNotification: boolean | undefined;
|
||||||
audioMessage: boolean;
|
audioMessage: boolean;
|
||||||
|
autoConvertEmoji: boolean;
|
||||||
autoDownloadUpdate: boolean;
|
autoDownloadUpdate: boolean;
|
||||||
autoLaunch: boolean;
|
autoLaunch: boolean;
|
||||||
callRingtoneNotification: boolean;
|
callRingtoneNotification: boolean;
|
||||||
|
@ -344,6 +345,8 @@ export function createIPCEvents(
|
||||||
window.storage.get('auto-download-update', true),
|
window.storage.get('auto-download-update', true),
|
||||||
setAutoDownloadUpdate: value =>
|
setAutoDownloadUpdate: value =>
|
||||||
window.storage.put('auto-download-update', value),
|
window.storage.put('auto-download-update', value),
|
||||||
|
getAutoConvertEmoji: () => window.storage.get('autoConvertEmoji', false),
|
||||||
|
setAutoConvertEmoji: value => window.storage.put('autoConvertEmoji', value),
|
||||||
getSentMediaQualitySetting: () =>
|
getSentMediaQualitySetting: () =>
|
||||||
window.storage.get('sent-media-quality', 'standard'),
|
window.storage.get('sent-media-quality', 'standard'),
|
||||||
setSentMediaQualitySetting: value =>
|
setSentMediaQualitySetting: value =>
|
||||||
|
|
|
@ -38,6 +38,7 @@ installCallback('syncRequest');
|
||||||
installSetting('alwaysRelayCalls');
|
installSetting('alwaysRelayCalls');
|
||||||
installSetting('audioMessage');
|
installSetting('audioMessage');
|
||||||
installSetting('audioNotification');
|
installSetting('audioNotification');
|
||||||
|
installSetting('autoConvertEmoji');
|
||||||
installSetting('autoDownloadUpdate');
|
installSetting('autoDownloadUpdate');
|
||||||
installSetting('autoLaunch');
|
installSetting('autoLaunch');
|
||||||
installSetting('callRingtoneNotification');
|
installSetting('callRingtoneNotification');
|
||||||
|
|
|
@ -34,6 +34,7 @@ SettingsWindowProps.onRender(
|
||||||
executeMenuRole,
|
executeMenuRole,
|
||||||
getConversationsWithCustomColor,
|
getConversationsWithCustomColor,
|
||||||
hasAudioNotifications,
|
hasAudioNotifications,
|
||||||
|
hasAutoConvertEmoji,
|
||||||
hasAutoDownloadUpdate,
|
hasAutoDownloadUpdate,
|
||||||
hasAutoLaunch,
|
hasAutoLaunch,
|
||||||
hasCallNotifications,
|
hasCallNotifications,
|
||||||
|
@ -69,6 +70,7 @@ SettingsWindowProps.onRender(
|
||||||
makeSyncRequest,
|
makeSyncRequest,
|
||||||
notificationContent,
|
notificationContent,
|
||||||
onAudioNotificationsChange,
|
onAudioNotificationsChange,
|
||||||
|
onAutoConvertEmojiChange,
|
||||||
onAutoDownloadUpdateChange,
|
onAutoDownloadUpdateChange,
|
||||||
onAutoLaunchChange,
|
onAutoLaunchChange,
|
||||||
onCallNotificationsChange,
|
onCallNotificationsChange,
|
||||||
|
@ -135,6 +137,7 @@ SettingsWindowProps.onRender(
|
||||||
executeMenuRole={executeMenuRole}
|
executeMenuRole={executeMenuRole}
|
||||||
getConversationsWithCustomColor={getConversationsWithCustomColor}
|
getConversationsWithCustomColor={getConversationsWithCustomColor}
|
||||||
hasAudioNotifications={hasAudioNotifications}
|
hasAudioNotifications={hasAudioNotifications}
|
||||||
|
hasAutoConvertEmoji={hasAutoConvertEmoji}
|
||||||
hasAutoDownloadUpdate={hasAutoDownloadUpdate}
|
hasAutoDownloadUpdate={hasAutoDownloadUpdate}
|
||||||
hasAutoLaunch={hasAutoLaunch}
|
hasAutoLaunch={hasAutoLaunch}
|
||||||
hasCallNotifications={hasCallNotifications}
|
hasCallNotifications={hasCallNotifications}
|
||||||
|
@ -174,6 +177,7 @@ SettingsWindowProps.onRender(
|
||||||
makeSyncRequest={makeSyncRequest}
|
makeSyncRequest={makeSyncRequest}
|
||||||
notificationContent={notificationContent}
|
notificationContent={notificationContent}
|
||||||
onAudioNotificationsChange={onAudioNotificationsChange}
|
onAudioNotificationsChange={onAudioNotificationsChange}
|
||||||
|
onAutoConvertEmojiChange={onAutoConvertEmojiChange}
|
||||||
onAutoDownloadUpdateChange={onAutoDownloadUpdateChange}
|
onAutoDownloadUpdateChange={onAutoDownloadUpdateChange}
|
||||||
onAutoLaunchChange={onAutoLaunchChange}
|
onAutoLaunchChange={onAutoLaunchChange}
|
||||||
onCallNotificationsChange={onCallNotificationsChange}
|
onCallNotificationsChange={onCallNotificationsChange}
|
||||||
|
|
|
@ -22,6 +22,7 @@ function doneRendering() {
|
||||||
|
|
||||||
const settingMessageAudio = createSetting('audioMessage');
|
const settingMessageAudio = createSetting('audioMessage');
|
||||||
const settingAudioNotification = createSetting('audioNotification');
|
const settingAudioNotification = createSetting('audioNotification');
|
||||||
|
const settingAutoConvertEmoji = createSetting('autoConvertEmoji');
|
||||||
const settingAutoDownloadUpdate = createSetting('autoDownloadUpdate');
|
const settingAutoDownloadUpdate = createSetting('autoDownloadUpdate');
|
||||||
const settingAutoLaunch = createSetting('autoLaunch');
|
const settingAutoLaunch = createSetting('autoLaunch');
|
||||||
const settingCallRingtoneNotification = createSetting(
|
const settingCallRingtoneNotification = createSetting(
|
||||||
|
@ -140,6 +141,7 @@ async function renderPreferences() {
|
||||||
blockedCount,
|
blockedCount,
|
||||||
deviceName,
|
deviceName,
|
||||||
hasAudioNotifications,
|
hasAudioNotifications,
|
||||||
|
hasAutoConvertEmoji,
|
||||||
hasAutoDownloadUpdate,
|
hasAutoDownloadUpdate,
|
||||||
hasAutoLaunch,
|
hasAutoLaunch,
|
||||||
hasCallNotifications,
|
hasCallNotifications,
|
||||||
|
@ -181,6 +183,7 @@ async function renderPreferences() {
|
||||||
blockedCount: settingBlockedCount.getValue(),
|
blockedCount: settingBlockedCount.getValue(),
|
||||||
deviceName: settingDeviceName.getValue(),
|
deviceName: settingDeviceName.getValue(),
|
||||||
hasAudioNotifications: settingAudioNotification.getValue(),
|
hasAudioNotifications: settingAudioNotification.getValue(),
|
||||||
|
hasAutoConvertEmoji: settingAutoConvertEmoji.getValue(),
|
||||||
hasAutoDownloadUpdate: settingAutoDownloadUpdate.getValue(),
|
hasAutoDownloadUpdate: settingAutoDownloadUpdate.getValue(),
|
||||||
hasAutoLaunch: settingAutoLaunch.getValue(),
|
hasAutoLaunch: settingAutoLaunch.getValue(),
|
||||||
hasCallNotifications: settingCallSystemNotification.getValue(),
|
hasCallNotifications: settingCallSystemNotification.getValue(),
|
||||||
|
@ -247,6 +250,7 @@ async function renderPreferences() {
|
||||||
defaultConversationColor,
|
defaultConversationColor,
|
||||||
deviceName,
|
deviceName,
|
||||||
hasAudioNotifications,
|
hasAudioNotifications,
|
||||||
|
hasAutoConvertEmoji,
|
||||||
hasAutoDownloadUpdate,
|
hasAutoDownloadUpdate,
|
||||||
hasAutoLaunch,
|
hasAutoLaunch,
|
||||||
hasCallNotifications,
|
hasCallNotifications,
|
||||||
|
@ -320,6 +324,9 @@ async function renderPreferences() {
|
||||||
onAudioNotificationsChange: attachRenderCallback(
|
onAudioNotificationsChange: attachRenderCallback(
|
||||||
settingAudioNotification.setValue
|
settingAudioNotification.setValue
|
||||||
),
|
),
|
||||||
|
onAutoConvertEmojiChange: attachRenderCallback(
|
||||||
|
settingAutoConvertEmoji.setValue
|
||||||
|
),
|
||||||
onAutoDownloadUpdateChange: attachRenderCallback(
|
onAutoDownloadUpdateChange: attachRenderCallback(
|
||||||
settingAutoDownloadUpdate.setValue
|
settingAutoDownloadUpdate.setValue
|
||||||
),
|
),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue