diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 3eecbce..9d752de 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -119,6 +119,7 @@ import { usePowerLevelTags } from '../../hooks/usePowerLevelTags'; import { useComposingCheck } from '../../hooks/useComposingCheck'; import { useOtherUserColor } from '../../hooks/useUserColor'; import { getMemberAvatarMxc } from '../../utils/room'; +import { convertEmoticons, convertEmoticonsInHtml } from '../../utils/emoticonConverter'; interface RoomInputProps { editor: Editor; @@ -134,6 +135,7 @@ export const RoomInput = forwardRef( const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown'); const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor'); + const [autoConvertEmoticons] = useSetting(settingsAtom, 'autoConvertEmoticons'); const direct = useIsDirectRoom(); const commands = useCommands(mx, room); const emojiBtnRef = useRef(null); @@ -348,8 +350,15 @@ export const RoomInput = forwardRef( if (plainText === '') return; - const body = plainText; - const formattedBody = customHtml; + let body = plainText; + let formattedBody = customHtml; + + // Apply emoticon conversion if enabled + if (autoConvertEmoticons) { + body = convertEmoticons(body); + formattedBody = convertEmoticonsInHtml(formattedBody); + } + const mentionData = getMentions(mx, roomId, editor); const content: IContent = { @@ -385,7 +394,7 @@ export const RoomInput = forwardRef( resetEditorHistory(editor); setReplyDraft(undefined); sendTypingStatus(false); - }, [mx, roomId, editor, replyDraft, sendTypingStatus, setReplyDraft, isMarkdown, commands, editorRef]); + }, [mx, roomId, editor, replyDraft, sendTypingStatus, setReplyDraft, isMarkdown, commands, autoConvertEmoticons]); const handleKeyDown: KeyboardEventHandler = useCallback( (evt) => { diff --git a/src/app/features/settings/emojis-stickers/EmojisStickers.tsx b/src/app/features/settings/emojis-stickers/EmojisStickers.tsx index d3b13a0..f8efd47 100644 --- a/src/app/features/settings/emojis-stickers/EmojisStickers.tsx +++ b/src/app/features/settings/emojis-stickers/EmojisStickers.tsx @@ -1,17 +1,22 @@ import React, { useState } from 'react'; -import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds'; +import { Box, Text, IconButton, Icon, Icons, Scroll, config, Switch } from 'folds'; import { Page, PageContent, PageHeader } from '../../../components/page'; +import { SequenceCard } from '../../../components/sequence-card'; import { GlobalPacks } from './GlobalPacks'; import { UserPack } from './UserPack'; import { TelegramImport } from './TelegramImport'; import { ImagePack } from '../../../plugins/custom-emoji'; import { ImagePackView } from '../../../components/image-pack-view'; +import { useSetting } from '../../../state/hooks/settings'; +import { settingsAtom } from '../../../state/settings'; +import { SettingTile } from '../../../components/setting-tile'; type EmojisStickersProps = { requestClose: () => void; }; export function EmojisStickers({ requestClose }: EmojisStickersProps) { const [imagePack, setImagePack] = useState(); + const [autoConvertEmoticons, setAutoConvertEmoticons] = useSetting(settingsAtom, 'autoConvertEmoticons'); const handleImagePackViewClose = () => { setImagePack(undefined); @@ -41,6 +46,22 @@ export function EmojisStickers({ requestClose }: EmojisStickersProps) { + + Options + + + } + /> + + diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index 420962a..0406d1e 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -24,6 +24,7 @@ export interface Settings { isMarkdown: boolean; editorToolbar: boolean; emojiStyle: EmojiStyle; + autoConvertEmoticons: boolean; pageZoom: number; hideActivity: boolean; @@ -65,6 +66,7 @@ const defaultSettings: Settings = { isMarkdown: true, editorToolbar: false, emojiStyle: EmojiStyle.Apple, + autoConvertEmoticons: true, pageZoom: 100, hideActivity: false, diff --git a/src/app/utils/emoticonConverter.ts b/src/app/utils/emoticonConverter.ts new file mode 100644 index 0000000..c6f5a1e --- /dev/null +++ b/src/app/utils/emoticonConverter.ts @@ -0,0 +1,164 @@ +/** + * Text emoticon to emoji converter + * Automatically converts common text emoticons like :) :D etc. to their emoji equivalents + */ + +// Map of text emoticons to emoji +// Using common patterns that don't interfere with markdown links or other syntax +const EMOTICON_MAP: Record = { + // Happy faces + ':D': '😀', + ':-D': '😀', + ':)': '🙂', + ':-)': '🙂', + '=)': '😊', + ':]': '😊', + ':>': '😊', + + // Sad faces + ':(': 'â˜šī¸', + ':-(': 'â˜šī¸', + '=[': 'â˜šī¸', + ':<': 'â˜šī¸', + + // Other emotions + ';)': '😉', + ';-)': '😉', + ':P': '😛', + ':-P': '😛', + ':p': '😛', + ':-p': '😛', + 'xD': '😆', + 'XD': '😆', + ':|': '😐', + ':-|': '😐', + ':O': '😮', + ':-O': '😮', + ':o': '😮', + ':-o': '😮', + 'O:)': '😇', + 'O:-)': '😇', + ':*': '😘', + ':-*': '😘', + '<3': 'â¤ī¸', + ' b.length - a.length); + + for (const emoticon of emoticons) { + // Escape special regex characters + const escapedEmoticon = emoticon.replace(/[.*+?^${}()|[\]\\<>]/g, '\\$&'); + + // Match emoticons with optional backslash escape + // Matches either \:) (escaped) or :) (not escaped, and not preceded by backslash) + const regex = new RegExp( + `(^|\\s)(\\\\)?(${escapedEmoticon})(?=\\s|$)`, + 'g' + ); + + result = result.replace(regex, (match, prefix, backslash, emote) => { + // If backslash is present, remove it but keep the emoticon literal + if (backslash) { + return prefix + emote; + } + // Otherwise convert to emoji + return prefix + EMOTICON_MAP[emoticon]; + }); + } + + return result; +} + +/** + * Converts text emoticons in HTML to their emoji equivalents + * Handles HTML entities like < and > + * Use backslash to escape: \:) will remain as :) without converting + * @param html The HTML text to process + * @returns The HTML with emoticons converted to emoji + */ +export function convertEmoticonsInHtml(html: string): string { + let result = html; + + // Sort emoticons by length (longest first) to handle overlapping patterns + const emoticons = Object.keys(EMOTICON_MAP).sort((a, b) => b.length - a.length); + + for (const emoticon of emoticons) { + // Convert emoticon to HTML entity version (for < and >) + const htmlEmoticon = emoticon + .replace(//g, '>'); + + // Escape special regex characters + const escapedEmoticon = htmlEmoticon.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + // Match emoticons at start of string, after whitespace, or after HTML tags + // With optional backslash escape + const regex = new RegExp( + `(^|\\s|>)(\\\\)?(${escapedEmoticon})(?=\\s|<|$)`, + 'g' + ); + + result = result.replace(regex, (match, prefix, backslash, emote) => { + // If backslash is present, remove it but keep the emoticon literal + if (backslash) { + return prefix + emote; + } + // Otherwise convert to emoji + return prefix + EMOTICON_MAP[emoticon]; + }); + } + + return result; +} + +/** + * Checks if a string contains any text emoticons + * @param text The text to check + * @returns True if the text contains emoticons + */ +export function containsEmoticons(text: string): boolean { + const emoticons = Object.keys(EMOTICON_MAP); + + for (const emoticon of emoticons) { + const escapedEmoticon = emoticon.replace(/[.*+?^${}()|[\]\\<>]/g, '\\$&'); + const regex = new RegExp(`(^|\\s)(${escapedEmoticon})(?=\\s|$)`); + + if (regex.test(text)) { + return true; + } + } + + return false; +} diff --git a/src/index.css b/src/index.css index d963b2e..7704600 100644 --- a/src/index.css +++ b/src/index.css @@ -69,9 +69,6 @@ html { --safe-area-inset-bottom: env(safe-area-inset-bottom, 0px); --safe-area-inset-left: env(safe-area-inset-left, 0px); --safe-area-inset-right: env(safe-area-inset-right, 0px); - /* Window rounding for borderless window */ - border-radius: 4px; - overflow: hidden; } body { @@ -88,9 +85,6 @@ body { font-weight: 400; /* Default to dark theme background for safe areas */ background-color: #1A1A1A; - /* Window rounding */ - border-radius: 4px; - overflow: hidden; /*Why font-variant-ligatures => https://github.com/rsms/inter/issues/222 */ font-variant-ligatures: no-contextual; @@ -114,8 +108,6 @@ body.butter-theme { height: 100%; display: flex; flex-direction: column; - border-radius: 4px; - overflow: hidden; } *,