Apply mention ranges to body when multi-forwarding
This commit is contained in:
parent
063a1d9df3
commit
6bd802a03d
5 changed files with 738 additions and 208 deletions
|
@ -32,7 +32,7 @@ import { LinkPreviewSourceType } from '../types/LinkPreview';
|
||||||
import { ToastType } from '../types/Toast';
|
import { ToastType } from '../types/Toast';
|
||||||
import type { ShowToastAction } from '../state/ducks/toast';
|
import type { ShowToastAction } from '../state/ducks/toast';
|
||||||
import type { HydratedBodyRangesType } from '../types/BodyRange';
|
import type { HydratedBodyRangesType } from '../types/BodyRange';
|
||||||
import { BodyRange } from '../types/BodyRange';
|
import { applyRangesToText } from '../types/BodyRange';
|
||||||
import { UserText } from './UserText';
|
import { UserText } from './UserText';
|
||||||
import { Modal } from './Modal';
|
import { Modal } from './Modal';
|
||||||
import { SizeObserver } from '../hooks/useSizeObserver';
|
import { SizeObserver } from '../hooks/useSizeObserver';
|
||||||
|
@ -135,11 +135,25 @@ export function ForwardMessagesModal({
|
||||||
} else {
|
} else {
|
||||||
doForwardMessages(
|
doForwardMessages(
|
||||||
conversationIds,
|
conversationIds,
|
||||||
drafts.map(draft => ({
|
drafts.map(draft => {
|
||||||
...draft,
|
|
||||||
// We don't keep @mention bodyRanges in multi-forward scenarios
|
// We don't keep @mention bodyRanges in multi-forward scenarios
|
||||||
bodyRanges: draft.bodyRanges?.filter(BodyRange.isFormatting),
|
const result = applyRangesToText(
|
||||||
}))
|
{
|
||||||
|
body: draft.messageBody ?? '',
|
||||||
|
bodyRanges: draft.bodyRanges ?? [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
replaceMentions: true,
|
||||||
|
replaceSpoilers: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...draft,
|
||||||
|
messageBody: result.body,
|
||||||
|
bodyRanges: result.bodyRanges,
|
||||||
|
};
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
|
|
@ -51,11 +51,8 @@ import type {
|
||||||
|
|
||||||
import type { EmbeddedContactType } from '../../types/EmbeddedContact';
|
import type { EmbeddedContactType } from '../../types/EmbeddedContact';
|
||||||
import { embeddedContactSelector } from '../../types/EmbeddedContact';
|
import { embeddedContactSelector } from '../../types/EmbeddedContact';
|
||||||
import type {
|
import type { HydratedBodyRangesType } from '../../types/BodyRange';
|
||||||
HydratedBodyRangeMention,
|
import { hydrateRanges } from '../../types/BodyRange';
|
||||||
HydratedBodyRangesType,
|
|
||||||
} from '../../types/BodyRange';
|
|
||||||
import { BodyRange, hydrateRanges } from '../../types/BodyRange';
|
|
||||||
import type { AssertProps } from '../../types/Util';
|
import type { AssertProps } from '../../types/Util';
|
||||||
import type { LinkPreviewType } from '../../types/message/LinkPreviews';
|
import type { LinkPreviewType } from '../../types/message/LinkPreviews';
|
||||||
import { getMentionsRegex } from '../../types/Message';
|
import { getMentionsRegex } from '../../types/Message';
|
||||||
|
@ -336,29 +333,6 @@ export const processBodyRanges = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const extractHydratedMentions = (
|
|
||||||
{ bodyRanges }: Pick<MessageWithUIFieldsType, 'bodyRanges'>,
|
|
||||||
options: { conversationSelector: GetConversationByIdType }
|
|
||||||
): ReadonlyArray<HydratedBodyRangeMention> | undefined => {
|
|
||||||
if (!bodyRanges) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bodyRanges
|
|
||||||
.filter(BodyRange.isMention)
|
|
||||||
.map(range => {
|
|
||||||
const { conversationSelector } = options;
|
|
||||||
const conversation = conversationSelector(range.mentionAci);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...range,
|
|
||||||
conversationID: conversation.id,
|
|
||||||
replacementText: conversation.title,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.sort((a, b) => b.start - a.start);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAuthorForMessage = (
|
const getAuthorForMessage = (
|
||||||
message: MessageWithUIFieldsType,
|
message: MessageWithUIFieldsType,
|
||||||
options: GetContactOptions
|
options: GetContactOptions
|
||||||
|
|
|
@ -2,11 +2,15 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import type { RangeNode } from '../../types/BodyRange';
|
import type {
|
||||||
|
HydratedBodyRangeMention,
|
||||||
|
RangeNode,
|
||||||
|
} from '../../types/BodyRange';
|
||||||
import {
|
import {
|
||||||
BodyRange,
|
BodyRange,
|
||||||
DisplayStyle,
|
DisplayStyle,
|
||||||
applyRangesForText,
|
applyRangeToText,
|
||||||
|
applyRangesToText,
|
||||||
collapseRangeTree,
|
collapseRangeTree,
|
||||||
insertRange,
|
insertRange,
|
||||||
processBodyRangesForSearchResult,
|
processBodyRangesForSearchResult,
|
||||||
|
@ -15,8 +19,6 @@ import { generateAci } from '../../types/ServiceId';
|
||||||
|
|
||||||
const SERVICE_ID_1 = generateAci();
|
const SERVICE_ID_1 = generateAci();
|
||||||
const SERVICE_ID_2 = generateAci();
|
const SERVICE_ID_2 = generateAci();
|
||||||
const SERVICE_ID_3 = generateAci();
|
|
||||||
const SERVICE_ID_4 = generateAci();
|
|
||||||
|
|
||||||
const mentionInfo = {
|
const mentionInfo = {
|
||||||
mentionAci: SERVICE_ID_1,
|
mentionAci: SERVICE_ID_1,
|
||||||
|
@ -941,123 +943,513 @@ describe('BodyRanges', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('applyRangesForText', () => {
|
describe('applying ranges', () => {
|
||||||
it('handles mentions, replaces in reverse order', () => {
|
function mention(start: number, title: string): HydratedBodyRangeMention {
|
||||||
const mentions = [
|
return {
|
||||||
{
|
start,
|
||||||
start: 0,
|
|
||||||
length: 1,
|
length: 1,
|
||||||
mentionAci: SERVICE_ID_3,
|
mentionAci: generateAci(),
|
||||||
replacementText: 'jerry',
|
replacementText: title,
|
||||||
conversationID: 'x',
|
conversationID: '',
|
||||||
},
|
};
|
||||||
{
|
}
|
||||||
start: 7,
|
|
||||||
length: 1,
|
function style(
|
||||||
mentionAci: SERVICE_ID_4,
|
start: number,
|
||||||
replacementText: 'fred',
|
length: number,
|
||||||
conversationID: 'x',
|
styleValue: BodyRange.Style
|
||||||
},
|
): BodyRange<BodyRange.Formatting> {
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
length,
|
||||||
|
style: styleValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('applyRangesToText', () => {
|
||||||
|
it('handles mentions', () => {
|
||||||
|
const replacement = mention(3, 'jamie');
|
||||||
|
const body = '012\uFFFC456';
|
||||||
|
const result = applyRangeToText({ body, bodyRanges: [] }, replacement);
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
body: '012@jamie456',
|
||||||
|
bodyRanges: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles spoilers', () => {
|
||||||
|
const replacement = style(3, 4, BodyRange.Style.SPOILER);
|
||||||
|
const body = '012|45|789';
|
||||||
|
const result = applyRangeToText({ body, bodyRanges: [] }, replacement);
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
body: '012■■■■789',
|
||||||
|
bodyRanges: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updating ranges', () => {
|
||||||
|
describe('replacement same length', () => {
|
||||||
|
function check(
|
||||||
|
input: { start: number; length: number },
|
||||||
|
expected: { start: number; length: number } | null
|
||||||
|
) {
|
||||||
|
const replacement = style(3, 4, BodyRange.Style.SPOILER);
|
||||||
|
const body = 'abc|ef|hij';
|
||||||
|
const bodyRanges = [
|
||||||
|
style(input.start, input.length, BodyRange.Style.BOLD),
|
||||||
];
|
];
|
||||||
const text = "\uFFFC says \uFFFC, I'm here";
|
const result = applyRangeToText({ body, bodyRanges }, replacement);
|
||||||
assert.strictEqual(
|
assert.deepEqual(result, {
|
||||||
applyRangesForText({ text, mentions, spoilers: [] }),
|
body: 'abc■■■■hij',
|
||||||
"@jerry says @fred, I'm here"
|
bodyRanges:
|
||||||
|
expected == null
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
style(
|
||||||
|
expected.start,
|
||||||
|
expected.length,
|
||||||
|
BodyRange.Style.BOLD
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// start before
|
||||||
|
it('start before, end before', () => {
|
||||||
|
// abc|ef|hij -> abc■■■■hij
|
||||||
|
// ^^ -> ^^
|
||||||
|
// 0123456789 -> 0123456789
|
||||||
|
check({ start: 0, length: 2 }, { start: 0, length: 2 });
|
||||||
|
});
|
||||||
|
it('start before, end at start', () => {
|
||||||
|
// abc|ef|hij -> abc■■■■hij
|
||||||
|
// ^^^ -> ^^^
|
||||||
|
// 0123456789 -> 0123456789
|
||||||
|
check({ start: 0, length: 3 }, { start: 0, length: 3 });
|
||||||
|
});
|
||||||
|
it('start before, end in middle', () => {
|
||||||
|
// abc|ef|hij -> abc■■■■hij
|
||||||
|
// ^^^^^ -> ^^^^^^^
|
||||||
|
// 0123456789 -> 0123456789
|
||||||
|
check({ start: 0, length: 5 }, { start: 0, length: 7 });
|
||||||
|
});
|
||||||
|
it('start before, end at end', () => {
|
||||||
|
// abc|ef|hij -> abc■■■■hij
|
||||||
|
// ^^^^^^^ -> ^^^^^^^
|
||||||
|
// 0123456789 -> 0123456789
|
||||||
|
check({ start: 0, length: 7 }, { start: 0, length: 7 });
|
||||||
|
});
|
||||||
|
it('start before, end after', () => {
|
||||||
|
// abc|ef|hij -> abc■■■■hij
|
||||||
|
// ^^^^^^^^^^ -> ^^^^^^^^^^
|
||||||
|
// 0123456789 -> 0123456789
|
||||||
|
check({ start: 0, length: 10 }, { start: 0, length: 10 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// start at start
|
||||||
|
it('start at start, end at start', () => {
|
||||||
|
// abc|ef|hij -> abc■■■■hij
|
||||||
|
// \ -> null
|
||||||
|
// 0123456789 -> 0123456789
|
||||||
|
check({ start: 3, length: 0 }, null);
|
||||||
|
});
|
||||||
|
it('start at start, end in middle', () => {
|
||||||
|
// abc|ef|hij -> abc■■■■hij
|
||||||
|
// ^^ -> null
|
||||||
|
// 0123456789 -> 0123456789
|
||||||
|
check({ start: 3, length: 2 }, null);
|
||||||
|
});
|
||||||
|
it('start at start, end at end', () => {
|
||||||
|
// abc|ef|hij -> abc■■■■hij
|
||||||
|
// ^^^^ -> ^^^^
|
||||||
|
// 0123456789 -> 0123456789
|
||||||
|
check({ start: 3, length: 4 }, { start: 3, length: 4 });
|
||||||
|
});
|
||||||
|
it('start at start, end after', () => {
|
||||||
|
// abc|ef|hij -> abc■■■■hij
|
||||||
|
// ^^^^^^ -> ^^^^^^
|
||||||
|
// 0123456789 -> 0123456789
|
||||||
|
check({ start: 3, length: 6 }, { start: 3, length: 6 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// start in middle
|
||||||
|
it('start in middle, end in middle', () => {
|
||||||
|
// abc|ef|hij -> abc■■■■hij
|
||||||
|
// ^^ -> null
|
||||||
|
// 0123456789 -> 0123456789
|
||||||
|
check({ start: 4, length: 2 }, null);
|
||||||
|
});
|
||||||
|
it('start in middle, end at end', () => {
|
||||||
|
// abc|ef|hij -> abc■■■■hij
|
||||||
|
// ^^^ -> null
|
||||||
|
// 0123456789 -> 0123456789
|
||||||
|
check({ start: 4, length: 3 }, null);
|
||||||
|
});
|
||||||
|
it('start in middle, end after', () => {
|
||||||
|
// abc|ef|hij -> abc■■■■hij
|
||||||
|
// ^^^^^ -> ^^^^^^
|
||||||
|
// 0123456789 -> 0123456789
|
||||||
|
check({ start: 4, length: 5 }, { start: 3, length: 6 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// start at end
|
||||||
|
it('start at end, end at end', () => {
|
||||||
|
// abc|ef|hij -> abc■■■■hij
|
||||||
|
// \ -> null
|
||||||
|
// 0123456789 -> 0123456789
|
||||||
|
check({ start: 7, length: 0 }, null);
|
||||||
|
});
|
||||||
|
it('start at end, end after', () => {
|
||||||
|
// abc|ef|hij -> abc■■■■hij
|
||||||
|
// ^^ -> ^^
|
||||||
|
// 0123456789 -> 0123456789
|
||||||
|
check({ start: 7, length: 2 }, { start: 7, length: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// start after
|
||||||
|
it('start after, end after', () => {
|
||||||
|
// abc|ef|hij -> abc■■■■hij
|
||||||
|
// ^^ -> ^^
|
||||||
|
// 0123456789 -> 0123456789
|
||||||
|
check({ start: 8, length: 2 }, { start: 8, length: 2 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('replacement shortens', () => {
|
||||||
|
function check(
|
||||||
|
input: { start: number; length: number },
|
||||||
|
expected: { start: number; length: number } | null
|
||||||
|
) {
|
||||||
|
const replacement = style(3, 5, BodyRange.Style.SPOILER);
|
||||||
|
const body = 'abc|efg|ijk';
|
||||||
|
const bodyRanges = [
|
||||||
|
style(input.start, input.length, BodyRange.Style.BOLD),
|
||||||
|
];
|
||||||
|
const result = applyRangeToText({ body, bodyRanges }, replacement);
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
body: 'abc■■■■ijk',
|
||||||
|
bodyRanges:
|
||||||
|
expected == null
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
style(
|
||||||
|
expected.start,
|
||||||
|
expected.length,
|
||||||
|
BodyRange.Style.BOLD
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// start before
|
||||||
|
it('start before, end before', () => {
|
||||||
|
// abc|efg|ijk -> abc■■■■ijk
|
||||||
|
// ^^ -> ^^
|
||||||
|
// 01234567890 -> 0123456789
|
||||||
|
check({ start: 0, length: 2 }, { start: 0, length: 2 });
|
||||||
|
});
|
||||||
|
it('start before, end at start', () => {
|
||||||
|
// abc|efg|ijk -> abc■■■■ijk
|
||||||
|
// ^^^ -> ^^^
|
||||||
|
// 01234567890 -> 0123456789
|
||||||
|
check({ start: 0, length: 3 }, { start: 0, length: 3 });
|
||||||
|
});
|
||||||
|
it('start before, end in middle', () => {
|
||||||
|
// abc|efg|ijk -> abc■■■■ijk
|
||||||
|
// ^^^^^ -> ^^^^^^^
|
||||||
|
// 01234567890 -> 0123456789
|
||||||
|
check({ start: 0, length: 5 }, { start: 0, length: 7 });
|
||||||
|
});
|
||||||
|
it('start before, end at end', () => {
|
||||||
|
// abc|efg|ijk -> abc■■■■ijk
|
||||||
|
// ^^^^^^^^ -> ^^^^^^^
|
||||||
|
// 01234567890 -> 0123456789
|
||||||
|
check({ start: 0, length: 8 }, { start: 0, length: 7 });
|
||||||
|
});
|
||||||
|
it('start before, end after', () => {
|
||||||
|
// abc|efg|ijk -> abc■■■■ijk
|
||||||
|
// ^^^^^^^^^^^ -> ^^^^^^^^^^
|
||||||
|
// 01234567890 -> 0123456789
|
||||||
|
check({ start: 0, length: 11 }, { start: 0, length: 10 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// start at start
|
||||||
|
it('start at start, end at start', () => {
|
||||||
|
// abc|efg|ijk -> abc■■■■ijk
|
||||||
|
// \ -> null
|
||||||
|
// 01234567890 -> 0123456789
|
||||||
|
check({ start: 3, length: 0 }, null);
|
||||||
|
});
|
||||||
|
it('start at start, end in middle', () => {
|
||||||
|
// abc|efg|ijk -> abc■■■■ijk
|
||||||
|
// ^^ -> null
|
||||||
|
// 01234567890 -> 0123456789
|
||||||
|
check({ start: 3, length: 2 }, null);
|
||||||
|
});
|
||||||
|
it('start at start, end at end', () => {
|
||||||
|
// abc|efg|ijk -> abc■■■■ijk
|
||||||
|
// ^^^^^ -> ^^^^
|
||||||
|
// 01234567890 -> 0123456789
|
||||||
|
check({ start: 3, length: 5 }, { start: 3, length: 4 });
|
||||||
|
});
|
||||||
|
it('start at start, end after', () => {
|
||||||
|
// abc|efg|ijk -> abc■■■■ijk
|
||||||
|
// ^^^^^^ -> ^^^^^
|
||||||
|
// 01234567890 -> 0123456789
|
||||||
|
check({ start: 3, length: 6 }, { start: 3, length: 5 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// start in middle
|
||||||
|
it('start in middle, end in middle', () => {
|
||||||
|
// abc|efg|ijk -> abc■■■■ijk
|
||||||
|
// ^^ -> null
|
||||||
|
// 01234567890 -> 0123456789
|
||||||
|
check({ start: 4, length: 2 }, null);
|
||||||
|
});
|
||||||
|
it('start in middle, end at end', () => {
|
||||||
|
// abc|efg|ijk -> abc■■■■ijk
|
||||||
|
// ^^^ -> null
|
||||||
|
// 01234567890 -> 0123456789
|
||||||
|
check({ start: 4, length: 3 }, null);
|
||||||
|
});
|
||||||
|
it('start in middle, end after', () => {
|
||||||
|
// abc|efg|ijk -> abc■■■■ijk
|
||||||
|
// ^^^^^^ -> ^^^^^^
|
||||||
|
// 01234567890 -> 0123456789
|
||||||
|
check({ start: 4, length: 6 }, { start: 3, length: 6 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// start at end
|
||||||
|
it('start at end, end at end', () => {
|
||||||
|
// abc|efg|ijk -> abc■■■■ijk
|
||||||
|
// \ -> null
|
||||||
|
// 01234567890 -> 0123456789
|
||||||
|
check({ start: 7, length: 0 }, null);
|
||||||
|
});
|
||||||
|
it('start at end, end after', () => {
|
||||||
|
// abc|efg|ijk -> abc■■■■ijk
|
||||||
|
// ^^ -> ^^
|
||||||
|
// 01234567890 -> 0123456789
|
||||||
|
check({ start: 8, length: 2 }, { start: 7, length: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// start after
|
||||||
|
it('start after, end after', () => {
|
||||||
|
// abc|efg|ijk -> abc■■■■ijk
|
||||||
|
// ^^ -> ^^
|
||||||
|
// 01234567890 -> 0123456789
|
||||||
|
check({ start: 8, length: 2 }, { start: 7, length: 2 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('replacement lengthens', () => {
|
||||||
|
function check(
|
||||||
|
input: { start: number; length: number },
|
||||||
|
expected: { start: number; length: number } | null
|
||||||
|
) {
|
||||||
|
const replacement = style(3, 3, BodyRange.Style.SPOILER);
|
||||||
|
const body = 'abc|e|ghi';
|
||||||
|
const bodyRanges = [
|
||||||
|
style(input.start, input.length, BodyRange.Style.BOLD),
|
||||||
|
];
|
||||||
|
const result = applyRangeToText({ body, bodyRanges }, replacement);
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
body: 'abc■■■■ghi',
|
||||||
|
bodyRanges:
|
||||||
|
expected == null
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
style(
|
||||||
|
expected.start,
|
||||||
|
expected.length,
|
||||||
|
BodyRange.Style.BOLD
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// start before
|
||||||
|
it('start before, end before', () => {
|
||||||
|
// abc|e|ghi -> abc■■■■ghi
|
||||||
|
// ^^ -> ^^
|
||||||
|
// 012345678 -> 0123456789
|
||||||
|
check({ start: 0, length: 2 }, { start: 0, length: 2 });
|
||||||
|
});
|
||||||
|
it('start before, end at start', () => {
|
||||||
|
// abc|e|ghi -> abc■■■■ghi
|
||||||
|
// ^^^ -> ^^^
|
||||||
|
// 012345678 -> 0123456789
|
||||||
|
check({ start: 0, length: 3 }, { start: 0, length: 3 });
|
||||||
|
});
|
||||||
|
it('start before, end in middle', () => {
|
||||||
|
// abc|e|ghi -> abc■■■■ghi
|
||||||
|
// ^^^^^ -> ^^^^^^^
|
||||||
|
// 012345678 -> 0123456789
|
||||||
|
check({ start: 0, length: 5 }, { start: 0, length: 7 });
|
||||||
|
});
|
||||||
|
it('start before, end at end', () => {
|
||||||
|
// abc|e|ghi -> abc■■■■ghi
|
||||||
|
// ^^^^^^ -> ^^^^^^^
|
||||||
|
// 012345678 -> 0123456789
|
||||||
|
check({ start: 0, length: 6 }, { start: 0, length: 7 });
|
||||||
|
});
|
||||||
|
it('start before, end after', () => {
|
||||||
|
// abc|e|ghi -> abc■■■■ghi
|
||||||
|
// ^^^^^^^^^ -> ^^^^^^^^^^
|
||||||
|
// 012345678 -> 0123456789
|
||||||
|
check({ start: 0, length: 9 }, { start: 0, length: 10 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// start at start
|
||||||
|
it('start at start, end at start', () => {
|
||||||
|
// abc|e|ghi -> abc■■■■ghi
|
||||||
|
// \ -> null
|
||||||
|
// 012345678 -> 0123456789
|
||||||
|
check({ start: 3, length: 0 }, null);
|
||||||
|
});
|
||||||
|
it('start at start, end in middle', () => {
|
||||||
|
// abc|e|ghi -> abc■■■■ghi
|
||||||
|
// ^^ -> null
|
||||||
|
// 012345678 -> 0123456789
|
||||||
|
check({ start: 3, length: 2 }, null);
|
||||||
|
});
|
||||||
|
it('start at start, end at end', () => {
|
||||||
|
// abc|e|ghi -> abc■■■■ghi
|
||||||
|
// ^^^ -> ^^^^
|
||||||
|
// 012345678 -> 0123456789
|
||||||
|
check({ start: 3, length: 3 }, { start: 3, length: 4 });
|
||||||
|
});
|
||||||
|
it('start at start, end after', () => {
|
||||||
|
// abc|e|ghi -> abc■■■■ghi
|
||||||
|
// ^^^^^^ -> ^^^^^^^
|
||||||
|
// 012345678 -> 0123456789
|
||||||
|
check({ start: 3, length: 6 }, { start: 3, length: 7 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// start in middle
|
||||||
|
it('start in middle, end in middle', () => {
|
||||||
|
// abc|e|ghi -> abc■■■■ghi
|
||||||
|
// ^ -> null
|
||||||
|
// 012345678 -> 0123456789
|
||||||
|
check({ start: 4, length: 1 }, null);
|
||||||
|
});
|
||||||
|
it('start in middle, end at end', () => {
|
||||||
|
// abc|e|ghi -> abc■■■■ghi
|
||||||
|
// ^^ -> null
|
||||||
|
// 012345678 -> 0123456789
|
||||||
|
check({ start: 4, length: 2 }, null);
|
||||||
|
});
|
||||||
|
it('start in middle, end after', () => {
|
||||||
|
// abc|e|ghi -> abc■■■■ghi
|
||||||
|
// ^^^^^ -> ^^^^^^^
|
||||||
|
// 012345678 -> 0123456789
|
||||||
|
check({ start: 4, length: 5 }, { start: 3, length: 7 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// start at end
|
||||||
|
it('start at end, end at end', () => {
|
||||||
|
// abc|e|ghi -> abc■■■■ghi
|
||||||
|
// \ -> null
|
||||||
|
// 012345678 -> 0123456789
|
||||||
|
check({ start: 6, length: 0 }, null);
|
||||||
|
});
|
||||||
|
it('start at end, end after', () => {
|
||||||
|
// abc|e|ghi -> abc■■■■ghi
|
||||||
|
// ^^ -> ^^
|
||||||
|
// 012345678 -> 0123456789
|
||||||
|
check({ start: 6, length: 2 }, { start: 7, length: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// start after
|
||||||
|
it('start after, end after', () => {
|
||||||
|
// abc|e|ghi -> abc■■■■ghi
|
||||||
|
// ^^ -> ^^
|
||||||
|
// 012345678 -> 0123456789
|
||||||
|
check({ start: 7, length: 2 }, { start: 8, length: 2 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('applyRangesToText', () => {
|
||||||
|
it('handles mentions, replaces in reverse order', () => {
|
||||||
|
const body = "\uFFFC says \uFFFC, I'm here";
|
||||||
|
const bodyRanges = [mention(0, 'jerry'), mention(7, 'fred')];
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
applyRangesToText(
|
||||||
|
{ body, bodyRanges },
|
||||||
|
{
|
||||||
|
replaceMentions: true,
|
||||||
|
replaceSpoilers: true,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
body: "@jerry says @fred, I'm here",
|
||||||
|
bodyRanges: [],
|
||||||
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles spoilers, replaces in reverse order', () => {
|
it('handles spoilers, replaces in reverse order', () => {
|
||||||
const spoilers = [
|
const body =
|
||||||
{
|
|
||||||
start: 18,
|
|
||||||
length: 16,
|
|
||||||
style: BodyRange.Style.SPOILER,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
start: 46,
|
|
||||||
length: 17,
|
|
||||||
style: BodyRange.Style.SPOILER,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const text =
|
|
||||||
"It's so cool when the balrog fight happens in Lord of the Rings!";
|
"It's so cool when the balrog fight happens in Lord of the Rings!";
|
||||||
assert.strictEqual(
|
const bodyRanges = [
|
||||||
applyRangesForText({ text, mentions: [], spoilers }),
|
style(18, 16, BodyRange.Style.SPOILER),
|
||||||
"It's so cool when ■■■■ happens in ■■■■!"
|
style(46, 17, BodyRange.Style.SPOILER),
|
||||||
|
];
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
applyRangesToText(
|
||||||
|
{ body, bodyRanges },
|
||||||
|
{ replaceMentions: true, replaceSpoilers: true }
|
||||||
|
),
|
||||||
|
{ body: "It's so cool when ■■■■ happens in ■■■■!", bodyRanges: [] }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles mentions that are removed by spoilers', () => {
|
it('handles mentions that are removed by spoilers', () => {
|
||||||
const mentions = [
|
const body =
|
||||||
{
|
|
||||||
start: 49,
|
|
||||||
length: 1,
|
|
||||||
mentionAci: SERVICE_ID_4,
|
|
||||||
replacementText: 'alice',
|
|
||||||
conversationID: 'x',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
start: 55,
|
|
||||||
length: 1,
|
|
||||||
mentionAci: SERVICE_ID_4,
|
|
||||||
replacementText: 'bob',
|
|
||||||
conversationID: 'x',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const spoilers = [
|
|
||||||
{
|
|
||||||
start: 49,
|
|
||||||
length: 7,
|
|
||||||
style: BodyRange.Style.SPOILER,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const text =
|
|
||||||
"The recipients of today's appreciation award are \uFFFC and \uFFFC!";
|
"The recipients of today's appreciation award are \uFFFC and \uFFFC!";
|
||||||
assert.strictEqual(
|
const bodyRanges = [
|
||||||
applyRangesForText({ text, mentions, spoilers }),
|
mention(49, 'alice'),
|
||||||
"The recipients of today's appreciation award are ■■■■!"
|
mention(55, 'bob'),
|
||||||
|
style(49, 7, BodyRange.Style.SPOILER),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
applyRangesToText(
|
||||||
|
{ body, bodyRanges },
|
||||||
|
{ replaceMentions: true, replaceSpoilers: true }
|
||||||
|
),
|
||||||
|
{
|
||||||
|
body: "The recipients of today's appreciation award are ■■■■!",
|
||||||
|
bodyRanges: [],
|
||||||
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles mentions that need to be moved because of spoilers', () => {
|
it('handles applying mentions but not spoilers', () => {
|
||||||
const mentions = [
|
const body = 'before \uFFFC after';
|
||||||
{
|
const bodyRanges = [
|
||||||
start: 0,
|
mention(7, 'jamie'),
|
||||||
length: 1,
|
style(0, 8, BodyRange.Style.BOLD),
|
||||||
mentionAci: SERVICE_ID_4,
|
style(7, 1, BodyRange.Style.SPOILER),
|
||||||
replacementText: 'eve',
|
style(7, 6, BodyRange.Style.ITALIC),
|
||||||
conversationID: 'x',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
start: 52,
|
|
||||||
length: 1,
|
|
||||||
mentionAci: SERVICE_ID_4,
|
|
||||||
replacementText: 'alice',
|
|
||||||
conversationID: 'x',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
start: 58,
|
|
||||||
length: 1,
|
|
||||||
mentionAci: SERVICE_ID_4,
|
|
||||||
replacementText: 'bob',
|
|
||||||
conversationID: 'x',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
const spoilers = [
|
assert.deepStrictEqual(
|
||||||
|
applyRangesToText(
|
||||||
|
{ body, bodyRanges },
|
||||||
|
{ replaceMentions: true, replaceSpoilers: false }
|
||||||
|
),
|
||||||
{
|
{
|
||||||
start: 21,
|
body: 'before @jamie after',
|
||||||
length: 26,
|
bodyRanges: [
|
||||||
style: BodyRange.Style.SPOILER,
|
style(0, 13, BodyRange.Style.BOLD),
|
||||||
},
|
style(7, 6, BodyRange.Style.SPOILER),
|
||||||
];
|
style(7, 11, BodyRange.Style.ITALIC),
|
||||||
|
],
|
||||||
const text =
|
}
|
||||||
"\uFFFC: The recipients of today's appreciation award are \uFFFC and \uFFFC!";
|
|
||||||
assert.strictEqual(
|
|
||||||
applyRangesForText({ text, mentions, spoilers }),
|
|
||||||
'@eve: The recipients of ■■■■ are @alice and @bob!'
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-namespace */
|
/* eslint-disable @typescript-eslint/no-namespace */
|
||||||
|
|
||||||
import { escapeRegExp, isNumber, omit } from 'lodash';
|
import { escapeRegExp, isNumber, omit, partition } from 'lodash';
|
||||||
|
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
@ -643,55 +643,208 @@ export function processBodyRangesForSearchResult({
|
||||||
|
|
||||||
export const SPOILER_REPLACEMENT = '■■■■';
|
export const SPOILER_REPLACEMENT = '■■■■';
|
||||||
|
|
||||||
export function applyRangesForText({
|
/**
|
||||||
text,
|
* Replace text in a string at a given range, returning the new string. The
|
||||||
mentions,
|
* replacement can be a different length than the text it's replacing.
|
||||||
spoilers,
|
* @example
|
||||||
}: {
|
* ```ts
|
||||||
text: string | undefined;
|
* replaceText('hello world!!!', 'jamie', 6, 11) === 'hello jamie!!!'
|
||||||
mentions: ReadonlyArray<HydratedBodyRangeMention>;
|
* ```
|
||||||
spoilers: ReadonlyArray<BodyRange<BodyRange.Formatting>>;
|
*/
|
||||||
}): string | undefined {
|
function replaceText(
|
||||||
if (!text) {
|
input: string,
|
||||||
return text;
|
insert: string,
|
||||||
|
start: number,
|
||||||
|
end: number
|
||||||
|
): string {
|
||||||
|
return input.slice(0, start) + insert + input.slice(end);
|
||||||
}
|
}
|
||||||
|
|
||||||
let updatedText = text;
|
export type BodyWithBodyRanges = {
|
||||||
let sortableMentions: Array<HydratedBodyRangeMention> = mentions.slice();
|
body: string;
|
||||||
|
bodyRanges: HydratedBodyRangesType;
|
||||||
const sortableSpoilers: Array<BodyRange<BodyRange.Formatting>> =
|
|
||||||
spoilers.slice();
|
|
||||||
updatedText = sortableSpoilers
|
|
||||||
.sort((a, b) => b.start - a.start)
|
|
||||||
.reduce((acc, { start, length }) => {
|
|
||||||
const left = acc.slice(0, start);
|
|
||||||
const end = start + length;
|
|
||||||
const right = acc.slice(end);
|
|
||||||
|
|
||||||
// Note: this is a simplified filter because mentions always have length=1
|
|
||||||
sortableMentions = sortableMentions
|
|
||||||
.filter(mention => {
|
|
||||||
return mention.start < start || mention.start >= end;
|
|
||||||
})
|
|
||||||
.map(mention => {
|
|
||||||
if (mention.start >= end) {
|
|
||||||
return {
|
|
||||||
...mention,
|
|
||||||
start: mention.start - (length - SPOILER_REPLACEMENT.length),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Span = {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function snapSpanToEdgesOfReplacement(
|
||||||
|
span: Span,
|
||||||
|
replacement: Span
|
||||||
|
): Span | null {
|
||||||
|
// If the span is empty, we can just remove it
|
||||||
|
if (span.start >= span.end) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return mention;
|
// If the span is inside the replacement (not exactly the same), we remove it
|
||||||
|
if (
|
||||||
|
(span.start > replacement.start && span.end <= replacement.end) ||
|
||||||
|
(span.start >= replacement.start && span.end < replacement.end)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let start: number;
|
||||||
|
if (span.start < replacement.start) {
|
||||||
|
start = span.start;
|
||||||
|
} else if (span.start === replacement.start) {
|
||||||
|
start = replacement.start;
|
||||||
|
} else if (span.start < replacement.end) {
|
||||||
|
start = replacement.start; // snap to the start of the replacement
|
||||||
|
} else if (span.start === replacement.end) {
|
||||||
|
start = replacement.end; // snap to the end of the replacement
|
||||||
|
} else {
|
||||||
|
start = span.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
let end: number;
|
||||||
|
if (span.end < replacement.start) {
|
||||||
|
end = span.end;
|
||||||
|
} else if (span.end === replacement.start) {
|
||||||
|
end = replacement.start;
|
||||||
|
} else if (span.end < replacement.end) {
|
||||||
|
end = replacement.end; // snap to the start of the replacement
|
||||||
|
} else if (span.end === replacement.end) {
|
||||||
|
end = replacement.end; // snap to the end of the replacement
|
||||||
|
} else {
|
||||||
|
end = span.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this made the span empty, we can remove it
|
||||||
|
if (start === end) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { start, end };
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSpan(range: HydratedBodyRangeType) {
|
||||||
|
return { start: range.start, end: range.start + range.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a single replacement range to a string, returning the new string and
|
||||||
|
* updated ranges. This only works for mentions and spoilers. The other ranges
|
||||||
|
* are updated to stay outside of the replaced text, or removed if are only
|
||||||
|
* inside the replaced text.
|
||||||
|
*/
|
||||||
|
export function applyRangeToText(
|
||||||
|
input: BodyWithBodyRanges,
|
||||||
|
// mention or spoiler
|
||||||
|
replacement: HydratedBodyRangeType
|
||||||
|
): BodyWithBodyRanges {
|
||||||
|
let insert: string;
|
||||||
|
|
||||||
|
if (BodyRange.isMention(replacement)) {
|
||||||
|
insert = `@${replacement.replacementText}`;
|
||||||
|
} else if (
|
||||||
|
BodyRange.isFormatting(replacement) &&
|
||||||
|
replacement.style === BodyRange.Style.SPOILER
|
||||||
|
) {
|
||||||
|
insert = SPOILER_REPLACEMENT;
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid range');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedBody = replaceText(
|
||||||
|
input.body,
|
||||||
|
insert,
|
||||||
|
replacement.start,
|
||||||
|
replacement.start + replacement.length
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedRanges = input.bodyRanges
|
||||||
|
.map((otherRange): HydratedBodyRangeType | null => {
|
||||||
|
// It is easier to work with a `start-end` here because we can easily
|
||||||
|
// adjust it at the end based on the diff of the inserted text
|
||||||
|
const otherRangeSpan = toSpan(otherRange);
|
||||||
|
const replacementSpan = toSpan(replacement);
|
||||||
|
|
||||||
|
const result = snapSpanToEdgesOfReplacement(
|
||||||
|
otherRangeSpan,
|
||||||
|
replacementSpan
|
||||||
|
);
|
||||||
|
if (result == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { start, end } = result;
|
||||||
|
|
||||||
|
// The difference between the length of the range we're inserting and the
|
||||||
|
// length of the inserted text
|
||||||
|
// - "\uFFFC".length == 1 -> "@jamie".length == 6, so diff == 5
|
||||||
|
// - "spoiler".length == 7 -> "■■■■".length == 4, so diff == -3
|
||||||
|
const insertionDiff = insert.length - replacement.length;
|
||||||
|
// We only need to adjust positions at or after the end of the replacement
|
||||||
|
if (start >= replacementSpan.end) {
|
||||||
|
start += insertionDiff;
|
||||||
|
}
|
||||||
|
if (end >= replacementSpan.end) {
|
||||||
|
end += insertionDiff;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...otherRange, start, length: end - start };
|
||||||
|
})
|
||||||
|
.filter((r): r is HydratedBodyRangeType => {
|
||||||
|
return r != null;
|
||||||
});
|
});
|
||||||
|
|
||||||
return `${left}${SPOILER_REPLACEMENT}${right}`;
|
return { body: updatedBody, bodyRanges: updatedRanges };
|
||||||
}, updatedText);
|
}
|
||||||
|
|
||||||
return sortableMentions
|
function _applyRangeOfType(
|
||||||
.sort((a, b) => b.start - a.start)
|
input: BodyWithBodyRanges,
|
||||||
.reduce((acc, { start, length, replacementText }) => {
|
condition: (bodyRange: HydratedBodyRangeType) => boolean
|
||||||
const left = acc.slice(0, start);
|
) {
|
||||||
const right = acc.slice(start + length);
|
const [matchedRanges, otherRanges] = partition(input.bodyRanges, condition);
|
||||||
return `${left}@${replacementText}${right}`;
|
return matchedRanges
|
||||||
}, updatedText);
|
.sort((a, b) => {
|
||||||
|
return b.start - a.start;
|
||||||
|
})
|
||||||
|
.reduce<BodyWithBodyRanges>(
|
||||||
|
(prev, matchedRange) => {
|
||||||
|
return applyRangeToText(prev, matchedRange);
|
||||||
|
},
|
||||||
|
{ body: input.body, bodyRanges: otherRanges }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply some body ranges to body, returning the new string and updated ranges.
|
||||||
|
* This only works for mentions and spoilers. The other ranges are updated to
|
||||||
|
* stay outside of the replaced text, or removed if are only inside the
|
||||||
|
* replaced text.
|
||||||
|
*
|
||||||
|
* You can optionally enable/disable replacing mentions and spoilers.
|
||||||
|
*/
|
||||||
|
export function applyRangesToText(
|
||||||
|
input: BodyWithBodyRanges,
|
||||||
|
options: {
|
||||||
|
replaceMentions: boolean; // "@jamie"
|
||||||
|
replaceSpoilers: boolean; // "■■■■"
|
||||||
|
}
|
||||||
|
): BodyWithBodyRanges {
|
||||||
|
let state = input;
|
||||||
|
|
||||||
|
// Short-circuit if there are no ranges
|
||||||
|
if (state.bodyRanges.length === 0) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.replaceSpoilers) {
|
||||||
|
state = _applyRangeOfType(state, bodyRange => {
|
||||||
|
return BodyRange.isFormatting(bodyRange) && bodyRange.style === SPOILER;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.replaceMentions) {
|
||||||
|
state = _applyRangeOfType(state, bodyRange => {
|
||||||
|
return BodyRange.isMention(bodyRange);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { MessageAttributesType } from '../model-types.d';
|
import type { MessageAttributesType } from '../model-types.d';
|
||||||
import { BodyRange, applyRangesForText } from '../types/BodyRange';
|
import { applyRangesToText, hydrateRanges } from '../types/BodyRange';
|
||||||
import { extractHydratedMentions } from '../state/selectors/message';
|
|
||||||
import { findAndFormatContact } from './findAndFormatContact';
|
import { findAndFormatContact } from './findAndFormatContact';
|
||||||
import { getNotificationDataForMessage } from './getNotificationDataForMessage';
|
import { getNotificationDataForMessage } from './getNotificationDataForMessage';
|
||||||
import { isConversationAccepted } from './isConversationAccepted';
|
import { isConversationAccepted } from './isConversationAccepted';
|
||||||
|
@ -12,7 +11,7 @@ import { strictAssert } from './assert';
|
||||||
export function getNotificationTextForMessage(
|
export function getNotificationTextForMessage(
|
||||||
attributes: MessageAttributesType
|
attributes: MessageAttributesType
|
||||||
): string {
|
): string {
|
||||||
const { text, emoji } = getNotificationDataForMessage(attributes);
|
const { text, emoji, bodyRanges } = getNotificationDataForMessage(attributes);
|
||||||
|
|
||||||
const conversation = window.ConversationController.get(
|
const conversation = window.ConversationController.get(
|
||||||
attributes.conversationId
|
attributes.conversationId
|
||||||
|
@ -64,25 +63,23 @@ export function getNotificationTextForMessage(
|
||||||
return window.i18n('icu:Quote__story-reaction--single');
|
return window.i18n('icu:Quote__story-reaction--single');
|
||||||
}
|
}
|
||||||
|
|
||||||
const mentions =
|
const result = applyRangesToText(
|
||||||
extractHydratedMentions(attributes, {
|
{
|
||||||
conversationSelector: findAndFormatContact,
|
body: text,
|
||||||
}) || [];
|
bodyRanges: hydrateRanges(bodyRanges, findAndFormatContact) ?? [],
|
||||||
const spoilers = (attributes.bodyRanges || []).filter(
|
},
|
||||||
range =>
|
{ replaceMentions: true, replaceSpoilers: true }
|
||||||
BodyRange.isFormatting(range) && range.style === BodyRange.Style.SPOILER
|
);
|
||||||
) as Array<BodyRange<BodyRange.Formatting>>;
|
|
||||||
const modifiedText = applyRangesForText({ text, mentions, spoilers });
|
|
||||||
|
|
||||||
// Linux emoji support is mixed, so we disable it. (Note that this doesn't touch
|
// Linux emoji support is mixed, so we disable it. (Note that this doesn't touch
|
||||||
// the `text`, which can contain emoji.)
|
// the `text`, which can contain emoji.)
|
||||||
const shouldIncludeEmoji = Boolean(emoji) && !window.Signal.OS.isLinux();
|
const shouldIncludeEmoji = Boolean(emoji) && !window.Signal.OS.isLinux();
|
||||||
if (shouldIncludeEmoji) {
|
if (shouldIncludeEmoji) {
|
||||||
return window.i18n('icu:message--getNotificationText--text-with-emoji', {
|
return window.i18n('icu:message--getNotificationText--text-with-emoji', {
|
||||||
text: modifiedText,
|
text: result.body,
|
||||||
emoji,
|
emoji,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return modifiedText || '';
|
return result.body ?? '';
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue