diff --git a/src/app/components/editor/Editor.tsx b/src/app/components/editor/Editor.tsx index 1902c2a..8f5dd3f 100644 --- a/src/app/components/editor/Editor.tsx +++ b/src/app/components/editor/Editor.tsx @@ -6,12 +6,11 @@ import React, { forwardRef, useCallback, useState, - useEffect, - useRef, } from 'react'; import { Box, Scroll, Text } from 'folds'; import { Descendant, Editor, createEditor, Transforms, Range, Element as SlateElement, Text as SlateText, Point } from 'slate'; import { + ReactEditor, Slate, Editable, withReact, @@ -172,48 +171,41 @@ export const CustomEditor = forwardRef( [editor, onKeyDown] ); - const handleBeforeInput = useCallback( - (event: Event) => { - const inputEvent = event as InputEvent; - - // Handle autocorrect replacement that causes text duplication - if (inputEvent.inputType === 'insertReplacementText' || - inputEvent.inputType === 'insertFromComposition') { - const { selection } = editor; - if (!selection) return; + /** + * Spellcheck / right-click autocorrect (insertReplacementText). + * Must run as Editable's onDOMBeforeInput and return true so Slate skips its + * own insert — slate-react ignores event.defaultPrevented when this prop is unset, + * which previously duplicated the corrected word (especially on Windows Electron). + */ + const handleDOMBeforeInput = useCallback( + (event: InputEvent) => { + if (event.inputType !== 'insertReplacementText') return false; - // Get the data being inserted - const data = inputEvent.data || inputEvent.dataTransfer?.getData('text/plain'); - - if (data) { - event.preventDefault(); - - // If there's selected text, delete it first - if (selection && !Range.isCollapsed(selection)) { - Transforms.delete(editor, { at: selection }); - } - - // Insert the replacement text - editor.insertText(data); + const data = + event.dataTransfer?.getData('text/plain') || event.data || ''; + if (!data) return false; + + event.preventDefault(); + + const [targetRange] = event.getTargetRanges(); + if (targetRange) { + const slateRange = ReactEditor.toSlateRange(editor, targetRange, { + exactMatch: false, + suppressThrow: true, + }); + if (slateRange) { + Transforms.select(editor, slateRange); } + } else if (editor.selection && !Range.isCollapsed(editor.selection)) { + Transforms.delete(editor, { at: editor.selection }); } + + Transforms.insertText(editor, data); + return true; }, [editor] ); - const editableRef = useRef(null); - - useEffect(() => { - const editableElement = editableRef.current?.querySelector('[data-slate-editor="true"]'); - if (!editableElement) return; - - editableElement.addEventListener('beforeinput', handleBeforeInput, { capture: true }); - - return () => { - editableElement.removeEventListener('beforeinput', handleBeforeInput, { capture: true }); - }; - }, [handleBeforeInput]); - const renderPlaceholder = useCallback( ({ attributes, children }: RenderPlaceholderProps) => ( @@ -259,7 +251,6 @@ export const CustomEditor = forwardRef( size="300" visibility="Hover" hideTrack - ref={editableRef} > ( renderElement={renderElement} renderLeaf={renderLeaf} decorate={decorate} + onDOMBeforeInput={handleDOMBeforeInput} onKeyDown={handleKeydown} onKeyUp={onKeyUp} onPaste={onPaste} diff --git a/src/app/components/message/Reply.css.ts b/src/app/components/message/Reply.css.ts index 9a1d9cb..8e30e30 100644 --- a/src/app/components/message/Reply.css.ts +++ b/src/app/components/message/Reply.css.ts @@ -35,6 +35,8 @@ export const ReplyContent = style({ overflow: 'clip', overflowY: 'clip', lineHeight: '1.5', + paddingBlock: toRem(4), + marginBlock: toRem(-4), selectors: { [`${Reply}:hover &`]: { opacity: config.opacity.P500, diff --git a/src/app/components/message/layout/layout.css.ts b/src/app/components/message/layout/layout.css.ts index 017943e..136100d 100644 --- a/src/app/components/message/layout/layout.css.ts +++ b/src/app/components/message/layout/layout.css.ts @@ -1,6 +1,7 @@ import { createVar, keyframes, style, styleVariants } from '@vanilla-extract/css'; import { recipe, RecipeVariants } from '@vanilla-extract/recipes'; import { DefaultReset, color, config, toRem } from 'folds'; +import * as htmlCss from '../../../styles/CustomHtml.css'; export const StickySection = style({ position: 'sticky', @@ -212,9 +213,12 @@ export const UsernameBold = style({ export const MessageTextBody = recipe({ base: { wordBreak: 'break-word', + // Clip zalgo/combining marks with room so normal text + emoji aren't shaved overflow: 'clip', overflowY: 'clip', lineHeight: '1.5', + padding: toRem(20), + margin: toRem(-20), userSelect: 'text', WebkitUserSelect: 'text', }, @@ -226,8 +230,27 @@ export const MessageTextBody = recipe({ }, jumboEmoji: { true: { - fontSize: '1.504em', - lineHeight: '1.4962em', + fontSize: toRem(100), + lineHeight: toRem(100), + selectors: { + [`& .${htmlCss.EmoticonBase}`]: { + height: toRem(100), + padding: 0, + verticalAlign: 'bottom', + }, + [`& .${htmlCss.Emoticon.classNames.base}`]: { + fontSize: toRem(100), + height: toRem(100), + minWidth: toRem(100), + lineHeight: toRem(100), + top: 0, + }, + [`& .${htmlCss.EmoticonImg}`]: { + height: toRem(100), + width: toRem(100), + objectFit: 'contain', + }, + }, }, }, emote: { diff --git a/src/app/styles/CustomHtml.css.ts b/src/app/styles/CustomHtml.css.ts index 1fb68d1..0dab8c6 100644 --- a/src/app/styles/CustomHtml.css.ts +++ b/src/app/styles/CustomHtml.css.ts @@ -19,7 +19,7 @@ export const MarginSpaced = style({ export const Paragraph = style([ DefaultReset, { - overflow: 'hidden', + // Zalgo clipping is handled by MessageTextBody padding + overflow lineHeight: '1.5', }, ]); @@ -29,7 +29,6 @@ export const Heading = style([ MarginSpaced, { marginTop: config.space.S400, - overflow: 'hidden', lineHeight: '1.5', selectors: { '&:first-child': { @@ -46,7 +45,6 @@ export const BlockQuote = style([ paddingLeft: config.space.S200, borderLeft: `${config.borderWidth.B700} solid ${color.SurfaceVariant.ContainerLine}`, fontStyle: 'italic', - overflow: 'hidden', lineHeight: '1.5', }, ]); @@ -73,7 +71,6 @@ export const Code = style([ { padding: `0 ${config.space.S100}`, display: 'inline', - overflow: 'hidden', lineHeight: '1.5', }, ]); @@ -120,7 +117,6 @@ export const Spoiler = recipe({ padding: `0 ${config.space.S100}`, backgroundColor: color.SurfaceVariant.ContainerActive, borderRadius: config.radii.R300, - overflow: 'hidden', lineHeight: '1.5', selectors: { '&[aria-pressed=true]': { @@ -182,7 +178,6 @@ export const List = style([ { padding: `0 ${config.space.S100}`, paddingLeft: config.space.S600, - overflow: 'hidden', lineHeight: '1.5', }, ]); @@ -211,7 +206,6 @@ export const Mention = recipe({ padding: `0 ${toRem(2)}`, borderRadius: config.radii.R300, fontWeight: config.fontWeight.W500, - overflow: 'hidden', lineHeight: '1.5', }, ], @@ -238,7 +232,6 @@ export const Command = recipe({ padding: `0 ${toRem(2)}`, borderRadius: config.radii.R300, fontWeight: config.fontWeight.W500, - overflow: 'hidden', lineHeight: '1.5', }, ], @@ -308,7 +301,6 @@ export const highlightText = style([ { backgroundColor: 'yellow', color: 'black', - overflow: 'hidden', lineHeight: '1.5', }, ]); diff --git a/src/index.css b/src/index.css index a69627f..b2f6197 100644 --- a/src/index.css +++ b/src/index.css @@ -257,7 +257,7 @@ body.mocha-theme { } .twilight-theme ::-webkit-scrollbar-track { - background: rgba(20, 18, 31, 0.5); + background: transparent; } .twilight-theme ::-webkit-scrollbar-thumb { @@ -355,7 +355,7 @@ body.mocha-theme { } .mocha-theme ::-webkit-scrollbar-track { - background: rgba(26, 22, 20, 0.5); + background: transparent; } .mocha-theme ::-webkit-scrollbar-thumb {