feat: Implement microphone monitoring feature; enhance text overflow handling and line height across components
This commit is contained in:
19
config.json
19
config.json
@@ -9,30 +9,13 @@
|
|||||||
"xmr.se"
|
"xmr.se"
|
||||||
],
|
],
|
||||||
"allowCustomHomeservers": true,
|
"allowCustomHomeservers": true,
|
||||||
|
|
||||||
"calling": {
|
|
||||||
"livekitServiceUrl": "https://b.ruv.wtf/matrix-rtc/livekit/jwt"
|
|
||||||
},
|
|
||||||
|
|
||||||
"featuredCommunities": {
|
"featuredCommunities": {
|
||||||
"openAsDefault": false,
|
"openAsDefault": false,
|
||||||
"spaces": [
|
"spaces": [
|
||||||
"#cinny-space:matrix.org",
|
|
||||||
"#community:matrix.org",
|
|
||||||
"#space:envs.net",
|
|
||||||
"#science-space:matrix.org",
|
|
||||||
"#libregaming-games:tchncs.de",
|
|
||||||
"#mathematics-on:matrix.org"
|
|
||||||
],
|
],
|
||||||
"rooms": [
|
"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": {
|
"hashRouter": {
|
||||||
|
|||||||
@@ -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) {
|
if (msgType === MsgType.Text) {
|
||||||
return (
|
return (
|
||||||
<MText
|
<MText
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ export const Reply = style({
|
|||||||
|
|
||||||
export const ReplyContent = style({
|
export const ReplyContent = style({
|
||||||
opacity: config.opacity.P300,
|
opacity: config.opacity.P300,
|
||||||
|
overflow: 'clip',
|
||||||
|
overflowY: 'clip',
|
||||||
|
lineHeight: '1.5',
|
||||||
selectors: {
|
selectors: {
|
||||||
[`${Reply}:hover &`]: {
|
[`${Reply}:hover &`]: {
|
||||||
opacity: config.opacity.P500,
|
opacity: config.opacity.P500,
|
||||||
|
|||||||
@@ -188,8 +188,10 @@ export const BubbleLeftArrow = style({
|
|||||||
|
|
||||||
export const Username = style({
|
export const Username = style({
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
overflowY: 'clip',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
|
lineHeight: '1.5',
|
||||||
// Allow text selection while keeping button clickable
|
// Allow text selection while keeping button clickable
|
||||||
userSelect: 'text',
|
userSelect: 'text',
|
||||||
WebkitUserSelect: 'text',
|
WebkitUserSelect: 'text',
|
||||||
@@ -210,6 +212,9 @@ export const UsernameBold = style({
|
|||||||
export const MessageTextBody = recipe({
|
export const MessageTextBody = recipe({
|
||||||
base: {
|
base: {
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
|
overflow: 'clip',
|
||||||
|
overflowY: 'clip',
|
||||||
|
lineHeight: '1.5',
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
preWrap: {
|
preWrap: {
|
||||||
|
|||||||
@@ -24,4 +24,7 @@ export const RoomViewTyping = style([
|
|||||||
]);
|
]);
|
||||||
export const TypingText = style({
|
export const TypingText = style({
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
|
overflow: 'clip',
|
||||||
|
overflowY: 'clip',
|
||||||
|
lineHeight: '1.5',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -454,7 +454,11 @@ export function Audio({ requestClose }: AudioProps) {
|
|||||||
const [micTestState, setMicTestState] = useState<'idle' | 'testing' | 'done' | 'error'>('idle');
|
const [micTestState, setMicTestState] = useState<'idle' | 'testing' | 'done' | 'error'>('idle');
|
||||||
const [micLevel, setMicLevel] = useState(0);
|
const [micLevel, setMicLevel] = useState(0);
|
||||||
const [speakerTestState, setSpeakerTestState] = useState<'idle' | 'testing' | 'done' | 'error'>('idle');
|
const [speakerTestState, setSpeakerTestState] = useState<'idle' | 'testing' | 'done' | 'error'>('idle');
|
||||||
|
const [micMonitoring, setMicMonitoring] = useState(false);
|
||||||
const micAnimFrameRef = useRef<number | null>(null);
|
const micAnimFrameRef = useRef<number | null>(null);
|
||||||
|
const micMonitorContextRef = useRef<AudioContext | null>(null);
|
||||||
|
const micMonitorStreamRef = useRef<MediaStream | null>(null);
|
||||||
|
const micMonitorGainRef = useRef<GainNode | null>(null);
|
||||||
const [showRemoteCursor, setShowRemoteCursor] = useSetting(settingsAtom, 'showRemoteCursor');
|
const [showRemoteCursor, setShowRemoteCursor] = useSetting(settingsAtom, 'showRemoteCursor');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -627,6 +631,101 @@ export function Audio({ requestClose }: AudioProps) {
|
|||||||
}
|
}
|
||||||
}, [speakerTestState]);
|
}, [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 (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
@@ -686,21 +785,35 @@ export function Audio({ requestClose }: AudioProps) {
|
|||||||
onSelect={handleMicrophoneSelect}
|
onSelect={handleMicrophoneSelect}
|
||||||
/>
|
/>
|
||||||
<Box direction="Column" gap="100" alignItems="End">
|
<Box direction="Column" gap="100" alignItems="End">
|
||||||
<Button
|
<Box gap="200" alignItems="Center">
|
||||||
size="300"
|
<Button
|
||||||
variant={micTestState === 'error' ? 'Critical' : micTestState === 'done' ? 'Success' : 'Secondary'}
|
size="300"
|
||||||
fill="Soft"
|
variant={micTestState === 'error' ? 'Critical' : micTestState === 'done' ? 'Success' : 'Secondary'}
|
||||||
radii="300"
|
fill="Soft"
|
||||||
onClick={handleTestMicrophone}
|
radii="300"
|
||||||
disabled={micTestState === 'testing'}
|
onClick={handleTestMicrophone}
|
||||||
>
|
disabled={micTestState === 'testing' || micMonitoring}
|
||||||
<Text size="T300">
|
>
|
||||||
{micTestState === 'testing' ? 'Testing...' : micTestState === 'done' ? 'Done ✓' : micTestState === 'error' ? 'Failed' : 'Test'}
|
<Text size="T300">
|
||||||
</Text>
|
{micTestState === 'testing' ? 'Testing...' : micTestState === 'done' ? 'Done ✓' : micTestState === 'error' ? 'Failed' : 'Test'}
|
||||||
</Button>
|
</Text>
|
||||||
{micTestState === 'testing' && (
|
</Button>
|
||||||
<Box style={{ width: toRem(80), height: toRem(6), background: color.Surface.Container, borderRadius: toRem(3), overflow: 'hidden' }}>
|
<Button
|
||||||
<Box style={{ width: `${micLevel}%`, height: '100%', background: color.Primary.Main, transition: 'width 0.05s ease' }} />
|
size="300"
|
||||||
|
variant={micMonitoring ? 'Primary' : 'Secondary'}
|
||||||
|
fill={micMonitoring ? 'Solid' : 'Soft'}
|
||||||
|
radii="300"
|
||||||
|
onClick={handleMicMonitoringToggle}
|
||||||
|
disabled={micTestState === 'testing'}
|
||||||
|
>
|
||||||
|
<Text size="T300">
|
||||||
|
{micMonitoring ? 'Stop' : 'Monitor'}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
{(micTestState === 'testing' || micMonitoring) && (
|
||||||
|
<Box style={{ width: toRem(160), height: toRem(6), background: color.Surface.Container, borderRadius: toRem(3), overflow: 'hidden' }}>
|
||||||
|
<Box style={{ width: `${micLevel}%`, height: '100%', background: micMonitoring ? color.Success.Main : color.Primary.Main, transition: 'width 0.05s ease' }} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -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([
|
export const Heading = style([
|
||||||
DefaultReset,
|
DefaultReset,
|
||||||
MarginSpaced,
|
MarginSpaced,
|
||||||
{
|
{
|
||||||
marginTop: config.space.S400,
|
marginTop: config.space.S400,
|
||||||
|
overflow: 'hidden',
|
||||||
|
lineHeight: '1.5',
|
||||||
selectors: {
|
selectors: {
|
||||||
'&:first-child': {
|
'&:first-child': {
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
@@ -38,6 +46,8 @@ export const BlockQuote = style([
|
|||||||
paddingLeft: config.space.S200,
|
paddingLeft: config.space.S200,
|
||||||
borderLeft: `${config.borderWidth.B700} solid ${color.SurfaceVariant.ContainerLine}`,
|
borderLeft: `${config.borderWidth.B700} solid ${color.SurfaceVariant.ContainerLine}`,
|
||||||
fontStyle: 'italic',
|
fontStyle: 'italic',
|
||||||
|
overflow: 'hidden',
|
||||||
|
lineHeight: '1.5',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -63,6 +73,8 @@ export const Code = style([
|
|||||||
{
|
{
|
||||||
padding: `0 ${config.space.S100}`,
|
padding: `0 ${config.space.S100}`,
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
|
overflow: 'hidden',
|
||||||
|
lineHeight: '1.5',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -108,6 +120,8 @@ export const Spoiler = recipe({
|
|||||||
padding: `0 ${config.space.S100}`,
|
padding: `0 ${config.space.S100}`,
|
||||||
backgroundColor: color.SurfaceVariant.ContainerActive,
|
backgroundColor: color.SurfaceVariant.ContainerActive,
|
||||||
borderRadius: config.radii.R300,
|
borderRadius: config.radii.R300,
|
||||||
|
overflow: 'hidden',
|
||||||
|
lineHeight: '1.5',
|
||||||
selectors: {
|
selectors: {
|
||||||
'&[aria-pressed=true]': {
|
'&[aria-pressed=true]': {
|
||||||
color: 'transparent',
|
color: 'transparent',
|
||||||
@@ -132,6 +146,7 @@ export const CodeBlock = style([
|
|||||||
fontStyle: 'normal',
|
fontStyle: 'normal',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
lineHeight: '1.5',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
export const CodeBlockHeader = style([
|
export const CodeBlockHeader = style([
|
||||||
@@ -167,6 +182,8 @@ export const List = style([
|
|||||||
{
|
{
|
||||||
padding: `0 ${config.space.S100}`,
|
padding: `0 ${config.space.S100}`,
|
||||||
paddingLeft: config.space.S600,
|
paddingLeft: config.space.S600,
|
||||||
|
overflow: 'hidden',
|
||||||
|
lineHeight: '1.5',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -194,6 +211,8 @@ export const Mention = recipe({
|
|||||||
padding: `0 ${toRem(2)}`,
|
padding: `0 ${toRem(2)}`,
|
||||||
borderRadius: config.radii.R300,
|
borderRadius: config.radii.R300,
|
||||||
fontWeight: config.fontWeight.W500,
|
fontWeight: config.fontWeight.W500,
|
||||||
|
overflow: 'hidden',
|
||||||
|
lineHeight: '1.5',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
variants: {
|
variants: {
|
||||||
@@ -219,6 +238,8 @@ export const Command = recipe({
|
|||||||
padding: `0 ${toRem(2)}`,
|
padding: `0 ${toRem(2)}`,
|
||||||
borderRadius: config.radii.R300,
|
borderRadius: config.radii.R300,
|
||||||
fontWeight: config.fontWeight.W500,
|
fontWeight: config.fontWeight.W500,
|
||||||
|
overflow: 'hidden',
|
||||||
|
lineHeight: '1.5',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
variants: {
|
variants: {
|
||||||
@@ -287,5 +308,7 @@ export const highlightText = style([
|
|||||||
{
|
{
|
||||||
backgroundColor: 'yellow',
|
backgroundColor: 'yellow',
|
||||||
color: 'black',
|
color: 'black',
|
||||||
|
overflow: 'hidden',
|
||||||
|
lineHeight: '1.5',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -94,6 +94,11 @@ body {
|
|||||||
font-variant-ligatures: no-contextual;
|
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 */
|
/* Theme-specific body backgrounds for safe area padding */
|
||||||
body.light-theme {
|
body.light-theme {
|
||||||
background-color: #F0F0F0;
|
background-color: #F0F0F0;
|
||||||
|
|||||||
Reference in New Issue
Block a user