Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 5a08859

Browse files
authored
fix regional emojis converted to flags (#9294)
Co-authored-by: grimhilt <[email protected]> Co-authored-by: Michael Telatynski <[email protected]> Co-authored-by: Faye Duxovni <[email protected]> Co-authored-by: Faye Duxovni <[email protected]> Fixes element-hq/element-web#19000
1 parent 262c2fc commit 5a08859

File tree

4 files changed

+71
-16
lines changed

4 files changed

+71
-16
lines changed

cypress/e2e/timeline/timeline.spec.ts

+19
Original file line numberDiff line numberDiff line change
@@ -384,5 +384,24 @@ describe("Timeline", () => {
384384
1,
385385
);
386386
});
387+
388+
it("should not be possible to send flag with regional emojis", () => {
389+
cy.visit("/#/room/" + roomId);
390+
391+
// Send a message
392+
cy.getComposer().type(":regional_indicator_a");
393+
cy.contains(".mx_Autocomplete_Completion_title", ":regional_indicator_a:").click();
394+
cy.getComposer().type(":regional_indicator_r");
395+
cy.contains(".mx_Autocomplete_Completion_title", ":regional_indicator_r:").click();
396+
cy.getComposer().type(" :regional_indicator_z");
397+
cy.contains(".mx_Autocomplete_Completion_title", ":regional_indicator_z:").click();
398+
cy.getComposer().type(":regional_indicator_a");
399+
cy.contains(".mx_Autocomplete_Completion_title", ":regional_indicator_a:").click();
400+
cy.getComposer().type("{enter}");
401+
402+
cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line .mx_MTextBody .mx_EventTile_bigEmoji")
403+
.children()
404+
.should("have.length", 4);
405+
});
387406
});
388407
});

src/HtmlUtils.tsx

+7-13
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,8 @@ const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
4949
// (with plenty of false positives, but that's OK)
5050
const SYMBOL_PATTERN = /([\u2100-\u2bff])/;
5151

52-
// Regex pattern for Zero-Width joiner unicode characters
53-
const ZWJ_REGEX = /[\u200D\u2003]/g;
54-
55-
// Regex pattern for whitespace characters
56-
const WHITESPACE_REGEX = /\s/g;
52+
// Regex pattern for non-emoji characters that can appear in an "all-emoji" message (Zero-Width Joiner, Zero-Width Space, other whitespace)
53+
const EMOJI_SEPARATOR_REGEX = /[\u200D\u200B\s]/g;
5754

5855
const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, "i");
5956

@@ -591,14 +588,11 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
591588
if (!opts.disableBigEmoji && bodyHasEmoji) {
592589
let contentBodyTrimmed = contentBody !== undefined ? contentBody.trim() : "";
593590

594-
// Ignore spaces in body text. Emojis with spaces in between should
595-
// still be counted as purely emoji messages.
596-
contentBodyTrimmed = contentBodyTrimmed.replace(WHITESPACE_REGEX, "");
597-
598-
// Remove zero width joiner characters from emoji messages. This ensures
599-
// that emojis that are made up of multiple unicode characters are still
600-
// presented as large.
601-
contentBodyTrimmed = contentBodyTrimmed.replace(ZWJ_REGEX, "");
591+
// Remove zero width joiner, zero width spaces and other spaces in body
592+
// text. This ensures that emojis with spaces in between or that are made
593+
// up of multiple unicode characters are still counted as purely emoji
594+
// messages.
595+
contentBodyTrimmed = contentBodyTrimmed.replace(EMOJI_SEPARATOR_REGEX, "");
602596

603597
const match = BIGEMOJI_REGEX.exec(contentBodyTrimmed);
604598
emojiBody =

src/editor/parts.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import defaultDispatcher from "../dispatcher/dispatcher";
2727
import { Action } from "../dispatcher/actions";
2828
import SettingsStore from "../settings/SettingsStore";
2929

30+
const REGIONAL_EMOJI_SEPARATOR = String.fromCodePoint(0x200b);
31+
3032
interface ISerializedPart {
3133
type: Type.Plain | Type.Newline | Type.Emoji | Type.Command | Type.PillCandidate;
3234
text: string;
@@ -209,9 +211,13 @@ abstract class PlainBasePart extends BasePart {
209211
return false;
210212
}
211213

212-
// or split if the previous character is a space
214+
// or split if the previous character is a space or regional emoji separator
213215
// or if it is a + and this is a :
214-
return this._text[offset - 1] !== " " && (this._text[offset - 1] !== "+" || chr !== ":");
216+
return (
217+
this._text[offset - 1] !== " " &&
218+
this._text[offset - 1] !== REGIONAL_EMOJI_SEPARATOR &&
219+
(this._text[offset - 1] !== "+" || chr !== ":")
220+
);
215221
}
216222
return true;
217223
}
@@ -626,8 +632,13 @@ export class PartCreator {
626632
return new UserPillPart(userId, displayName, member);
627633
}
628634

635+
private static isRegionalIndicator(c: string): boolean {
636+
const codePoint = c.codePointAt(0) ?? 0;
637+
return codePoint != 0 && c.length == 2 && 0x1f1e6 <= codePoint && codePoint <= 0x1f1ff;
638+
}
639+
629640
public plainWithEmoji(text: string): (PlainPart | EmojiPart)[] {
630-
const parts = [];
641+
const parts: (PlainPart | EmojiPart)[] = [];
631642
let plainText = "";
632643

633644
// We use lodash's grapheme splitter to avoid breaking apart compound emojis
@@ -638,6 +649,9 @@ export class PartCreator {
638649
plainText = "";
639650
}
640651
parts.push(this.emoji(char));
652+
if (PartCreator.isRegionalIndicator(text)) {
653+
parts.push(this.plain(REGIONAL_EMOJI_SEPARATOR));
654+
}
641655
} else {
642656
plainText += char;
643657
}

test/editor/model-test.ts

+28
Original file line numberDiff line numberDiff line change
@@ -348,4 +348,32 @@ describe("editor/model", function () {
348348
expect(model.parts[0].text).toBe("foo@a");
349349
});
350350
});
351+
describe("emojis", function () {
352+
it("regional emojis should be separated to prevent them to be converted to flag", () => {
353+
const renderer = createRenderer();
354+
const pc = createPartCreator();
355+
const model = new EditorModel([], pc, renderer);
356+
const regionalEmojiA = String.fromCodePoint(127462);
357+
const regionalEmojiZ = String.fromCodePoint(127487);
358+
const caret = new DocumentOffset(0, true);
359+
360+
const regionalEmojis: string[] = [];
361+
regionalEmojis.push(regionalEmojiA);
362+
regionalEmojis.push(regionalEmojiZ);
363+
for (let i = 0; i < 2; i++) {
364+
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
365+
model.transform(() => {
366+
const addedLen = model.insert(pc.plainWithEmoji(regionalEmojis[i]), position);
367+
caret.offset += addedLen;
368+
return model.positionForOffset(caret.offset, true);
369+
});
370+
}
371+
372+
expect(model.parts.length).toBeGreaterThanOrEqual(4);
373+
expect(model.parts[0].type).toBe("emoji");
374+
expect(model.parts[1].type).not.toBe("emoji");
375+
expect(model.parts[2].type).toBe("emoji");
376+
expect(model.parts[3].type).not.toBe("emoji");
377+
});
378+
});
351379
});

0 commit comments

Comments
 (0)