From dedbd54199d35c6514adf346467437d5bba20643 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Tue, 2 Jun 2026 15:36:45 -0400 Subject: [PATCH] feat: typing indicator orange dots, push-to-deafen hotkey, night light filter, message length counter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #108: TypingIndicator reads lotusTerminal setting; applies var(--lt-accent-orange) to container so dots inherit via backgroundColor:currentColor - #100: CallControls registers KeyM as push-to-deafen (e.code, e.repeat guard, ownerDocument.body iframe-safe editable check, [callEmbed] dep array) - P5-5: nightLightEnabled/nightLightOpacity settings; position:fixed rgba(255,140,0) overlay inside JotaiProvider; Night Light tile + intensity slider (5–80%) in Settings → Appearance - #101: RoomInput charCount state via Slate onChange + toPlainText; resets on room switch; displayed before send button when count > 0 Co-Authored-By: Claude Sonnet 4.6 --- .../typing-indicator/TypingIndicator.tsx | 37 +++++++++++------- src/app/features/call/CallControls.tsx | 23 +++++++++++ src/app/features/room/RoomInput.tsx | 20 ++++++++++ src/app/features/settings/general/General.tsx | 38 +++++++++++++++++++ src/app/pages/App.tsx | 21 +++++++++- src/app/state/settings.ts | 6 +++ 6 files changed, 130 insertions(+), 15 deletions(-) diff --git a/src/app/components/typing-indicator/TypingIndicator.tsx b/src/app/components/typing-indicator/TypingIndicator.tsx index 78d0c3a18..0469a70fd 100644 --- a/src/app/components/typing-indicator/TypingIndicator.tsx +++ b/src/app/components/typing-indicator/TypingIndicator.tsx @@ -1,5 +1,7 @@ import React from 'react'; import { Box, as, toRem } from 'folds'; +import { useSetting } from '../../state/hooks/settings'; +import { settingsAtom } from '../../state/settings'; import * as css from './TypingIndicator.css'; export type TypingIndicatorProps = { @@ -8,18 +10,25 @@ export type TypingIndicatorProps = { }; export const TypingIndicator = as<'div', TypingIndicatorProps>( - ({ size, disableAnimation, style, ...props }, ref) => ( - - - - - - ), + function TypingIndicatorInner({ size, disableAnimation, style, ...props }, ref) { + const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal'); + return ( + + + + + + ); + }, ); diff --git a/src/app/features/call/CallControls.tsx b/src/app/features/call/CallControls.tsx index 6c49a4806..4ffccc7ff 100644 --- a/src/app/features/call/CallControls.tsx +++ b/src/app/features/call/CallControls.tsx @@ -196,6 +196,29 @@ export function CallControls({ callEmbed }: CallControlsProps) { // microphone intentionally read via microphoneRef — excluded from deps to avoid listener churn }, [pttMode, pttKey, callEmbed]); + useEffect(() => { + const isEditable = (el: HTMLElement): boolean => { + const tag = el.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true; + let node: HTMLElement | null = el; + while (node && node !== el.ownerDocument.body) { + if (node.contentEditable === 'true') return true; + if (node.contentEditable === 'false') return false; + node = node.parentElement; + } + return false; + }; + const onKeyDown = (e: KeyboardEvent) => { + if (e.code !== 'KeyM') return; + if (e.repeat) return; + if (isEditable(e.target as HTMLElement)) return; + e.preventDefault(); + callEmbed.control.toggleSound(); + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [callEmbed]); + const [hangupState, hangup] = useAsyncCallback( useCallback(() => callEmbed.hangup(), [callEmbed]), ); diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 0a8c33180..d316a0d30 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -150,6 +150,9 @@ export const RoomInput = forwardRef( const powerLevels = usePowerLevelsContext(); const creators = useRoomCreators(room); + const [charCount, setCharCount] = useState(0); + useEffect(() => { setCharCount(0); }, [roomId]); + const alive = useAlive(); const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId)); const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId)); @@ -718,6 +721,7 @@ export const RoomInput = forwardRef( onKeyDown={handleKeyDown} onKeyUp={handleKeyUp} onPaste={handlePaste} + onChange={(value) => setCharCount(toPlainText(value, isMarkdown).trim().length)} top={ replyDraft && (
@@ -953,6 +957,22 @@ export const RoomInput = forwardRef( setTimeout(() => setLocationError(null), 4000); }} /> + {charCount > 0 && ( + + {charCount} + + )} @@ -393,6 +395,42 @@ function Appearance() { } /> + + + } + /> + {nightLightEnabled && ( + + + Intensity: {nightLightOpacity}% + + ) => + setNightLightOpacity(parseInt(e.target.value, 10)) + } + style={{ width: '100%' }} + /> + + )} + +