diff --git a/config.json b/config.json index 57c577a..2272551 100644 --- a/config.json +++ b/config.json @@ -9,30 +9,13 @@ "xmr.se" ], "allowCustomHomeservers": true, - - "calling": { - "livekitServiceUrl": "https://b.ruv.wtf/matrix-rtc/livekit/jwt" - }, - "featuredCommunities": { "openAsDefault": false, "spaces": [ - "#cinny-space:matrix.org", - "#community:matrix.org", - "#space:envs.net", - "#science-space:matrix.org", - "#libregaming-games:tchncs.de", - "#mathematics-on:matrix.org" ], "rooms": [ - "#cinny:matrix.org", - "#freesoftware:matrix.org", - "#pcapdroid:matrix.org", - "#gentoo:matrix.org", - "#PrivSec.dev:arcticfoxes.net", - "#disroot:aria-net.org" ], - "servers": ["envs.net", "matrix.org", "monero.social", "mozilla.org"] + "servers": [] }, "hashRouter": { diff --git a/src/app/components/RenderMessageContent.tsx b/src/app/components/RenderMessageContent.tsx index 468241c..9891f98 100644 --- a/src/app/components/RenderMessageContent.tsx +++ b/src/app/components/RenderMessageContent.tsx @@ -159,6 +159,24 @@ export function RenderMessageContent({ ); + // Check for custom renderer from plugins BEFORE standard type handling + // so plugins can intercept any msgtype including custom ones. + const customRendererEarly = pluginRegistry.getRenderer('message'); + if (customRendererEarly) { + try { + const messageDataEarly = { + msgtype: msgType, + ...getContent(), + }; + const customContentEarly = customRendererEarly(messageDataEarly, () => null); + if (customContentEarly) { + return customContentEarly as React.ReactElement; + } + } catch (error) { + console.error('[RenderMessageContent] Custom renderer error (early):', error); + } + } + if (msgType === MsgType.Text) { return ( ('idle'); const [micLevel, setMicLevel] = useState(0); const [speakerTestState, setSpeakerTestState] = useState<'idle' | 'testing' | 'done' | 'error'>('idle'); + const [micMonitoring, setMicMonitoring] = useState(false); const micAnimFrameRef = useRef(null); + const micMonitorContextRef = useRef(null); + const micMonitorStreamRef = useRef(null); + const micMonitorGainRef = useRef(null); const [showRemoteCursor, setShowRemoteCursor] = useSetting(settingsAtom, 'showRemoteCursor'); useEffect(() => { @@ -627,6 +631,101 @@ export function Audio({ requestClose }: AudioProps) { } }, [speakerTestState]); + const handleMicMonitoringToggle = useCallback(async () => { + if (micMonitoring) { + // Stop monitoring + if (micAnimFrameRef.current !== null) { + cancelAnimationFrame(micAnimFrameRef.current); + micAnimFrameRef.current = null; + } + if (micMonitorStreamRef.current) { + micMonitorStreamRef.current.getTracks().forEach((track) => track.stop()); + micMonitorStreamRef.current = null; + } + if (micMonitorContextRef.current) { + micMonitorContextRef.current.close(); + micMonitorContextRef.current = null; + } + micMonitorGainRef.current = null; + setMicLevel(0); + setMicMonitoring(false); + } else { + // Start monitoring + try { + const constraints: MediaStreamConstraints = { + audio: settings.microphoneId + ? { + deviceId: { exact: settings.microphoneId }, + noiseSuppression: settings.noiseSuppression, + echoCancellation: settings.echoCancellation, + autoGainControl: settings.autoGainControl, + } + : { + noiseSuppression: settings.noiseSuppression, + echoCancellation: settings.echoCancellation, + autoGainControl: settings.autoGainControl, + }, + }; + + const stream = await navigator.mediaDevices.getUserMedia(constraints); + const audioContext = new AudioContext(); + const analyser = audioContext.createAnalyser(); + analyser.fftSize = 256; + const dataArray = new Uint8Array(analyser.frequencyBinCount); + const microphone = audioContext.createMediaStreamSource(stream); + const gainNode = audioContext.createGain(); + + // Connect microphone to analyser for level monitoring + microphone.connect(analyser); + + // Connect microphone to speakers for playback + microphone.connect(gainNode); + gainNode.connect(audioContext.destination); + gainNode.gain.value = 1.0; // Full volume playback + + micMonitorContextRef.current = audioContext; + micMonitorStreamRef.current = stream; + micMonitorGainRef.current = gainNode; + + const tick = () => { + analyser.getByteFrequencyData(dataArray); + const avg = dataArray.reduce((a, b) => a + b, 0) / dataArray.length; + setMicLevel(Math.min(100, Math.round((avg / 255) * 100 * 3))); + micAnimFrameRef.current = requestAnimationFrame(tick); + }; + micAnimFrameRef.current = requestAnimationFrame(tick); + + setMicMonitoring(true); + } catch (e) { + console.error('Microphone monitoring failed:', e); + } + } + }, [micMonitoring, settings.microphoneId, settings.noiseSuppression, settings.echoCancellation, settings.autoGainControl]); + + // Cleanup monitoring on unmount or when settings change + useEffect(() => { + return () => { + if (micAnimFrameRef.current !== null) { + cancelAnimationFrame(micAnimFrameRef.current); + } + if (micMonitorStreamRef.current) { + micMonitorStreamRef.current.getTracks().forEach((track) => track.stop()); + } + if (micMonitorContextRef.current) { + micMonitorContextRef.current.close(); + } + }; + }, []); + + // Restart monitoring when mic settings change + useEffect(() => { + if (micMonitoring) { + handleMicMonitoringToggle(); // Stop + setTimeout(() => handleMicMonitoringToggle(), 100); // Restart + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [settings.microphoneId, settings.noiseSuppression, settings.echoCancellation, settings.autoGainControl]); + return ( @@ -686,21 +785,35 @@ export function Audio({ requestClose }: AudioProps) { onSelect={handleMicrophoneSelect} /> - - {micTestState === 'testing' && ( - - + + + + + {(micTestState === 'testing' || micMonitoring) && ( + + )} diff --git a/src/app/styles/CustomHtml.css.ts b/src/app/styles/CustomHtml.css.ts index 2b8ae7e..2fc94be 100644 --- a/src/app/styles/CustomHtml.css.ts +++ b/src/app/styles/CustomHtml.css.ts @@ -16,13 +16,21 @@ export const MarginSpaced = style({ }, }); -export const Paragraph = style([DefaultReset]); +export const Paragraph = style([ + DefaultReset, + { + overflow: 'hidden', + lineHeight: '1.5', + }, +]); export const Heading = style([ DefaultReset, MarginSpaced, { marginTop: config.space.S400, + overflow: 'hidden', + lineHeight: '1.5', selectors: { '&:first-child': { marginTop: 0, @@ -38,6 +46,8 @@ export const BlockQuote = style([ paddingLeft: config.space.S200, borderLeft: `${config.borderWidth.B700} solid ${color.SurfaceVariant.ContainerLine}`, fontStyle: 'italic', + overflow: 'hidden', + lineHeight: '1.5', }, ]); @@ -63,6 +73,8 @@ export const Code = style([ { padding: `0 ${config.space.S100}`, display: 'inline-block', + overflow: 'hidden', + lineHeight: '1.5', }, ]); @@ -108,6 +120,8 @@ 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]': { color: 'transparent', @@ -132,6 +146,7 @@ export const CodeBlock = style([ fontStyle: 'normal', position: 'relative', overflow: 'hidden', + lineHeight: '1.5', }, ]); export const CodeBlockHeader = style([ @@ -167,6 +182,8 @@ export const List = style([ { padding: `0 ${config.space.S100}`, paddingLeft: config.space.S600, + overflow: 'hidden', + lineHeight: '1.5', }, ]); @@ -194,6 +211,8 @@ export const Mention = recipe({ padding: `0 ${toRem(2)}`, borderRadius: config.radii.R300, fontWeight: config.fontWeight.W500, + overflow: 'hidden', + lineHeight: '1.5', }, ], variants: { @@ -219,6 +238,8 @@ export const Command = recipe({ padding: `0 ${toRem(2)}`, borderRadius: config.radii.R300, fontWeight: config.fontWeight.W500, + overflow: 'hidden', + lineHeight: '1.5', }, ], variants: { @@ -287,5 +308,7 @@ export const highlightText = style([ { backgroundColor: 'yellow', color: 'black', + overflow: 'hidden', + lineHeight: '1.5', }, ]); diff --git a/src/index.css b/src/index.css index 7c00dea..33d9f9a 100644 --- a/src/index.css +++ b/src/index.css @@ -94,6 +94,11 @@ body { font-variant-ligatures: no-contextual; } +/* Prevent zalgo text and combining diacritics from overflowing line boundaries */ +p, span, div, a, button, li, td, th, h1, h2, h3, h4, h5, h6 { + line-height: 1.5; +} + /* Theme-specific body backgrounds for safe area padding */ body.light-theme { background-color: #F0F0F0;