Compare commits

...
Sign in to create a new pull request.

14 commits

13 changed files with 131 additions and 44 deletions

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "twemoji"]
path = twemoji
url = https://github.com/jdecked/twemoji

56
package-lock.json generated
View file

@ -491,23 +491,25 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
"integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
"integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
"license": "MIT",
"dependencies": {
"@babel/template": "^7.25.9",
"@babel/types": "^7.26.0"
"@babel/template": "^7.27.0",
"@babel/types": "^7.27.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz",
"integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
"integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.26.5"
"@babel/types": "^7.27.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@ -1591,9 +1593,10 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@ -1602,10 +1605,11 @@
}
},
"node_modules/@babel/runtime-corejs3": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.26.0.tgz",
"integrity": "sha512-YXHu5lN8kJCb1LOb9PgV6pvak43X2h4HvRApcN5SdWeaItQOzfn1hgP6jasD6KWQyJDBxrVmA9o9OivlnNJK/w==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.0.tgz",
"integrity": "sha512-UWjX6t+v+0ckwZ50Y5ShZLnlk95pP5MyW/pon9tiYzl3+18pkTHTFNTKr7rQbfRXPkowt2QAn30o1b6oswszew==",
"dev": true,
"license": "MIT",
"dependencies": {
"core-js-pure": "^3.30.2",
"regenerator-runtime": "^0.14.0"
@ -1615,13 +1619,14 @@
}
},
"node_modules/@babel/template": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
"integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.25.9",
"@babel/parser": "^7.25.9",
"@babel/types": "^7.25.9"
"@babel/code-frame": "^7.26.2",
"@babel/parser": "^7.27.0",
"@babel/types": "^7.27.0"
},
"engines": {
"node": ">=6.9.0"
@ -1645,9 +1650,10 @@
}
},
"node_modules/@babel/types": {
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz",
"integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
"integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
@ -6254,6 +6260,7 @@
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/emojibase/-/emojibase-15.3.1.tgz",
"integrity": "sha512-GNsjHnG2J3Ktg684Fs/vZR/6XpOSkZPMAv85EHrr6br2RN2cJNwdS4am/3YSK3y+/gOv2kmoK3GGdahXdMxg2g==",
"license": "MIT",
"funding": {
"type": "ko-fi",
"url": "https://ko-fi.com/milesjohnson"
@ -6263,6 +6270,7 @@
"version": "15.3.2",
"resolved": "https://registry.npmjs.org/emojibase-data/-/emojibase-data-15.3.2.tgz",
"integrity": "sha512-TpDyTDDTdqWIJixV5sTA6OQ0P0JfIIeK2tFRR3q56G9LK65ylAZ7z3KyBXokpvTTJ+mLUXQXbLNyVkjvnTLE+A==",
"license": "MIT",
"funding": {
"type": "ko-fi",
"url": "https://ko-fi.com/milesjohnson"

View file

@ -11,6 +11,7 @@ import {
import * as css from '../../styles/CustomHtml.css';
import { CommandElement, EmoticonElement, LinkElement, MentionElement } from './slate';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { getEmojiUrl, isUsingTwemoji } from '../../plugins/emoji';
import { getBeginCommand } from './utils';
import { BlockType } from './types';
import { mxcUrlToHttp } from '../../utils/matrix';
@ -97,7 +98,12 @@ function RenderEmoticonElement({
alt={element.shortcode}
/>
) : (
element.key
isUsingTwemoji() ? <img
className={css.EmoticonImg}
src={getEmojiUrl(element.key)}
alt={element.key}
title={element.shortcode}
/> : element.key
)}
{children}
</span>

View file

@ -126,6 +126,15 @@ export const CustomEmojiImg = style([
},
]);
export const EmoticonImg = style([
DefaultReset,
{
width: toRem(32),
height: toRem(32),
objectFit: 'contain',
},
]);
export const StickerImg = style([
DefaultReset,
{

View file

@ -34,7 +34,15 @@ import { MatrixClient, Room } from 'matrix-js-sdk';
import { atom, useAtomValue, useSetAtom } from 'jotai';
import * as css from './EmojiBoard.css';
import { EmojiGroupId, IEmoji, IEmojiGroup, emojiGroups, emojis } from '../../plugins/emoji';
import {
EmojiGroupId,
IEmoji,
IEmojiGroup,
emojiGroups,
emojis,
getEmojiUrl,
isUsingTwemoji,
} from '../../plugins/emoji';
import { IEmojiGroupLabels, useEmojiGroupLabels } from './useEmojiGroupLabels';
import { IEmojiGroupIcons, useEmojiGroupIcons } from './useEmojiGroupIcons';
import { preventScrollWithArrowKey, stopPropagation } from '../../utils/keyboard';
@ -272,6 +280,15 @@ export const EmojiGroup = as<
</Box>
));
const NativeEmoji = (emoji: IEmoji) =>
isUsingTwemoji() ? <img
loading="lazy"
className={css.EmoticonImg}
src={getEmojiUrl(emoji.unicode)}
alt={emoji.unicode}
title={emoji.shortcode}
/> : emoji.unicode;
export function EmojiItem({
label,
type,
@ -447,7 +464,7 @@ export function RecentEmojiGroup({
data={emoji.unicode}
shortcode={emoji.shortcode}
>
{emoji.unicode}
{NativeEmoji(emoji)}
</EmojiItem>
))}
</EmojiGroup>
@ -481,7 +498,7 @@ export function SearchEmojiGroup({
data={emoji.unicode}
shortcode={emoji.shortcode}
>
{emoji.unicode}
{NativeEmoji(emoji)}
</EmojiItem>
) : (
<EmojiItem
@ -628,7 +645,7 @@ export const NativeEmojiGroups = memo(
data={emoji.unicode}
shortcode={emoji.shortcode}
>
{emoji.unicode}
{NativeEmoji(emoji)}
</EmojiItem>
))}
</EmojiGroup>
@ -758,7 +775,16 @@ export function EmojiBoard({
const emojiInfo = getEmojiItemInfo(element);
if (!emojiInfo || !emojiPreviewTextRef.current) return;
if (emojiInfo.type === EmojiType.Emoji && emojiPreviewRef.current) {
emojiPreviewRef.current.textContent = emojiInfo.data;
if (isUsingTwemoji()) {
const img = document.createElement('img');
img.className = css.CustomEmojiImg;
img.setAttribute('src', getEmojiUrl(emojiInfo.data));
img.setAttribute('alt', emojiInfo.shortcode);
emojiPreviewRef.current.textContent = '';
emojiPreviewRef.current.appendChild(img);
} else {
emojiPreviewRef.current.textContent = emojiInfo.data;
}
} else if (emojiInfo.type === EmojiType.CustomEmoji && emojiPreviewRef.current) {
const img = document.createElement('img');
img.className = css.CustomEmojiImg;

View file

@ -3,7 +3,7 @@ import { Box, Text, as } from 'folds';
import classNames from 'classnames';
import { MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk';
import * as css from './Reaction.css';
import { getHexcodeForEmoji, getShortcodeFor } from '../../plugins/emoji';
import { getHexcodeForEmoji, getShortcodeFor, getEmojiUrl, isUsingTwemoji } from '../../plugins/emoji';
import { getMemberDisplayName } from '../../utils/room';
import { eventWithShortcode, getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
@ -33,6 +33,13 @@ export const Reaction = as<
}
alt={reaction}
/>
) : isUsingTwemoji() ? (
<img
className={css.ReactionImg}
src={getEmojiUrl(reaction)}
alt={reaction}
title={getShortcodeFor(getHexcodeForEmoji(reaction))}
/>
) : (
<Text as="span" size="Inherit" truncate>
{reaction}

View file

@ -277,10 +277,12 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
});
handleCancelUpload(uploads);
const contents = fulfilledPromiseSettledResult(await Promise.allSettled(contentsPromises));
contents.forEach((content) => mx.sendMessage(roomId, content));
for (const content of contents) {
await mx.sendMessage(roomId, content);
}
};
const submit = useCallback(() => {
const submit = useCallback(async () => {
uploadBoardHandlers.current?.handleSend();
const commandName = getBeginCommand(editor);
@ -356,7 +358,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
content['m.relates_to'].is_falling_back = false;
}
}
mx.sendMessage(roomId, content);
await mx.sendMessage(roomId, content);
resetEditor(editor);
resetEditorHistory(editor);
setReplyDraft(undefined);

View file

@ -33,7 +33,7 @@ export function WelcomePage() {
<Box grow="Yes" style={{ maxWidth: toRem(300) }} direction="Column" gap="300">
<Button
as="a"
href="https://github.com/cinnyapp/cinny"
href="https://forgejo.sup39.dev/supnas/cinny"
target="_blank"
rel="noreferrer noopener"
before={<Icon size="200" src={Icons.Code} />}

View file

@ -112,3 +112,13 @@ emojisData.forEach((emoji) => {
emojis.push(em);
}
});
export const isUsingTwemoji = () =>
document.documentElement.style.getPropertyValue('--font-emoji') !== 'Twemoji_DISABLED';
const TWEMOJI_BASE_URL = '/twemoji'; // TODO
export function getEmojiUrl(char: string): string {
let codes = Array.from(char).flatMap(x => x.codePointAt(0)?.toString(16) ?? []);
if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
return `${TWEMOJI_BASE_URL}/${codes.join('-')}.svg`;
}

View file

@ -22,7 +22,7 @@ import {
} from '../utils/matrix';
import { getMemberDisplayName } from '../utils/room';
import { EMOJI_PATTERN, sanitizeForRegex, URL_NEG_LB } from '../utils/regex';
import { getHexcodeForEmoji, getShortcodeFor } from './emoji';
import { getHexcodeForEmoji, getShortcodeFor, getEmojiUrl, isUsingTwemoji } from './emoji';
import { findAndReplace } from '../utils/findAndReplace';
import {
parseMatrixToRoom,
@ -160,13 +160,22 @@ export const scaleSystemEmoji = (text: string): (string | JSX.Element)[] =>
findAndReplace(
text,
EMOJI_REG_G,
(match, pushIndex) => (
<span key={`scaleSystemEmoji-${pushIndex}`} className={css.EmoticonBase}>
<span className={css.Emoticon()} title={getShortcodeFor(getHexcodeForEmoji(match[0]))}>
{match[0]}
</span>
</span>
),
(match, pushIndex) => {
const char = match[0];
const className = css.Emoticon();
const title = getShortcodeFor(getHexcodeForEmoji(char));
return <span key={`scaleSystemEmoji-${pushIndex}`} className={css.EmoticonBase}>{
isUsingTwemoji() ? <img
className={className}
alt={char}
title={title}
src={getEmojiUrl(char)}
/> : <span
className={className}
title={title}
>{match[0]}</span>
}</span>;
},
(txt) => txt
);

View file

@ -19,7 +19,9 @@ function hashCode(str) {
export function cssColorMXID(userId) {
const colorNumber = hashCode(userId) % 8;
return `--mx-uc-${colorNumber + 1}`;
// @user:a.b.c => -user-a_b_c
const escapedUserId = userId.replace(/[@:]/g, '-').replace(/[^\w-]/g, '_');
return `--mx-uc-${escapedUserId}, var(--mx-uc-${colorNumber + 1})`;
}
export default function colorMXID(userId) {

1
twemoji Submodule

@ -0,0 +1 @@
Subproject commit 310184baaae6f797e9ef2650bea35187242ffafb

View file

@ -36,6 +36,10 @@ const copyFiles = {
src: 'public/locales',
dest: 'public/',
},
{
src: 'twemoji/assets/svg/*',
dest: 'twemoji/',
},
],
};