From d3fba7fd5ff11a62353da7ce83d3215df9421150 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 18 Jan 2023 10:52:12 -0500 Subject: [PATCH 001/286] WIP minus unfinished split grid layouts --- src/room/InCallView.module.css | 1 + src/room/InCallView.tsx | 5 +- src/video-grid/NewVideoGrid.module.css | 18 ++++ src/video-grid/NewVideoGrid.tsx | 132 +++++++++++++++++++++++++ src/video-grid/VideoGrid.tsx | 2 +- src/video-grid/VideoTile.module.css | 1 + 6 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 src/video-grid/NewVideoGrid.module.css create mode 100644 src/video-grid/NewVideoGrid.tsx diff --git a/src/room/InCallView.module.css b/src/room/InCallView.module.css index 8c37d465..ef565731 100644 --- a/src/room/InCallView.module.css +++ b/src/room/InCallView.module.css @@ -17,6 +17,7 @@ limitations under the License. .inRoom { position: relative; display: flex; + gap: 8px; flex-direction: column; overflow: hidden; min-height: 100%; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 22145012..88330dcc 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -64,6 +64,7 @@ import { ParticipantInfo } from "./useGroupCall"; import { TileDescriptor } from "../video-grid/TileDescriptor"; import { AudioSink } from "../video-grid/AudioSink"; import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; +import { NewVideoGrid } from "../video-grid/NewVideoGrid"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); // There is currently a bug in Safari our our code with cloning and sending MediaStreams @@ -305,7 +306,7 @@ export function InCallView({ } return ( - )} - + ); }; diff --git a/src/video-grid/NewVideoGrid.module.css b/src/video-grid/NewVideoGrid.module.css new file mode 100644 index 00000000..b035e655 --- /dev/null +++ b/src/video-grid/NewVideoGrid.module.css @@ -0,0 +1,18 @@ +.grid { + position: relative; + flex-grow: 1; + padding: 0 22px; + overflow-y: scroll; +} + +.slotGrid { + position: relative; + display: grid; + grid-auto-rows: 183px; + column-gap: 18px; + row-gap: 21px; +} + +.slot { + background-color: red; +} diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx new file mode 100644 index 00000000..6bc78e15 --- /dev/null +++ b/src/video-grid/NewVideoGrid.tsx @@ -0,0 +1,132 @@ +import { useTransition } from "@react-spring/web"; +import React, { FC, memo, ReactNode, useMemo, useRef } from "react"; +import useMeasure from "react-use-measure"; +import styles from "./NewVideoGrid.module.css"; +import { TileDescriptor } from "./TileDescriptor"; +import { VideoGridProps as Props } from "./VideoGrid"; + +interface Cell { + /** + * The item held by the slot containing this cell. + */ + item: TileDescriptor + /** + * Whether this cell is the first cell of the containing slot. + */ + slot: boolean + /** + * The width, in columns, of the containing slot. + */ + columns: number + /** + * The height, in rows, of the containing slot. + */ + rows: number +} + +interface Rect { + x: number + y: number + width: number + height: number +} + +interface Tile extends Rect { + item: TileDescriptor + dragging: boolean +} + +interface SlotsProps { + count: number +} + +/** + * Generates a number of empty slot divs. + */ +const Slots: FC = memo(({ count }) => { + const slots = new Array(count) + for (let i = 0; i < count; i++) slots[i] =
+ return <>{slots} +}) + +export const NewVideoGrid: FC = ({ items, children }) => { + const slotGridRef = useRef(null); + const [gridRef, gridBounds] = useMeasure(); + + const slotRects = useMemo(() => { + if (slotGridRef.current === null) return []; + + const slots = slotGridRef.current.getElementsByClassName(styles.slot) + const rects = new Array(slots.length) + for (let i = 0; i < slots.length; i++) { + const slot = slots[i] as HTMLElement + rects[i] = { + x: slot.offsetLeft, + y: slot.offsetTop, + width: slot.offsetWidth, + height: slot.offsetHeight, + } + } + + return rects; + }, [items, gridBounds]); + + const cells: Cell[] = useMemo(() => items.map(item => ({ + item, + slot: true, + columns: 1, + rows: 1, + })), [items]) + + const slotCells = useMemo(() => cells.filter(cell => cell.slot), [cells]) + + const tiles: Tile[] = useMemo(() => slotRects.map((slot, i) => { + const cell = slotCells[i] + return { + item: cell.item, + x: slot.x, + y: slot.y, + width: slot.width, + height: slot.height, + dragging: false, + } + }), [slotRects, cells]) + + const [tileTransitions] = useTransition(tiles, () => ({ + key: ({ item }: Tile) => item.id, + from: { opacity: 0 }, + enter: ({ x, y, width, height }: Tile) => ({ opacity: 1, x, y, width, height }), + update: ({ x, y, width, height }: Tile) => ({ x, y, width, height }), + leave: { opacity: 0 }, + }), [tiles]) + + const slotGridStyle = useMemo(() => { + const columnCount = gridBounds.width >= 800 ? 6 : 3; + return { + gridTemplateColumns: `repeat(${columnCount}, 1fr)`, + }; + }, [gridBounds]); + + // Render nothing if the bounds are not yet known + if (gridBounds.width === 0) { + return
+ } + + return ( +
+
+ +
+ {tileTransitions((style, tile) => children({ + key: tile.item.id, + style: style as any, + width: tile.width, + height: tile.height, + item: tile.item, + }))} +
+ ); +}; diff --git a/src/video-grid/VideoGrid.tsx b/src/video-grid/VideoGrid.tsx index 70633e9c..fc6e44d8 100644 --- a/src/video-grid/VideoGrid.tsx +++ b/src/video-grid/VideoGrid.tsx @@ -705,7 +705,7 @@ interface ChildrenProperties extends ReactDOMAttributes { [index: string]: unknown; } -interface VideoGridProps { +export interface VideoGridProps { items: TileDescriptor[]; layout: Layout; disableAnimations?: boolean; diff --git a/src/video-grid/VideoTile.module.css b/src/video-grid/VideoTile.module.css index d6f6e066..c13976d1 100644 --- a/src/video-grid/VideoTile.module.css +++ b/src/video-grid/VideoTile.module.css @@ -16,6 +16,7 @@ limitations under the License. .videoTile { position: absolute; + top: 0; will-change: transform, width, height, opacity, box-shadow; border-radius: 20px; overflow: hidden; From 486674c442e1ee9d9930326df769239a64f2777f Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 18 Jan 2023 11:32:51 -0500 Subject: [PATCH 002/286] fixes --- src/video-grid/NewVideoGrid.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index 6bc78e15..003c0709 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -80,16 +80,18 @@ export const NewVideoGrid: FC = ({ items, children }) => { const slotCells = useMemo(() => cells.filter(cell => cell.slot), [cells]) - const tiles: Tile[] = useMemo(() => slotRects.map((slot, i) => { + const tiles: Tile[] = useMemo(() => slotRects.flatMap((slot, i) => { const cell = slotCells[i] - return { + if (cell === undefined) return [] + + return [{ item: cell.item, x: slot.x, y: slot.y, width: slot.width, height: slot.height, dragging: false, - } + }] }), [slotRects, cells]) const [tileTransitions] = useTransition(tiles, () => ({ @@ -109,7 +111,11 @@ export const NewVideoGrid: FC = ({ items, children }) => { // Render nothing if the bounds are not yet known if (gridBounds.width === 0) { - return
+ return
+ {/* It's important that we always attach slotGridRef to something, + or else we may not receive the initial slot rects. */} +
+
} return ( From 2318d75bc78b5ce5e1f46a902567d481db222bd9 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 18 Jan 2023 11:33:40 -0500 Subject: [PATCH 003/286] prettier --- src/video-grid/NewVideoGrid.tsx | 142 ++++++++++++++++++-------------- 1 file changed, 82 insertions(+), 60 deletions(-) diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index 003c0709..82c4b32e 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -9,45 +9,46 @@ interface Cell { /** * The item held by the slot containing this cell. */ - item: TileDescriptor + item: TileDescriptor; /** * Whether this cell is the first cell of the containing slot. */ - slot: boolean + slot: boolean; /** * The width, in columns, of the containing slot. */ - columns: number + columns: number; /** * The height, in rows, of the containing slot. */ - rows: number + rows: number; } interface Rect { - x: number - y: number - width: number - height: number + x: number; + y: number; + width: number; + height: number; } interface Tile extends Rect { - item: TileDescriptor - dragging: boolean + item: TileDescriptor; + dragging: boolean; } interface SlotsProps { - count: number + count: number; } /** * Generates a number of empty slot divs. */ const Slots: FC = memo(({ count }) => { - const slots = new Array(count) - for (let i = 0; i < count; i++) slots[i] =
- return <>{slots} -}) + const slots = new Array(count); + for (let i = 0; i < count; i++) + slots[i] =
; + return <>{slots}; +}); export const NewVideoGrid: FC = ({ items, children }) => { const slotGridRef = useRef(null); @@ -56,51 +57,71 @@ export const NewVideoGrid: FC = ({ items, children }) => { const slotRects = useMemo(() => { if (slotGridRef.current === null) return []; - const slots = slotGridRef.current.getElementsByClassName(styles.slot) - const rects = new Array(slots.length) + const slots = slotGridRef.current.getElementsByClassName(styles.slot); + const rects = new Array(slots.length); for (let i = 0; i < slots.length; i++) { - const slot = slots[i] as HTMLElement + const slot = slots[i] as HTMLElement; rects[i] = { x: slot.offsetLeft, y: slot.offsetTop, width: slot.offsetWidth, height: slot.offsetHeight, - } + }; } return rects; }, [items, gridBounds]); - const cells: Cell[] = useMemo(() => items.map(item => ({ - item, - slot: true, - columns: 1, - rows: 1, - })), [items]) + const cells: Cell[] = useMemo( + () => + items.map((item) => ({ + item, + slot: true, + columns: 1, + rows: 1, + })), + [items] + ); - const slotCells = useMemo(() => cells.filter(cell => cell.slot), [cells]) + const slotCells = useMemo(() => cells.filter((cell) => cell.slot), [cells]); - const tiles: Tile[] = useMemo(() => slotRects.flatMap((slot, i) => { - const cell = slotCells[i] - if (cell === undefined) return [] + const tiles: Tile[] = useMemo( + () => + slotRects.flatMap((slot, i) => { + const cell = slotCells[i]; + if (cell === undefined) return []; - return [{ - item: cell.item, - x: slot.x, - y: slot.y, - width: slot.width, - height: slot.height, - dragging: false, - }] - }), [slotRects, cells]) + return [ + { + item: cell.item, + x: slot.x, + y: slot.y, + width: slot.width, + height: slot.height, + dragging: false, + }, + ]; + }), + [slotRects, cells] + ); - const [tileTransitions] = useTransition(tiles, () => ({ - key: ({ item }: Tile) => item.id, - from: { opacity: 0 }, - enter: ({ x, y, width, height }: Tile) => ({ opacity: 1, x, y, width, height }), - update: ({ x, y, width, height }: Tile) => ({ x, y, width, height }), - leave: { opacity: 0 }, - }), [tiles]) + const [tileTransitions] = useTransition( + tiles, + () => ({ + key: ({ item }: Tile) => item.id, + from: { opacity: 0 }, + enter: ({ x, y, width, height }: Tile) => ({ + opacity: 1, + x, + y, + width, + height, + }), + update: ({ x, y, width, height }: Tile) => ({ x, y, width, height }), + leave: { opacity: 0 }, + }), + [tiles] + ); const slotGridStyle = useMemo(() => { const columnCount = gridBounds.width >= 800 ? 6 : 3; @@ -111,28 +132,29 @@ export const NewVideoGrid: FC = ({ items, children }) => { // Render nothing if the bounds are not yet known if (gridBounds.width === 0) { - return
- {/* It's important that we always attach slotGridRef to something, + return ( +
+ {/* It's important that we always attach slotGridRef to something, or else we may not receive the initial slot rects. */} -
-
+
+
+ ); } return ( -
+
- {tileTransitions((style, tile) => children({ - key: tile.item.id, - style: style as any, - width: tile.width, - height: tile.height, - item: tile.item, - }))} + {tileTransitions((style, tile) => + children({ + key: tile.item.id, + style: style as any, + width: tile.width, + height: tile.height, + item: tile.item, + }) + )}
); }; From 46d1351d8358158cc9d1369bc5087dfb5bac89ad Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 18 Jan 2023 13:38:29 -0500 Subject: [PATCH 004/286] More fixes --- src/video-grid/NewVideoGrid.tsx | 75 ++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index 82c4b32e..2c334447 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -1,5 +1,13 @@ import { useTransition } from "@react-spring/web"; -import React, { FC, memo, ReactNode, useMemo, useRef } from "react"; +import React, { + FC, + memo, + ReactNode, + useCallback, + useMemo, + useRef, + useState, +} from "react"; import useMeasure from "react-use-measure"; import styles from "./NewVideoGrid.module.css"; import { TileDescriptor } from "./TileDescriptor"; @@ -33,7 +41,6 @@ interface Rect { interface Tile extends Rect { item: TileDescriptor; - dragging: boolean; } interface SlotsProps { @@ -50,14 +57,18 @@ const Slots: FC = memo(({ count }) => { return <>{slots}; }); -export const NewVideoGrid: FC = ({ items, children }) => { - const slotGridRef = useRef(null); +export const NewVideoGrid: FC = ({ + items, + disableAnimations, + children, +}) => { + const [slotGrid, setSlotGrid] = useState(null); const [gridRef, gridBounds] = useMeasure(); const slotRects = useMemo(() => { - if (slotGridRef.current === null) return []; + if (slotGrid === null) return []; - const slots = slotGridRef.current.getElementsByClassName(styles.slot); + const slots = slotGrid.getElementsByClassName(styles.slot); const rects = new Array(slots.length); for (let i = 0; i < slots.length; i++) { const slot = slots[i] as HTMLElement; @@ -70,7 +81,7 @@ export const NewVideoGrid: FC = ({ items, children }) => { } return rects; - }, [items, gridBounds]); + }, [items, gridBounds, slotGrid]); const cells: Cell[] = useMemo( () => @@ -98,7 +109,6 @@ export const NewVideoGrid: FC = ({ items, children }) => { y: slot.y, width: slot.width, height: slot.height, - dragging: false, }, ]; }), @@ -109,18 +119,36 @@ export const NewVideoGrid: FC = ({ items, children }) => { tiles, () => ({ key: ({ item }: Tile) => item.id, - from: { opacity: 0 }, - enter: ({ x, y, width, height }: Tile) => ({ - opacity: 1, + from: (({ x, y, width, height }: Tile) => ({ + opacity: 0, + scale: 0, + shadow: 1, x, y, width, height, - }), + // react-spring's types are bugged and need this to be a function with no + // parameters to infer the spring type + })) as unknown as () => { + opacity: number; + scale: number; + shadow: number; + x: number; + y: number; + width: number; + height: number; + }, + enter: { opacity: 1, scale: 1 }, update: ({ x, y, width, height }: Tile) => ({ x, y, width, height }), - leave: { opacity: 0 }, + leave: { opacity: 0, scale: 0 }, + immediate: (key: string) => + disableAnimations || key === "zIndex" || key === "shadow", + // If we just stopped dragging a tile, give it time for the + // animation to settle before pushing its z-index back down + delay: (key: string) => (key === "zIndex" ? 500 : 0), + trail: 20, }), - [tiles] + [tiles, disableAnimations] ); const slotGridStyle = useMemo(() => { @@ -132,24 +160,23 @@ export const NewVideoGrid: FC = ({ items, children }) => { // Render nothing if the bounds are not yet known if (gridBounds.width === 0) { - return ( -
- {/* It's important that we always attach slotGridRef to something, - or else we may not receive the initial slot rects. */} -
-
- ); + return
; } return (
-
+
- {tileTransitions((style, tile) => + {tileTransitions(({ shadow, ...style }, tile) => children({ key: tile.item.id, - style: style as any, + style: { + boxShadow: shadow.to( + (s) => `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px 0px` + ), + ...style, + }, width: tile.width, height: tile.height, item: tile.item, From 045103dbc97abb418a33c8b58dde8434752c88dc Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 25 Jan 2023 02:30:52 -0500 Subject: [PATCH 005/286] Backfill the grid as people leave by moving tiles along paths --- package.json | 1 + src/useReactiveState.ts | 46 ++++ src/video-grid/NewVideoGrid.module.css | 4 +- src/video-grid/NewVideoGrid.tsx | 301 +++++++++++++++++++++---- yarn.lock | 5 + 5 files changed, 315 insertions(+), 42 deletions(-) create mode 100644 src/useReactiveState.ts diff --git a/package.json b/package.json index c3a511f7..4242b19c 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "react-use-clipboard": "^1.0.7", "react-use-measure": "^2.1.1", "sdp-transform": "^2.14.1", + "tinyqueue": "^2.0.3", "unique-names-generator": "^4.6.0" }, "devDependencies": { diff --git a/src/useReactiveState.ts b/src/useReactiveState.ts new file mode 100644 index 00000000..fe995724 --- /dev/null +++ b/src/useReactiveState.ts @@ -0,0 +1,46 @@ +import { + DependencyList, + Dispatch, + SetStateAction, + useCallback, + useRef, + useState, +} from "react"; + +export const useReactiveState = ( + updateFn: (prevState?: T) => T, + deps: DependencyList +): [T, Dispatch>] => { + const state = useRef(); + if (state.current === undefined) state.current = updateFn(); + const prevDeps = useRef(); + + // Since we store the state in a ref, we use this counter to force an update + // when someone calls setState + const [, setNumUpdates] = useState(0); + + // If this is the first render or the deps have changed, recalculate the state + if ( + prevDeps.current === undefined || + deps.length !== prevDeps.current.length || + deps.some((d, i) => d !== prevDeps.current![i]) + ) { + state.current = updateFn(state.current); + } + prevDeps.current = deps; + + return [ + state.current, + useCallback( + (action) => { + if (typeof action === "function") { + state.current = (action as (prevValue: T) => T)(state.current!); + } else { + state.current = action; + } + setNumUpdates((n) => n + 1); // Force an update + }, + [setNumUpdates] + ), + ]; +}; diff --git a/src/video-grid/NewVideoGrid.module.css b/src/video-grid/NewVideoGrid.module.css index b035e655..d654beb8 100644 --- a/src/video-grid/NewVideoGrid.module.css +++ b/src/video-grid/NewVideoGrid.module.css @@ -13,6 +13,4 @@ row-gap: 21px; } -.slot { - background-color: red; -} +.slot {} diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index 2c334447..5cda71ec 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -1,17 +1,21 @@ import { useTransition } from "@react-spring/web"; +import { useDrag } from "@use-gesture/react"; import React, { FC, memo, ReactNode, useCallback, + useEffect, useMemo, - useRef, useState, } from "react"; import useMeasure from "react-use-measure"; import styles from "./NewVideoGrid.module.css"; import { TileDescriptor } from "./TileDescriptor"; import { VideoGridProps as Props } from "./VideoGrid"; +import { useReactiveState } from "../useReactiveState"; +import TinyQueue from "tinyqueue"; +import { zipWith } from "lodash"; interface Cell { /** @@ -32,6 +36,12 @@ interface Cell { rows: number; } +interface Grid { + generation: number; + columns: number; + cells: (Cell | undefined)[]; +} + interface Rect { x: number; y: number; @@ -43,6 +53,162 @@ interface Tile extends Rect { item: TileDescriptor; } +interface TileSpring { + opacity: number; + scale: number; + shadow: number; + x: number; + y: number; + width: number; + height: number; +} + +const dijkstra = (g: Grid): number[] => { + const end = findLast1By1Index(g) ?? 0; + const endRow = row(end, g); + const endColumn = column(end, g); + + const distances = new Array(end + 1).fill(Infinity); + distances[end] = 0; + const edges = new Array(end).fill(undefined); + const heap = new TinyQueue([end], (i) => distances[i]); + + const visit = (curr: number, via: number) => { + const viaCell = g.cells[via]; + const viaLargeSlot = + viaCell !== undefined && (viaCell.rows > 1 || viaCell.columns > 1); + const distanceVia = distances[via] + (viaLargeSlot ? 4 : 1); + + if (distanceVia < distances[curr]) { + distances[curr] = distanceVia; + edges[curr] = via; + heap.push(curr); + } + }; + + while (heap.length > 0) { + const via = heap.pop()!; + const viaRow = row(via, g); + const viaColumn = column(via, g); + + if (viaRow > 0) visit(via - g.columns, via); + if (viaColumn > 0) visit(via - 1, via); + if (viaColumn < (viaRow === endRow ? endColumn : g.columns - 1)) + visit(via + 1, via); + if ( + viaRow < endRow - 1 || + (viaRow === endRow - 1 && viaColumn <= endColumn) + ) + visit(via + g.columns, via); + } + + return edges as number[]; +}; + +const findLastIndex = ( + array: T[], + predicate: (item: T) => boolean +): number | null => { + for (let i = array.length - 1; i > 0; i--) { + if (predicate(array[i])) return i; + } + + return null; +}; + +const findLast1By1Index = (g: Grid): number | null => + findLastIndex(g.cells, (c) => c?.rows === 1 && c?.columns === 1); + +const row = (index: number, g: Grid): number => Math.floor(index / g.columns); +const column = (index: number, g: Grid): number => index % g.columns; + +/** + * Gets the index of the next gap in the grid that should be backfilled by 1×1 + * tiles. + */ +const getNextGap = (g: Grid): number | null => { + const last1By1Index = findLast1By1Index(g); + if (last1By1Index === null) return null; + + for (let i = 0; i < last1By1Index; i++) { + // To make the backfilling process look natural when there are multiple + // gaps, we actually scan each row from right to left + const j = + (row(i, g) === row(last1By1Index, g) + ? last1By1Index + : (row(i, g) + 1) * g.columns) - + 1 - + column(i, g); + + if (g.cells[j] === undefined) return j; + } + + return null; +}; + +const fillGaps = (g: Grid): Grid => { + const result: Grid = { ...g, cells: [...g.cells] }; + let gap = getNextGap(result); + + if (gap !== null) { + const pathToEnd = dijkstra(result); + + do { + let filled = false; + let to = gap; + let from: number | undefined = pathToEnd[gap]; + + // First, attempt to fill the gap by moving 1×1 tiles backwards from the + // end of the grid along a set path + while (from !== undefined) { + const toCell = result.cells[to]; + const fromCell = result.cells[from]; + + // Skip over large tiles + if (toCell !== undefined) { + to = pathToEnd[to]; + // Skip over large tiles. Also, we might run into gaps along the path + // created during the filling of previous gaps. Skip over those too; + // they'll be picked up on the next iteration of the outer loop. + } else if ( + fromCell === undefined || + fromCell.rows > 1 || + fromCell.columns > 1 + ) { + from = pathToEnd[from]; + } else { + result.cells[to] = result.cells[from]; + result.cells[from] = undefined; + filled = true; + to = pathToEnd[to]; + from = pathToEnd[from]; + } + } + + // In case the path approach failed, fall back to taking the very last 1×1 + // tile, and just dropping it into place + if (!filled) { + const last1By1Index = findLast1By1Index(result)!; + result.cells[gap] = result.cells[last1By1Index]; + result.cells[last1By1Index] = undefined; + } + + gap = getNextGap(result); + } while (gap !== null); + } + + // TODO: If there are any large tiles on the last row, shuffle them back + // upwards into a full row + + // Shrink the array to remove trailing gaps + const finalLength = + (findLastIndex(result.cells, (c) => c !== undefined) ?? -1) + 1; + if (finalLength < result.cells.length) + result.cells = result.cells.slice(0, finalLength); + + return result; +}; + interface SlotsProps { count: number; } @@ -63,8 +229,24 @@ export const NewVideoGrid: FC = ({ children, }) => { const [slotGrid, setSlotGrid] = useState(null); + const [slotGridGeneration, setSlotGridGeneration] = useState(0) const [gridRef, gridBounds] = useMeasure(); + useEffect(() => { + if (slotGrid !== null) { + setSlotGridGeneration(parseInt(slotGrid.getAttribute("data-generation")!)) + + const observer = new MutationObserver(mutations => { + if (mutations.some(m => m.type === "attributes")) { + setSlotGridGeneration(parseInt(slotGrid.getAttribute("data-generation")!)) + } + }) + + observer.observe(slotGrid, { attributes: true }) + return () => observer.disconnect() + } + }, [slotGrid, setSlotGridGeneration]) + const slotRects = useMemo(() => { if (slotGrid === null) return []; @@ -81,38 +263,66 @@ export const NewVideoGrid: FC = ({ } return rects; - }, [items, gridBounds, slotGrid]); + }, [items, slotGridGeneration, slotGrid]); - const cells: Cell[] = useMemo( - () => - items.map((item) => ({ - item, - slot: true, - columns: 1, - rows: 1, - })), + const [grid, setGrid] = useReactiveState( + (prevGrid = { generation: 0, columns: 6, cells: [] }) => { + // Step 1: Update tiles that still exist, and remove tiles that have left + // the grid + const itemsById = new Map(items.map((i) => [i.id, i])); + const grid1: Grid = { + ...prevGrid, + generation: prevGrid.generation + 1, + cells: prevGrid.cells.map((c) => { + if (c === undefined) return undefined; + const item = itemsById.get(c.item.id); + return item === undefined ? undefined : { ...c, item }; + }), + }; + + // Step 2: Backfill gaps left behind by removed tiles + const grid2 = fillGaps(grid1); + + // Step 3: Add new tiles to the end of the grid + const existingItemIds = new Set( + grid2.cells.filter((c) => c !== undefined).map((c) => c!.item.id) + ); + const newItems = items.filter((i) => !existingItemIds.has(i.id)); + const grid3: Grid = { + ...grid2, + cells: [ + ...grid2.cells, + ...newItems.map((i) => ({ + item: i, + slot: true, + columns: 1, + rows: 1, + })), + ], + }; + + return grid3; + }, [items] ); - const slotCells = useMemo(() => cells.filter((cell) => cell.slot), [cells]); + const [tiles] = useReactiveState( + (prevTiles) => { + // If React hasn't yet rendered the current generation of the layout, skip + // the update, because grid and slotRects will be out of sync + if (slotGridGeneration !== grid.generation) return prevTiles ?? []; - const tiles: Tile[] = useMemo( - () => - slotRects.flatMap((slot, i) => { - const cell = slotCells[i]; - if (cell === undefined) return []; - - return [ - { - item: cell.item, - x: slot.x, - y: slot.y, - width: slot.width, - height: slot.height, - }, - ]; - }), - [slotRects, cells] + const slotCells = grid.cells.filter((c) => c?.slot) as Cell[]; + console.log(slotGridGeneration, grid.generation, slotCells.length, slotRects.length, slotGrid?.getElementsByClassName(styles.slot).length) + return zipWith(slotCells, slotRects, (cell, rect) => ({ + item: cell.item, + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + })); + }, + [slotRects, grid, slotGridGeneration] ); const [tileTransitions] = useTransition( @@ -129,15 +339,7 @@ export const NewVideoGrid: FC = ({ height, // react-spring's types are bugged and need this to be a function with no // parameters to infer the spring type - })) as unknown as () => { - opacity: number; - scale: number; - shadow: number; - x: number; - y: number; - width: number; - height: number; - }, + })) as unknown as () => TileSpring, enter: { opacity: 1, scale: 1 }, update: ({ x, y, width, height }: Tile) => ({ x, y, width, height }), leave: { opacity: 0, scale: 0 }, @@ -146,7 +348,6 @@ export const NewVideoGrid: FC = ({ // If we just stopped dragging a tile, give it time for the // animation to settle before pushing its z-index back down delay: (key: string) => (key === "zIndex" ? 500 : 0), - trail: 20, }), [tiles, disableAnimations] ); @@ -158,6 +359,22 @@ export const NewVideoGrid: FC = ({ }; }, [gridBounds]); + const bindTile = useDrag( + useCallback(({ event, tap }) => { + event.preventDefault(); + + if (tap) { + // TODO: When enlarging tiles, add the minimum number of rows required + // to not need to force any tiles towards the end, find the right number + // of consecutive spots for a tile of size w * (h - added rows), + // displace overlapping tiles, and then backfill. + // When unenlarging tiles, consider doing that in reverse (deleting + // rows and displacing tiles. pushing tiles outwards might be necessary) + } + }, []), + { filterTaps: true, pointer: { buttons: [1] } } + ); + // Render nothing if the bounds are not yet known if (gridBounds.width === 0) { return
; @@ -165,11 +382,17 @@ export const NewVideoGrid: FC = ({ return (
-
+
{tileTransitions(({ shadow, ...style }, tile) => children({ + ...bindTile(tile.item.id), key: tile.item.id, style: { boxShadow: shadow.to( diff --git a/yarn.lock b/yarn.lock index f5719527..7980fa2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13717,6 +13717,11 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.3: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tinyqueue@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08" + integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA== + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" From 8912daa9221a9c4316e3aaa3801090c03007e124 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 25 Jan 2023 23:51:36 -0500 Subject: [PATCH 006/286] Make tiles resizable and fix some miscellaneous bugs --- src/video-grid/NewVideoGrid.module.css | 2 +- src/video-grid/NewVideoGrid.tsx | 170 ++++++++++++++++++++----- 2 files changed, 142 insertions(+), 30 deletions(-) diff --git a/src/video-grid/NewVideoGrid.module.css b/src/video-grid/NewVideoGrid.module.css index d654beb8..5be19d3f 100644 --- a/src/video-grid/NewVideoGrid.module.css +++ b/src/video-grid/NewVideoGrid.module.css @@ -2,7 +2,7 @@ position: relative; flex-grow: 1; padding: 0 22px; - overflow-y: scroll; + overflow-y: auto; } .slotGrid { diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index 5cda71ec..d9313d18 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -1,10 +1,8 @@ -import { useTransition } from "@react-spring/web"; +import { SpringRef, TransitionFn, useTransition } from "@react-spring/web"; import { useDrag } from "@use-gesture/react"; import React, { FC, - memo, ReactNode, - useCallback, useEffect, useMemo, useState, @@ -25,6 +23,7 @@ interface Cell { /** * Whether this cell is the first cell of the containing slot. */ + // TODO: Rename to 'start'? slot: boolean; /** * The width, in columns, of the containing slot. @@ -122,6 +121,25 @@ const findLast1By1Index = (g: Grid): number | null => const row = (index: number, g: Grid): number => Math.floor(index / g.columns); const column = (index: number, g: Grid): number => index % g.columns; +function* cellsInArea(start: number, end: number, g: Grid): Generator{ + const startColumn = column(start, g) + const endColumn = column(end, g) + for (let i = start; i <= end; i = column(i, g) === endColumn ? i + g.columns + startColumn - endColumn : i + 1) + yield i +} + +const forEachCellInArea = (start: number, end: number, g: Grid, fn: (c: Cell | undefined, i: number) => void) => { + for (const i of cellsInArea(start, end, g)) fn(g.cells[i], i) +} + +const allCellsInArea = (start: number, end: number, g: Grid, fn: (c: Cell | undefined, i: number) => boolean) => { + for (const i of cellsInArea(start, end, g)) { + if (!fn(g.cells[i], i)) return false + } + + return true +} + /** * Gets the index of the next gap in the grid that should be backfilled by 1×1 * tiles. @@ -209,20 +227,6 @@ const fillGaps = (g: Grid): Grid => { return result; }; -interface SlotsProps { - count: number; -} - -/** - * Generates a number of empty slot divs. - */ -const Slots: FC = memo(({ count }) => { - const slots = new Array(count); - for (let i = 0; i < count; i++) - slots[i] =
; - return <>{slots}; -}); - export const NewVideoGrid: FC = ({ items, disableAnimations, @@ -263,7 +267,7 @@ export const NewVideoGrid: FC = ({ } return rects; - }, [items, slotGridGeneration, slotGrid]); + }, [items, slotGridGeneration, slotGrid, gridBounds]); const [grid, setGrid] = useReactiveState( (prevGrid = { generation: 0, columns: 6, cells: [] }) => { @@ -313,7 +317,6 @@ export const NewVideoGrid: FC = ({ if (slotGridGeneration !== grid.generation) return prevTiles ?? []; const slotCells = grid.cells.filter((c) => c?.slot) as Cell[]; - console.log(slotGridGeneration, grid.generation, slotCells.length, slotRects.length, slotGrid?.getElementsByClassName(styles.slot).length) return zipWith(slotCells, slotRects, (cell, rect) => ({ item: cell.item, x: rect.x, @@ -329,7 +332,7 @@ export const NewVideoGrid: FC = ({ tiles, () => ({ key: ({ item }: Tile) => item.id, - from: (({ x, y, width, height }: Tile) => ({ + from: ({ x, y, width, height }: Tile) => ({ opacity: 0, scale: 0, shadow: 1, @@ -337,9 +340,7 @@ export const NewVideoGrid: FC = ({ y, width, height, - // react-spring's types are bugged and need this to be a function with no - // parameters to infer the spring type - })) as unknown as () => TileSpring, + }), enter: { opacity: 1, scale: 1 }, update: ({ x, y, width, height }: Tile) => ({ x, y, width, height }), leave: { opacity: 0, scale: 0 }, @@ -350,18 +351,35 @@ export const NewVideoGrid: FC = ({ delay: (key: string) => (key === "zIndex" ? 500 : 0), }), [tiles, disableAnimations] - ); + // react-spring's types are bugged and can't infer the spring type + ) as unknown as [TransitionFn, SpringRef]; const slotGridStyle = useMemo(() => { - const columnCount = gridBounds.width >= 800 ? 6 : 3; + const columnCount = 6 + + const areas = new Array<(number | null)[]>(Math.ceil(grid.cells.length / grid.columns)) + for (let i = 0; i < areas.length; i++) areas[i] = new Array(grid.columns).fill(null) + + let slotId = 0 + for (let i = 0; i < grid.cells.length; i++) { + const cell = grid.cells[i] + if (cell?.slot) { + const slotEnd = i + cell.columns - 1 + grid.columns * (cell.rows - 1) + forEachCellInArea(i, slotEnd, grid, (_c, j) => areas[row(j, grid)][column(j, grid)] = slotId) + slotId++ + } + } + return { + gridTemplateAreas: areas.map(row => `'${row.map(slotId => slotId === null ? "." : `s${slotId}`).join(" ")}'`).join(" "), gridTemplateColumns: `repeat(${columnCount}, 1fr)`, }; - }, [gridBounds]); + }, [grid]); const bindTile = useDrag( - useCallback(({ event, tap }) => { + ({ event, tap, args }) => { event.preventDefault(); + const tileId = args[0] as string if (tap) { // TODO: When enlarging tiles, add the minimum number of rows required @@ -370,11 +388,105 @@ export const NewVideoGrid: FC = ({ // displace overlapping tiles, and then backfill. // When unenlarging tiles, consider doing that in reverse (deleting // rows and displacing tiles. pushing tiles outwards might be necessary) + setGrid(g => { + const from = g.cells.findIndex(c => c?.item.id === tileId) + if (from === -1) return g // Tile removed, no change + const fromWidth = g.cells[from]!.columns + const fromHeight = g.cells[from]!.rows + + const [toWidth, toHeight] = fromWidth === 1 && fromHeight === 1 ? [3, 2] : [1, 1] + const newRows = Math.ceil((toWidth * toHeight - fromWidth * fromHeight) / g.columns) + + const candidateWidth = toWidth + const candidateHeight = toHeight - newRows + + const slotStarts = new Array(g.cells.length) + g.cells.forEach((c, start) => { + if (c === undefined || c.item.id === tileId) { + slotStarts[start] = start + } else if (c.slot) { + const end = start + c.columns - 1 + g.columns * (c.rows - 1) + forEachCellInArea(start, end, g, (_c, i) => slotStarts[i] = start) + } else if (slotStarts[start] === undefined) { + slotStarts[start] = start + } + }) + + const nextScanLocations = new Set([from]) + const scanColumnOffset = Math.floor((toWidth - 1) / 2) + const scanRowOffset = Math.floor((toHeight - 1) / 2) + let to: number | null = null + + const displaceable = (c: Cell | undefined, i: number): boolean => c === undefined || (c.columns === 1 && c.rows === 1) || g.cells[slotStarts[i]]?.item.id === tileId + + for (const scanLocation of nextScanLocations) { + const start = scanLocation - scanColumnOffset - g.columns * scanRowOffset + const end = start + candidateWidth - 1 + g.columns * (candidateHeight - 1) + const startColumn = column(start, g); + const endColumn = column(end, g); + + if (start >= 0 && end < g.cells.length && endColumn - startColumn + 1 === candidateWidth) { + if (allCellsInArea(start, end, g, displaceable)) { + to = start + break + } + } + + if (startColumn > 0) nextScanLocations.add(scanLocation - 1) + if (endColumn < g.columns - 1) nextScanLocations.add(scanLocation + 1) + nextScanLocations.add(scanLocation - g.columns) + nextScanLocations.add(scanLocation + g.columns) + } + + if (to === null) return g + + const gappyGrid: Grid = { + ...g, + generation: g.generation + 1, + cells: new Array(g.cells.length + newRows * g.columns), + } + + const toRow = row(to, g) + + for (let src = 0; src < g.cells.length; src++) { + if (g.cells[src]?.item.id !== tileId) { + const dest = row(src, g) > toRow + toHeight - 1 ? src + g.columns * newRows : src + gappyGrid.cells[dest] = g.cells[src] + } + } + + const displacedTiles: Cell[] = [] + const toEnd = to + toWidth - 1 + g.columns * (toHeight - 1) + forEachCellInArea(to, toEnd, gappyGrid, (c, i) => { + if (c !== undefined) displacedTiles.push(c) + gappyGrid.cells[i] = { + item: g.cells[from]!.item, + slot: i === to, + columns: toWidth, + rows: toHeight, + } + }) + + for (let i = 0; displacedTiles.length > 0; i++) { + if (gappyGrid.cells[i] === undefined) gappyGrid.cells[i] = displacedTiles.shift() + } + + const nonGappy = fillGaps(gappyGrid) + console.log(`${g.cells.length} => ${nonGappy.cells.length}, ${g.generation} => ${nonGappy.generation}`) + return nonGappy + }) } - }, []), + }, { filterTaps: true, pointer: { buttons: [1] } } ); + const slots = useMemo(() => { + const slots = new Array(items.length); + for (let i = 0; i < items.length; i++) + slots[i] =
; + return slots + }, [items.length]) + // Render nothing if the bounds are not yet known if (gridBounds.width === 0) { return
; @@ -388,7 +500,7 @@ export const NewVideoGrid: FC = ({ className={styles.slotGrid} data-generation={grid.generation} > - + {slots}
{tileTransitions(({ shadow, ...style }, tile) => children({ From e99294c3f12f50ab97be2f9b15201fd73b360262 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 29 Jan 2023 21:45:10 -0500 Subject: [PATCH 007/286] Simplify some code --- src/video-grid/NewVideoGrid.tsx | 39 ++++++++++++++------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index d9313d18..ecae8174 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -121,6 +121,12 @@ const findLast1By1Index = (g: Grid): number | null => const row = (index: number, g: Grid): number => Math.floor(index / g.columns); const column = (index: number, g: Grid): number => index % g.columns; +const inArea = (index: number, start: number, end: number, g: Grid): boolean => { + const indexColumn = column(index, g) + const indexRow = column(index, g) + return indexRow >= row(start, g) && indexRow <= row(end, g) && indexColumn >= column(start, g) && indexColumn <= column(end, g) +} + function* cellsInArea(start: number, end: number, g: Grid): Generator{ const startColumn = column(start, g) const endColumn = column(end, g) @@ -140,6 +146,9 @@ const allCellsInArea = (start: number, end: number, g: Grid, fn: (c: Cell | unde return true } +const areaEnd = (start: number, columns: number, rows: number, g: Grid): number => + start + columns - 1 + g.columns * (rows - 1) + /** * Gets the index of the next gap in the grid that should be backfilled by 1×1 * tiles. @@ -382,17 +391,14 @@ export const NewVideoGrid: FC = ({ const tileId = args[0] as string if (tap) { - // TODO: When enlarging tiles, add the minimum number of rows required - // to not need to force any tiles towards the end, find the right number - // of consecutive spots for a tile of size w * (h - added rows), - // displace overlapping tiles, and then backfill. - // When unenlarging tiles, consider doing that in reverse (deleting + // TODO: When unenlarging tiles, do this in reverse somehow (deleting // rows and displacing tiles. pushing tiles outwards might be necessary) setGrid(g => { const from = g.cells.findIndex(c => c?.item.id === tileId) if (from === -1) return g // Tile removed, no change const fromWidth = g.cells[from]!.columns const fromHeight = g.cells[from]!.rows + const fromEnd = areaEnd(from, fromWidth, fromHeight, g) const [toWidth, toHeight] = fromWidth === 1 && fromHeight === 1 ? [3, 2] : [1, 1] const newRows = Math.ceil((toWidth * toHeight - fromWidth * fromHeight) / g.columns) @@ -400,28 +406,16 @@ export const NewVideoGrid: FC = ({ const candidateWidth = toWidth const candidateHeight = toHeight - newRows - const slotStarts = new Array(g.cells.length) - g.cells.forEach((c, start) => { - if (c === undefined || c.item.id === tileId) { - slotStarts[start] = start - } else if (c.slot) { - const end = start + c.columns - 1 + g.columns * (c.rows - 1) - forEachCellInArea(start, end, g, (_c, i) => slotStarts[i] = start) - } else if (slotStarts[start] === undefined) { - slotStarts[start] = start - } - }) - const nextScanLocations = new Set([from]) const scanColumnOffset = Math.floor((toWidth - 1) / 2) const scanRowOffset = Math.floor((toHeight - 1) / 2) let to: number | null = null - const displaceable = (c: Cell | undefined, i: number): boolean => c === undefined || (c.columns === 1 && c.rows === 1) || g.cells[slotStarts[i]]?.item.id === tileId + const displaceable = (c: Cell | undefined, i: number): boolean => c === undefined || (c.columns === 1 && c.rows === 1) || inArea(i, from, fromEnd, g) for (const scanLocation of nextScanLocations) { const start = scanLocation - scanColumnOffset - g.columns * scanRowOffset - const end = start + candidateWidth - 1 + g.columns * (candidateHeight - 1) + const end = areaEnd(start, candidateWidth, candidateHeight, g) const startColumn = column(start, g); const endColumn = column(end, g); @@ -438,6 +432,7 @@ export const NewVideoGrid: FC = ({ nextScanLocations.add(scanLocation + g.columns) } + // TODO: Don't give up on placing the tile yet if (to === null) return g const gappyGrid: Grid = { @@ -456,7 +451,7 @@ export const NewVideoGrid: FC = ({ } const displacedTiles: Cell[] = [] - const toEnd = to + toWidth - 1 + g.columns * (toHeight - 1) + const toEnd = areaEnd(to, toWidth, toHeight, g) forEachCellInArea(to, toEnd, gappyGrid, (c, i) => { if (c !== undefined) displacedTiles.push(c) gappyGrid.cells[i] = { @@ -471,9 +466,7 @@ export const NewVideoGrid: FC = ({ if (gappyGrid.cells[i] === undefined) gappyGrid.cells[i] = displacedTiles.shift() } - const nonGappy = fillGaps(gappyGrid) - console.log(`${g.cells.length} => ${nonGappy.cells.length}, ${g.generation} => ${nonGappy.generation}`) - return nonGappy + return fillGaps(gappyGrid) }) } }, From 4e3598490016f9dc6aa301211bf042422d5dba66 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 29 Jan 2023 21:54:53 -0500 Subject: [PATCH 008/286] Extract tile size change logic into a function --- src/video-grid/NewVideoGrid.tsx | 159 ++++++++++++++++---------------- 1 file changed, 82 insertions(+), 77 deletions(-) diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index ecae8174..df14183d 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -236,6 +236,85 @@ const fillGaps = (g: Grid): Grid => { return result; }; +const cycleTileSize = (tileId: string, g: Grid): Grid => { + // TODO: When unenlarging tiles, do all this in reverse somehow (deleting + // rows and displacing tiles. pushing tiles outwards might be necessary) + + const from = g.cells.findIndex(c => c?.item.id === tileId) + if (from === -1) return g // Tile removed, no change + const fromWidth = g.cells[from]!.columns + const fromHeight = g.cells[from]!.rows + const fromEnd = areaEnd(from, fromWidth, fromHeight, g) + + const [toWidth, toHeight] = fromWidth === 1 && fromHeight === 1 ? [3, 2] : [1, 1] + const newRows = Math.ceil((toWidth * toHeight - fromWidth * fromHeight) / g.columns) + + const candidateWidth = toWidth + const candidateHeight = toHeight - newRows + + const nextScanLocations = new Set([from]) + const scanColumnOffset = Math.floor((toWidth - 1) / 2) + const scanRowOffset = Math.floor((toHeight - 1) / 2) + let to: number | null = null + + const displaceable = (c: Cell | undefined, i: number): boolean => c === undefined || (c.columns === 1 && c.rows === 1) || inArea(i, from, fromEnd, g) + + for (const scanLocation of nextScanLocations) { + const start = scanLocation - scanColumnOffset - g.columns * scanRowOffset + const end = areaEnd(start, candidateWidth, candidateHeight, g) + const startColumn = column(start, g); + const endColumn = column(end, g); + + if (start >= 0 && end < g.cells.length && endColumn - startColumn + 1 === candidateWidth) { + if (allCellsInArea(start, end, g, displaceable)) { + to = start + break + } + } + + if (startColumn > 0) nextScanLocations.add(scanLocation - 1) + if (endColumn < g.columns - 1) nextScanLocations.add(scanLocation + 1) + nextScanLocations.add(scanLocation - g.columns) + nextScanLocations.add(scanLocation + g.columns) + } + + // TODO: Don't give up on placing the tile yet + if (to === null) return g + + const gappyGrid: Grid = { + ...g, + generation: g.generation + 1, + cells: new Array(g.cells.length + newRows * g.columns), + } + + const toRow = row(to, g) + + for (let src = 0; src < g.cells.length; src++) { + if (g.cells[src]?.item.id !== tileId) { + const dest = row(src, g) > toRow + toHeight - 1 ? src + g.columns * newRows : src + gappyGrid.cells[dest] = g.cells[src] + } + } + + const displacedTiles: Cell[] = [] + const toEnd = areaEnd(to, toWidth, toHeight, g) + forEachCellInArea(to, toEnd, gappyGrid, (c, i) => { + if (c !== undefined) displacedTiles.push(c) + gappyGrid.cells[i] = { + item: g.cells[from]!.item, + slot: i === to, + columns: toWidth, + rows: toHeight, + } + }) + + for (let i = 0; displacedTiles.length > 0; i++) { + if (gappyGrid.cells[i] === undefined) gappyGrid.cells[i] = displacedTiles.shift() + } + + return fillGaps(gappyGrid) +} + export const NewVideoGrid: FC = ({ items, disableAnimations, @@ -391,83 +470,9 @@ export const NewVideoGrid: FC = ({ const tileId = args[0] as string if (tap) { - // TODO: When unenlarging tiles, do this in reverse somehow (deleting - // rows and displacing tiles. pushing tiles outwards might be necessary) - setGrid(g => { - const from = g.cells.findIndex(c => c?.item.id === tileId) - if (from === -1) return g // Tile removed, no change - const fromWidth = g.cells[from]!.columns - const fromHeight = g.cells[from]!.rows - const fromEnd = areaEnd(from, fromWidth, fromHeight, g) - - const [toWidth, toHeight] = fromWidth === 1 && fromHeight === 1 ? [3, 2] : [1, 1] - const newRows = Math.ceil((toWidth * toHeight - fromWidth * fromHeight) / g.columns) - - const candidateWidth = toWidth - const candidateHeight = toHeight - newRows - - const nextScanLocations = new Set([from]) - const scanColumnOffset = Math.floor((toWidth - 1) / 2) - const scanRowOffset = Math.floor((toHeight - 1) / 2) - let to: number | null = null - - const displaceable = (c: Cell | undefined, i: number): boolean => c === undefined || (c.columns === 1 && c.rows === 1) || inArea(i, from, fromEnd, g) - - for (const scanLocation of nextScanLocations) { - const start = scanLocation - scanColumnOffset - g.columns * scanRowOffset - const end = areaEnd(start, candidateWidth, candidateHeight, g) - const startColumn = column(start, g); - const endColumn = column(end, g); - - if (start >= 0 && end < g.cells.length && endColumn - startColumn + 1 === candidateWidth) { - if (allCellsInArea(start, end, g, displaceable)) { - to = start - break - } - } - - if (startColumn > 0) nextScanLocations.add(scanLocation - 1) - if (endColumn < g.columns - 1) nextScanLocations.add(scanLocation + 1) - nextScanLocations.add(scanLocation - g.columns) - nextScanLocations.add(scanLocation + g.columns) - } - - // TODO: Don't give up on placing the tile yet - if (to === null) return g - - const gappyGrid: Grid = { - ...g, - generation: g.generation + 1, - cells: new Array(g.cells.length + newRows * g.columns), - } - - const toRow = row(to, g) - - for (let src = 0; src < g.cells.length; src++) { - if (g.cells[src]?.item.id !== tileId) { - const dest = row(src, g) > toRow + toHeight - 1 ? src + g.columns * newRows : src - gappyGrid.cells[dest] = g.cells[src] - } - } - - const displacedTiles: Cell[] = [] - const toEnd = areaEnd(to, toWidth, toHeight, g) - forEachCellInArea(to, toEnd, gappyGrid, (c, i) => { - if (c !== undefined) displacedTiles.push(c) - gappyGrid.cells[i] = { - item: g.cells[from]!.item, - slot: i === to, - columns: toWidth, - rows: toHeight, - } - }) - - for (let i = 0; displacedTiles.length > 0; i++) { - if (gappyGrid.cells[i] === undefined) gappyGrid.cells[i] = displacedTiles.shift() - } - - return fillGaps(gappyGrid) - }) + setGrid(g => cycleTileSize(tileId, g)) + } else { + // TODO } }, { filterTaps: true, pointer: { buttons: [1] } } From 3805a2f20e5df29a53330accb3dec244fe905db3 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 29 Jan 2023 21:56:07 -0500 Subject: [PATCH 009/286] Format with Prettier --- src/video-grid/NewVideoGrid.module.css | 3 +- src/video-grid/NewVideoGrid.tsx | 234 ++++++++++++++++--------- 2 files changed, 150 insertions(+), 87 deletions(-) diff --git a/src/video-grid/NewVideoGrid.module.css b/src/video-grid/NewVideoGrid.module.css index 5be19d3f..dff0e871 100644 --- a/src/video-grid/NewVideoGrid.module.css +++ b/src/video-grid/NewVideoGrid.module.css @@ -13,4 +13,5 @@ row-gap: 21px; } -.slot {} +.slot { +} diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index df14183d..e5c15df5 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -1,12 +1,6 @@ import { SpringRef, TransitionFn, useTransition } from "@react-spring/web"; import { useDrag } from "@use-gesture/react"; -import React, { - FC, - ReactNode, - useEffect, - useMemo, - useState, -} from "react"; +import React, { FC, ReactNode, useEffect, useMemo, useState } from "react"; import useMeasure from "react-use-measure"; import styles from "./NewVideoGrid.module.css"; import { TileDescriptor } from "./TileDescriptor"; @@ -121,33 +115,68 @@ const findLast1By1Index = (g: Grid): number | null => const row = (index: number, g: Grid): number => Math.floor(index / g.columns); const column = (index: number, g: Grid): number => index % g.columns; -const inArea = (index: number, start: number, end: number, g: Grid): boolean => { - const indexColumn = column(index, g) - const indexRow = column(index, g) - return indexRow >= row(start, g) && indexRow <= row(end, g) && indexColumn >= column(start, g) && indexColumn <= column(end, g) +const inArea = ( + index: number, + start: number, + end: number, + g: Grid +): boolean => { + const indexColumn = column(index, g); + const indexRow = column(index, g); + return ( + indexRow >= row(start, g) && + indexRow <= row(end, g) && + indexColumn >= column(start, g) && + indexColumn <= column(end, g) + ); +}; + +function* cellsInArea( + start: number, + end: number, + g: Grid +): Generator { + const startColumn = column(start, g); + const endColumn = column(end, g); + for ( + let i = start; + i <= end; + i = + column(i, g) === endColumn + ? i + g.columns + startColumn - endColumn + : i + 1 + ) + yield i; } -function* cellsInArea(start: number, end: number, g: Grid): Generator{ - const startColumn = column(start, g) - const endColumn = column(end, g) - for (let i = start; i <= end; i = column(i, g) === endColumn ? i + g.columns + startColumn - endColumn : i + 1) - yield i -} +const forEachCellInArea = ( + start: number, + end: number, + g: Grid, + fn: (c: Cell | undefined, i: number) => void +) => { + for (const i of cellsInArea(start, end, g)) fn(g.cells[i], i); +}; -const forEachCellInArea = (start: number, end: number, g: Grid, fn: (c: Cell | undefined, i: number) => void) => { - for (const i of cellsInArea(start, end, g)) fn(g.cells[i], i) -} - -const allCellsInArea = (start: number, end: number, g: Grid, fn: (c: Cell | undefined, i: number) => boolean) => { +const allCellsInArea = ( + start: number, + end: number, + g: Grid, + fn: (c: Cell | undefined, i: number) => boolean +) => { for (const i of cellsInArea(start, end, g)) { - if (!fn(g.cells[i], i)) return false + if (!fn(g.cells[i], i)) return false; } - return true -} + return true; +}; -const areaEnd = (start: number, columns: number, rows: number, g: Grid): number => - start + columns - 1 + g.columns * (rows - 1) +const areaEnd = ( + start: number, + columns: number, + rows: number, + g: Grid +): number => start + columns - 1 + g.columns * (rows - 1); /** * Gets the index of the next gap in the grid that should be backfilled by 1×1 @@ -240,80 +269,92 @@ const cycleTileSize = (tileId: string, g: Grid): Grid => { // TODO: When unenlarging tiles, do all this in reverse somehow (deleting // rows and displacing tiles. pushing tiles outwards might be necessary) - const from = g.cells.findIndex(c => c?.item.id === tileId) - if (from === -1) return g // Tile removed, no change - const fromWidth = g.cells[from]!.columns - const fromHeight = g.cells[from]!.rows - const fromEnd = areaEnd(from, fromWidth, fromHeight, g) + const from = g.cells.findIndex((c) => c?.item.id === tileId); + if (from === -1) return g; // Tile removed, no change + const fromWidth = g.cells[from]!.columns; + const fromHeight = g.cells[from]!.rows; + const fromEnd = areaEnd(from, fromWidth, fromHeight, g); - const [toWidth, toHeight] = fromWidth === 1 && fromHeight === 1 ? [3, 2] : [1, 1] - const newRows = Math.ceil((toWidth * toHeight - fromWidth * fromHeight) / g.columns) + const [toWidth, toHeight] = + fromWidth === 1 && fromHeight === 1 ? [3, 2] : [1, 1]; + const newRows = Math.ceil( + (toWidth * toHeight - fromWidth * fromHeight) / g.columns + ); - const candidateWidth = toWidth - const candidateHeight = toHeight - newRows + const candidateWidth = toWidth; + const candidateHeight = toHeight - newRows; - const nextScanLocations = new Set([from]) - const scanColumnOffset = Math.floor((toWidth - 1) / 2) - const scanRowOffset = Math.floor((toHeight - 1) / 2) - let to: number | null = null + const nextScanLocations = new Set([from]); + const scanColumnOffset = Math.floor((toWidth - 1) / 2); + const scanRowOffset = Math.floor((toHeight - 1) / 2); + let to: number | null = null; - const displaceable = (c: Cell | undefined, i: number): boolean => c === undefined || (c.columns === 1 && c.rows === 1) || inArea(i, from, fromEnd, g) + const displaceable = (c: Cell | undefined, i: number): boolean => + c === undefined || + (c.columns === 1 && c.rows === 1) || + inArea(i, from, fromEnd, g); for (const scanLocation of nextScanLocations) { - const start = scanLocation - scanColumnOffset - g.columns * scanRowOffset - const end = areaEnd(start, candidateWidth, candidateHeight, g) + const start = scanLocation - scanColumnOffset - g.columns * scanRowOffset; + const end = areaEnd(start, candidateWidth, candidateHeight, g); const startColumn = column(start, g); const endColumn = column(end, g); - if (start >= 0 && end < g.cells.length && endColumn - startColumn + 1 === candidateWidth) { + if ( + start >= 0 && + end < g.cells.length && + endColumn - startColumn + 1 === candidateWidth + ) { if (allCellsInArea(start, end, g, displaceable)) { - to = start - break + to = start; + break; } } - if (startColumn > 0) nextScanLocations.add(scanLocation - 1) - if (endColumn < g.columns - 1) nextScanLocations.add(scanLocation + 1) - nextScanLocations.add(scanLocation - g.columns) - nextScanLocations.add(scanLocation + g.columns) + if (startColumn > 0) nextScanLocations.add(scanLocation - 1); + if (endColumn < g.columns - 1) nextScanLocations.add(scanLocation + 1); + nextScanLocations.add(scanLocation - g.columns); + nextScanLocations.add(scanLocation + g.columns); } // TODO: Don't give up on placing the tile yet - if (to === null) return g + if (to === null) return g; const gappyGrid: Grid = { ...g, generation: g.generation + 1, cells: new Array(g.cells.length + newRows * g.columns), - } + }; - const toRow = row(to, g) + const toRow = row(to, g); for (let src = 0; src < g.cells.length; src++) { if (g.cells[src]?.item.id !== tileId) { - const dest = row(src, g) > toRow + toHeight - 1 ? src + g.columns * newRows : src - gappyGrid.cells[dest] = g.cells[src] + const dest = + row(src, g) > toRow + toHeight - 1 ? src + g.columns * newRows : src; + gappyGrid.cells[dest] = g.cells[src]; } } - const displacedTiles: Cell[] = [] - const toEnd = areaEnd(to, toWidth, toHeight, g) + const displacedTiles: Cell[] = []; + const toEnd = areaEnd(to, toWidth, toHeight, g); forEachCellInArea(to, toEnd, gappyGrid, (c, i) => { - if (c !== undefined) displacedTiles.push(c) + if (c !== undefined) displacedTiles.push(c); gappyGrid.cells[i] = { item: g.cells[from]!.item, slot: i === to, columns: toWidth, rows: toHeight, - } - }) + }; + }); for (let i = 0; displacedTiles.length > 0; i++) { - if (gappyGrid.cells[i] === undefined) gappyGrid.cells[i] = displacedTiles.shift() + if (gappyGrid.cells[i] === undefined) + gappyGrid.cells[i] = displacedTiles.shift(); } - return fillGaps(gappyGrid) -} + return fillGaps(gappyGrid); +}; export const NewVideoGrid: FC = ({ items, @@ -321,23 +362,27 @@ export const NewVideoGrid: FC = ({ children, }) => { const [slotGrid, setSlotGrid] = useState(null); - const [slotGridGeneration, setSlotGridGeneration] = useState(0) + const [slotGridGeneration, setSlotGridGeneration] = useState(0); const [gridRef, gridBounds] = useMeasure(); useEffect(() => { if (slotGrid !== null) { - setSlotGridGeneration(parseInt(slotGrid.getAttribute("data-generation")!)) + setSlotGridGeneration( + parseInt(slotGrid.getAttribute("data-generation")!) + ); - const observer = new MutationObserver(mutations => { - if (mutations.some(m => m.type === "attributes")) { - setSlotGridGeneration(parseInt(slotGrid.getAttribute("data-generation")!)) + const observer = new MutationObserver((mutations) => { + if (mutations.some((m) => m.type === "attributes")) { + setSlotGridGeneration( + parseInt(slotGrid.getAttribute("data-generation")!) + ); } - }) + }); - observer.observe(slotGrid, { attributes: true }) - return () => observer.disconnect() + observer.observe(slotGrid, { attributes: true }); + return () => observer.disconnect(); } - }, [slotGrid, setSlotGridGeneration]) + }, [slotGrid, setSlotGridGeneration]); const slotRects = useMemo(() => { if (slotGrid === null) return []; @@ -443,23 +488,38 @@ export const NewVideoGrid: FC = ({ ) as unknown as [TransitionFn, SpringRef]; const slotGridStyle = useMemo(() => { - const columnCount = 6 + const columnCount = 6; - const areas = new Array<(number | null)[]>(Math.ceil(grid.cells.length / grid.columns)) - for (let i = 0; i < areas.length; i++) areas[i] = new Array(grid.columns).fill(null) + const areas = new Array<(number | null)[]>( + Math.ceil(grid.cells.length / grid.columns) + ); + for (let i = 0; i < areas.length; i++) + areas[i] = new Array(grid.columns).fill(null); - let slotId = 0 + let slotId = 0; for (let i = 0; i < grid.cells.length; i++) { - const cell = grid.cells[i] + const cell = grid.cells[i]; if (cell?.slot) { - const slotEnd = i + cell.columns - 1 + grid.columns * (cell.rows - 1) - forEachCellInArea(i, slotEnd, grid, (_c, j) => areas[row(j, grid)][column(j, grid)] = slotId) - slotId++ + const slotEnd = i + cell.columns - 1 + grid.columns * (cell.rows - 1); + forEachCellInArea( + i, + slotEnd, + grid, + (_c, j) => (areas[row(j, grid)][column(j, grid)] = slotId) + ); + slotId++; } } return { - gridTemplateAreas: areas.map(row => `'${row.map(slotId => slotId === null ? "." : `s${slotId}`).join(" ")}'`).join(" "), + gridTemplateAreas: areas + .map( + (row) => + `'${row + .map((slotId) => (slotId === null ? "." : `s${slotId}`)) + .join(" ")}'` + ) + .join(" "), gridTemplateColumns: `repeat(${columnCount}, 1fr)`, }; }, [grid]); @@ -467,10 +527,10 @@ export const NewVideoGrid: FC = ({ const bindTile = useDrag( ({ event, tap, args }) => { event.preventDefault(); - const tileId = args[0] as string + const tileId = args[0] as string; if (tap) { - setGrid(g => cycleTileSize(tileId, g)) + setGrid((g) => cycleTileSize(tileId, g)); } else { // TODO } @@ -481,9 +541,11 @@ export const NewVideoGrid: FC = ({ const slots = useMemo(() => { const slots = new Array(items.length); for (let i = 0; i < items.length; i++) - slots[i] =
; - return slots - }, [items.length]) + slots[i] = ( +
+ ); + return slots; + }, [items.length]); // Render nothing if the bounds are not yet known if (gridBounds.width === 0) { From 4e73c07cb23d98050898f955bb73ba6fa752e0ce Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 30 Jan 2023 09:35:40 -0500 Subject: [PATCH 010/286] Try out scroll snapping --- src/room/InCallView.module.css | 2 +- src/video-grid/NewVideoGrid.module.css | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/room/InCallView.module.css b/src/room/InCallView.module.css index ef565731..896e1f99 100644 --- a/src/room/InCallView.module.css +++ b/src/room/InCallView.module.css @@ -17,7 +17,6 @@ limitations under the License. .inRoom { position: relative; display: flex; - gap: 8px; flex-direction: column; overflow: hidden; min-height: 100%; @@ -41,6 +40,7 @@ limitations under the License. .footer { position: relative; + flex-shrink: 0; display: flex; justify-content: center; align-items: center; diff --git a/src/video-grid/NewVideoGrid.module.css b/src/video-grid/NewVideoGrid.module.css index dff0e871..c8e0d806 100644 --- a/src/video-grid/NewVideoGrid.module.css +++ b/src/video-grid/NewVideoGrid.module.css @@ -3,6 +3,7 @@ flex-grow: 1; padding: 0 22px; overflow-y: auto; + scroll-snap-type: both mandatory; } .slotGrid { @@ -14,4 +15,12 @@ } .slot { + scroll-snap-align: start; +} + +.slot:last-child { + /* This causes the grid to scroll up smoothly to the last item when its height + shrinks, and ensures that the user can always scroll the last row fully in + view */ + scroll-snap-align: end; } From 55dece274f71c0bcec7d050c605ba05acafbe99c Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 30 Jan 2023 17:04:43 -0500 Subject: [PATCH 011/286] Fix some tile resizing bugs --- src/video-grid/NewVideoGrid.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index e5c15df5..2ad5e3ba 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -328,13 +328,15 @@ const cycleTileSize = (tileId: string, g: Grid): Grid => { const toRow = row(to, g); - for (let src = 0; src < g.cells.length; src++) { - if (g.cells[src]?.item.id !== tileId) { - const dest = - row(src, g) > toRow + toHeight - 1 ? src + g.columns * newRows : src; - gappyGrid.cells[dest] = g.cells[src]; + g.cells.forEach((c, src) => { + if (c?.slot && c.item.id !== tileId) { + const offset = + row(src, g) > toRow + candidateHeight - 1 ? g.columns * newRows : 0; + forEachCellInArea(src, areaEnd(src, c.columns, c.rows, g), g, (c, i) => { + gappyGrid.cells[i + offset] = c + }) } - } + }) const displacedTiles: Cell[] = []; const toEnd = areaEnd(to, toWidth, toHeight, g); From f540f48461c13b05af2fe7fbeb053275ae3ffba0 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 30 Jan 2023 23:32:00 -0500 Subject: [PATCH 012/286] Fix some layout bugs --- src/video-grid/NewVideoGrid.tsx | 30 ++++++++++++++--------------- src/video-grid/VideoTile.module.css | 7 ------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index 2ad5e3ba..28b9e0aa 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -122,7 +122,7 @@ const inArea = ( g: Grid ): boolean => { const indexColumn = column(index, g); - const indexRow = column(index, g); + const indexRow = row(index, g); return ( indexRow >= row(start, g) && indexRow <= row(end, g) && @@ -266,9 +266,6 @@ const fillGaps = (g: Grid): Grid => { }; const cycleTileSize = (tileId: string, g: Grid): Grid => { - // TODO: When unenlarging tiles, do all this in reverse somehow (deleting - // rows and displacing tiles. pushing tiles outwards might be necessary) - const from = g.cells.findIndex((c) => c?.item.id === tileId); if (from === -1) return g; // Tile removed, no change const fromWidth = g.cells[from]!.columns; @@ -277,13 +274,20 @@ const cycleTileSize = (tileId: string, g: Grid): Grid => { const [toWidth, toHeight] = fromWidth === 1 && fromHeight === 1 ? [3, 2] : [1, 1]; - const newRows = Math.ceil( - (toWidth * toHeight - fromWidth * fromHeight) / g.columns + const newRows = Math.max( + 0, + Math.ceil((toWidth * toHeight - fromWidth * fromHeight) / g.columns) ); const candidateWidth = toWidth; const candidateHeight = toHeight - newRows; + const gappyGrid: Grid = { + ...g, + generation: g.generation + 1, + cells: new Array(g.cells.length + newRows * g.columns), + }; + const nextScanLocations = new Set([from]); const scanColumnOffset = Math.floor((toWidth - 1) / 2); const scanRowOffset = Math.floor((toHeight - 1) / 2); @@ -302,7 +306,7 @@ const cycleTileSize = (tileId: string, g: Grid): Grid => { if ( start >= 0 && - end < g.cells.length && + end < gappyGrid.cells.length && endColumn - startColumn + 1 === candidateWidth ) { if (allCellsInArea(start, end, g, displaceable)) { @@ -320,12 +324,6 @@ const cycleTileSize = (tileId: string, g: Grid): Grid => { // TODO: Don't give up on placing the tile yet if (to === null) return g; - const gappyGrid: Grid = { - ...g, - generation: g.generation + 1, - cells: new Array(g.cells.length + newRows * g.columns), - }; - const toRow = row(to, g); g.cells.forEach((c, src) => { @@ -333,10 +331,10 @@ const cycleTileSize = (tileId: string, g: Grid): Grid => { const offset = row(src, g) > toRow + candidateHeight - 1 ? g.columns * newRows : 0; forEachCellInArea(src, areaEnd(src, c.columns, c.rows, g), g, (c, i) => { - gappyGrid.cells[i + offset] = c - }) + gappyGrid.cells[i + offset] = c; + }); } - }) + }); const displacedTiles: Cell[] = []; const toEnd = areaEnd(to, toWidth, toHeight, g); diff --git a/src/video-grid/VideoTile.module.css b/src/video-grid/VideoTile.module.css index c13976d1..00429867 100644 --- a/src/video-grid/VideoTile.module.css +++ b/src/video-grid/VideoTile.module.css @@ -144,13 +144,6 @@ limitations under the License. white-space: nowrap; } -.videoMutedAvatar { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); -} - .videoMutedOverlay { width: 100%; height: 100%; From e3081c1c069f3d43c0879c6dfd45d818c2ec0e18 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 30 Jan 2023 23:32:26 -0500 Subject: [PATCH 013/286] Try out a snappier spring --- src/video-grid/NewVideoGrid.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index 28b9e0aa..a7db7dda 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -477,6 +477,7 @@ export const NewVideoGrid: FC = ({ enter: { opacity: 1, scale: 1 }, update: ({ x, y, width, height }: Tile) => ({ x, y, width, height }), leave: { opacity: 0, scale: 0 }, + config: { mass: 0.7, tension: 252, friction: 25 }, immediate: (key: string) => disableAnimations || key === "zIndex" || key === "shadow", // If we just stopped dragging a tile, give it time for the From 0166eb67fb35bb0d22cf7bd650f1c67f691608bb Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 30 Jan 2023 23:43:45 -0500 Subject: [PATCH 014/286] Make avatars scale smoothly during animations --- src/room/InCallView.module.css | 6 ++++++ src/video-grid/NewVideoGrid.tsx | 4 +++- src/video-grid/VideoTile.module.css | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/room/InCallView.module.css b/src/room/InCallView.module.css index 896e1f99..310cf47b 100644 --- a/src/room/InCallView.module.css +++ b/src/room/InCallView.module.css @@ -66,6 +66,12 @@ limitations under the License. top: 50%; left: 50%; transform: translate(-50%, -50%); + /* To make avatars scale smoothly with their tiles during animations, we + override the styles set on the element */ + --avatarSize: calc(min(var(--tileWidth), var(--tileHeight)) / 2); + width: var(--avatarSize) !important; + height: var(--avatarSize) !important; + border-radius: 10000px !important; } @media (min-height: 300px) { diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index a7db7dda..04300dda 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -563,7 +563,7 @@ export const NewVideoGrid: FC = ({ > {slots}
- {tileTransitions(({ shadow, ...style }, tile) => + {tileTransitions(({ shadow, width, height, ...style }, tile) => children({ ...bindTile(tile.item.id), key: tile.item.id, @@ -571,6 +571,8 @@ export const NewVideoGrid: FC = ({ boxShadow: shadow.to( (s) => `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px 0px` ), + "--tileWidth": width.to((w) => `${w}px`), + "--tileHeight": height.to((h) => `${h}px`), ...style, }, width: tile.width, diff --git a/src/video-grid/VideoTile.module.css b/src/video-grid/VideoTile.module.css index 00429867..f475d0c5 100644 --- a/src/video-grid/VideoTile.module.css +++ b/src/video-grid/VideoTile.module.css @@ -18,6 +18,8 @@ limitations under the License. position: absolute; top: 0; will-change: transform, width, height, opacity, box-shadow; + width: var(--tileWidth); + height: var(--tileHeight); border-radius: 20px; overflow: hidden; cursor: pointer; From 82c7293308418161136bc01d6a383a1ef75fb8fd Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 30 Jan 2023 23:44:19 -0500 Subject: [PATCH 015/286] Replace premature animation optimization with a potentially wiser one --- src/video-grid/NewVideoGrid.module.css | 3 +++ src/video-grid/VideoTile.module.css | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/video-grid/NewVideoGrid.module.css b/src/video-grid/NewVideoGrid.module.css index c8e0d806..f30f12c8 100644 --- a/src/video-grid/NewVideoGrid.module.css +++ b/src/video-grid/NewVideoGrid.module.css @@ -1,4 +1,5 @@ .grid { + contain: strict; position: relative; flex-grow: 1; padding: 0 22px; @@ -7,6 +8,7 @@ } .slotGrid { + contain: strict; position: relative; display: grid; grid-auto-rows: 183px; @@ -15,6 +17,7 @@ } .slot { + contain: strict; scroll-snap-align: start; } diff --git a/src/video-grid/VideoTile.module.css b/src/video-grid/VideoTile.module.css index f475d0c5..05f3a0ce 100644 --- a/src/video-grid/VideoTile.module.css +++ b/src/video-grid/VideoTile.module.css @@ -16,8 +16,8 @@ limitations under the License. .videoTile { position: absolute; + contain: strict; top: 0; - will-change: transform, width, height, opacity, box-shadow; width: var(--tileWidth); height: var(--tileHeight); border-radius: 20px; From d7db845f3b243e05387f9035ed0746a991500bc3 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 30 Jan 2023 23:52:46 -0500 Subject: [PATCH 016/286] Scroll snap was a bad idea --- src/video-grid/NewVideoGrid.module.css | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/video-grid/NewVideoGrid.module.css b/src/video-grid/NewVideoGrid.module.css index f30f12c8..27c453ff 100644 --- a/src/video-grid/NewVideoGrid.module.css +++ b/src/video-grid/NewVideoGrid.module.css @@ -4,7 +4,6 @@ flex-grow: 1; padding: 0 22px; overflow-y: auto; - scroll-snap-type: both mandatory; } .slotGrid { @@ -18,12 +17,4 @@ .slot { contain: strict; - scroll-snap-align: start; -} - -.slot:last-child { - /* This causes the grid to scroll up smoothly to the last item when its height - shrinks, and ensures that the user can always scroll the last row fully in - view */ - scroll-snap-align: end; } From eedf8a6d1b7369a60b5e93d7d7db69ed8baf95ef Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 1 Feb 2023 00:17:22 -0500 Subject: [PATCH 017/286] Make tiles draggable (but not yet droppable) --- src/video-grid/NewVideoGrid.tsx | 115 +++++++++++++++++++++++++++----- 1 file changed, 100 insertions(+), 15 deletions(-) diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index 04300dda..13d1860f 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -1,6 +1,13 @@ import { SpringRef, TransitionFn, useTransition } from "@react-spring/web"; -import { useDrag } from "@use-gesture/react"; -import React, { FC, ReactNode, useEffect, useMemo, useState } from "react"; +import { useDrag, useScroll } from "@use-gesture/react"; +import React, { + FC, + ReactNode, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import useMeasure from "react-use-measure"; import styles from "./NewVideoGrid.module.css"; import { TileDescriptor } from "./TileDescriptor"; @@ -50,12 +57,19 @@ interface TileSpring { opacity: number; scale: number; shadow: number; + zIndex: number; x: number; y: number; width: number; height: number; } +interface DragState { + tileId: string; + x: number; + y: number; +} + const dijkstra = (g: Grid): number[] => { const end = findLast1By1Index(g) ?? 0; const endRow = row(end, g); @@ -461,7 +475,11 @@ export const NewVideoGrid: FC = ({ [slotRects, grid, slotGridGeneration] ); - const [tileTransitions] = useTransition( + // Drag state is stored in a ref rather than component state, because we use + // react-spring's imperative API during gestures to improve responsiveness + const dragState = useRef(null); + + const [tileTransitions, springRef] = useTransition( tiles, () => ({ key: ({ item }: Tile) => item.id, @@ -469,20 +487,26 @@ export const NewVideoGrid: FC = ({ opacity: 0, scale: 0, shadow: 1, + zIndex: 1, x, y, width, height, + immediate: disableAnimations, }), - enter: { opacity: 1, scale: 1 }, - update: ({ x, y, width, height }: Tile) => ({ x, y, width, height }), - leave: { opacity: 0, scale: 0 }, + enter: { opacity: 1, scale: 1, immediate: disableAnimations }, + update: ({ item, x, y, width, height }: Tile) => + item.id === dragState.current?.tileId + ? {} + : { + x, + y, + width, + height, + immediate: disableAnimations, + }, + leave: { opacity: 0, scale: 0, immediate: disableAnimations }, config: { mass: 0.7, tension: 252, friction: 25 }, - immediate: (key: string) => - disableAnimations || key === "zIndex" || key === "shadow", - // If we just stopped dragging a tile, give it time for the - // animation to settle before pushing its z-index back down - delay: (key: string) => (key === "zIndex" ? 500 : 0), }), [tiles, disableAnimations] // react-spring's types are bugged and can't infer the spring type @@ -525,20 +549,81 @@ export const NewVideoGrid: FC = ({ }; }, [grid]); + const animateDraggedTile = (endOfGesture: boolean) => + springRef.start((_i, controller) => { + const { tileId, x, y } = dragState.current!; + + // react-spring appears to not update a controller's item as long as the + // key remains stable, so we can use it to look up the tile's ID but not + // its position + if ((controller.item as Tile).item.id === tileId) { + if (endOfGesture) { + const tile = tiles.find((t) => t.item.id === tileId)!; + + return { + scale: 1, + zIndex: 1, + shadow: 1, + x: tile.x, + y: tile.y, + immediate: disableAnimations || ((key) => key === "zIndex"), + // Allow the tile's position to settle before pushing its + // z-index back down + delay: (key) => (key === "zIndex" ? 500 : 0), + }; + } else { + return { + scale: 1.1, + zIndex: 2, + shadow: 15, + x, + y, + immediate: + disableAnimations || + ((key) => key === "zIndex" || key === "x" || key === "y"), + }; + } + } else { + return {}; + } + }); + const bindTile = useDrag( - ({ event, tap, args }) => { - event.preventDefault(); + ({ tap, args, delta: [dx, dy], last }) => { const tileId = args[0] as string; if (tap) { setGrid((g) => cycleTileSize(tileId, g)); } else { - // TODO + const tileSpring = springRef.current + .find((c) => (c.item as Tile).item.id === tileId)! + .get(); + + if (dragState.current === null) { + dragState.current = { tileId, x: tileSpring.x, y: tileSpring.y }; + } + dragState.current.x += dx; + dragState.current.y += dy; + + animateDraggedTile(last); + + if (last) dragState.current = null; } }, { filterTaps: true, pointer: { buttons: [1] } } ); + const scrollOffset = useRef(0); + + const bindGrid = useScroll(({ xy: [, y], delta: [, dy] }) => { + scrollOffset.current = y; + + if (dragState.current !== null) { + dragState.current.y += dy; + animateDraggedTile(false); + } + }); + const slots = useMemo(() => { const slots = new Array(items.length); for (let i = 0; i < items.length; i++) @@ -554,7 +639,7 @@ export const NewVideoGrid: FC = ({ } return ( -
+
Date: Wed, 1 Feb 2023 11:32:10 -0500 Subject: [PATCH 018/286] Implement somewhat working drag & drop and improve render memoization --- src/useMergedRefs.ts | 16 +++ src/video-grid/NewVideoGrid.tsx | 144 ++++++++++++-------- src/video-grid/VideoGrid.tsx | 17 ++- src/video-grid/VideoTile.tsx | 36 ++++- src/video-grid/VideoTileContainer.tsx | 185 +++++++++++++++----------- src/video-grid/useMediaStream.ts | 4 +- 6 files changed, 256 insertions(+), 146 deletions(-) create mode 100644 src/useMergedRefs.ts diff --git a/src/useMergedRefs.ts b/src/useMergedRefs.ts new file mode 100644 index 00000000..9931505d --- /dev/null +++ b/src/useMergedRefs.ts @@ -0,0 +1,16 @@ +import { MutableRefObject, RefCallback, useCallback } from "react"; + +export const useMergedRefs = ( + ...refs: (MutableRefObject | RefCallback)[] +): RefCallback => + useCallback( + (value) => + refs.forEach((ref) => { + if (typeof ref === "function") { + ref(value); + } else { + ref.current = value; + } + }), + refs + ); diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index 13d1860f..a916f681 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -1,5 +1,5 @@ import { SpringRef, TransitionFn, useTransition } from "@react-spring/web"; -import { useDrag, useScroll } from "@use-gesture/react"; +import { EventTypes, Handler, useScroll } from "@use-gesture/react"; import React, { FC, ReactNode, @@ -15,6 +15,7 @@ import { VideoGridProps as Props } from "./VideoGrid"; import { useReactiveState } from "../useReactiveState"; import TinyQueue from "tinyqueue"; import { zipWith } from "lodash"; +import { useMergedRefs } from "../useMergedRefs"; interface Cell { /** @@ -66,8 +67,10 @@ interface TileSpring { interface DragState { tileId: string; - x: number; - y: number; + tileX: number; + tileY: number; + cursorX: number; + cursorY: number; } const dijkstra = (g: Grid): number[] => { @@ -377,7 +380,10 @@ export const NewVideoGrid: FC = ({ }) => { const [slotGrid, setSlotGrid] = useState(null); const [slotGridGeneration, setSlotGridGeneration] = useState(0); - const [gridRef, gridBounds] = useMeasure(); + + const [gridRef1, gridBounds] = useMeasure(); + const gridRef2 = useRef(null); + const gridRef = useMergedRefs(gridRef1, gridRef2); useEffect(() => { if (slotGrid !== null) { @@ -549,23 +555,21 @@ export const NewVideoGrid: FC = ({ }; }, [grid]); - const animateDraggedTile = (endOfGesture: boolean) => - springRef.start((_i, controller) => { - const { tileId, x, y } = dragState.current!; + const animateDraggedTile = (endOfGesture: boolean) => { + const { tileId, tileX, tileY, cursorX, cursorY } = dragState.current!; + const tile = tiles.find((t) => t.item.id === tileId)!; - // react-spring appears to not update a controller's item as long as the - // key remains stable, so we can use it to look up the tile's ID but not - // its position + springRef.start((_i, controller) => { if ((controller.item as Tile).item.id === tileId) { if (endOfGesture) { - const tile = tiles.find((t) => t.item.id === tileId)!; - return { scale: 1, zIndex: 1, shadow: 1, x: tile.x, y: tile.y, + width: tile.width, + height: tile.height, immediate: disableAnimations || ((key) => key === "zIndex"), // Allow the tile's position to settle before pushing its // z-index back down @@ -576,8 +580,8 @@ export const NewVideoGrid: FC = ({ scale: 1.1, zIndex: 2, shadow: 15, - x, - y, + x: tileX, + y: tileY, immediate: disableAnimations || ((key) => key === "zIndex" || key === "x" || key === "y"), @@ -588,41 +592,78 @@ export const NewVideoGrid: FC = ({ } }); - const bindTile = useDrag( - ({ tap, args, delta: [dx, dy], last }) => { - const tileId = args[0] as string; + const overTile = tiles.find( + (t) => + cursorX >= t.x && + cursorX < t.x + t.width && + cursorY >= t.y && + cursorY < t.y + t.height + ); + if (overTile !== undefined && overTile.item.id !== tileId) { + setGrid((g) => ({ + ...g, + cells: g.cells.map((c) => { + if (c?.item === overTile.item) return { ...c, item: tile.item }; + if (c?.item === tile.item) return { ...c, item: overTile.item }; + return c; + }), + })); + } + }; - if (tap) { - setGrid((g) => cycleTileSize(tileId, g)); - } else { - const tileSpring = springRef.current - .find((c) => (c.item as Tile).item.id === tileId)! - .get(); + const onTileDrag = ( + tileId: string, + { + tap, + initial: [initialX, initialY], + delta: [dx, dy], + last, + }: Parameters>[0] + ) => { + if (tap) { + setGrid((g) => cycleTileSize(tileId, g)); + } else { + const tileSpring = springRef.current + .find((c) => (c.item as Tile).item.id === tileId)! + .get(); - if (dragState.current === null) { - dragState.current = { tileId, x: tileSpring.x, y: tileSpring.y }; - } - dragState.current.x += dx; - dragState.current.y += dy; - - animateDraggedTile(last); - - if (last) dragState.current = null; + if (dragState.current === null) { + dragState.current = { + tileId, + tileX: tileSpring.x, + tileY: tileSpring.y, + cursorX: initialX - gridBounds.x, + cursorY: initialY - gridBounds.y + scrollOffset.current, + }; } - }, - { filterTaps: true, pointer: { buttons: [1] } } - ); + dragState.current.tileX += dx; + dragState.current.tileY += dy; + dragState.current.cursorX += dx; + dragState.current.cursorY += dy; + + animateDraggedTile(last); + + if (last) dragState.current = null; + } + }; + + const onTileDragRef = useRef(onTileDrag); + onTileDragRef.current = onTileDrag; const scrollOffset = useRef(0); - const bindGrid = useScroll(({ xy: [, y], delta: [, dy] }) => { - scrollOffset.current = y; + useScroll( + ({ xy: [, y], delta: [, dy] }) => { + scrollOffset.current = y; - if (dragState.current !== null) { - dragState.current.y += dy; - animateDraggedTile(false); - } - }); + if (dragState.current !== null) { + dragState.current.tileY += dy; + dragState.current.cursorY += dy; + animateDraggedTile(false); + } + }, + { target: gridRef2 } + ); const slots = useMemo(() => { const slots = new Array(items.length); @@ -639,7 +680,7 @@ export const NewVideoGrid: FC = ({ } return ( -
+
= ({ > {slots}
- {tileTransitions(({ shadow, width, height, ...style }, tile) => + {tileTransitions((style, tile) => children({ - ...bindTile(tile.item.id), + ...style, key: tile.item.id, - style: { - boxShadow: shadow.to( - (s) => `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px 0px` - ), - "--tileWidth": width.to((w) => `${w}px`), - "--tileHeight": height.to((h) => `${h}px`), - ...style, - }, - width: tile.width, - height: tile.height, + targetWidth: tile.width, + targetHeight: tile.height, item: tile.item, + onDragRef: onTileDragRef, }) )}
diff --git a/src/video-grid/VideoGrid.tsx b/src/video-grid/VideoGrid.tsx index fc6e44d8..2c6cb5b9 100644 --- a/src/video-grid/VideoGrid.tsx +++ b/src/video-grid/VideoGrid.tsx @@ -694,14 +694,17 @@ interface DragTileData { interface ChildrenProperties extends ReactDOMAttributes { key: Key; - style: { - scale: SpringValue; - opacity: SpringValue; - boxShadow: Interpolation; - }; - width: number; - height: number; + targetWidth: number; + targetHeight: number; item: TileDescriptor; + opacity: SpringValue; + scale: SpringValue; + shadow: SpringValue; + zIndex: SpringValue; + x: SpringValue; + y: SpringValue; + width: SpringValue; + height: SpringValue; [index: string]: unknown; } diff --git a/src/video-grid/VideoTile.tsx b/src/video-grid/VideoTile.tsx index 4e04199f..ca9ebbea 100644 --- a/src/video-grid/VideoTile.tsx +++ b/src/video-grid/VideoTile.tsx @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { forwardRef } from "react"; -import { animated } from "@react-spring/web"; +import React, { ForwardedRef, forwardRef } from "react"; +import { animated, SpringValue } from "@react-spring/web"; import classNames from "classnames"; import { useTranslation } from "react-i18next"; @@ -44,9 +44,17 @@ interface Props { showOptions?: boolean; isLocal?: boolean; disableSpeakingIndicator?: boolean; + opacity: SpringValue; + scale: SpringValue; + shadow: SpringValue; + zIndex: SpringValue; + x: SpringValue; + y: SpringValue; + width: SpringValue; + height: SpringValue; } -export const VideoTile = forwardRef( +export const VideoTile = forwardRef( ( { name, @@ -68,6 +76,14 @@ export const VideoTile = forwardRef( isLocal, // TODO: disableSpeakingIndicator is not used atm. disableSpeakingIndicator, + opacity, + scale, + shadow, + zIndex, + x, + y, + width, + height, ...rest }, ref @@ -122,7 +138,19 @@ export const VideoTile = forwardRef( [styles.screenshare]: screenshare, [styles.maximised]: maximised, })} - ref={ref} + style={{ + opacity, + scale, + boxShadow: shadow.to( + (s) => `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px 0px` + ), + zIndex, + x, + y, + "--tileWidth": width.to((w) => `${w}px`), + "--tileHeight": height.to((h) => `${h}px`), + }} + ref={ref as ForwardedRef} {...rest} > {toolbarButtons.length > 0 && !maximised && ( diff --git a/src/video-grid/VideoTileContainer.tsx b/src/video-grid/VideoTileContainer.tsx index 1741ca85..28aec61d 100644 --- a/src/video-grid/VideoTileContainer.tsx +++ b/src/video-grid/VideoTileContainer.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes"; -import React from "react"; +import React, { FC, memo, RefObject } from "react"; import { useCallback } from "react"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; @@ -26,11 +26,13 @@ import { VideoTile } from "./VideoTile"; import { VideoTileSettingsModal } from "./VideoTileSettingsModal"; import { useModalTriggerState } from "../Modal"; import { TileDescriptor } from "./TileDescriptor"; +import { SpringValue } from "@react-spring/web"; +import { EventTypes, Handler, useDrag } from "@use-gesture/react"; interface Props { item: TileDescriptor; - width?: number; - height?: number; + targetWidth: number; + targetHeight: number; getAvatar: ( roomMember: RoomMember, width: number, @@ -42,86 +44,113 @@ interface Props { maximised: boolean; fullscreen: boolean; onFullscreen: (item: TileDescriptor) => void; + opacity: SpringValue; + scale: SpringValue; + shadow: SpringValue; + zIndex: SpringValue; + x: SpringValue; + y: SpringValue; + width: SpringValue; + height: SpringValue; + onDragRef: RefObject< + ( + tileId: string, + state: Parameters>[0] + ) => void + >; } -export function VideoTileContainer({ - item, - width, - height, - getAvatar, - audioContext, - audioDestination, - disableSpeakingIndicator, - maximised, - fullscreen, - onFullscreen, - ...rest -}: Props) { - const { - isLocal, - audioMuted, - videoMuted, - localVolume, - hasAudio, - speaking, - stream, - purpose, - } = useCallFeed(item.callFeed); - const { rawDisplayName } = useRoomMemberName(item.member); - const [tileRef, mediaRef] = useSpatialMediaStream( - stream ?? null, +export const VideoTileContainer: FC = memo( + ({ + item, + targetWidth, + targetHeight, + getAvatar, audioContext, audioDestination, - localVolume, - // The feed is muted if it's local audio (because we don't want our own audio, - // but it's a hook and we can't call it conditionally so we're stuck with it) - // or if there's a maximised feed in which case we always render audio via audio - // elements because we wire it up at the video tile container level and only one - // video tile container is displayed. - isLocal || maximised - ); - const { - modalState: videoTileSettingsModalState, - modalProps: videoTileSettingsModalProps, - } = useModalTriggerState(); - const onOptionsPress = () => { - videoTileSettingsModalState.open(); - }; + disableSpeakingIndicator, + maximised, + fullscreen, + onFullscreen, + onDragRef, + ...rest + }) => { + const { + isLocal, + audioMuted, + videoMuted, + localVolume, + hasAudio, + speaking, + stream, + purpose, + } = useCallFeed(item.callFeed); + const { rawDisplayName } = useRoomMemberName(item.member); - const onFullscreenCallback = useCallback(() => { - onFullscreen(item); - }, [onFullscreen, item]); + const [tileRef, mediaRef] = useSpatialMediaStream( + stream ?? null, + audioContext, + audioDestination, + localVolume, + // The feed is muted if it's local audio (because we don't want our own audio, + // but it's a hook and we can't call it conditionally so we're stuck with it) + // or if there's a maximised feed in which case we always render audio via audio + // elements because we wire it up at the video tile container level and only one + // video tile container is displayed. + isLocal || maximised + ); - // Firefox doesn't respect the disablePictureInPicture attribute - // https://bugzilla.mozilla.org/show_bug.cgi?id=1611831 + useDrag((state) => onDragRef.current!(item.id, state), { + target: tileRef, + filterTaps: true, + pointer: { buttons: [1] }, + }); - return ( - <> - - {videoTileSettingsModalState.isOpen && !maximised && item.callFeed && ( - { + videoTileSettingsModalState.open(); + }; + + const onFullscreenCallback = useCallback(() => { + onFullscreen(item); + }, [onFullscreen, item]); + + // Firefox doesn't respect the disablePictureInPicture attribute + // https://bugzilla.mozilla.org/show_bug.cgi?id=1611831 + + return ( + <> + - )} - - ); -} + {videoTileSettingsModalState.isOpen && !maximised && item.callFeed && ( + + )} + + ); + } +); diff --git a/src/video-grid/useMediaStream.ts b/src/video-grid/useMediaStream.ts index 259ede7b..9eb8e5aa 100644 --- a/src/video-grid/useMediaStream.ts +++ b/src/video-grid/useMediaStream.ts @@ -158,8 +158,8 @@ export const useSpatialMediaStream = ( audioDestination: AudioNode, localVolume: number, mute = false -): [RefObject, RefObject] => { - const tileRef = useRef(null); +): [RefObject, RefObject] => { + const tileRef = useRef(null); const [spatialAudio] = useSpatialAudio(); // This media stream is only used for the video - the audio goes via the audio From 6784d2ba97e7aecc032796beca8cbf09c749adb3 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 1 Feb 2023 11:50:25 -0500 Subject: [PATCH 019/286] Remove redundant key prop --- src/room/InCallView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 88330dcc..a0f7917f 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -319,7 +319,6 @@ export function InCallView({ [x: string]: unknown; }) => ( Date: Wed, 1 Feb 2023 11:50:52 -0500 Subject: [PATCH 020/286] Keep tile elements in a stable order --- src/video-grid/NewVideoGrid.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index a916f681..4a9d3f73 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -470,13 +470,8 @@ export const NewVideoGrid: FC = ({ if (slotGridGeneration !== grid.generation) return prevTiles ?? []; const slotCells = grid.cells.filter((c) => c?.slot) as Cell[]; - return zipWith(slotCells, slotRects, (cell, rect) => ({ - item: cell.item, - x: rect.x, - y: rect.y, - width: rect.width, - height: rect.height, - })); + const tileRects = new Map(zipWith(slotCells, slotRects, (cell, rect) => [cell.item, rect])) + return items.map(item => ({ ...tileRects.get(item)!, item })) }, [slotRects, grid, slotGridGeneration] ); From 42e4f6ce83f0be8589e325df21eedc9a83a5982d Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 3 Feb 2023 08:44:35 -0500 Subject: [PATCH 021/286] Don't allow the grid to overflow horizontally --- src/video-grid/NewVideoGrid.module.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/video-grid/NewVideoGrid.module.css b/src/video-grid/NewVideoGrid.module.css index 27c453ff..a7179f4d 100644 --- a/src/video-grid/NewVideoGrid.module.css +++ b/src/video-grid/NewVideoGrid.module.css @@ -4,6 +4,7 @@ flex-grow: 1; padding: 0 22px; overflow-y: auto; + overflow-x: none; } .slotGrid { From 6cd939db0c2bbdce69913b838d558b684e7dcda3 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 3 Feb 2023 09:11:25 -0500 Subject: [PATCH 022/286] Fix a crash when there's only 1 tile and it gets shrunk --- src/video-grid/NewVideoGrid.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index 4a9d3f73..bf77c918 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -119,7 +119,7 @@ const findLastIndex = ( array: T[], predicate: (item: T) => boolean ): number | null => { - for (let i = array.length - 1; i > 0; i--) { + for (let i = array.length - 1; i >= 0; i--) { if (predicate(array[i])) return i; } From 22382413dcb0227c3863440fc747fe6aee66ea02 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 3 Feb 2023 15:42:47 -0500 Subject: [PATCH 023/286] Make drag and drop mobile-friendly --- src/video-grid/VideoTile.module.css | 1 - src/video-grid/VideoTileContainer.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/video-grid/VideoTile.module.css b/src/video-grid/VideoTile.module.css index 05f3a0ce..543e4fa3 100644 --- a/src/video-grid/VideoTile.module.css +++ b/src/video-grid/VideoTile.module.css @@ -23,7 +23,6 @@ limitations under the License. border-radius: 20px; overflow: hidden; cursor: pointer; - touch-action: none; /* HACK: This has no visual effect due to the short duration, but allows the JS to detect movement via the transform property for audio spatialization */ diff --git a/src/video-grid/VideoTileContainer.tsx b/src/video-grid/VideoTileContainer.tsx index 28aec61d..5bffc6c6 100644 --- a/src/video-grid/VideoTileContainer.tsx +++ b/src/video-grid/VideoTileContainer.tsx @@ -103,7 +103,7 @@ export const VideoTileContainer: FC = memo( useDrag((state) => onDragRef.current!(item.id, state), { target: tileRef, filterTaps: true, - pointer: { buttons: [1] }, + preventScroll: true, }); const { From 1e858f6ba3037d69f03bb8e31316e0073e87cf98 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 3 Feb 2023 16:27:49 -0500 Subject: [PATCH 024/286] Fix a typo --- src/video-grid/NewVideoGrid.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video-grid/NewVideoGrid.module.css b/src/video-grid/NewVideoGrid.module.css index a7179f4d..7d0a8964 100644 --- a/src/video-grid/NewVideoGrid.module.css +++ b/src/video-grid/NewVideoGrid.module.css @@ -4,7 +4,7 @@ flex-grow: 1; padding: 0 22px; overflow-y: auto; - overflow-x: none; + overflow-x: hidden; } .slotGrid { From 206730ffc0b28c4413356b5127a272f0fb762ec7 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 3 Feb 2023 16:52:42 -0500 Subject: [PATCH 025/286] Fix infinite loop when a tile can't be enlarged --- src/video-grid/NewVideoGrid.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index bf77c918..f4b5811d 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -308,6 +308,7 @@ const cycleTileSize = (tileId: string, g: Grid): Grid => { const nextScanLocations = new Set([from]); const scanColumnOffset = Math.floor((toWidth - 1) / 2); const scanRowOffset = Math.floor((toHeight - 1) / 2); + const rows = row(g.cells.length - 1, g) + 1 let to: number | null = null; const displaceable = (c: Cell | undefined, i: number): boolean => @@ -319,6 +320,7 @@ const cycleTileSize = (tileId: string, g: Grid): Grid => { const start = scanLocation - scanColumnOffset - g.columns * scanRowOffset; const end = areaEnd(start, candidateWidth, candidateHeight, g); const startColumn = column(start, g); + const startRow = row(start, g) const endColumn = column(end, g); if ( @@ -334,8 +336,8 @@ const cycleTileSize = (tileId: string, g: Grid): Grid => { if (startColumn > 0) nextScanLocations.add(scanLocation - 1); if (endColumn < g.columns - 1) nextScanLocations.add(scanLocation + 1); - nextScanLocations.add(scanLocation - g.columns); - nextScanLocations.add(scanLocation + g.columns); + if (startRow > 0) nextScanLocations.add(scanLocation - g.columns); + if (startRow <= rows) nextScanLocations.add(scanLocation + g.columns); } // TODO: Don't give up on placing the tile yet From 6adcf95aaa08b2148c5dbd7aaa99da09ffa89ac7 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 4 Feb 2023 00:43:53 -0500 Subject: [PATCH 026/286] Implement different column counts and mobile layout --- src/room/InCallView.module.css | 32 ++++++++++---- src/video-grid/NewVideoGrid.module.css | 21 ++++++--- src/video-grid/NewVideoGrid.tsx | 60 ++++++++++++++++++-------- src/video-grid/VideoTile.module.css | 8 +++- 4 files changed, 90 insertions(+), 31 deletions(-) diff --git a/src/room/InCallView.module.css b/src/room/InCallView.module.css index 310cf47b..04228aa9 100644 --- a/src/room/InCallView.module.css +++ b/src/room/InCallView.module.css @@ -20,9 +20,10 @@ limitations under the License. flex-direction: column; overflow: hidden; min-height: 100%; - position: fixed; height: 100%; width: 100%; + --footerPadding: 8px; + --footerHeight: calc(50px + 2 * var(--footerPadding)); } .centerMessage { @@ -39,12 +40,27 @@ limitations under the License. } .footer { - position: relative; - flex-shrink: 0; + position: absolute; + left: 0; + bottom: 0; + width: 100%; display: flex; justify-content: center; align-items: center; - height: calc(50px + 2 * 8px); + padding: var(--footerPadding) 0; + /* TODO: Un-hardcode these colors */ + background: linear-gradient( + 360deg, + #15191e 0%, + rgba(21, 25, 30, 0.9) 37%, + rgba(21, 25, 30, 0.8) 49.68%, + rgba(21, 25, 30, 0.7) 56.68%, + rgba(21, 25, 30, 0.427397) 72.92%, + rgba(21, 25, 30, 0.257534) 81.06%, + rgba(21, 25, 30, 0.136986) 87.29%, + rgba(21, 25, 30, 0.0658079) 92.4%, + rgba(21, 25, 30, 0) 100% + ); } .footer > * { @@ -75,13 +91,13 @@ limitations under the License. } @media (min-height: 300px) { - .footer { - height: calc(50px + 2 * 24px); + .inRoom { + --footerPadding: 24px; } } @media (min-width: 800px) { - .footer { - height: calc(50px + 2 * 32px); + .inRoom { + --footerPadding: 32px; } } diff --git a/src/video-grid/NewVideoGrid.module.css b/src/video-grid/NewVideoGrid.module.css index 7d0a8964..e6f64ba9 100644 --- a/src/video-grid/NewVideoGrid.module.css +++ b/src/video-grid/NewVideoGrid.module.css @@ -2,20 +2,31 @@ contain: strict; position: relative; flex-grow: 1; - padding: 0 22px; + padding: 0 20px; overflow-y: auto; overflow-x: hidden; } .slotGrid { - contain: strict; position: relative; display: grid; - grid-auto-rows: 183px; - column-gap: 18px; - row-gap: 21px; + grid-auto-rows: 163px; + gap: 8px; + padding-bottom: var(--footerHeight); } .slot { contain: strict; } + +@media (min-width: 800px) { + .grid { + padding: 0 22px; + } + + .slotGrid { + grid-auto-rows: 183px; + column-gap: 18px; + row-gap: 21px; + } +} diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index f4b5811d..b4d13fc0 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -290,7 +290,9 @@ const cycleTileSize = (tileId: string, g: Grid): Grid => { const fromEnd = areaEnd(from, fromWidth, fromHeight, g); const [toWidth, toHeight] = - fromWidth === 1 && fromHeight === 1 ? [3, 2] : [1, 1]; + fromWidth === 1 && fromHeight === 1 + ? [Math.min(3, Math.max(2, g.columns - 1)), 2] + : [1, 1]; const newRows = Math.max( 0, Math.ceil((toWidth * toHeight - fromWidth * fromHeight) / g.columns) @@ -308,7 +310,7 @@ const cycleTileSize = (tileId: string, g: Grid): Grid => { const nextScanLocations = new Set([from]); const scanColumnOffset = Math.floor((toWidth - 1) / 2); const scanRowOffset = Math.floor((toHeight - 1) / 2); - const rows = row(g.cells.length - 1, g) + 1 + const rows = row(g.cells.length - 1, g) + 1; let to: number | null = null; const displaceable = (c: Cell | undefined, i: number): boolean => @@ -320,7 +322,7 @@ const cycleTileSize = (tileId: string, g: Grid): Grid => { const start = scanLocation - scanColumnOffset - g.columns * scanRowOffset; const end = areaEnd(start, candidateWidth, candidateHeight, g); const startColumn = column(start, g); - const startRow = row(start, g) + const startRow = row(start, g); const endColumn = column(end, g); if ( @@ -424,8 +426,30 @@ export const NewVideoGrid: FC = ({ return rects; }, [items, slotGridGeneration, slotGrid, gridBounds]); - const [grid, setGrid] = useReactiveState( - (prevGrid = { generation: 0, columns: 6, cells: [] }) => { + const [columns] = useReactiveState( + // Since grid resizing isn't implemented yet, pick a column count on mount + // and stick to it + (prevColumns) => + prevColumns !== undefined && prevColumns !== null + ? prevColumns + : // The grid bounds might not be known yet + gridBounds.width === 0 + ? null + : Math.max(2, Math.floor(gridBounds.width * 0.0045)), + [gridBounds] + ); + + const [grid, setGrid] = useReactiveState( + (prevGrid = null) => { + if (prevGrid === null) { + // We can't do anything if the column count isn't known yet + if (columns === null) { + return null; + } else { + prevGrid = { generation: slotGridGeneration, columns, cells: [] }; + } + } + // Step 1: Update tiles that still exist, and remove tiles that have left // the grid const itemsById = new Map(items.map((i) => [i.id, i])); @@ -462,18 +486,20 @@ export const NewVideoGrid: FC = ({ return grid3; }, - [items] + [items, columns] ); const [tiles] = useReactiveState( (prevTiles) => { // If React hasn't yet rendered the current generation of the layout, skip // the update, because grid and slotRects will be out of sync - if (slotGridGeneration !== grid.generation) return prevTiles ?? []; + if (slotGridGeneration !== grid?.generation) return prevTiles ?? []; const slotCells = grid.cells.filter((c) => c?.slot) as Cell[]; - const tileRects = new Map(zipWith(slotCells, slotRects, (cell, rect) => [cell.item, rect])) - return items.map(item => ({ ...tileRects.get(item)!, item })) + const tileRects = new Map( + zipWith(slotCells, slotRects, (cell, rect) => [cell.item, rect]) + ); + return items.map((item) => ({ ...tileRects.get(item)!, item })); }, [slotRects, grid, slotGridGeneration] ); @@ -516,7 +542,7 @@ export const NewVideoGrid: FC = ({ ) as unknown as [TransitionFn, SpringRef]; const slotGridStyle = useMemo(() => { - const columnCount = 6; + if (grid === null) return {}; const areas = new Array<(number | null)[]>( Math.ceil(grid.cells.length / grid.columns) @@ -548,9 +574,9 @@ export const NewVideoGrid: FC = ({ .join(" ")}'` ) .join(" "), - gridTemplateColumns: `repeat(${columnCount}, 1fr)`, + gridTemplateColumns: `repeat(${columns}, 1fr)`, }; - }, [grid]); + }, [grid, columns]); const animateDraggedTile = (endOfGesture: boolean) => { const { tileId, tileX, tileY, cursorX, cursorY } = dragState.current!; @@ -598,8 +624,8 @@ export const NewVideoGrid: FC = ({ ); if (overTile !== undefined && overTile.item.id !== tileId) { setGrid((g) => ({ - ...g, - cells: g.cells.map((c) => { + ...g!, + cells: g!.cells.map((c) => { if (c?.item === overTile.item) return { ...c, item: tile.item }; if (c?.item === tile.item) return { ...c, item: overTile.item }; return c; @@ -618,7 +644,7 @@ export const NewVideoGrid: FC = ({ }: Parameters>[0] ) => { if (tap) { - setGrid((g) => cycleTileSize(tileId, g)); + setGrid((g) => cycleTileSize(tileId, g!)); } else { const tileSpring = springRef.current .find((c) => (c.item as Tile).item.id === tileId)! @@ -671,8 +697,8 @@ export const NewVideoGrid: FC = ({ return slots; }, [items.length]); - // Render nothing if the bounds are not yet known - if (gridBounds.width === 0) { + // Render nothing if the grid has yet to be generated + if (grid === null) { return
; } diff --git a/src/video-grid/VideoTile.module.css b/src/video-grid/VideoTile.module.css index 543e4fa3..ce44d225 100644 --- a/src/video-grid/VideoTile.module.css +++ b/src/video-grid/VideoTile.module.css @@ -20,7 +20,7 @@ limitations under the License. top: 0; width: var(--tileWidth); height: var(--tileHeight); - border-radius: 20px; + border-radius: 8px; overflow: hidden; cursor: pointer; @@ -174,3 +174,9 @@ limitations under the License. max-width: 360px; border-radius: 20px; } + +@media (min-width: 800px) { + .videoTile { + border-radius: 20px; + } +} From 82ac775124e927bf0e04481706ab3335d4ab7fb5 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 5 Feb 2023 00:55:12 -0500 Subject: [PATCH 027/286] Fix scrolling on mobile --- src/video-grid/VideoTile.module.css | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/video-grid/VideoTile.module.css b/src/video-grid/VideoTile.module.css index ce44d225..8da1f760 100644 --- a/src/video-grid/VideoTile.module.css +++ b/src/video-grid/VideoTile.module.css @@ -30,9 +30,6 @@ limitations under the License. } .videoTile * { - touch-action: none; - -moz-user-select: none; - -webkit-user-drag: none; user-select: none; } From 374c68e3c053cd103e849d6db3cc57b990de31be Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 5 Feb 2023 01:17:28 -0500 Subject: [PATCH 028/286] Fix tiles enlarging to the wrong place on mobile --- src/video-grid/NewVideoGrid.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index b4d13fc0..d22ad6e2 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -130,7 +130,8 @@ const findLast1By1Index = (g: Grid): number | null => findLastIndex(g.cells, (c) => c?.rows === 1 && c?.columns === 1); const row = (index: number, g: Grid): number => Math.floor(index / g.columns); -const column = (index: number, g: Grid): number => index % g.columns; +const column = (index: number, g: Grid): number => + ((index % g.columns) + g.columns) % g.columns; const inArea = ( index: number, From 978b0f08e8f2ccd7d7eb7d599afc7b77fb4873e8 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 7 Feb 2023 22:13:50 -0500 Subject: [PATCH 029/286] Move grid algorithms into a separate file --- src/video-grid/NewVideoGrid.tsx | 357 +++----------------------------- src/video-grid/model.ts | 348 +++++++++++++++++++++++++++++++ 2 files changed, 373 insertions(+), 332 deletions(-) create mode 100644 src/video-grid/model.ts diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index d22ad6e2..2c25f62d 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -1,3 +1,19 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import { SpringRef, TransitionFn, useTransition } from "@react-spring/web"; import { EventTypes, Handler, useScroll } from "@use-gesture/react"; import React, { @@ -13,35 +29,17 @@ import styles from "./NewVideoGrid.module.css"; import { TileDescriptor } from "./TileDescriptor"; import { VideoGridProps as Props } from "./VideoGrid"; import { useReactiveState } from "../useReactiveState"; -import TinyQueue from "tinyqueue"; import { zipWith } from "lodash"; import { useMergedRefs } from "../useMergedRefs"; - -interface Cell { - /** - * The item held by the slot containing this cell. - */ - item: TileDescriptor; - /** - * Whether this cell is the first cell of the containing slot. - */ - // TODO: Rename to 'start'? - slot: boolean; - /** - * The width, in columns, of the containing slot. - */ - columns: number; - /** - * The height, in rows, of the containing slot. - */ - rows: number; -} - -interface Grid { - generation: number; - columns: number; - cells: (Cell | undefined)[]; -} +import { + Grid, + Cell, + row, + column, + fillGaps, + forEachCellInArea, + cycleTileSize, +} from "./model"; interface Rect { x: number; @@ -73,311 +71,6 @@ interface DragState { cursorY: number; } -const dijkstra = (g: Grid): number[] => { - const end = findLast1By1Index(g) ?? 0; - const endRow = row(end, g); - const endColumn = column(end, g); - - const distances = new Array(end + 1).fill(Infinity); - distances[end] = 0; - const edges = new Array(end).fill(undefined); - const heap = new TinyQueue([end], (i) => distances[i]); - - const visit = (curr: number, via: number) => { - const viaCell = g.cells[via]; - const viaLargeSlot = - viaCell !== undefined && (viaCell.rows > 1 || viaCell.columns > 1); - const distanceVia = distances[via] + (viaLargeSlot ? 4 : 1); - - if (distanceVia < distances[curr]) { - distances[curr] = distanceVia; - edges[curr] = via; - heap.push(curr); - } - }; - - while (heap.length > 0) { - const via = heap.pop()!; - const viaRow = row(via, g); - const viaColumn = column(via, g); - - if (viaRow > 0) visit(via - g.columns, via); - if (viaColumn > 0) visit(via - 1, via); - if (viaColumn < (viaRow === endRow ? endColumn : g.columns - 1)) - visit(via + 1, via); - if ( - viaRow < endRow - 1 || - (viaRow === endRow - 1 && viaColumn <= endColumn) - ) - visit(via + g.columns, via); - } - - return edges as number[]; -}; - -const findLastIndex = ( - array: T[], - predicate: (item: T) => boolean -): number | null => { - for (let i = array.length - 1; i >= 0; i--) { - if (predicate(array[i])) return i; - } - - return null; -}; - -const findLast1By1Index = (g: Grid): number | null => - findLastIndex(g.cells, (c) => c?.rows === 1 && c?.columns === 1); - -const row = (index: number, g: Grid): number => Math.floor(index / g.columns); -const column = (index: number, g: Grid): number => - ((index % g.columns) + g.columns) % g.columns; - -const inArea = ( - index: number, - start: number, - end: number, - g: Grid -): boolean => { - const indexColumn = column(index, g); - const indexRow = row(index, g); - return ( - indexRow >= row(start, g) && - indexRow <= row(end, g) && - indexColumn >= column(start, g) && - indexColumn <= column(end, g) - ); -}; - -function* cellsInArea( - start: number, - end: number, - g: Grid -): Generator { - const startColumn = column(start, g); - const endColumn = column(end, g); - for ( - let i = start; - i <= end; - i = - column(i, g) === endColumn - ? i + g.columns + startColumn - endColumn - : i + 1 - ) - yield i; -} - -const forEachCellInArea = ( - start: number, - end: number, - g: Grid, - fn: (c: Cell | undefined, i: number) => void -) => { - for (const i of cellsInArea(start, end, g)) fn(g.cells[i], i); -}; - -const allCellsInArea = ( - start: number, - end: number, - g: Grid, - fn: (c: Cell | undefined, i: number) => boolean -) => { - for (const i of cellsInArea(start, end, g)) { - if (!fn(g.cells[i], i)) return false; - } - - return true; -}; - -const areaEnd = ( - start: number, - columns: number, - rows: number, - g: Grid -): number => start + columns - 1 + g.columns * (rows - 1); - -/** - * Gets the index of the next gap in the grid that should be backfilled by 1×1 - * tiles. - */ -const getNextGap = (g: Grid): number | null => { - const last1By1Index = findLast1By1Index(g); - if (last1By1Index === null) return null; - - for (let i = 0; i < last1By1Index; i++) { - // To make the backfilling process look natural when there are multiple - // gaps, we actually scan each row from right to left - const j = - (row(i, g) === row(last1By1Index, g) - ? last1By1Index - : (row(i, g) + 1) * g.columns) - - 1 - - column(i, g); - - if (g.cells[j] === undefined) return j; - } - - return null; -}; - -const fillGaps = (g: Grid): Grid => { - const result: Grid = { ...g, cells: [...g.cells] }; - let gap = getNextGap(result); - - if (gap !== null) { - const pathToEnd = dijkstra(result); - - do { - let filled = false; - let to = gap; - let from: number | undefined = pathToEnd[gap]; - - // First, attempt to fill the gap by moving 1×1 tiles backwards from the - // end of the grid along a set path - while (from !== undefined) { - const toCell = result.cells[to]; - const fromCell = result.cells[from]; - - // Skip over large tiles - if (toCell !== undefined) { - to = pathToEnd[to]; - // Skip over large tiles. Also, we might run into gaps along the path - // created during the filling of previous gaps. Skip over those too; - // they'll be picked up on the next iteration of the outer loop. - } else if ( - fromCell === undefined || - fromCell.rows > 1 || - fromCell.columns > 1 - ) { - from = pathToEnd[from]; - } else { - result.cells[to] = result.cells[from]; - result.cells[from] = undefined; - filled = true; - to = pathToEnd[to]; - from = pathToEnd[from]; - } - } - - // In case the path approach failed, fall back to taking the very last 1×1 - // tile, and just dropping it into place - if (!filled) { - const last1By1Index = findLast1By1Index(result)!; - result.cells[gap] = result.cells[last1By1Index]; - result.cells[last1By1Index] = undefined; - } - - gap = getNextGap(result); - } while (gap !== null); - } - - // TODO: If there are any large tiles on the last row, shuffle them back - // upwards into a full row - - // Shrink the array to remove trailing gaps - const finalLength = - (findLastIndex(result.cells, (c) => c !== undefined) ?? -1) + 1; - if (finalLength < result.cells.length) - result.cells = result.cells.slice(0, finalLength); - - return result; -}; - -const cycleTileSize = (tileId: string, g: Grid): Grid => { - const from = g.cells.findIndex((c) => c?.item.id === tileId); - if (from === -1) return g; // Tile removed, no change - const fromWidth = g.cells[from]!.columns; - const fromHeight = g.cells[from]!.rows; - const fromEnd = areaEnd(from, fromWidth, fromHeight, g); - - const [toWidth, toHeight] = - fromWidth === 1 && fromHeight === 1 - ? [Math.min(3, Math.max(2, g.columns - 1)), 2] - : [1, 1]; - const newRows = Math.max( - 0, - Math.ceil((toWidth * toHeight - fromWidth * fromHeight) / g.columns) - ); - - const candidateWidth = toWidth; - const candidateHeight = toHeight - newRows; - - const gappyGrid: Grid = { - ...g, - generation: g.generation + 1, - cells: new Array(g.cells.length + newRows * g.columns), - }; - - const nextScanLocations = new Set([from]); - const scanColumnOffset = Math.floor((toWidth - 1) / 2); - const scanRowOffset = Math.floor((toHeight - 1) / 2); - const rows = row(g.cells.length - 1, g) + 1; - let to: number | null = null; - - const displaceable = (c: Cell | undefined, i: number): boolean => - c === undefined || - (c.columns === 1 && c.rows === 1) || - inArea(i, from, fromEnd, g); - - for (const scanLocation of nextScanLocations) { - const start = scanLocation - scanColumnOffset - g.columns * scanRowOffset; - const end = areaEnd(start, candidateWidth, candidateHeight, g); - const startColumn = column(start, g); - const startRow = row(start, g); - const endColumn = column(end, g); - - if ( - start >= 0 && - end < gappyGrid.cells.length && - endColumn - startColumn + 1 === candidateWidth - ) { - if (allCellsInArea(start, end, g, displaceable)) { - to = start; - break; - } - } - - if (startColumn > 0) nextScanLocations.add(scanLocation - 1); - if (endColumn < g.columns - 1) nextScanLocations.add(scanLocation + 1); - if (startRow > 0) nextScanLocations.add(scanLocation - g.columns); - if (startRow <= rows) nextScanLocations.add(scanLocation + g.columns); - } - - // TODO: Don't give up on placing the tile yet - if (to === null) return g; - - const toRow = row(to, g); - - g.cells.forEach((c, src) => { - if (c?.slot && c.item.id !== tileId) { - const offset = - row(src, g) > toRow + candidateHeight - 1 ? g.columns * newRows : 0; - forEachCellInArea(src, areaEnd(src, c.columns, c.rows, g), g, (c, i) => { - gappyGrid.cells[i + offset] = c; - }); - } - }); - - const displacedTiles: Cell[] = []; - const toEnd = areaEnd(to, toWidth, toHeight, g); - forEachCellInArea(to, toEnd, gappyGrid, (c, i) => { - if (c !== undefined) displacedTiles.push(c); - gappyGrid.cells[i] = { - item: g.cells[from]!.item, - slot: i === to, - columns: toWidth, - rows: toHeight, - }; - }); - - for (let i = 0; displacedTiles.length > 0; i++) { - if (gappyGrid.cells[i] === undefined) - gappyGrid.cells[i] = displacedTiles.shift(); - } - - return fillGaps(gappyGrid); -}; - export const NewVideoGrid: FC = ({ items, disableAnimations, diff --git a/src/video-grid/model.ts b/src/video-grid/model.ts new file mode 100644 index 00000000..0a4136c6 --- /dev/null +++ b/src/video-grid/model.ts @@ -0,0 +1,348 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import TinyQueue from "tinyqueue"; +import { TileDescriptor } from "./TileDescriptor"; + +export interface Cell { + /** + * The item held by the slot containing this cell. + */ + item: TileDescriptor; + /** + * Whether this cell is the first cell of the containing slot. + */ + // TODO: Rename to 'start'? + slot: boolean; + /** + * The width, in columns, of the containing slot. + */ + columns: number; + /** + * The height, in rows, of the containing slot. + */ + rows: number; +} + +export interface Grid { + generation: number; + columns: number; + cells: (Cell | undefined)[]; +} + +export function dijkstra(g: Grid): number[] { + const end = findLast1By1Index(g) ?? 0; + const endRow = row(end, g); + const endColumn = column(end, g); + + const distances = new Array(end + 1).fill(Infinity); + distances[end] = 0; + const edges = new Array(end).fill(undefined); + const heap = new TinyQueue([end], (i) => distances[i]); + + const visit = (curr: number, via: number) => { + const viaCell = g.cells[via]; + const viaLargeSlot = + viaCell !== undefined && (viaCell.rows > 1 || viaCell.columns > 1); + const distanceVia = distances[via] + (viaLargeSlot ? 4 : 1); + + if (distanceVia < distances[curr]) { + distances[curr] = distanceVia; + edges[curr] = via; + heap.push(curr); + } + }; + + while (heap.length > 0) { + const via = heap.pop()!; + const viaRow = row(via, g); + const viaColumn = column(via, g); + + if (viaRow > 0) visit(via - g.columns, via); + if (viaColumn > 0) visit(via - 1, via); + if (viaColumn < (viaRow === endRow ? endColumn : g.columns - 1)) + visit(via + 1, via); + if ( + viaRow < endRow - 1 || + (viaRow === endRow - 1 && viaColumn <= endColumn) + ) + visit(via + g.columns, via); + } + + return edges as number[]; +} + +function findLastIndex( + array: T[], + predicate: (item: T) => boolean +): number | null { + for (let i = array.length - 1; i >= 0; i--) { + if (predicate(array[i])) return i; + } + + return null; +} + +const findLast1By1Index = (g: Grid): number | null => + findLastIndex(g.cells, (c) => c?.rows === 1 && c?.columns === 1); + +export function row(index: number, g: Grid): number { + return Math.floor(index / g.columns); +} + +export function column(index: number, g: Grid): number { + return ((index % g.columns) + g.columns) % g.columns; +} + +function inArea(index: number, start: number, end: number, g: Grid): boolean { + const indexColumn = column(index, g); + const indexRow = row(index, g); + return ( + indexRow >= row(start, g) && + indexRow <= row(end, g) && + indexColumn >= column(start, g) && + indexColumn <= column(end, g) + ); +} + +function* cellsInArea( + start: number, + end: number, + g: Grid +): Generator { + const startColumn = column(start, g); + const endColumn = column(end, g); + for ( + let i = start; + i <= end; + i = + column(i, g) === endColumn + ? i + g.columns + startColumn - endColumn + : i + 1 + ) + yield i; +} + +export function forEachCellInArea( + start: number, + end: number, + g: Grid, + fn: (c: Cell | undefined, i: number) => void +): void { + for (const i of cellsInArea(start, end, g)) fn(g.cells[i], i); +} + +function allCellsInArea( + start: number, + end: number, + g: Grid, + fn: (c: Cell | undefined, i: number) => boolean +): boolean { + for (const i of cellsInArea(start, end, g)) { + if (!fn(g.cells[i], i)) return false; + } + + return true; +} + +const areaEnd = ( + start: number, + columns: number, + rows: number, + g: Grid +): number => start + columns - 1 + g.columns * (rows - 1); + +/** + * Gets the index of the next gap in the grid that should be backfilled by 1×1 + * tiles. + */ +function getNextGap(g: Grid): number | null { + const last1By1Index = findLast1By1Index(g); + if (last1By1Index === null) return null; + + for (let i = 0; i < last1By1Index; i++) { + // To make the backfilling process look natural when there are multiple + // gaps, we actually scan each row from right to left + const j = + (row(i, g) === row(last1By1Index, g) + ? last1By1Index + : (row(i, g) + 1) * g.columns) - + 1 - + column(i, g); + + if (g.cells[j] === undefined) return j; + } + + return null; +} + +export function fillGaps(g: Grid): Grid { + const result: Grid = { ...g, cells: [...g.cells] }; + let gap = getNextGap(result); + + if (gap !== null) { + const pathToEnd = dijkstra(result); + + do { + let filled = false; + let to = gap; + let from: number | undefined = pathToEnd[gap]; + + // First, attempt to fill the gap by moving 1×1 tiles backwards from the + // end of the grid along a set path + while (from !== undefined) { + const toCell = result.cells[to]; + const fromCell = result.cells[from]; + + // Skip over large tiles + if (toCell !== undefined) { + to = pathToEnd[to]; + // Skip over large tiles. Also, we might run into gaps along the path + // created during the filling of previous gaps. Skip over those too; + // they'll be picked up on the next iteration of the outer loop. + } else if ( + fromCell === undefined || + fromCell.rows > 1 || + fromCell.columns > 1 + ) { + from = pathToEnd[from]; + } else { + result.cells[to] = result.cells[from]; + result.cells[from] = undefined; + filled = true; + to = pathToEnd[to]; + from = pathToEnd[from]; + } + } + + // In case the path approach failed, fall back to taking the very last 1×1 + // tile, and just dropping it into place + if (!filled) { + const last1By1Index = findLast1By1Index(result)!; + result.cells[gap] = result.cells[last1By1Index]; + result.cells[last1By1Index] = undefined; + } + + gap = getNextGap(result); + } while (gap !== null); + } + + // TODO: If there are any large tiles on the last row, shuffle them back + // upwards into a full row + + // Shrink the array to remove trailing gaps + const finalLength = + (findLastIndex(result.cells, (c) => c !== undefined) ?? -1) + 1; + if (finalLength < result.cells.length) + result.cells = result.cells.slice(0, finalLength); + + return result; +} + +export function cycleTileSize(tileId: string, g: Grid): Grid { + const from = g.cells.findIndex((c) => c?.item.id === tileId); + if (from === -1) return g; // Tile removed, no change + const fromWidth = g.cells[from]!.columns; + const fromHeight = g.cells[from]!.rows; + const fromEnd = areaEnd(from, fromWidth, fromHeight, g); + + const [toWidth, toHeight] = + fromWidth === 1 && fromHeight === 1 + ? [Math.min(3, Math.max(2, g.columns - 1)), 2] + : [1, 1]; + const newRows = Math.max( + 0, + Math.ceil((toWidth * toHeight - fromWidth * fromHeight) / g.columns) + ); + + const candidateWidth = toWidth; + const candidateHeight = toHeight - newRows; + + const gappyGrid: Grid = { + ...g, + generation: g.generation + 1, + cells: new Array(g.cells.length + newRows * g.columns), + }; + + const nextScanLocations = new Set([from]); + const scanColumnOffset = Math.floor((toWidth - 1) / 2); + const scanRowOffset = Math.floor((toHeight - 1) / 2); + const rows = row(g.cells.length - 1, g) + 1; + let to: number | null = null; + + const displaceable = (c: Cell | undefined, i: number): boolean => + c === undefined || + (c.columns === 1 && c.rows === 1) || + inArea(i, from, fromEnd, g); + + for (const scanLocation of nextScanLocations) { + const start = scanLocation - scanColumnOffset - g.columns * scanRowOffset; + const end = areaEnd(start, candidateWidth, candidateHeight, g); + const startColumn = column(start, g); + const startRow = row(start, g); + const endColumn = column(end, g); + + if ( + start >= 0 && + end < gappyGrid.cells.length && + endColumn - startColumn + 1 === candidateWidth + ) { + if (allCellsInArea(start, end, g, displaceable)) { + to = start; + break; + } + } + + if (startColumn > 0) nextScanLocations.add(scanLocation - 1); + if (endColumn < g.columns - 1) nextScanLocations.add(scanLocation + 1); + if (startRow > 0) nextScanLocations.add(scanLocation - g.columns); + if (startRow <= rows) nextScanLocations.add(scanLocation + g.columns); + } + + // TODO: Don't give up on placing the tile yet + if (to === null) return g; + + const toRow = row(to, g); + + g.cells.forEach((c, src) => { + if (c?.slot && c.item.id !== tileId) { + const offset = + row(src, g) > toRow + candidateHeight - 1 ? g.columns * newRows : 0; + forEachCellInArea(src, areaEnd(src, c.columns, c.rows, g), g, (c, i) => { + gappyGrid.cells[i + offset] = c; + }); + } + }); + + const displacedTiles: Cell[] = []; + const toEnd = areaEnd(to, toWidth, toHeight, g); + forEachCellInArea(to, toEnd, gappyGrid, (c, i) => { + if (c !== undefined) displacedTiles.push(c); + gappyGrid.cells[i] = { + item: g.cells[from]!.item, + slot: i === to, + columns: toWidth, + rows: toHeight, + }; + }); + + for (let i = 0; displacedTiles.length > 0; i++) { + if (gappyGrid.cells[i] === undefined) + gappyGrid.cells[i] = displacedTiles.shift(); + } + + return fillGaps(gappyGrid); +} From 8d46687a54b7fafb10cfd224e191188bce62ca53 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 7 Feb 2023 23:27:49 -0500 Subject: [PATCH 030/286] Refactor grid state tracking --- src/video-grid/NewVideoGrid.tsx | 133 +++++++++++++++++++------------- src/video-grid/model.ts | 43 ++++++++--- 2 files changed, 112 insertions(+), 64 deletions(-) diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index 2c25f62d..aba499ea 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -17,8 +17,11 @@ limitations under the License. import { SpringRef, TransitionFn, useTransition } from "@react-spring/web"; import { EventTypes, Handler, useScroll } from "@use-gesture/react"; import React, { + Dispatch, FC, ReactNode, + SetStateAction, + useCallback, useEffect, useMemo, useRef, @@ -39,8 +42,82 @@ import { fillGaps, forEachCellInArea, cycleTileSize, + appendItems, } from "./model"; +interface GridState extends Grid { + /** + * The ID of the current state of the grid. + */ + generation: number; +} + +const useGridState = ( + columns: number | null, + items: TileDescriptor[] +): [GridState | null, Dispatch>] => { + const [grid, setGrid_] = useReactiveState( + (prevGrid = null) => { + if (prevGrid === null) { + // We can't do anything if the column count isn't known yet + if (columns === null) { + return null; + } else { + prevGrid = { generation: 0, columns, cells: [] }; + } + } + + // Step 1: Update tiles that still exist, and remove tiles that have left + // the grid + const itemsById = new Map(items.map((i) => [i.id, i])); + const grid1: Grid = { + ...prevGrid, + cells: prevGrid.cells.map((c) => { + if (c === undefined) return undefined; + const item = itemsById.get(c.item.id); + return item === undefined ? undefined : { ...c, item }; + }), + }; + + // Step 2: Backfill gaps left behind by removed tiles + const grid2 = fillGaps(grid1); + + // Step 3: Add new tiles to the end of the grid + const existingItemIds = new Set( + grid2.cells.filter((c) => c !== undefined).map((c) => c!.item.id) + ); + const newItems = items.filter((i) => !existingItemIds.has(i.id)); + const grid3 = appendItems(newItems, grid2); + + return { ...grid3, generation: prevGrid.generation + 1 }; + }, + [columns, items] + ); + + const setGrid: Dispatch> = useCallback( + (action) => { + if (typeof action === "function") { + setGrid_((prevGrid) => + prevGrid === null + ? null + : { + ...(action as (prev: Grid) => Grid)(prevGrid), + generation: prevGrid.generation + 1, + } + ); + } else { + setGrid_((prevGrid) => ({ + ...action, + generation: prevGrid?.generation ?? 1, + })); + } + }, + [setGrid_] + ); + + return [grid, setGrid]; +}; + interface Rect { x: number; y: number; @@ -133,55 +210,7 @@ export const NewVideoGrid: FC = ({ [gridBounds] ); - const [grid, setGrid] = useReactiveState( - (prevGrid = null) => { - if (prevGrid === null) { - // We can't do anything if the column count isn't known yet - if (columns === null) { - return null; - } else { - prevGrid = { generation: slotGridGeneration, columns, cells: [] }; - } - } - - // Step 1: Update tiles that still exist, and remove tiles that have left - // the grid - const itemsById = new Map(items.map((i) => [i.id, i])); - const grid1: Grid = { - ...prevGrid, - generation: prevGrid.generation + 1, - cells: prevGrid.cells.map((c) => { - if (c === undefined) return undefined; - const item = itemsById.get(c.item.id); - return item === undefined ? undefined : { ...c, item }; - }), - }; - - // Step 2: Backfill gaps left behind by removed tiles - const grid2 = fillGaps(grid1); - - // Step 3: Add new tiles to the end of the grid - const existingItemIds = new Set( - grid2.cells.filter((c) => c !== undefined).map((c) => c!.item.id) - ); - const newItems = items.filter((i) => !existingItemIds.has(i.id)); - const grid3: Grid = { - ...grid2, - cells: [ - ...grid2.cells, - ...newItems.map((i) => ({ - item: i, - slot: true, - columns: 1, - rows: 1, - })), - ], - }; - - return grid3; - }, - [items, columns] - ); + const [grid, setGrid] = useGridState(columns, items); const [tiles] = useReactiveState( (prevTiles) => { @@ -189,9 +218,9 @@ export const NewVideoGrid: FC = ({ // the update, because grid and slotRects will be out of sync if (slotGridGeneration !== grid?.generation) return prevTiles ?? []; - const slotCells = grid.cells.filter((c) => c?.slot) as Cell[]; + const tileCells = grid.cells.filter((c) => c?.origin) as Cell[]; const tileRects = new Map( - zipWith(slotCells, slotRects, (cell, rect) => [cell.item, rect]) + zipWith(tileCells, slotRects, (cell, rect) => [cell.item, rect]) ); return items.map((item) => ({ ...tileRects.get(item)!, item })); }, @@ -247,7 +276,7 @@ export const NewVideoGrid: FC = ({ let slotId = 0; for (let i = 0; i < grid.cells.length; i++) { const cell = grid.cells[i]; - if (cell?.slot) { + if (cell?.origin) { const slotEnd = i + cell.columns - 1 + grid.columns * (cell.rows - 1); forEachCellInArea( i, diff --git a/src/video-grid/model.ts b/src/video-grid/model.ts index 0a4136c6..54f3c817 100644 --- a/src/video-grid/model.ts +++ b/src/video-grid/model.ts @@ -17,29 +17,34 @@ limitations under the License. import TinyQueue from "tinyqueue"; import { TileDescriptor } from "./TileDescriptor"; +/** + * A 1×1 cell in a grid which belongs to a tile. + */ export interface Cell { /** - * The item held by the slot containing this cell. + * The item displayed on the tile. */ item: TileDescriptor; /** - * Whether this cell is the first cell of the containing slot. + * Whether this cell is the origin (top left corner) of the tile. */ - // TODO: Rename to 'start'? - slot: boolean; + origin: boolean; /** - * The width, in columns, of the containing slot. + * The width, in columns, of the tile. */ columns: number; /** - * The height, in rows, of the containing slot. + * The height, in rows, of the tile. */ rows: number; } export interface Grid { - generation: number; columns: number; + /** + * The cells of the grid, in left-to-right top-to-bottom order. + * undefined = empty. + */ cells: (Cell | undefined)[]; } @@ -55,9 +60,9 @@ export function dijkstra(g: Grid): number[] { const visit = (curr: number, via: number) => { const viaCell = g.cells[via]; - const viaLargeSlot = + const viaLargeTile = viaCell !== undefined && (viaCell.rows > 1 || viaCell.columns > 1); - const distanceVia = distances[via] + (viaLargeSlot ? 4 : 1); + const distanceVia = distances[via] + (viaLargeTile ? 4 : 1); if (distanceVia < distances[curr]) { distances[curr] = distanceVia; @@ -252,6 +257,21 @@ export function fillGaps(g: Grid): Grid { return result; } +export function appendItems(items: TileDescriptor[], g: Grid): Grid { + return { + ...g, + cells: [ + ...g.cells, + ...items.map((i) => ({ + item: i, + origin: true, + columns: 1, + rows: 1, + })), + ], + }; +} + export function cycleTileSize(tileId: string, g: Grid): Grid { const from = g.cells.findIndex((c) => c?.item.id === tileId); if (from === -1) return g; // Tile removed, no change @@ -273,7 +293,6 @@ export function cycleTileSize(tileId: string, g: Grid): Grid { const gappyGrid: Grid = { ...g, - generation: g.generation + 1, cells: new Array(g.cells.length + newRows * g.columns), }; @@ -318,7 +337,7 @@ export function cycleTileSize(tileId: string, g: Grid): Grid { const toRow = row(to, g); g.cells.forEach((c, src) => { - if (c?.slot && c.item.id !== tileId) { + if (c?.origin && c.item.id !== tileId) { const offset = row(src, g) > toRow + candidateHeight - 1 ? g.columns * newRows : 0; forEachCellInArea(src, areaEnd(src, c.columns, c.rows, g), g, (c, i) => { @@ -333,7 +352,7 @@ export function cycleTileSize(tileId: string, g: Grid): Grid { if (c !== undefined) displacedTiles.push(c); gappyGrid.cells[i] = { item: g.cells[from]!.item, - slot: i === to, + origin: i === to, columns: toWidth, rows: toHeight, }; From d852e334130c0e6d0cd53bac185118a6ce21dcca Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 8 Feb 2023 00:32:08 -0500 Subject: [PATCH 031/286] Document the component --- src/video-grid/NewVideoGrid.tsx | 102 +++++++++++++++++++------------- 1 file changed, 60 insertions(+), 42 deletions(-) diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index aba499ea..810946bc 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -148,18 +148,27 @@ interface DragState { cursorY: number; } +/** + * An interactive, animated grid of video tiles. + */ export const NewVideoGrid: FC = ({ items, disableAnimations, children, }) => { + // Overview: This component lays out tiles by rendering an invisible template + // grid of "slots" for tiles to go in. Once rendered, it uses the DOM API to + // get the dimensions of each slot, feeding these numbers back into + // react-spring to let the actual tiles move freely atop the template. + + // To know when the rendered grid becomes consistent with the layout we've + // requested, we give it a data-generation attribute which holds the ID of the + // most recently rendered generation of the grid, and watch it with a + // MutationObserver. + const [slotGrid, setSlotGrid] = useState(null); const [slotGridGeneration, setSlotGridGeneration] = useState(0); - const [gridRef1, gridBounds] = useMeasure(); - const gridRef2 = useRef(null); - const gridRef = useMergedRefs(gridRef1, gridRef2); - useEffect(() => { if (slotGrid !== null) { setSlotGridGeneration( @@ -179,6 +188,10 @@ export const NewVideoGrid: FC = ({ } }, [slotGrid, setSlotGridGeneration]); + const [gridRef1, gridBounds] = useMeasure(); + const gridRef2 = useRef(null); + const gridRef = useMergedRefs(gridRef1, gridRef2); + const slotRects = useMemo(() => { if (slotGrid === null) return []; @@ -214,7 +227,7 @@ export const NewVideoGrid: FC = ({ const [tiles] = useReactiveState( (prevTiles) => { - // If React hasn't yet rendered the current generation of the layout, skip + // If React hasn't yet rendered the current generation of the grid, skip // the update, because grid and slotRects will be out of sync if (slotGridGeneration !== grid?.generation) return prevTiles ?? []; @@ -264,43 +277,6 @@ export const NewVideoGrid: FC = ({ // react-spring's types are bugged and can't infer the spring type ) as unknown as [TransitionFn, SpringRef]; - const slotGridStyle = useMemo(() => { - if (grid === null) return {}; - - const areas = new Array<(number | null)[]>( - Math.ceil(grid.cells.length / grid.columns) - ); - for (let i = 0; i < areas.length; i++) - areas[i] = new Array(grid.columns).fill(null); - - let slotId = 0; - for (let i = 0; i < grid.cells.length; i++) { - const cell = grid.cells[i]; - if (cell?.origin) { - const slotEnd = i + cell.columns - 1 + grid.columns * (cell.rows - 1); - forEachCellInArea( - i, - slotEnd, - grid, - (_c, j) => (areas[row(j, grid)][column(j, grid)] = slotId) - ); - slotId++; - } - } - - return { - gridTemplateAreas: areas - .map( - (row) => - `'${row - .map((slotId) => (slotId === null ? "." : `s${slotId}`)) - .join(" ")}'` - ) - .join(" "), - gridTemplateColumns: `repeat(${columns}, 1fr)`, - }; - }, [grid, columns]); - const animateDraggedTile = (endOfGesture: boolean) => { const { tileId, tileX, tileY, cursorX, cursorY } = dragState.current!; const tile = tiles.find((t) => t.item.id === tileId)!; @@ -357,6 +333,11 @@ export const NewVideoGrid: FC = ({ } }; + // Callback for useDrag. We could call useDrag here, but the default + // pattern of spreading {...bind()} across the children to bind the gesture + // ends up breaking memoization and ruining this component's performance. + // Instead, we pass this callback to each tile via a ref, to let them bind the + // gesture using the much more sensible ref-based method. const onTileDrag = ( tileId: string, { @@ -411,6 +392,43 @@ export const NewVideoGrid: FC = ({ { target: gridRef2 } ); + const slotGridStyle = useMemo(() => { + if (grid === null) return {}; + + const areas = new Array<(number | null)[]>( + Math.ceil(grid.cells.length / grid.columns) + ); + for (let i = 0; i < areas.length; i++) + areas[i] = new Array(grid.columns).fill(null); + + let slotId = 0; + for (let i = 0; i < grid.cells.length; i++) { + const cell = grid.cells[i]; + if (cell?.origin) { + const slotEnd = i + cell.columns - 1 + grid.columns * (cell.rows - 1); + forEachCellInArea( + i, + slotEnd, + grid, + (_c, j) => (areas[row(j, grid)][column(j, grid)] = slotId) + ); + slotId++; + } + } + + return { + gridTemplateAreas: areas + .map( + (row) => + `'${row + .map((slotId) => (slotId === null ? "." : `s${slotId}`)) + .join(" ")}'` + ) + .join(" "), + gridTemplateColumns: `repeat(${columns}, 1fr)`, + }; + }, [grid, columns]); + const slots = useMemo(() => { const slots = new Array(items.length); for (let i = 0; i < items.length; i++) From d40e467b7d3eb7b7a1320214bd328e352920412c Mon Sep 17 00:00:00 2001 From: Abhi Jain Date: Wed, 8 Feb 2023 14:26:56 +0530 Subject: [PATCH 032/286] changes Loading room... to Loading... --- src/room/GroupCallLoader.tsx | 2 +- src/room/GroupCallView.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/room/GroupCallLoader.tsx b/src/room/GroupCallLoader.tsx index 4af3dc39..ab1b1877 100644 --- a/src/room/GroupCallLoader.tsx +++ b/src/room/GroupCallLoader.tsx @@ -51,7 +51,7 @@ export function GroupCallLoader({ if (loading) { return ( -

{t("Loading room…")}

+

{t("Loading...")}

); } diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 8867ed4d..da6196e7 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -279,7 +279,7 @@ export function GroupCallView({ } else if (isEmbedded) { return ( -

{t("Loading room…")}

+

{t("Loading...")}

); } else { From eda11cfc08fe6e29dc0ca08a3cca3a24eb9ed5b1 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Thu, 9 Feb 2023 12:57:54 +0100 Subject: [PATCH 033/286] Inform that the user that config keys are missing (#880) --- src/PosthogAnalytics.ts | 9 +++++---- src/initializer.tsx | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 28f50d22..4e91a6d6 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -94,7 +94,7 @@ export class PosthogAnalytics { private static ANALYTICS_EVENT_TYPE = "im.vector.analytics"; // set true during the constructor if posthog config is present, otherwise false - private static internalInstance = null; + private static internalInstance: PosthogAnalytics | null = null; private identificationPromise: Promise; private readonly enabled: boolean = false; @@ -137,6 +137,9 @@ export class PosthogAnalytics { }); this.enabled = true; } else { + logger.info( + "Posthog is not enabled because there is no api key or no host given in the config" + ); this.enabled = false; } this.startListeningToSettingsChanges(); @@ -225,9 +228,7 @@ export class PosthogAnalytics { } public async identifyUser(analyticsIdGenerator: () => string) { - // There might be a better way to get the client here. - - if (this.anonymity == Anonymity.Pseudonymous) { + if (this.anonymity == Anonymity.Pseudonymous && this.enabled) { // Check the user's account_data for an analytics ID to use. Storing the ID in account_data allows // different devices to send the same ID. let analyticsID = await this.getAnalyticsId(); diff --git a/src/initializer.tsx b/src/initializer.tsx index 3de39de5..e10a4ec5 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -214,5 +214,5 @@ export class Initializer { resolve(); } } - private initPromise: Promise; + private initPromise: Promise | null; } From 33dd2758d72bf1ce7a789885b2321eda3a7fdd06 Mon Sep 17 00:00:00 2001 From: Abhi Jain Date: Mon, 13 Feb 2023 09:55:32 +0530 Subject: [PATCH 034/286] =?UTF-8?q?change=20...=20to=20=E2=80=A6=20unicode?= =?UTF-8?q?=20for=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/room/GroupCallLoader.tsx | 2 +- src/room/GroupCallView.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/room/GroupCallLoader.tsx b/src/room/GroupCallLoader.tsx index ab1b1877..15a5f90b 100644 --- a/src/room/GroupCallLoader.tsx +++ b/src/room/GroupCallLoader.tsx @@ -51,7 +51,7 @@ export function GroupCallLoader({ if (loading) { return ( -

{t("Loading...")}

+

{t("Loading…")}

); } diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index da6196e7..76f062fd 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -279,7 +279,7 @@ export function GroupCallView({ } else if (isEmbedded) { return ( -

{t("Loading...")}

+

{t("Loading…")}

); } else { From 07a4de638f25201e0fcf2c3949c6eebaf07abebe Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 13 Feb 2023 15:20:48 +0000 Subject: [PATCH 035/286] Don't pause audio streams on media actions This adds handlers for the media actions to do nothing, otherwise they cause the audio element for a random participant to get paused. Fixes https://github.com/vector-im/element-call/issues/855 --- src/room/useGroupCall.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index f2d7f27e..d0157ff7 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -158,6 +158,38 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { [setState] ); + const doNothingMediaActionCallback = useCallback( + (details: MediaSessionActionDetails) => {}, + [] + ); + + useEffect(() => { + // disable the media action keys, otherwise audio elements get paused when + // the user presses media keys or unplugs headphones, etc. + // Note there are actions for muting / unmuting a microphone & hanging up + // which we could wire up. + const mediaActions: MediaSessionAction[] = [ + "play", + "pause", + "stop", + "nexttrack", + "previoustrack", + ]; + + for (const mediaAction of mediaActions) { + navigator.mediaSession.setActionHandler( + mediaAction, + doNothingMediaActionCallback + ); + } + + return () => { + for (const mediaAction of mediaActions) { + navigator.mediaSession.setActionHandler(mediaAction, null); + } + }; + }, [doNothingMediaEventCallback]); + useEffect(() => { function onGroupCallStateChanged() { updateState({ From 605dd44df0d33c30b0fcd060f564a5eacf4f1f07 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 13 Feb 2023 15:49:58 +0000 Subject: [PATCH 036/286] Rename other instance of variable --- src/room/useGroupCall.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index d0157ff7..9d509872 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -188,7 +188,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { navigator.mediaSession.setActionHandler(mediaAction, null); } }; - }, [doNothingMediaEventCallback]); + }, [doNothingMediaActionCallback]); useEffect(() => { function onGroupCallStateChanged() { From 544874487101f7f95b90f6e0e044bc6c9fe3ef96 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 13 Feb 2023 12:17:09 -0500 Subject: [PATCH 037/286] Document grid operations --- src/video-grid/model.ts | 114 ++++++++++++++++++++++++++++------------ 1 file changed, 81 insertions(+), 33 deletions(-) diff --git a/src/video-grid/model.ts b/src/video-grid/model.ts index 54f3c817..7d9729ca 100644 --- a/src/video-grid/model.ts +++ b/src/video-grid/model.ts @@ -48,21 +48,33 @@ export interface Grid { cells: (Cell | undefined)[]; } -export function dijkstra(g: Grid): number[] { - const end = findLast1By1Index(g) ?? 0; - const endRow = row(end, g); - const endColumn = column(end, g); +/** + * Gets the paths that tiles should travel along in the grid to reach a + * particular destination. + * @param dest The destination index. + * @param g The grid. + * @returns An array in which each cell holds the index of the next cell to move + * to to reach the destination, or null if it is the destination. + */ +export function getPaths(dest: number, g: Grid): (number | null)[] { + const destRow = row(dest, g); + const destColumn = column(dest, g); - const distances = new Array(end + 1).fill(Infinity); - distances[end] = 0; - const edges = new Array(end).fill(undefined); - const heap = new TinyQueue([end], (i) => distances[i]); + // This is Dijkstra's algorithm + + const distances = new Array(dest + 1).fill(Infinity); + distances[dest] = 0; + const edges = new Array(dest).fill(undefined); + edges[dest] = null; + const heap = new TinyQueue([dest], (i) => distances[i]); const visit = (curr: number, via: number) => { const viaCell = g.cells[via]; const viaLargeTile = viaCell !== undefined && (viaCell.rows > 1 || viaCell.columns > 1); - const distanceVia = distances[via] + (viaLargeTile ? 4 : 1); + // Since it looks nicer to have paths go around large tiles, we impose an + // increased cost for moving through them + const distanceVia = distances[via] + (viaLargeTile ? 8 : 1); if (distanceVia < distances[curr]) { distances[curr] = distanceVia; @@ -76,18 +88,20 @@ export function dijkstra(g: Grid): number[] { const viaRow = row(via, g); const viaColumn = column(via, g); + // Visit each neighbor if (viaRow > 0) visit(via - g.columns, via); if (viaColumn > 0) visit(via - 1, via); - if (viaColumn < (viaRow === endRow ? endColumn : g.columns - 1)) + if (viaColumn < (viaRow === destRow ? destColumn : g.columns - 1)) visit(via + 1, via); if ( - viaRow < endRow - 1 || - (viaRow === endRow - 1 && viaColumn <= endColumn) + viaRow < destRow - 1 || + (viaRow === destRow - 1 && viaColumn <= destColumn) ) visit(via + g.columns, via); } - return edges as number[]; + // The heap is empty, so we've generated all paths + return edges as (number | null)[]; } function findLastIndex( @@ -194,27 +208,30 @@ function getNextGap(g: Grid): number | null { return null; } +/** + * Backfill any gaps in the grid. + */ export function fillGaps(g: Grid): Grid { const result: Grid = { ...g, cells: [...g.cells] }; let gap = getNextGap(result); if (gap !== null) { - const pathToEnd = dijkstra(result); + const pathsToEnd = getPaths(findLast1By1Index(result)!, result); do { let filled = false; let to = gap; - let from: number | undefined = pathToEnd[gap]; + let from = pathsToEnd[gap]; // First, attempt to fill the gap by moving 1×1 tiles backwards from the // end of the grid along a set path - while (from !== undefined) { + while (from !== null) { const toCell = result.cells[to]; const fromCell = result.cells[from]; - // Skip over large tiles + // Skip over slots that are already full if (toCell !== undefined) { - to = pathToEnd[to]; + to = pathsToEnd[to]!; // Skip over large tiles. Also, we might run into gaps along the path // created during the filling of previous gaps. Skip over those too; // they'll be picked up on the next iteration of the outer loop. @@ -223,13 +240,13 @@ export function fillGaps(g: Grid): Grid { fromCell.rows > 1 || fromCell.columns > 1 ) { - from = pathToEnd[from]; + from = pathsToEnd[from]; } else { result.cells[to] = result.cells[from]; result.cells[from] = undefined; filled = true; - to = pathToEnd[to]; - from = pathToEnd[from]; + to = pathsToEnd[to]!; + from = pathsToEnd[from]; } } @@ -272,6 +289,12 @@ export function appendItems(items: TileDescriptor[], g: Grid): Grid { }; } +/** + * Changes the size of a tile, rearranging the grid to make space. + * @param tileId The ID of the tile to modify. + * @param g The grid. + * @returns The updated grid. + */ export function cycleTileSize(tileId: string, g: Grid): Grid { const from = g.cells.findIndex((c) => c?.item.id === tileId); if (from === -1) return g; // Tile removed, no change @@ -279,63 +302,84 @@ export function cycleTileSize(tileId: string, g: Grid): Grid { const fromHeight = g.cells[from]!.rows; const fromEnd = areaEnd(from, fromWidth, fromHeight, g); + // The target dimensions, which toggle between 1×1 and larger than 1×1 const [toWidth, toHeight] = fromWidth === 1 && fromHeight === 1 ? [Math.min(3, Math.max(2, g.columns - 1)), 2] : [1, 1]; + + // If we're expanding the tile, we want to create enough new rows at the + // tile's target position such that every new unit of grid area created during + // the expansion can fit within the new rows. + // We do it this way, since it's easier to backfill gaps in the grid than it + // is to push colliding tiles outwards. const newRows = Math.max( 0, Math.ceil((toWidth * toHeight - fromWidth * fromHeight) / g.columns) ); - const candidateWidth = toWidth; - const candidateHeight = toHeight - newRows; - + // This is the grid with the new rows added const gappyGrid: Grid = { ...g, cells: new Array(g.cells.length + newRows * g.columns), }; - const nextScanLocations = new Set([from]); + // The next task is to scan for a spot to place the modified tile. Since we + // might be creating new rows at the target position, this spot can be shorter + // than the target height. + const candidateWidth = toWidth; + const candidateHeight = toHeight - newRows; + + // To make the tile appear to expand outwards from its center, we're actually + // scanning for locations to put the *center* of the tile. These numbers are + // the offsets between the tile's origin and its center. const scanColumnOffset = Math.floor((toWidth - 1) / 2); const scanRowOffset = Math.floor((toHeight - 1) / 2); + + const nextScanLocations = new Set([from]); const rows = row(g.cells.length - 1, g) + 1; let to: number | null = null; + // The contents of a given cell are 'displaceable' if it's empty, holds a 1×1 + // tile, or is part of the original tile we're trying to reposition const displaceable = (c: Cell | undefined, i: number): boolean => c === undefined || (c.columns === 1 && c.rows === 1) || inArea(i, from, fromEnd, g); + // Do the scanning for (const scanLocation of nextScanLocations) { const start = scanLocation - scanColumnOffset - g.columns * scanRowOffset; const end = areaEnd(start, candidateWidth, candidateHeight, g); const startColumn = column(start, g); const startRow = row(start, g); const endColumn = column(end, g); + const endRow = row(end, g); if ( start >= 0 && - end < gappyGrid.cells.length && - endColumn - startColumn + 1 === candidateWidth + endColumn - startColumn + 1 === candidateWidth && + allCellsInArea(start, end, g, displaceable) ) { - if (allCellsInArea(start, end, g, displaceable)) { - to = start; - break; - } + // This location works! + to = start; + break; } + // Scan outwards in all directions if (startColumn > 0) nextScanLocations.add(scanLocation - 1); if (endColumn < g.columns - 1) nextScanLocations.add(scanLocation + 1); if (startRow > 0) nextScanLocations.add(scanLocation - g.columns); - if (startRow <= rows) nextScanLocations.add(scanLocation + g.columns); + if (endRow < rows - 1) nextScanLocations.add(scanLocation + g.columns); } - // TODO: Don't give up on placing the tile yet + // If there is no space in the grid, give up if (to === null) return g; const toRow = row(to, g); + // Copy tiles from the original grid to the new one, with the new rows + // inserted at the target location g.cells.forEach((c, src) => { if (c?.origin && c.item.id !== tileId) { const offset = @@ -346,6 +390,8 @@ export function cycleTileSize(tileId: string, g: Grid): Grid { } }); + // Place the tile in its target position, making a note of the tiles being + // overwritten const displacedTiles: Cell[] = []; const toEnd = areaEnd(to, toWidth, toHeight, g); forEachCellInArea(to, toEnd, gappyGrid, (c, i) => { @@ -358,10 +404,12 @@ export function cycleTileSize(tileId: string, g: Grid): Grid { }; }); + // Place the displaced tiles in the remaining space for (let i = 0; displacedTiles.length > 0; i++) { if (gappyGrid.cells[i] === undefined) gappyGrid.cells[i] = displacedTiles.shift(); } + // Fill any gaps that remain return fillGaps(gappyGrid); } From 8d0bf4caccf0a4b10b9da9145a7794cefb95e3b2 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 13 Feb 2023 18:35:50 -0500 Subject: [PATCH 038/286] Test grid operations --- test/video-grid/model-test.ts | 247 ++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 test/video-grid/model-test.ts diff --git a/test/video-grid/model-test.ts b/test/video-grid/model-test.ts new file mode 100644 index 00000000..d71fd2f5 --- /dev/null +++ b/test/video-grid/model-test.ts @@ -0,0 +1,247 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { + column, + cycleTileSize, + fillGaps, + forEachCellInArea, + Grid, + row, +} from "../../src/video-grid/model"; +import { TileDescriptor } from "../../src/video-grid/TileDescriptor"; + +/** + * Builds a grid from a string specifying the contents of each cell as a letter. + */ +function mkGrid(spec: string): Grid { + const secondNewline = spec.indexOf("\n", 1); + const columns = secondNewline === -1 ? spec.length : secondNewline - 1; + const cells = spec.match(/[a-z ]/g) ?? []; + const areas = new Set(cells); + areas.delete(" "); // Space represents an empty cell, not an area + const grid: Grid = { columns, cells: new Array(cells.length) }; + + for (const area of areas) { + const start = cells.indexOf(area); + const end = cells.lastIndexOf(area); + const rows = row(end, grid) - row(start, grid) + 1; + const columns = column(end, grid) - column(start, grid) + 1; + + forEachCellInArea(start, end, grid, (_c, i) => { + grid.cells[i] = { + item: { id: area } as unknown as TileDescriptor, + origin: i === start, + rows, + columns, + }; + }); + } + + return grid; +} + +/** + * Turns a grid into a string showing the contents of each cell as a letter. + */ +function showGrid(g: Grid): string { + let result = "\n"; + g.cells.forEach((c, i) => { + if (i > 0 && i % g.columns == 0) result += "\n"; + result += c?.item.id ?? " "; + }); + return result; +} + +function testFillGaps(title: string, input: string, output: string): void { + test(`fillGaps ${title}`, () => { + expect(showGrid(fillGaps(mkGrid(input)))).toBe(output); + }); +} + +testFillGaps( + "fills a gap", + ` +a b +cde +f`, + ` +cab +fde` +); + +testFillGaps( + "fills multiple gaps", + ` +a bc +defgh + ijkl +mno`, + ` +aebch +difgl +monjk` +); + +testFillGaps( + "fills a big gap", + ` +abcd +e f +g h +ijkl`, + ` +abcd +elhf +gkji` +); + +testFillGaps( + "only moves 1×1 tiles", + ` + +aa +bc`, + ` +bc +aa` +); + +testFillGaps( + "prefers moving around large tiles", + ` +a bc +ddde +dddf +ghij +k`, + ` +abce +dddf +dddj +kghi` +); + +testFillGaps( + "moves through large tiles if necessary", + ` +a bc +dddd +efgh +i`, + ` +afbc +dddd +iegh` +); + +function testCycleTileSize( + title: string, + tileId: string, + input: string, + output: string +): void { + test(`cycleTileSize ${title}`, () => { + expect(showGrid(cycleTileSize(tileId, mkGrid(input)))).toBe(output); + }); +} + +testCycleTileSize( + "does nothing if the tile is not present", + "z", + ` +abcd +efgh`, + ` +abcd +efgh` +); + +testCycleTileSize( + "expands a tile to 2×2 in a 3 column layout", + "c", + ` +abc +def +ghi`, + ` +acc +bcc +def +ghi` +); + +testCycleTileSize( + "expands a tile to 3×3 in a 4 column layout", + "g", + ` +abcd +efgh`, + ` +abcd +eggg +fggg +h` +); + +testCycleTileSize( + "restores a tile to 1×1", + "b", + ` +abbc +dbbe +fghi +jk`, + ` +abhc +djge +fik` +); + +testCycleTileSize( + "expands a tile even in a crowded grid", + "c", + ` +abb +cbb +dde +ddf +ghi +klm`, + ` +abb +gbb +dde +ddf +cci +cch +klm` +); + +testCycleTileSize( + "does nothing if the tile has no room to expand", + "c", + ` +abb +cbb +dde +ddf`, + ` +abb +cbb +dde +ddf` +); From 69e6ba93c18585ea190eee6cbb1c68e5f75abf9a Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 13 Feb 2023 20:36:42 -0500 Subject: [PATCH 039/286] Add a switch to toggle between the new and old grids --- src/room/InCallView.tsx | 12 +++++++++--- src/settings/SettingsModal.tsx | 13 +++++++++++++ src/settings/useSetting.ts | 1 + src/video-grid/VideoGrid.module.css | 1 + src/video-grid/VideoGrid.tsx | 18 ++++++++---------- src/video-grid/VideoTileContainer.tsx | 4 ++-- 6 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index a0f7917f..d4717c1b 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -51,7 +51,11 @@ import { UserMenuContainer } from "../UserMenuContainer"; import { useRageshakeRequestModal } from "../settings/submit-rageshake"; import { RageshakeRequestModal } from "./RageshakeRequestModal"; import { useMediaHandler } from "../settings/useMediaHandler"; -import { useShowInspector, useSpatialAudio } from "../settings/useSetting"; +import { + useNewGrid, + useShowInspector, + useSpatialAudio, +} from "../settings/useSetting"; import { useModalTriggerState } from "../Modal"; import { useAudioContext } from "../video-grid/useMediaStream"; import { useFullscreen } from "../video-grid/useFullscreen"; @@ -277,6 +281,8 @@ export function InCallView({ [] ); + const [newGrid] = useNewGrid(); + const Grid = newGrid ? NewVideoGrid : VideoGrid; const prefersReducedMotion = usePrefersReducedMotion(); const renderContent = (): JSX.Element => { @@ -306,7 +312,7 @@ export function InCallView({ } return ( - )} - +
); }; diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 106ef9e0..244d1d15 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -33,6 +33,7 @@ import { useShowInspector, useOptInAnalytics, canEnableSpatialAudio, + useNewGrid, } from "./useSetting"; import { FieldRow, InputField } from "../input/Input"; import { Button } from "../button"; @@ -62,6 +63,7 @@ export const SettingsModal = (props: Props) => { const [showInspector, setShowInspector] = useShowInspector(); const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics(); const [keyboardShortcuts, setKeyboardShortcuts] = useKeyboardShortcuts(); + const [newGrid, setNewGrid] = useNewGrid(); const downloadDebugLog = useDownloadDebugLog(); @@ -216,6 +218,17 @@ export const SettingsModal = (props: Props) => { } /> + + ) => + setNewGrid(e.target.checked) + } + /> +
diff --git a/src/video-grid/VideoTile.tsx b/src/video-grid/VideoTile.tsx index ca9ebbea..eea617f2 100644 --- a/src/video-grid/VideoTile.tsx +++ b/src/video-grid/VideoTile.tsx @@ -44,14 +44,14 @@ interface Props { showOptions?: boolean; isLocal?: boolean; disableSpeakingIndicator?: boolean; - opacity: SpringValue; - scale: SpringValue; - shadow: SpringValue; - zIndex: SpringValue; - x: SpringValue; - y: SpringValue; - width: SpringValue; - height: SpringValue; + opacity?: SpringValue; + scale?: SpringValue; + shadow?: SpringValue; + zIndex?: SpringValue; + x?: SpringValue; + y?: SpringValue; + width?: SpringValue; + height?: SpringValue; } export const VideoTile = forwardRef( @@ -141,14 +141,17 @@ export const VideoTile = forwardRef( style={{ opacity, scale, - boxShadow: shadow.to( + boxShadow: shadow?.to( (s) => `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px 0px` ), zIndex, x, y, - "--tileWidth": width.to((w) => `${w}px`), - "--tileHeight": height.to((h) => `${h}px`), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore React does in fact support assigning custom properties, + // but React's types say no + "--tileWidth": width?.to((w) => `${w}px`), + "--tileHeight": height?.to((h) => `${h}px`), }} ref={ref as ForwardedRef} {...rest} diff --git a/src/video-grid/VideoTileContainer.tsx b/src/video-grid/VideoTileContainer.tsx index 86140339..c3a4c611 100644 --- a/src/video-grid/VideoTileContainer.tsx +++ b/src/video-grid/VideoTileContainer.tsx @@ -18,6 +18,8 @@ import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventType import React, { FC, memo, RefObject } from "react"; import { useCallback } from "react"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { SpringValue } from "@react-spring/web"; +import { EventTypes, Handler, useDrag } from "@use-gesture/react"; import { useCallFeed } from "./useCallFeed"; import { useSpatialMediaStream } from "./useMediaStream"; @@ -26,8 +28,6 @@ import { VideoTile } from "./VideoTile"; import { VideoTileSettingsModal } from "./VideoTileSettingsModal"; import { useModalTriggerState } from "../Modal"; import { TileDescriptor } from "./TileDescriptor"; -import { SpringValue } from "@react-spring/web"; -import { EventTypes, Handler, useDrag } from "@use-gesture/react"; interface Props { item: TileDescriptor; @@ -44,14 +44,14 @@ interface Props { maximised: boolean; fullscreen: boolean; onFullscreen: (item: TileDescriptor) => void; - opacity: SpringValue; - scale: SpringValue; - shadow: SpringValue; - zIndex: SpringValue; - x: SpringValue; - y: SpringValue; - width: SpringValue; - height: SpringValue; + opacity?: SpringValue; + scale?: SpringValue; + shadow?: SpringValue; + zIndex?: SpringValue; + x?: SpringValue; + y?: SpringValue; + width?: SpringValue; + height?: SpringValue; onDragRef?: RefObject< ( tileId: string, diff --git a/src/video-grid/model.ts b/src/video-grid/model.ts index 7d9729ca..48f19b34 100644 --- a/src/video-grid/model.ts +++ b/src/video-grid/model.ts @@ -15,6 +15,7 @@ limitations under the License. */ import TinyQueue from "tinyqueue"; + import { TileDescriptor } from "./TileDescriptor"; /** From 8c818b9ce16dd2d51525d05f98c103e26ea0cc9d Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 13 Feb 2023 22:24:04 -0500 Subject: [PATCH 041/286] Get 100% test coverage on grid operations --- test/video-grid/model-test.ts | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/video-grid/model-test.ts b/test/video-grid/model-test.ts index d71fd2f5..cc7741d9 100644 --- a/test/video-grid/model-test.ts +++ b/test/video-grid/model-test.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { + appendItems, column, cycleTileSize, fillGaps, @@ -72,6 +73,26 @@ function testFillGaps(title: string, input: string, output: string): void { }); } +testFillGaps( + "does nothing on an empty grid", + ` +`, + ` +` +); + +testFillGaps( + "does nothing if there are no gaps", + ` +ab +cd +ef`, + ` +ab +cd +ef` +); + testFillGaps( "fills a gap", ` @@ -245,3 +266,18 @@ cbb dde ddf` ); + +test("appendItems appends 1×1 tiles", () => { + const grid1 = ` +aab +aac +d`; + const grid2 = ` +aab +aac +def`; + const newItems = ["e", "f"].map( + (i) => ({ id: i } as unknown as TileDescriptor) + ); + expect(showGrid(appendItems(newItems, mkGrid(grid1)))).toBe(grid2); +}); From ef4a62ca6243787c2eaf5c9d78c6ffd182b3f0d5 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 13 Feb 2023 22:38:27 -0500 Subject: [PATCH 042/286] Document useMergedRefs --- src/useMergedRefs.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/useMergedRefs.ts b/src/useMergedRefs.ts index f8712f53..7c6da658 100644 --- a/src/useMergedRefs.ts +++ b/src/useMergedRefs.ts @@ -1,5 +1,9 @@ import { MutableRefObject, RefCallback, useCallback } from "react"; +/** + * Combines multiple refs into one, useful for attaching multiple refs to the + * same DOM node. + */ export const useMergedRefs = ( ...refs: (MutableRefObject | RefCallback)[] ): RefCallback => From efbf319fa1048087662938639f104a7c1b212678 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 13 Feb 2023 22:40:26 -0500 Subject: [PATCH 043/286] Explain why we cast the tile springs --- src/video-grid/VideoGrid.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/video-grid/VideoGrid.tsx b/src/video-grid/VideoGrid.tsx index 6623eee4..03d95837 100644 --- a/src/video-grid/VideoGrid.tsx +++ b/src/video-grid/VideoGrid.tsx @@ -986,6 +986,7 @@ export function VideoGrid({ tilePositions, tiles, scrollPosition, + // react-spring's types are bugged and can't infer the spring type ]) as unknown as [SpringValues[], SpringRef]; const onTap = useCallback( From 9a0dfad5f9a105d6af55bb30b2234a3f5b2dc27d Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 15 Feb 2023 15:13:39 -0500 Subject: [PATCH 044/286] Add coverage reports to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f6245318..263dea00 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist-ssr *.local .idea/ public/config.json +/coverage From fbc72283d4855f1164042cf51a5a84221fe88225 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 15 Feb 2023 16:20:58 -0500 Subject: [PATCH 045/286] Add missing copyright headers --- src/useMergedRefs.ts | 16 ++++++++++++++++ src/useReactiveState.ts | 16 ++++++++++++++++ src/video-grid/NewVideoGrid.module.css | 16 ++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/src/useMergedRefs.ts b/src/useMergedRefs.ts index 7c6da658..3fab929d 100644 --- a/src/useMergedRefs.ts +++ b/src/useMergedRefs.ts @@ -1,3 +1,19 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import { MutableRefObject, RefCallback, useCallback } from "react"; /** diff --git a/src/useReactiveState.ts b/src/useReactiveState.ts index fe995724..f20399a4 100644 --- a/src/useReactiveState.ts +++ b/src/useReactiveState.ts @@ -1,3 +1,19 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import { DependencyList, Dispatch, diff --git a/src/video-grid/NewVideoGrid.module.css b/src/video-grid/NewVideoGrid.module.css index e6f64ba9..75ee7076 100644 --- a/src/video-grid/NewVideoGrid.module.css +++ b/src/video-grid/NewVideoGrid.module.css @@ -1,3 +1,19 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + .grid { contain: strict; position: relative; From 1fc181dc280bf6341dccf7dc781e281bb27359d3 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 15 Feb 2023 16:38:49 -0500 Subject: [PATCH 046/286] Document useReactiveState --- src/useReactiveState.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/useReactiveState.ts b/src/useReactiveState.ts index f20399a4..5693be54 100644 --- a/src/useReactiveState.ts +++ b/src/useReactiveState.ts @@ -23,6 +23,11 @@ import { useState, } from "react"; +/** + * Hook creating a stateful value that updates automatically whenever the + * dependencies change. Or equivalently, a version of useMemo that takes its own + * previous value as an input, and can be updated manually. + */ export const useReactiveState = ( updateFn: (prevState?: T) => T, deps: DependencyList From 53bc8eb82f112f78316b8f4e9a6c690d630067b6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 28 Feb 2023 13:50:24 +0000 Subject: [PATCH 047/286] Behave sensibly if a full room alias is entered Check explicitly to see if the room name that's enetered into the box looks like a room alias and if so, do the sensible thing. Fixes https://github.com/vector-im/element-call/issues/852 --- src/home/RegisteredView.tsx | 11 +++++++++-- src/home/UnauthenticatedView.tsx | 8 ++++++-- src/matrix-utils.ts | 25 +++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/home/RegisteredView.tsx b/src/home/RegisteredView.tsx index 4f96c42e..b08f22a6 100644 --- a/src/home/RegisteredView.tsx +++ b/src/home/RegisteredView.tsx @@ -24,7 +24,11 @@ import { useHistory } from "react-router-dom"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { useTranslation } from "react-i18next"; -import { createRoom, roomAliasLocalpartFromRoomName } from "../matrix-utils"; +import { + createRoom, + roomAliasLocalpartFromRoomName, + sanitiseRoomNameInput, +} from "../matrix-utils"; import { useGroupCallRooms } from "./useGroupCallRooms"; import { Header, HeaderLogo, LeftNav, RightNav } from "../Header"; import commonStyles from "./common.module.css"; @@ -57,7 +61,10 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) { e.preventDefault(); const data = new FormData(e.target as HTMLFormElement); const roomNameData = data.get("callName"); - const roomName = typeof roomNameData === "string" ? roomNameData : ""; + const roomName = + typeof roomNameData === "string" + ? sanitiseRoomNameInput(roomNameData) + : ""; const ptt = callType === CallType.Radio; async function submit() { diff --git a/src/home/UnauthenticatedView.tsx b/src/home/UnauthenticatedView.tsx index 089156cf..826468b2 100644 --- a/src/home/UnauthenticatedView.tsx +++ b/src/home/UnauthenticatedView.tsx @@ -24,7 +24,11 @@ import { Header, HeaderLogo, LeftNav, RightNav } from "../Header"; import { UserMenuContainer } from "../UserMenuContainer"; import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { Button } from "../button"; -import { createRoom, roomAliasLocalpartFromRoomName } from "../matrix-utils"; +import { + createRoom, + roomAliasLocalpartFromRoomName, + sanitiseRoomNameInput, +} from "../matrix-utils"; import { useInteractiveRegistration } from "../auth/useInteractiveRegistration"; import { useModalTriggerState } from "../Modal"; import { JoinExistingCallModal } from "./JoinExistingCallModal"; @@ -54,7 +58,7 @@ export const UnauthenticatedView: FC = () => { (e) => { e.preventDefault(); const data = new FormData(e.target as HTMLFormElement); - const roomName = data.get("callName") as string; + const roomName = sanitiseRoomNameInput(data.get("callName") as string); const displayName = data.get("displayName") as string; const ptt = callType === CallType.Radio; diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index 0a2ecfe5..7f70a236 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -211,6 +211,31 @@ export function fullAliasFromRoomName( return `#${roomAliasLocalpartFromRoomName(roomName)}:${client.getDomain()}`; } +/** + * Applies some basic sanitisation to an room name that the user + * has given us + * @param input The room name from the user + * @param client A matrix client object + */ +export function sanitiseRoomNameInput(input: string): string { + // check to see if the user has enetered a fully qualified room + // alias. If so, turn it into just the localpart because that's what + // we use + const parts = input.split(":", 2); + if (parts.length === 2 && parts[0][0] === "#") { + // looks like a room alias + if (parts[1] === Config.defaultServerName()) { + // it's local to our own homeserver + return parts[0]; + } else { + throw new Error("Unsupported remote room alias"); + } + } + + // that's all we do here right now + return input; +} + /** * XXX: What is this trying to do? It looks like it's getting the localpart from * a room alias, but why is it splitting on hyphens and then putting spaces in?? From 64703fd3cc722979ec354013d1b063b79cab1c0f Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 28 Feb 2023 14:09:52 +0000 Subject: [PATCH 048/286] Typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Šimon Brandner --- src/matrix-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index 7f70a236..fa3199b4 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -212,7 +212,7 @@ export function fullAliasFromRoomName( } /** - * Applies some basic sanitisation to an room name that the user + * Applies some basic sanitisation to a room name that the user * has given us * @param input The room name from the user * @param client A matrix client object From 0423a494c4364bf3d15cde5ee3d7dfc26f339cb0 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Wed, 1 Mar 2023 13:47:36 +0100 Subject: [PATCH 049/286] Checkbox for analytics opt in & settings redesign (#934) --- public/locales/en-GB/app.json | 7 +- src/ClientContext.tsx | 5 +- src/analytics/AnalyticsOptInDescription.tsx | 20 ++++ src/{ => analytics}/PosthogAnalytics.ts | 8 +- src/{ => analytics}/PosthogEvents.ts | 0 src/auth/LoginPage.tsx | 2 +- src/auth/RegisterPage.tsx | 2 +- src/home/RegisteredView.tsx | 12 ++ src/home/UnauthenticatedView.tsx | 12 ++ src/input/Input.module.css | 4 + src/input/Input.tsx | 13 ++- src/room/GroupCallInspector.tsx | 2 +- src/room/GroupCallView.tsx | 2 +- src/room/InCallView.tsx | 2 +- src/room/useGroupCall.ts | 2 +- src/settings/SettingsModal.module.css | 9 ++ src/settings/SettingsModal.tsx | 115 ++++++++++++-------- src/settings/useSetting.ts | 2 + 18 files changed, 154 insertions(+), 65 deletions(-) create mode 100644 src/analytics/AnalyticsOptInDescription.tsx rename src/{ => analytics}/PosthogAnalytics.ts (98%) rename src/{ => analytics}/PosthogEvents.ts (100%) diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index 809f5e4a..fe52c85f 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -16,13 +16,12 @@ "<0>Why not finish by setting up a password to keep your account?<1>You'll be able to keep your name and set an avatar for use on future calls": "<0>Why not finish by setting up a password to keep your account?<1>You'll be able to keep your name and set an avatar for use on future calls", "Accept camera/microphone permissions to join the call.": "Accept camera/microphone permissions to join the call.", "Accept microphone permissions to join the call.": "Accept microphone permissions to join the call.", - "Advanced": "Advanced", - "Allow analytics": "Allow analytics", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.", "Audio": "Audio", "Avatar": "Avatar", "By clicking \"Go\", you agree to our <2>Terms and conditions": "By clicking \"Go\", you agree to our <2>Terms and conditions", "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "By clicking \"Join call now\", you agree to our <2>Terms and conditions", + "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ", "Call link copied": "Call link copied", "Call type menu": "Call type menu", "Camera": "Camera", @@ -41,10 +40,12 @@ "Description (optional)": "Description (optional)", "Details": "Details", "Developer": "Developer", + "Developer Settings": "Developer Settings", "Display name": "Display name", "Download debug logs": "Download debug logs", "Element Call Home": "Element Call Home", "Exit full screen": "Exit full screen", + "Expose developer settings in the settings window.": "Expose developer settings in the settings window.", "Fetching group call timed out.": "Fetching group call timed out.", "Freedom": "Freedom", "Full screen": "Full screen", @@ -84,6 +85,7 @@ "Press and hold spacebar to talk over {{name}}": "Press and hold spacebar to talk over {{name}}", "Press and hold to talk": "Press and hold to talk", "Press and hold to talk over {{name}}": "Press and hold to talk over {{name}}", + "Privacy Policy": "Privacy Policy", "Profile": "Profile", "Recaptcha dismissed": "Recaptcha dismissed", "Recaptcha not loaded": "Recaptcha not loaded", @@ -120,7 +122,6 @@ "This feature is only supported on Firefox.": "This feature is only supported on Firefox.", "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions", "This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)", - "This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.", "Turn off camera": "Turn off camera", "Turn on camera": "Turn on camera", "Unmute microphone": "Unmute microphone", diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index fa83f5d9..46dfe5a5 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -36,7 +36,10 @@ import { fallbackICEServerAllowed, } from "./matrix-utils"; import { widget } from "./widget"; -import { PosthogAnalytics, RegistrationType } from "./PosthogAnalytics"; +import { + PosthogAnalytics, + RegistrationType, +} from "./analytics/PosthogAnalytics"; import { translatedError } from "./TranslatedError"; import { useEventTarget } from "./useEvents"; import { Config } from "./config/Config"; diff --git a/src/analytics/AnalyticsOptInDescription.tsx b/src/analytics/AnalyticsOptInDescription.tsx new file mode 100644 index 00000000..46727f5f --- /dev/null +++ b/src/analytics/AnalyticsOptInDescription.tsx @@ -0,0 +1,20 @@ +import { t } from "i18next"; +import React from "react"; + +import { Link } from "../typography/Typography"; + +export const optInDescription: () => JSX.Element = () => { + return ( + <> + <> + {t( + "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our " + )} + + + <>{t("Privacy Policy")} + + . + + ); +}; diff --git a/src/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts similarity index 98% rename from src/PosthogAnalytics.ts rename to src/analytics/PosthogAnalytics.ts index 4e91a6d6..e2e8fdae 100644 --- a/src/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -19,8 +19,8 @@ import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClient } from "matrix-js-sdk"; import { Buffer } from "buffer"; -import { widget } from "./widget"; -import { getSetting, setSetting, settingsBus } from "./settings/useSetting"; +import { widget } from "../widget"; +import { getSetting, setSetting, settingsBus } from "../settings/useSetting"; import { CallEndedTracker, CallStartedTracker, @@ -30,8 +30,8 @@ import { MuteMicrophoneTracker, UndecryptableToDeviceEventTracker, } from "./PosthogEvents"; -import { Config } from "./config/Config"; -import { getUrlParams } from "./UrlParams"; +import { Config } from "../config/Config"; +import { getUrlParams } from "../UrlParams"; /* Posthog analytics tracking. * diff --git a/src/PosthogEvents.ts b/src/analytics/PosthogEvents.ts similarity index 100% rename from src/PosthogEvents.ts rename to src/analytics/PosthogEvents.ts diff --git a/src/auth/LoginPage.tsx b/src/auth/LoginPage.tsx index 88296db4..20f6e00b 100644 --- a/src/auth/LoginPage.tsx +++ b/src/auth/LoginPage.tsx @@ -25,7 +25,7 @@ import { Button } from "../button"; import styles from "./LoginPage.module.css"; import { useInteractiveLogin } from "./useInteractiveLogin"; import { usePageTitle } from "../usePageTitle"; -import { PosthogAnalytics } from "../PosthogAnalytics"; +import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { Config } from "../config/Config"; export const LoginPage: FC = () => { diff --git a/src/auth/RegisterPage.tsx b/src/auth/RegisterPage.tsx index f5025b88..0464266e 100644 --- a/src/auth/RegisterPage.tsx +++ b/src/auth/RegisterPage.tsx @@ -38,7 +38,7 @@ import { LoadingView } from "../FullScreenView"; import { useRecaptcha } from "./useRecaptcha"; import { Caption, Link } from "../typography/Typography"; import { usePageTitle } from "../usePageTitle"; -import { PosthogAnalytics } from "../PosthogAnalytics"; +import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { Config } from "../config/Config"; export const RegisterPage: FC = () => { diff --git a/src/home/RegisteredView.tsx b/src/home/RegisteredView.tsx index b08f22a6..a6278c2e 100644 --- a/src/home/RegisteredView.tsx +++ b/src/home/RegisteredView.tsx @@ -42,6 +42,8 @@ import { JoinExistingCallModal } from "./JoinExistingCallModal"; import { Title } from "../typography/Typography"; import { Form } from "../form/Form"; import { CallType, CallTypeDropdown } from "./CallTypeDropdown"; +import { useOptInAnalytics } from "../settings/useSetting"; +import { optInDescription } from "../analytics/AnalyticsOptInDescription"; interface Props { client: MatrixClient; @@ -52,6 +54,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) { const [callType, setCallType] = useState(CallType.Video); const [loading, setLoading] = useState(false); const [error, setError] = useState(); + const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics(); const history = useHistory(); const { t } = useTranslation(); const { modalState, modalProps } = useModalTriggerState(); @@ -141,6 +144,15 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) { {loading ? t("Loading…") : t("Go")} + ) => + setOptInAnalytics(event.target.checked) + } + /> {error && ( diff --git a/src/home/UnauthenticatedView.tsx b/src/home/UnauthenticatedView.tsx index 826468b2..6339e42d 100644 --- a/src/home/UnauthenticatedView.tsx +++ b/src/home/UnauthenticatedView.tsx @@ -39,12 +39,15 @@ import { CallType, CallTypeDropdown } from "./CallTypeDropdown"; import styles from "./UnauthenticatedView.module.css"; import commonStyles from "./common.module.css"; import { generateRandomName } from "../auth/generateRandomName"; +import { useOptInAnalytics } from "../settings/useSetting"; +import { optInDescription } from "../analytics/AnalyticsOptInDescription"; export const UnauthenticatedView: FC = () => { const { setClient } = useClient(); const [callType, setCallType] = useState(CallType.Video); const [loading, setLoading] = useState(false); const [error, setError] = useState(); + const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics(); const [privacyPolicyUrl, recaptchaKey, register] = useInteractiveRegistration(); const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey); @@ -152,6 +155,15 @@ export const UnauthenticatedView: FC = () => { autoComplete="off" /> + ) => + setOptInAnalytics(event.target.checked) + } + /> By clicking "Go", you agree to our{" "} diff --git a/src/input/Input.module.css b/src/input/Input.module.css index 60b69936..2420cc43 100644 --- a/src/input/Input.module.css +++ b/src/input/Input.module.css @@ -209,3 +209,7 @@ limitations under the License. margin-left: 26px; width: 100%; /* Ensure that it breaks onto the next row */ } + +.description.noLabel { + margin-top: -20px; /* Ensures that there is no weired spacing if the checkbox doesn't have a label */ +} diff --git a/src/input/Input.tsx b/src/input/Input.tsx index bfc17d74..afecd6ae 100644 --- a/src/input/Input.tsx +++ b/src/input/Input.tsx @@ -55,14 +55,14 @@ export function Field({ children, className }: FieldProps): JSX.Element { } interface InputFieldProps { - label: string; + label?: string; type: string; prefix?: string; suffix?: string; id?: string; checked?: boolean; className?: string; - description?: string; + description?: string | ReactNode; disabled?: boolean; required?: boolean; // this is a hack. Those variables should be part of `HTMLAttributes | HTMLAttributes` @@ -140,7 +140,14 @@ export const InputField = forwardRef< {suffix && {suffix}} {description && ( -

+

{description}

)} diff --git a/src/room/GroupCallInspector.tsx b/src/room/GroupCallInspector.tsx index 73b2a000..648a0a1f 100644 --- a/src/room/GroupCallInspector.tsx +++ b/src/room/GroupCallInspector.tsx @@ -35,7 +35,7 @@ import { CallEvent } from "matrix-js-sdk/src/webrtc/call"; import styles from "./GroupCallInspector.module.css"; import { SelectInput } from "../input/SelectInput"; -import { PosthogAnalytics } from "../PosthogAnalytics"; +import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; interface InspectorContextState { eventsByUserId?: { [userId: string]: SequenceDiagramMatrixEvent[] }; diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 8867ed4d..7af31d63 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -32,7 +32,7 @@ import { CallEndedView } from "./CallEndedView"; import { useRoomAvatar } from "./useRoomAvatar"; import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler"; import { useLocationNavigation } from "../useLocationNavigation"; -import { PosthogAnalytics } from "../PosthogAnalytics"; +import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { useMediaHandler } from "../settings/useMediaHandler"; import { findDeviceByName, getDevices } from "../media-utils"; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 17a0fde4..48b1cbf1 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -63,7 +63,7 @@ import { import { useModalTriggerState } from "../Modal"; import { useAudioContext } from "../video-grid/useMediaStream"; import { useFullscreen } from "../video-grid/useFullscreen"; -import { PosthogAnalytics } from "../PosthogAnalytics"; +import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { widget, ElementWidgetActions } from "../widget"; import { useJoinRule } from "./useJoinRule"; import { useUrlParams } from "../UrlParams"; diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 9d509872..37484b43 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -29,7 +29,7 @@ import { useTranslation } from "react-i18next"; import { IWidgetApiRequest } from "matrix-widget-api"; import { usePageUnload } from "./usePageUnload"; -import { PosthogAnalytics } from "../PosthogAnalytics"; +import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { TranslatedError, translatedError } from "../TranslatedError"; import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget"; diff --git a/src/settings/SettingsModal.module.css b/src/settings/SettingsModal.module.css index 7eb39159..9b4951b4 100644 --- a/src/settings/SettingsModal.module.css +++ b/src/settings/SettingsModal.module.css @@ -26,3 +26,12 @@ limitations under the License. .fieldRowText { margin-bottom: 0; } + +/* +This style guarantees a fixed width of the tab bar in the settings window. +The "Developer" item in the tab bar can be toggled. +Without a defined width activating the developer tab makes the tab container jump to the right. +*/ +.tabLabel { + width: 80px; +} diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 244d1d15..90a1cb5f 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -34,11 +34,13 @@ import { useOptInAnalytics, canEnableSpatialAudio, useNewGrid, + useDeveloperSettingsTab, } from "./useSetting"; import { FieldRow, InputField } from "../input/Input"; import { Button } from "../button"; import { useDownloadDebugLog } from "./submit-rageshake"; import { Body } from "../typography/Typography"; +import { optInDescription } from "../analytics/AnalyticsOptInDescription"; interface Props { isOpen: boolean; @@ -62,6 +64,8 @@ export const SettingsModal = (props: Props) => { const [spatialAudio, setSpatialAudio] = useSpatialAudio(); const [showInspector, setShowInspector] = useShowInspector(); const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics(); + const [developerSettingsTab, setDeveloperSettingsTab] = + useDeveloperSettingsTab(); const [keyboardShortcuts, setKeyboardShortcuts] = useKeyboardShortcuts(); const [newGrid, setNewGrid] = useNewGrid(); @@ -80,7 +84,7 @@ export const SettingsModal = (props: Props) => { title={ <> - {t("Audio")} + {t("Audio")} } > @@ -158,24 +162,11 @@ export const SettingsModal = (props: Props) => { title={ <> - {t("Advanced")} + {t("More")} } > - - ) => - setOptInAnalytics(event.target.checked) - } - /> - +

Keyboard

{ } /> - - - - {t("Developer")} - - } - > - - - {t("Version: {{version}}", { - version: import.meta.env.VITE_APP_VERSION || "dev", - })} - - +

Analytics

) => - setShowInspector(e.target.checked) + checked={optInAnalytics} + description={optInDescription()} + onChange={(event: React.ChangeEvent) => + setOptInAnalytics(event.target.checked) } /> ) => - setNewGrid(e.target.checked) + checked={developerSettingsTab} + label={t("Developer Settings")} + description={t( + "Expose developer settings in the settings window." + )} + onChange={(event: React.ChangeEvent) => + setDeveloperSettingsTab(event.target.checked) } /> - - -
+ {developerSettingsTab && ( + + + {t("Developer")} + + } + > + + + {t("Version: {{version}}", { + version: import.meta.env.VITE_APP_VERSION || "dev", + })} + + + + ) => + setShowInspector(e.target.checked) + } + /> + + + ) => + setNewGrid(e.target.checked) + } + /> + + + + + + )} ); diff --git a/src/settings/useSetting.ts b/src/settings/useSetting.ts index 6729a301..756ac74b 100644 --- a/src/settings/useSetting.ts +++ b/src/settings/useSetting.ts @@ -91,3 +91,5 @@ export const useOptInAnalytics = () => useSetting("opt-in-analytics", false); export const useKeyboardShortcuts = () => useSetting("keyboard-shortcuts", true); export const useNewGrid = () => useSetting("new-grid", false); +export const useDeveloperSettingsTab = () => + useSetting("developer-settings-tab", false); From 58d87db55f9d75023a0a225d6f0c65e02b256f5a Mon Sep 17 00:00:00 2001 From: alariej Date: Wed, 1 Mar 2023 14:30:25 +0100 Subject: [PATCH 050/286] Add e2eEnabled parameter to Widget client --- src/widget.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/widget.ts b/src/widget.ts index c223c223..6a38f9dd 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -101,7 +101,7 @@ export const widget: WidgetHelpers | null = (() => { // We need to do this now rather than later because it has capabilities to // request, and is responsible for starting the transport (should it be?) - const { roomId, userId, deviceId, baseUrl } = getUrlParams(); + const { roomId, userId, deviceId, baseUrl, e2eEnabled } = getUrlParams(); if (!roomId) throw new Error("Room ID must be supplied"); if (!userId) throw new Error("User ID must be supplied"); if (!deviceId) throw new Error("Device ID must be supplied"); @@ -147,6 +147,7 @@ export const widget: WidgetHelpers | null = (() => { userId, deviceId, timelineSupport: true, + useE2eForGroupCall: e2eEnabled, } ); const clientPromise = client.startClient().then(() => client); From 29e41c722778affb6a9924408ee606e0fd7c963f Mon Sep 17 00:00:00 2001 From: Enrico Schwendig Date: Thu, 2 Mar 2023 18:48:32 +0100 Subject: [PATCH 051/286] Allow Element Call to be started without audio / video interface (#924) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * config: add feature in `config.json` * groupCall: adjust connection state in feed if allowCallWithoutVideoAndAudio * matrix-js-sdk: update version for allowCallWithoutVideoAndAudio - I modified the SDK so that mute unmute work without media and check device permission inside the SDK - allowCallWithoutVideoAndAudio is only checked at one point outside the SDK * docu: add join group call without media docu in READMe --------- Co-authored-by: Robin Townsend Co-authored-by: Šimon Brandner --- README.md | 16 ++++++++++++ package.json | 2 +- src/config/ConfigOptions.ts | 8 ++++++ src/matrix-utils.ts | 18 ++++++++------ src/room/InCallView.tsx | 49 +++++++++++++++++++++++-------------- src/room/useGroupCall.ts | 18 +++++++++++--- yarn.lock | 16 ++++++------ 7 files changed, 89 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index cd4b65de..511c7dc5 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,22 @@ Element Call requires a homeserver with registration enabled without any 3pid or Therefore, to use a self-hosted homeserver, this is recommended to be a new server where any user account created has not joined any normal rooms anywhere in the Matrix federated network. The homeserver used can be setup to disable federation, so as to prevent spam registrations (if you keep registrations open) and to ensure Element Call continues to work in case any user decides to log in to their Element Call account using the standard Element app and joins normal rooms that Element Call cannot handle. +### Features + +#### Allow joining group calls without a camera and a microphone + +You can allow joining a group call without video and audio enabling this feature in your `config.json`: + +```json +{ + ... + + "features": { + "feature_group_calls_without_video_and_audio": true + } +} +``` + ## Development Element Call is built against [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/pull/2553). To get started, clone, install, and link the package: diff --git a/package.json b/package.json index 4242b19c..94dadc03 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#64197bf4db6486d77708125d7fb2e8d7fe001f14", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#e782a2afa33032798eba7e92d2b3f28f9baa0564", "matrix-widget-api": "^1.0.0", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index e46ffece..5899d45f 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -44,6 +44,14 @@ export interface ConfigOptions { server_name: string; }; }; + + /** + * Allow to join a group calls without audio and video. + * TEMPORARY: Is a feature that's not proved and experimental + */ + features?: { + feature_group_calls_without_video_and_audio: boolean; + }; } // Overrides members from ConfigOptions that are always provided by the diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index fa3199b4..8cec21d0 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -92,10 +92,14 @@ export async function initClient( indexedDB = window.indexedDB; } catch (e) {} - const storeOpts = {} as ICreateClientOpts; + const baseOpts = { + fallbackICEServerAllowed: fallbackICEServerAllowed, + isVoipWithNoMediaAllowed: + Config.get().features?.feature_group_calls_without_video_and_audio, + } as ICreateClientOpts; if (indexedDB && localStorage) { - storeOpts.store = new IndexedDBStore({ + baseOpts.store = new IndexedDBStore({ indexedDB: window.indexedDB, localStorage, dbName: SYNC_STORE_NAME, @@ -107,7 +111,7 @@ export async function initClient( : () => new IndexedDBWorker(), }); } else if (localStorage) { - storeOpts.store = new MemoryStore({ localStorage }); + baseOpts.store = new MemoryStore({ localStorage }); } // Check whether we have crypto data store. If we are restoring a session @@ -139,14 +143,14 @@ export async function initClient( } if (indexedDB) { - storeOpts.cryptoStore = new IndexedDBCryptoStore( + baseOpts.cryptoStore = new IndexedDBCryptoStore( indexedDB, CRYPTO_STORE_NAME ); } else if (localStorage) { - storeOpts.cryptoStore = new LocalStorageCryptoStore(localStorage); + baseOpts.cryptoStore = new LocalStorageCryptoStore(localStorage); } else { - storeOpts.cryptoStore = new MemoryCryptoStore(); + baseOpts.cryptoStore = new MemoryCryptoStore(); } // XXX: we read from the URL params in RoomPage too: @@ -160,7 +164,7 @@ export async function initClient( } const client = createClient({ - ...storeOpts, + ...baseOpts, ...clientOptions, useAuthorizationHeader: true, // Use a relatively low timeout for API calls: this is a realtime app diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 48b1cbf1..1ed1be3a 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -372,27 +372,36 @@ export function InCallView({ if (noControls) { footer = null; - } else if (reducedControls) { - footer = ( -
- - - -
- ); } else { - footer = ( -
- - - {canScreenshare && !hideScreensharing && !isSafari && ( + const buttons: JSX.Element[] = []; + + buttons.push( + , + + ); + + if (!reducedControls) { + if (canScreenshare && !hideScreensharing && !isSafari) { + buttons.push( - )} - {!maximisedParticipant && ( + ); + } + if (!maximisedParticipant) { + buttons.push( - )} - -
- ); + ); + } + } + + buttons.push(); + footer =
{buttons}
; } return ( diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 37484b43..60f2d67d 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -98,12 +98,24 @@ function getParticipants( (f) => f.userId === member.userId && f.deviceId === deviceId ); - participantInfoMap.set(deviceId, { - connectionState: feed + let connectionState: ConnectionState; + // If we allow calls without media, we have no feeds and cannot read the connection status from them. + // @TODO: The connection state should generally not be determined by the feed. + if ( + groupCall.allowCallWithoutVideoAndAudio && + !feed && + !participant.screensharing + ) { + connectionState = ConnectionState.Connected; + } else { + connectionState = feed ? feed.connected ? ConnectionState.Connected : ConnectionState.WaitMedia - : ConnectionState.EstablishingCall, + : ConnectionState.EstablishingCall; + } + participantInfoMap.set(deviceId, { + connectionState, presenter: participant.screensharing, }); } diff --git a/yarn.lock b/yarn.lock index 7980fa2c..78035441 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1821,10 +1821,10 @@ resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0" integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw== -"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.2": - version "0.1.0-alpha.2" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.2.tgz#a09d0fea858e817da971a3c9f904632ef7b49eb6" - integrity sha512-oVkBCh9YP7H9i4gAoQbZzswniczfo/aIptNa4dxRi4Ff9lSvUCFv6Hvzi7C+90c0/PWZLXjIDTIAWZYmwyd2fA== +"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.3": + version "0.1.0-alpha.4" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.4.tgz#1b20294e0354c3dcc9c7dc810d883198a4042f04" + integrity sha512-mdaDKrw3P5ZVCpq0ioW0pV6ihviDEbS8ZH36kpt9stLKHwwDSopPogE6CkQhi0B1jn1yBUtOYi32mBV/zcOR7g== "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": version "3.2.14" @@ -10362,12 +10362,12 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#64197bf4db6486d77708125d7fb2e8d7fe001f14": - version "23.1.1" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/64197bf4db6486d77708125d7fb2e8d7fe001f14" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#e782a2afa33032798eba7e92d2b3f28f9baa0564": + version "23.4.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e782a2afa33032798eba7e92d2b3f28f9baa0564" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.2" + "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.3" another-json "^0.2.0" bs58 "^5.0.0" content-type "^1.0.4" From 0c66b32b49a69681c6a1039f3b674d7f828cb136 Mon Sep 17 00:00:00 2001 From: Enrico Schwendig Date: Fri, 3 Mar 2023 08:06:10 +0100 Subject: [PATCH 052/286] matrix-js-sdk: update to last develop branch with call group fix (#941) --- package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 94dadc03..3b6368da 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#e782a2afa33032798eba7e92d2b3f28f9baa0564", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#8cbbdaa239e449848e8874f041ef1879c1956696", "matrix-widget-api": "^1.0.0", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/yarn.lock b/yarn.lock index 78035441..3b6c84f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10362,9 +10362,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#e782a2afa33032798eba7e92d2b3f28f9baa0564": +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#8cbbdaa239e449848e8874f041ef1879c1956696": version "23.4.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e782a2afa33032798eba7e92d2b3f28f9baa0564" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8cbbdaa239e449848e8874f041ef1879c1956696" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.3" From ee4760351ca6abfc4aa515ac111e038d03814544 Mon Sep 17 00:00:00 2001 From: Enrico Schwendig Date: Mon, 6 Mar 2023 09:45:42 +0100 Subject: [PATCH 053/286] add end-to-end test workflow (#885) * e2e: add end-to-end test workflow - The tests are executed in a Docker container. - The static users are connected via `matrix-js-sdk Client`. - A test user connecting to the conference via EC. --- .github/workflows/e2e.yml | 24 ++++++++++++++++++++++++ README.md | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/e2e.yml diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..ba1172d4 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,24 @@ +name: Run E2E tests +on: + workflow_run: + workflows: ["deploy"] + types: + - completed + branches-ignore: + - "main" +jobs: + e2e: + name: E2E tests runs on Element Call + runs-on: ubuntu-latest + steps: + - name: Check out test private repo + uses: actions/checkout@v3 + with: + repository: vector-im/static-call-participant + ref: refs/heads/main + path: static-call-participant + token: ${{ secrets.GH_E2E_TEST_TOKEN }} + - name: Build E2E Image + run: "cd static-call-participant && docker build --no-cache --tag matrixdotorg/chrome-node-static-call-participant:latest ." + - name: Run E2E tests in container + run: "docker run --rm -v '${{ github.workspace }}/static-call-participant/callemshost-users.txt:/opt/app/callemshost-users.txt' matrixdotorg/chrome-node-static-call-participant:latest ./e2e.sh" diff --git a/README.md b/README.md index 511c7dc5..de5f1221 100644 --- a/README.md +++ b/README.md @@ -96,4 +96,4 @@ There are currently two different config files. `.env` holds variables that are ## Translation -If you'd like to help translate Element Call, head over to [translate.element.io](https://translate.element.io/engage/element-call/). You're also encouraged to join the [Element Translators](https://matrix.to/#/#translators:element.io) space to discuss and coordinate translation efforts. +Bf you'd like to help translate Element Call, head over to [translate.element.io](https://translate.element.io/engage/element-call/). You're also encouraged to join the [Element Translators](https://matrix.to/#/#translators:element.io) space to discuss and coordinate translation efforts. From d35c13b903ea7bdb1e7754a0762ae77273cc57f5 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 18:26:02 +0000 Subject: [PATCH 054/286] Translated using Weblate (Japanese) Currently translated at 75.8% (107 of 141 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/ --- public/locales/ja/app.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/locales/ja/app.json b/public/locales/ja/app.json index 71974160..6ac72db0 100644 --- a/public/locales/ja/app.json +++ b/public/locales/ja/app.json @@ -101,7 +101,9 @@ "Your recent calls": "最近の通話", "You can't talk at the same time": "同時に会話することはできません", "WebRTC is not supported or is being blocked in this browser.": "お使いのブラウザでWebRTCがサポートされていないか、またはブロックされています。", - "Login to your account": "お持ちのアカウントでログイン", + "Login to your account": "アカウントにログイン", "Freedom": "自由", - "{{displayName}}, your call is now ended": "{{displayName}}、通話が終了しました" + "{{displayName}}, your call is now ended": "{{displayName}}、通話が終了しました", + "Talking…": "話しています…", + "Remove": "削除" } From 7f8c0153ed2c76b1326c3c6d2b1ed2fe8f95a204 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Tue, 7 Feb 2023 18:10:53 +0000 Subject: [PATCH 055/286] Translated using Weblate (Japanese) Currently translated at 76.5% (108 of 141 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/ --- public/locales/ja/app.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/locales/ja/app.json b/public/locales/ja/app.json index 6ac72db0..98e7e17b 100644 --- a/public/locales/ja/app.json +++ b/public/locales/ja/app.json @@ -105,5 +105,6 @@ "Freedom": "自由", "{{displayName}}, your call is now ended": "{{displayName}}、通話が終了しました", "Talking…": "話しています…", - "Remove": "削除" + "Remove": "削除", + "No": "いいえ" } From 0fab702d705486e3d027f0d408bd203bf838db71 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 13 Feb 2023 06:42:55 +0000 Subject: [PATCH 056/286] Translated using Weblate (Japanese) Currently translated at 80.1% (113 of 141 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/ --- public/locales/ja/app.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/locales/ja/app.json b/public/locales/ja/app.json index 98e7e17b..c418d3e9 100644 --- a/public/locales/ja/app.json +++ b/public/locales/ja/app.json @@ -10,7 +10,7 @@ "Accept camera/microphone permissions to join the call.": "通話に参加するには、カメラ・マイクの許可が必要です。", "<0>Oops, something's gone wrong.": "<0>何かがうまく行きませんでした。", "Camera/microphone permissions needed to join the call.": "通話に参加する場合、カメラ・マイクの許可が必要です。", - "Allow analytics": "アナリティクスを許可", + "Allow analytics": "分析を許可", "Camera": "カメラ", "Call link copied": "通話リンクをコピーしました", "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "「今すぐ通話に参加」をクリックすると、<2>利用規約に同意したとみなされます", @@ -106,5 +106,10 @@ "{{displayName}}, your call is now ended": "{{displayName}}、通話が終了しました", "Talking…": "話しています…", "Remove": "削除", - "No": "いいえ" + "No": "いいえ", + "This feature is only supported on Firefox.": "この機能はFirefoxでのみサポートされています。", + "This call already exists, would you like to join?": "この通話は既に存在します。参加しますか?", + "Take me Home": "ホームに戻る", + "Press and hold to talk": "押し続けて会話", + "{{name}} is presenting": "{{name}}が画面を共有しています" } From ba48ade3b77157e0ff1ec5f1874383f27937e283 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Sun, 19 Feb 2023 19:21:14 +0000 Subject: [PATCH 057/286] Translated using Weblate (Japanese) Currently translated at 80.8% (114 of 141 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/ja/ --- public/locales/ja/app.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/locales/ja/app.json b/public/locales/ja/app.json index c418d3e9..8f75a621 100644 --- a/public/locales/ja/app.json +++ b/public/locales/ja/app.json @@ -111,5 +111,6 @@ "This call already exists, would you like to join?": "この通話は既に存在します。参加しますか?", "Take me Home": "ホームに戻る", "Press and hold to talk": "押し続けて会話", - "{{name}} is presenting": "{{name}}が画面を共有しています" + "{{name}} is presenting": "{{name}}が画面を共有しています", + "{{names}}, {{name}}": "{{names}}、{{name}}" } From bac795e1e8505625cd0c778ab941d4fa32cf3c96 Mon Sep 17 00:00:00 2001 From: Vri Date: Tue, 21 Feb 2023 05:43:05 +0000 Subject: [PATCH 058/286] Translated using Weblate (German) Currently translated at 100.0% (142 of 142 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/de/ --- public/locales/de/app.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/locales/de/app.json b/public/locales/de/app.json index 120f979b..4279002d 100644 --- a/public/locales/de/app.json +++ b/public/locales/de/app.json @@ -139,5 +139,6 @@ "{{name}} (Waiting for video...)": "{{name}} (Warte auf Video …)", "This feature is only supported on Firefox.": "Diese Funktion wird nur in Firefox unterstützt.", "<0>Submitting debug logs will help us track down the problem.": "<0>Übermittelte Problemberichte helfen uns, Fehler zu beheben.", - "<0>Oops, something's gone wrong.": "<0>Hoppla, etwas ist schiefgelaufen." + "<0>Oops, something's gone wrong.": "<0>Hoppla, etwas ist schiefgelaufen.", + "Use the upcoming grid system": "Nutze das kommende Rastersystem" } From 3e072aed3c59596727b45586b6919de6ffe9060d Mon Sep 17 00:00:00 2001 From: Linerly Date: Mon, 20 Feb 2023 22:40:00 +0000 Subject: [PATCH 059/286] Translated using Weblate (Indonesian) Currently translated at 100.0% (142 of 142 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/id/ --- public/locales/id/app.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/locales/id/app.json b/public/locales/id/app.json index dccfe8ff..267c0622 100644 --- a/public/locales/id/app.json +++ b/public/locales/id/app.json @@ -139,5 +139,6 @@ "{{name}} (Waiting for video...)": "{{name}} (Menunggu video...)", "This feature is only supported on Firefox.": "Fitur ini hanya didukung di Firefox.", "<0>Submitting debug logs will help us track down the problem.": "<0>Mengirim catatan pengawakutuan akan membantu kami melacak masalahnya.", - "<0>Oops, something's gone wrong.": "<0>Aduh, ada yang salah." + "<0>Oops, something's gone wrong.": "<0>Aduh, ada yang salah.", + "Use the upcoming grid system": "Gunakan sistem kisi yang akan segera datang" } From fa62b97b27a0dd9acab746da49989e02f1c5800c Mon Sep 17 00:00:00 2001 From: Glandos Date: Tue, 21 Feb 2023 08:07:13 +0000 Subject: [PATCH 060/286] Translated using Weblate (French) Currently translated at 100.0% (142 of 142 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/fr/ --- public/locales/fr/app.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/locales/fr/app.json b/public/locales/fr/app.json index c8e9cceb..00536365 100644 --- a/public/locales/fr/app.json +++ b/public/locales/fr/app.json @@ -139,5 +139,6 @@ "{{name}} (Waiting for video...)": "{{name}} (En attente de vidéo…)", "This feature is only supported on Firefox.": "Cette fonctionnalité est prise en charge dans Firefox uniquement.", "<0>Submitting debug logs will help us track down the problem.": "<0>Soumettre les journaux de débogage nous aidera à déterminer le problème.", - "<0>Oops, something's gone wrong.": "<0>Oups, quelque chose s’est mal passé." + "<0>Oops, something's gone wrong.": "<0>Oups, quelque chose s’est mal passé.", + "Use the upcoming grid system": "Utiliser le futur système de grille" } From 16ec643e840ab822a843c9abdf4f29af303c3075 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Mon, 20 Feb 2023 20:14:14 +0000 Subject: [PATCH 061/286] Translated using Weblate (Ukrainian) Currently translated at 100.0% (142 of 142 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/ --- public/locales/uk/app.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/locales/uk/app.json b/public/locales/uk/app.json index f7c60757..154be67a 100644 --- a/public/locales/uk/app.json +++ b/public/locales/uk/app.json @@ -139,5 +139,6 @@ "{{name}} (Waiting for video...)": "{{name}} (Очікування на відео...)", "This feature is only supported on Firefox.": "Ця функція підтримується лише в браузері Firefox.", "<0>Submitting debug logs will help us track down the problem.": "<0>Надсилання журналів зневадження допоможе нам виявити проблему.", - "<0>Oops, something's gone wrong.": "<0>Йой, щось пішло не за планом." + "<0>Oops, something's gone wrong.": "<0>Йой, щось пішло не за планом.", + "Use the upcoming grid system": "Використовувати майбутню сіткову систему" } From e31de966791082fbbf7c9f17e9084f28b5106a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Mon, 20 Feb 2023 21:23:22 +0000 Subject: [PATCH 062/286] Translated using Weblate (Estonian) Currently translated at 100.0% (142 of 142 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/et/ --- public/locales/et/app.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/locales/et/app.json b/public/locales/et/app.json index 0ca2e145..a896d107 100644 --- a/public/locales/et/app.json +++ b/public/locales/et/app.json @@ -139,5 +139,6 @@ "{{name}} (Waiting for video...)": "{{name}} (Ootame videovoo algust...)", "This feature is only supported on Firefox.": "See funktsionaalsus on toetatud vaid Firefoxis.", "<0>Submitting debug logs will help us track down the problem.": "<0>Kui saadad meile vealogid, siis on lihtsam vea põhjust otsida.", - "<0>Oops, something's gone wrong.": "<0>Ohoo, midagi on nüüd katki." + "<0>Oops, something's gone wrong.": "<0>Ohoo, midagi on nüüd katki.", + "Use the upcoming grid system": "Kasuta tulevast ruudustiku-põhist paigutust" } From 4cfa726df165912744895dc75ce484b544efb1e3 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Mon, 20 Feb 2023 20:03:03 +0000 Subject: [PATCH 063/286] Translated using Weblate (Slovak) Currently translated at 100.0% (142 of 142 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/sk/ --- public/locales/sk/app.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/locales/sk/app.json b/public/locales/sk/app.json index 98343472..dc2de00f 100644 --- a/public/locales/sk/app.json +++ b/public/locales/sk/app.json @@ -139,5 +139,6 @@ "{{count}} people connected|one": "{{count}} osoba pripojená", "This feature is only supported on Firefox.": "Táto funkcia je podporovaná len v prehliadači Firefox.", "<0>Submitting debug logs will help us track down the problem.": "<0>Odoslanie záznamov ladenia nám pomôže nájsť problém.", - "<0>Oops, something's gone wrong.": "<0>Hups, niečo sa pokazilo." + "<0>Oops, something's gone wrong.": "<0>Hups, niečo sa pokazilo.", + "Use the upcoming grid system": "Použiť pripravovaný systém mriežky" } From a1cb52c61308ecd580e8bda63dbf2113b5c102fe Mon Sep 17 00:00:00 2001 From: DarkCoder15 Date: Fri, 3 Mar 2023 06:26:07 +0000 Subject: [PATCH 064/286] Translated using Weblate (Russian) Currently translated at 100.0% (142 of 142 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/ru/ --- public/locales/ru/app.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/locales/ru/app.json b/public/locales/ru/app.json index b113bbf5..2611d604 100644 --- a/public/locales/ru/app.json +++ b/public/locales/ru/app.json @@ -139,5 +139,6 @@ "Single-key keyboard shortcuts": "Горячие клавиши", "<0>Submitting debug logs will help us track down the problem.": "<0>Отправка журналов поможет нам найти и устранить проблему.", "<0>Oops, something's gone wrong.": "<0>Упс, что-то пошло не так.", - "{{name}} (Waiting for video...)": "{{name}} (Ожидание видео...)" + "{{name}} (Waiting for video...)": "{{name}} (Ожидание видео...)", + "Use the upcoming grid system": "Использовать сеточный показ" } From 4a3b7bc6ddcff667fb0e63fe6c180333e2adde26 Mon Sep 17 00:00:00 2001 From: Weblate Date: Sat, 4 Mar 2023 06:33:41 +0000 Subject: [PATCH 065/286] Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/ --- public/locales/cs/app.json | 2 -- public/locales/de/app.json | 2 -- public/locales/es/app.json | 2 -- public/locales/et/app.json | 2 -- public/locales/fa/app.json | 2 -- public/locales/fr/app.json | 2 -- public/locales/id/app.json | 2 -- public/locales/ja/app.json | 2 -- public/locales/pl/app.json | 1 - public/locales/ru/app.json | 2 -- public/locales/sk/app.json | 2 -- public/locales/uk/app.json | 2 -- public/locales/zh-Hans/app.json | 2 -- 13 files changed, 25 deletions(-) diff --git a/public/locales/cs/app.json b/public/locales/cs/app.json index 70226c43..567a778f 100644 --- a/public/locales/cs/app.json +++ b/public/locales/cs/app.json @@ -93,7 +93,6 @@ "{{roomName}} - Walkie-talkie call": "{{roomName}} - Vysílačkový hovor", "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Zapnout jedno-klávesové zkratky, např. 'm' pro vypnutí/zapnutí mikrofonu.", "{{names}}, {{name}}": "{{names}}, {{name}}", - "This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Toto bude odesílat anonymizovaná data (jako délku a počet účastníků hovoru) týmu Element Call, aby nám pomohly zlepšovat aplikaci podle toho, jak je používaná.", "Talking…": "Mluvení…", "Talk over speaker": "Mluvit přes mluvčího", "Spotlight": "Soustředěný mód", @@ -131,7 +130,6 @@ "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Kliknutím na \"Připojit se do hovoru\", odsouhlasíte naše <2>Terms and conditions", "By clicking \"Go\", you agree to our <2>Terms and conditions": "Kliknutím na \"Pokračovat\", odsouhlasíte naše <2>Terms and conditions", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Jiný uživatel v tomto hovoru má problémy. Abychom mohli diagnostikovat problém, rádi bychom shromáždili protokoly ladění.", - "Allow analytics": "Povolit analytiku", "Advanced": "Pokročilé", "<0>Why not finish by setting up a password to keep your account?<1>You'll be able to keep your name and set an avatar for use on future calls": "<0>Proč neskončit nastavením hesla, abyste mohli účet použít znovu?<1>Budete si moci nechat své jméno a nastavit si avatar pro budoucí hovory ", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Připojit se<1>Or<2>Zkopírovat odkaz a připojit se později", diff --git a/public/locales/de/app.json b/public/locales/de/app.json index 4279002d..4115953d 100644 --- a/public/locales/de/app.json +++ b/public/locales/de/app.json @@ -129,11 +129,9 @@ "Sending debug logs…": "Sende Debug-Protokolle …", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Anruf beitreten<1>Oder<2>Anruflink kopieren und später beitreten", "{{name}} (Connecting...)": "{{name}} (verbindet sich …)", - "Allow analytics": "Analysedaten senden", "Advanced": "Erweitert", "Copy": "Kopieren", "Element Call Home": "Element Call-Startseite", - "This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Dies wird anonymisierte Daten (wie z. B. die Dauer eines Anrufs und die Zahl der Teilnehmenden) dem Element Call-Team senden, um uns bei der Optimierung der Anwendung basierend auf dem Nutzungsverhalten zu helfen.", "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Ob Tastenkürzel mit nur einer Taste aktiviert sein sollen, z. B. „m“ um das Mikrofon stumm/aktiv zu schalten.", "Single-key keyboard shortcuts": "Ein-Tasten-Tastenkürzel", "{{name}} (Waiting for video...)": "{{name}} (Warte auf Video …)", diff --git a/public/locales/es/app.json b/public/locales/es/app.json index c41d345c..4a71d8aa 100644 --- a/public/locales/es/app.json +++ b/public/locales/es/app.json @@ -129,11 +129,9 @@ "{{displayName}}, your call is now ended": "{{displayName}}, tu llamada ha finalizado", "{{count}} people connected|other": "{{count}} personas conectadas", "{{count}} people connected|one": "{{count}} persona conectada", - "Allow analytics": "Permitir analíticas", "Advanced": "Avanzado", "Element Call Home": "Inicio de Element Call", "Copy": "Copiar", - "This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Esto enviará datos anónimos (como la duración de la llamada y el número de participantes) al equipo de Element Call para ayudarnos a optimizar la aplicación dependiendo de cómo se use.", "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Habilita los atajos de teclado de una sola tecla, por ejemplo 'm' para silenciar/desilenciar el micrófono.", "Single-key keyboard shortcuts": "Atajos de teclado de una sola tecla", "{{name}} (Waiting for video...)": "{{name}} (Esperando al video...)", diff --git a/public/locales/et/app.json b/public/locales/et/app.json index a896d107..4cd51785 100644 --- a/public/locales/et/app.json +++ b/public/locales/et/app.json @@ -129,11 +129,9 @@ "WebRTC is not supported or is being blocked in this browser.": "WebRTC pole kas selles brauseris toetatud või on keelatud.", "This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Muudab kõneleja heli nii, nagu tuleks see sealt, kus on tema pilt ekraanil. (See on katseline funktsionaalsus ja võib mõjutada heli stabiilsust.)", "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "Siin saidis on kasutusel ReCAPTCHA ning kehtivad Google <2>privaatsuspoliitika ja <6>teenusetingimused.<9>Klikkides „Registreeru“, nõustud meie <12>kasutustingimustega", - "Allow analytics": "Luba analüütika", "Advanced": "Lisaseadistused", "Element Call Home": "Element Call Home", "Copy": "Kopeeri", - "This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Me saadame kõne anonüümsed andmed (nagu kõne kestus ja osalejate arv) meie arendustiimile ja see võimaldab levinud kasutusmustrite alusel arendust optimeerida.", "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Kas kasutame üheklahvilisi kiirklahve, näiteks „m“ mikrofoni sisse/välja lülitamiseks.", "Single-key keyboard shortcuts": "Üheklahvilised kiirklahvid", "{{name}} (Waiting for video...)": "{{name}} (Ootame videovoo algust...)", diff --git a/public/locales/fa/app.json b/public/locales/fa/app.json index fb984532..2a89ebd9 100644 --- a/public/locales/fa/app.json +++ b/public/locales/fa/app.json @@ -128,11 +128,9 @@ "Stop sharing screen": "توقف اشتراک‌گذاری صفحه نمایش", "Spatial audio": "صدای جهت‌دار", "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "این که میان‌برهای صفحه‌کلید تک‌کلیده مثل m برای خموشی و ناخموشی میکروفون به کار بیفتند یا نه.", - "This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "داده‌های ناشناس شده (از اطَلاعاتی مثل طول تماس و شمارهٔ طرف‌ها) را به گروه تماس المنت فرستاده تا در بهینه‌سازی برنامه بر پایهٔ چگونگی استفاده‌اش یاریمان کنند.", "Single-key keyboard shortcuts": "میان‌برهای صفحه‌کلید تک‌کلیده", "Element Call Home": "خانهٔ تماس المنت", "Copy": "رونوشت", - "Allow analytics": "نمایش تجزیه‌ها", "Advanced": "پیش رفته", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>اکنون به تماس پیوسته<1>یا<2>پیوند تماس را رونوشت کرده و بعداً بپیوندید", "{{name}} (Waiting for video...)": "{{name}} (منتظر تصویر…)", diff --git a/public/locales/fr/app.json b/public/locales/fr/app.json index 00536365..7cd305b3 100644 --- a/public/locales/fr/app.json +++ b/public/locales/fr/app.json @@ -129,11 +129,9 @@ "Sending debug logs…": "Envoi des journaux de débogage…", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Rejoindre l’appel maintenant<1>Ou<2>Copier le lien de l’appel et rejoindre plus tard", "{{name}} (Connecting...)": "{{name}} (Connexion…)", - "Allow analytics": "Autoriser les statistiques", "Advanced": "Avancé", "Element Call Home": "Accueil Element Call", "Copy": "Copier", - "This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Cela enverra des données anonymisées (telles que la durée d’un appel et le nombre de participants) à l’équipe de Element Call pour aider à optimiser l’application en fonction de l’utilisation.", "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Bascule sur les raccourcis clavier à touche unique, par exemple « m » pour désactiver / activer le micro.", "Single-key keyboard shortcuts": "Raccourcis clavier en une touche", "{{name}} (Waiting for video...)": "{{name}} (En attente de vidéo…)", diff --git a/public/locales/id/app.json b/public/locales/id/app.json index 267c0622..71fe64ac 100644 --- a/public/locales/id/app.json +++ b/public/locales/id/app.json @@ -129,11 +129,9 @@ "Sending debug logs…": "Mengirimkan catatan pengawakutuan…", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Bergabung panggilan sekarang<1>Atau<2>Salin tautan dan bergabung nanti", "{{name}} (Connecting...)": "{{name}} (Menghubungkan...)", - "Allow analytics": "Perbolehkan analitik", "Advanced": "Tingkat lanjut", "Element Call Home": "Beranda Element Call", "Copy": "Salin", - "This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Ini akan mengirimkan data anonim (seperti durasi dan jumlah peserta panggilan) ke tim Element Call untuk membantu kami mengoptimalkan aplikasi berdasarkan bagaimana penggunaannya.", "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Apakah pintasan papan ketik seharusnya diaktifkan, mis. 'm' untuk membisukan/menyuarakan mikrofon.", "Single-key keyboard shortcuts": "Pintasan papan ketik satu tombol", "{{name}} (Waiting for video...)": "{{name}} (Menunggu video...)", diff --git a/public/locales/ja/app.json b/public/locales/ja/app.json index 8f75a621..0550c780 100644 --- a/public/locales/ja/app.json +++ b/public/locales/ja/app.json @@ -10,7 +10,6 @@ "Accept camera/microphone permissions to join the call.": "通話に参加するには、カメラ・マイクの許可が必要です。", "<0>Oops, something's gone wrong.": "<0>何かがうまく行きませんでした。", "Camera/microphone permissions needed to join the call.": "通話に参加する場合、カメラ・マイクの許可が必要です。", - "Allow analytics": "分析を許可", "Camera": "カメラ", "Call link copied": "通話リンクをコピーしました", "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "「今すぐ通話に参加」をクリックすると、<2>利用規約に同意したとみなされます", @@ -18,7 +17,6 @@ "Avatar": "アバター", "Accept microphone permissions to join the call.": "通話に参加するには、マイクの許可が必要です。", "Audio": "音声", - "Advanced": "高度", "Connection lost": "接続が切断されました", "Confirm password": "パスワードを確認", "Close": "閉じる", diff --git a/public/locales/pl/app.json b/public/locales/pl/app.json index 0fdee20a..b8189060 100644 --- a/public/locales/pl/app.json +++ b/public/locales/pl/app.json @@ -129,7 +129,6 @@ "This feature is only supported on Firefox.": "Ta funkcjonalność jest dostępna tylko w Firefox.", "Single-key keyboard shortcuts": "Skróty klawiszowe (pojedyncze klawisze)", "Copy": "Kopiuj", - "Allow analytics": "Zezwól na analitykę", "Advanced": "Zaawansowane", "<0>Submitting debug logs will help us track down the problem.": "<0>Wysłanie logów debuggowania pomoże nam ustalić przyczynę problemu.", "<0>Oops, something's gone wrong.": "<0>Ojej, coś poszło nie tak.", diff --git a/public/locales/ru/app.json b/public/locales/ru/app.json index 2611d604..619a0f27 100644 --- a/public/locales/ru/app.json +++ b/public/locales/ru/app.json @@ -127,10 +127,8 @@ "{{displayName}}, your call is now ended": "{{displayName}}, ваш звонок завершён", "{{count}} people connected|other": "{{count}} подключилось", "{{count}} people connected|one": "{{count}} подключился", - "This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Это даст разрешение на отправку анонимизированных данных (таких, как продолжительность звонка и количество участников) команде Element Call, чтобы помочь нам оптимизировать работу приложения на основании того как оно используется.", "Element Call Home": "Главная Element Call", "Copy": "Копировать", - "Allow analytics": "Разрешить аналитику", "Advanced": "Расширенные", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Присоединиться сейчас<1>или<1><2>cкопировать ссылку на звонок и присоединиться позже", "{{name}} (Connecting...)": "{{name}} (Соединение...)", diff --git a/public/locales/sk/app.json b/public/locales/sk/app.json index dc2de00f..2afed738 100644 --- a/public/locales/sk/app.json +++ b/public/locales/sk/app.json @@ -8,7 +8,6 @@ "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Či chcete povoliť jednotlačidlové klávesové skratky, napr. \"m\" na stlmenie/zapnutie mikrofónu.", "Waiting for other participants…": "Čaká sa na ďalších účastníkov…", "Waiting for network": "Čakanie na sieť", - "This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Týmto spôsobom sa budú posielať anonymizované údaje (napríklad trvanie hovoru a počet účastníkov) tímu Element Call, aby nám pomohli optimalizovať aplikáciu na základe toho, ako sa používa.", "This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Zvuk reproduktora tak bude vyzerať, akoby vychádzal z miesta, kde je na obrazovke umiestnená jeho ikona. (Experimentálna funkcia: môže to mať vplyv na stabilitu zvuku.)", "Thanks! We'll get right on it.": "Vďaka! Hneď sa do toho pustíme.", "Talking…": "Rozprávanie…", @@ -120,7 +119,6 @@ "Avatar": "Obrázok", "Audio": "Audio", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ďalší používateľ v tomto hovore má problém. Aby sme mohli lepšie diagnostikovať tieto problémy, chceli by sme získať záznam o ladení.", - "Allow analytics": "Povoliť analytiku", "Advanced": "Pokročilé", "Accept camera/microphone permissions to join the call.": "Prijmite povolenia kamery/mikrofónu, aby ste sa mohli pripojiť k hovoru.", "Accept microphone permissions to join the call.": "Prijmite povolenia mikrofónu, aby ste sa mohli pripojiť k hovoru.", diff --git a/public/locales/uk/app.json b/public/locales/uk/app.json index 154be67a..093e6e68 100644 --- a/public/locales/uk/app.json +++ b/public/locales/uk/app.json @@ -129,11 +129,9 @@ "{{count}} people connected|one": "{{count}} під'єднується", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Приєднатися до виклику зараз<1>Or<2>Скопіювати посилання на виклик і приєднатися пізніше", "{{name}} (Connecting...)": "{{name}} (З'єднання...)", - "Allow analytics": "Дозволити аналітику", "Advanced": "Розширені", "Element Call Home": "Домівка Element Call", "Copy": "Копіювати", - "This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Це дозволить надсилати анонімні дані (такі як тривалість виклику та кількість учасників) команді Element Call, щоб допомогти нам оптимізувати роботу застосунку на основі того, як він використовується.", "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Чи вмикати/вимикати мікрофон однією клавішею, наприклад, «m» для ввімкнення/вимкнення мікрофона.", "Single-key keyboard shortcuts": "Одноклавішні комбінації клавіш", "{{name}} (Waiting for video...)": "{{name}} (Очікування на відео...)", diff --git a/public/locales/zh-Hans/app.json b/public/locales/zh-Hans/app.json index b9837b9b..290ca9a9 100644 --- a/public/locales/zh-Hans/app.json +++ b/public/locales/zh-Hans/app.json @@ -18,7 +18,6 @@ "Unmute microphone": "取消麦克风静音", "Turn on camera": "开启摄像头", "Turn off camera": "关闭摄像头", - "This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "这将向Element Call团队发送匿名数据(如通话的持续时间和参与者的数量),以帮助我们根据使用方式优化应用程序。", "This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "这将使发言人的音频看起来像是来自他们在屏幕上的位置。(实验性功能:这可能影响音频的稳定性)", "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "本网站受reCaptcha保护,并适用Google<2>隐私政策和<6>服务条款。<9>点击\"注册\"则表明您同意我们的<12>条款和条件", "This call already exists, would you like to join?": "该通话已存在,你想加入吗?", @@ -38,7 +37,6 @@ "Sign in": "登录", "Audio": "音频", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "这个通话中的另一个用户出现了问题。为了更好地诊断这些问题,我们想收集调试日志。", - "Allow analytics": "允许进行分析", "Advanced": "偏好", "Accept microphone permissions to join the call.": "授予麦克风权限以加入通话。", "Accept camera/microphone permissions to join the call.": "授予摄像头/麦克风权限以加入通话。", From a15ce32ecf6dea4535f80661310d7eaeaeb08ef3 Mon Sep 17 00:00:00 2001 From: Vri Date: Sat, 4 Mar 2023 13:07:45 +0000 Subject: [PATCH 066/286] Translated using Weblate (German) Currently translated at 100.0% (143 of 143 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/de/ --- public/locales/de/app.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/public/locales/de/app.json b/public/locales/de/app.json index 4115953d..23f5cd82 100644 --- a/public/locales/de/app.json +++ b/public/locales/de/app.json @@ -129,7 +129,6 @@ "Sending debug logs…": "Sende Debug-Protokolle …", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Anruf beitreten<1>Oder<2>Anruflink kopieren und später beitreten", "{{name}} (Connecting...)": "{{name}} (verbindet sich …)", - "Advanced": "Erweitert", "Copy": "Kopieren", "Element Call Home": "Element Call-Startseite", "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Ob Tastenkürzel mit nur einer Taste aktiviert sein sollen, z. B. „m“ um das Mikrofon stumm/aktiv zu schalten.", @@ -138,5 +137,9 @@ "This feature is only supported on Firefox.": "Diese Funktion wird nur in Firefox unterstützt.", "<0>Submitting debug logs will help us track down the problem.": "<0>Übermittelte Problemberichte helfen uns, Fehler zu beheben.", "<0>Oops, something's gone wrong.": "<0>Hoppla, etwas ist schiefgelaufen.", - "Use the upcoming grid system": "Nutze das kommende Rastersystem" + "Use the upcoming grid system": "Nutze das kommende Rastersystem", + "Privacy Policy": "Datenschutzerklärung", + "Expose developer settings in the settings window.": "Zeige die Entwicklereinstellungen im Einstellungsfenster.", + "Developer Settings": "Entwicklereinstellungen", + "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "Mit dem Anwählen dieses Feldes akzeptierst du die Sammlung anonymer Daten, die wir zur Verbesserung deiner Erfahrung verwenden. Weitere Informationen dazu, welche Daten wir sammeln, findest du in unserer " } From fa97f51907a1e1195d45a20520f41d251c4dd974 Mon Sep 17 00:00:00 2001 From: Linerly Date: Sat, 4 Mar 2023 14:22:54 +0000 Subject: [PATCH 067/286] Translated using Weblate (Indonesian) Currently translated at 100.0% (143 of 143 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/id/ --- public/locales/id/app.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/public/locales/id/app.json b/public/locales/id/app.json index 71fe64ac..705146f2 100644 --- a/public/locales/id/app.json +++ b/public/locales/id/app.json @@ -129,7 +129,6 @@ "Sending debug logs…": "Mengirimkan catatan pengawakutuan…", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Bergabung panggilan sekarang<1>Atau<2>Salin tautan dan bergabung nanti", "{{name}} (Connecting...)": "{{name}} (Menghubungkan...)", - "Advanced": "Tingkat lanjut", "Element Call Home": "Beranda Element Call", "Copy": "Salin", "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Apakah pintasan papan ketik seharusnya diaktifkan, mis. 'm' untuk membisukan/menyuarakan mikrofon.", @@ -138,5 +137,9 @@ "This feature is only supported on Firefox.": "Fitur ini hanya didukung di Firefox.", "<0>Submitting debug logs will help us track down the problem.": "<0>Mengirim catatan pengawakutuan akan membantu kami melacak masalahnya.", "<0>Oops, something's gone wrong.": "<0>Aduh, ada yang salah.", - "Use the upcoming grid system": "Gunakan sistem kisi yang akan segera datang" + "Use the upcoming grid system": "Gunakan sistem kisi yang akan segera datang", + "Privacy Policy": "Kebijakan Privasi kami", + "Expose developer settings in the settings window.": "Ekspos pengaturan pengembang dalam jendela pengaturan.", + "Developer Settings": "Pengaturan Pengembang", + "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "Dengan mencentang kotak ini Anda setuju untuk pengumpulan data anonim, yang kami gunakan untuk meningkatkan pengalaman. Anda dapat mempelajari lebih banyak informasi tentang data yang kami lacak di " } From 44762afd2a5fd5759164a68bbb04a06c9ef4f1a5 Mon Sep 17 00:00:00 2001 From: Glandos Date: Sun, 5 Mar 2023 09:42:16 +0000 Subject: [PATCH 068/286] Translated using Weblate (French) Currently translated at 100.0% (143 of 143 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/fr/ --- public/locales/fr/app.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/public/locales/fr/app.json b/public/locales/fr/app.json index 7cd305b3..5d80df38 100644 --- a/public/locales/fr/app.json +++ b/public/locales/fr/app.json @@ -129,7 +129,6 @@ "Sending debug logs…": "Envoi des journaux de débogage…", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Rejoindre l’appel maintenant<1>Ou<2>Copier le lien de l’appel et rejoindre plus tard", "{{name}} (Connecting...)": "{{name}} (Connexion…)", - "Advanced": "Avancé", "Element Call Home": "Accueil Element Call", "Copy": "Copier", "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Bascule sur les raccourcis clavier à touche unique, par exemple « m » pour désactiver / activer le micro.", @@ -138,5 +137,9 @@ "This feature is only supported on Firefox.": "Cette fonctionnalité est prise en charge dans Firefox uniquement.", "<0>Submitting debug logs will help us track down the problem.": "<0>Soumettre les journaux de débogage nous aidera à déterminer le problème.", "<0>Oops, something's gone wrong.": "<0>Oups, quelque chose s’est mal passé.", - "Use the upcoming grid system": "Utiliser le futur système de grille" + "Use the upcoming grid system": "Utiliser le futur système de grille", + "Privacy Policy": "Politique de confidentialité", + "Expose developer settings in the settings window.": "Affiche les paramètres développeurs dans la fenêtre des paramètres.", + "Developer Settings": "Paramètres développeurs", + "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "En cochant cette case vous consentez à la collecte de données anonymes, que nous nous utilisons pour améliorer votre expérience. Vous trouverez plus d’informations sur les données collectées dans notre " } From 6fceeec323b79f8d34157e3f10acecce67b2a8e4 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Sat, 4 Mar 2023 09:59:13 +0000 Subject: [PATCH 069/286] Translated using Weblate (Ukrainian) Currently translated at 100.0% (143 of 143 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/ --- public/locales/uk/app.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/public/locales/uk/app.json b/public/locales/uk/app.json index 093e6e68..cd1894ba 100644 --- a/public/locales/uk/app.json +++ b/public/locales/uk/app.json @@ -129,7 +129,6 @@ "{{count}} people connected|one": "{{count}} під'єднується", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Приєднатися до виклику зараз<1>Or<2>Скопіювати посилання на виклик і приєднатися пізніше", "{{name}} (Connecting...)": "{{name}} (З'єднання...)", - "Advanced": "Розширені", "Element Call Home": "Домівка Element Call", "Copy": "Копіювати", "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Чи вмикати/вимикати мікрофон однією клавішею, наприклад, «m» для ввімкнення/вимкнення мікрофона.", @@ -138,5 +137,9 @@ "This feature is only supported on Firefox.": "Ця функція підтримується лише в браузері Firefox.", "<0>Submitting debug logs will help us track down the problem.": "<0>Надсилання журналів зневадження допоможе нам виявити проблему.", "<0>Oops, something's gone wrong.": "<0>Йой, щось пішло не за планом.", - "Use the upcoming grid system": "Використовувати майбутню сіткову систему" + "Use the upcoming grid system": "Використовувати майбутню сіткову систему", + "Privacy Policy": "Політика приватності", + "Expose developer settings in the settings window.": "Відкрийте налаштування розробника у вікні налаштувань.", + "Developer Settings": "Налаштування розробника", + "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "Ставлячи прапорець у цьому полі, ви погоджуєтеся на збір анонімних даних, які ми використовуємо для поліпшення роботи. Ви можете знайти більше інформації про те, які дані ми відстежуємо в нашому " } From 03c15d5b077ccd1ebca16fa8d95c79f9920b1921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sat, 4 Mar 2023 08:04:10 +0000 Subject: [PATCH 070/286] Translated using Weblate (Estonian) Currently translated at 100.0% (143 of 143 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/et/ --- public/locales/et/app.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/public/locales/et/app.json b/public/locales/et/app.json index 4cd51785..d2fbab69 100644 --- a/public/locales/et/app.json +++ b/public/locales/et/app.json @@ -129,7 +129,6 @@ "WebRTC is not supported or is being blocked in this browser.": "WebRTC pole kas selles brauseris toetatud või on keelatud.", "This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Muudab kõneleja heli nii, nagu tuleks see sealt, kus on tema pilt ekraanil. (See on katseline funktsionaalsus ja võib mõjutada heli stabiilsust.)", "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "Siin saidis on kasutusel ReCAPTCHA ning kehtivad Google <2>privaatsuspoliitika ja <6>teenusetingimused.<9>Klikkides „Registreeru“, nõustud meie <12>kasutustingimustega", - "Advanced": "Lisaseadistused", "Element Call Home": "Element Call Home", "Copy": "Kopeeri", "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Kas kasutame üheklahvilisi kiirklahve, näiteks „m“ mikrofoni sisse/välja lülitamiseks.", @@ -138,5 +137,9 @@ "This feature is only supported on Firefox.": "See funktsionaalsus on toetatud vaid Firefoxis.", "<0>Submitting debug logs will help us track down the problem.": "<0>Kui saadad meile vealogid, siis on lihtsam vea põhjust otsida.", "<0>Oops, something's gone wrong.": "<0>Ohoo, midagi on nüüd katki.", - "Use the upcoming grid system": "Kasuta tulevast ruudustiku-põhist paigutust" + "Use the upcoming grid system": "Kasuta tulevast ruudustiku-põhist paigutust", + "Privacy Policy": "Privaatsuspoliitika", + "Expose developer settings in the settings window.": "Näita seadistuste aknas arendajale vajalikke seadeid.", + "Developer Settings": "Arendaja seadistused", + "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "Selle valiku märkimisel lubad meil koguda anonüümseid andmeid, mida me pruugime sinu kasutajakogemuse parandamiseks. Üksikasjalikumat teavet meie kogutavate andmete kohta leiad siit - " } From 3dc288574eedf1186fafac56387531500c4ada4e Mon Sep 17 00:00:00 2001 From: Avery Date: Mon, 6 Mar 2023 15:50:53 +0000 Subject: [PATCH 071/286] Translated using Weblate (Spanish) Currently translated at 98.6% (141 of 143 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/es/ --- public/locales/es/app.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/locales/es/app.json b/public/locales/es/app.json index 4a71d8aa..3f094307 100644 --- a/public/locales/es/app.json +++ b/public/locales/es/app.json @@ -129,7 +129,6 @@ "{{displayName}}, your call is now ended": "{{displayName}}, tu llamada ha finalizado", "{{count}} people connected|other": "{{count}} personas conectadas", "{{count}} people connected|one": "{{count}} persona conectada", - "Advanced": "Avanzado", "Element Call Home": "Inicio de Element Call", "Copy": "Copiar", "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Habilita los atajos de teclado de una sola tecla, por ejemplo 'm' para silenciar/desilenciar el micrófono.", @@ -137,5 +136,8 @@ "{{name}} (Waiting for video...)": "{{name}} (Esperando al video...)", "This feature is only supported on Firefox.": "Esta característica solo está disponible en Firefox.", "<0>Submitting debug logs will help us track down the problem.": "<0>Subir los registros de depuración nos ayudará a encontrar el problema.", - "<0>Oops, something's gone wrong.": "<0>Ups, algo ha salido mal." + "<0>Oops, something's gone wrong.": "<0>Ups, algo ha salido mal.", + "Privacy Policy": "Política de privacidad", + "Expose developer settings in the settings window.": "Muestra los ajustes de desarrollador en la ventana de ajustes.", + "Developer Settings": "Ajustes de desarrollador" } From 29082adb7389bb51c61c86ede48dc3b5f2c8241d Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Mon, 6 Mar 2023 02:34:43 +0000 Subject: [PATCH 072/286] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (143 of 143 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hant/ --- public/locales/zh-Hant/app.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/locales/zh-Hant/app.json b/public/locales/zh-Hant/app.json index a3d0a108..22ef710b 100644 --- a/public/locales/zh-Hant/app.json +++ b/public/locales/zh-Hant/app.json @@ -10,5 +10,8 @@ "{{name}} (Connecting...)": "{{name}} (連線中 ...)", "{{displayName}}, your call is now ended": "{{displayName}},您的通話現在已結束", "{{count}} people connected|other": "{{count}} 人已連線", - "{{count}} people connected|one": "{{count}} 人已連線" + "{{count}} people connected|one": "{{count}} 人已連線", + "Use the upcoming grid system": "使用即將推出的網格系統", + "Privacy Policy": "隱私權政策", + "Expose developer settings in the settings window.": "在設定視窗中顯示開發者設定。" } From 345891dbdf0482ed1deefe2f586a4f03871f518d Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Sat, 4 Mar 2023 14:28:54 +0000 Subject: [PATCH 073/286] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (143 of 143 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hant/ --- public/locales/zh-Hant/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/locales/zh-Hant/app.json b/public/locales/zh-Hant/app.json index 22ef710b..13c3eb05 100644 --- a/public/locales/zh-Hant/app.json +++ b/public/locales/zh-Hant/app.json @@ -13,5 +13,9 @@ "{{count}} people connected|one": "{{count}} 人已連線", "Use the upcoming grid system": "使用即將推出的網格系統", "Privacy Policy": "隱私權政策", - "Expose developer settings in the settings window.": "在設定視窗中顯示開發者設定。" + "Expose developer settings in the settings window.": "在設定視窗中顯示開發者設定。", + "Developer Settings": "開發者設定", + "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "勾選這個選取盒,代表您同意我們以匿名方式收集資料,用於改善您的使用體驗。您可以在下列位置找到我們會收集哪些資料的相關資訊: ", + "<0>Submitting debug logs will help us track down the problem.": "<0>送出除錯紀錄,可幫助我們修正問題。", + "<0>Oops, something's gone wrong.": "<0>喔喔,有些地方怪怪的。" } From 1913abc682a67b5581f2f60c5856f00122887746 Mon Sep 17 00:00:00 2001 From: Open Culture Foundation Date: Sat, 4 Mar 2023 12:03:33 +0000 Subject: [PATCH 074/286] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (143 of 143 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hant/ --- public/locales/zh-Hant/app.json | 150 +++++++++++++++++++++++++++++--- 1 file changed, 137 insertions(+), 13 deletions(-) diff --git a/public/locales/zh-Hant/app.json b/public/locales/zh-Hant/app.json index 13c3eb05..f5683c10 100644 --- a/public/locales/zh-Hant/app.json +++ b/public/locales/zh-Hant/app.json @@ -1,21 +1,145 @@ { - "<0>Join call now<1>Or<2>Copy call link and join later": "<0>加入通話<1>或<2>複製通話連結並稍候加入", - "<0>Create an account Or <2>Access as a guest": "<0>建立帳號 或 <2>訪客模式", - "<0>Already have an account?<1><0>Log in Or <2>Access as a guest": "<0>已經有帳號了?<1><0>登入 Or <2>訪客模式登入", - "{{roomName}} - Walkie-talkie call": "{{roomName}} - 無線電通話", - "{{names}}, {{name}}": "{{names}},{{name}}", - "{{name}} is talking…": "{{name}} 正在交談中…", - "{{name}} is presenting": "{{name}} 正在報告", - "{{name}} (Waiting for video...)": "{{name}} (等待視訊畫面 ...)", - "{{name}} (Connecting...)": "{{name}} (連線中 ...)", - "{{displayName}}, your call is now ended": "{{displayName}},您的通話現在已結束", - "{{count}} people connected|other": "{{count}} 人已連線", - "{{count}} people connected|one": "{{count}} 人已連線", + "<0>Join call now<1>Or<2>Copy call link and join later": "<0>現在加入通話<1>或<2>複製通話連結,稍後再加入", + "<0>Create an account Or <2>Access as a guest": "<0>建立帳號 或<2>以訪客身份登入", + "<0>Already have an account?<1><0>Log in Or <2>Access as a guest": "<0>已經有帳號?<1><0>登入 或<2>以訪客身份登入", + "{{roomName}} - Walkie-talkie call": "{{roomName}} - 對講機式通話", + "{{names}}, {{name}}": "{{names}}, {{name}}", + "{{name}} is talking…": "{{name}} 正在發言…", + "{{name}} is presenting": "{{name}} 已上線", + "{{name}} (Waiting for video...)": "{{name}} (等候視訊中...)", + "{{name}} (Connecting...)": "{{name}} (連結中...)", + "{{displayName}}, your call is now ended": "{{displayName}},您的通話已結束", + "{{count}} people connected|other": "{{count}} 人已連結", + "{{count}} people connected|one": "{{count}} 人已連結", "Use the upcoming grid system": "使用即將推出的網格系統", "Privacy Policy": "隱私權政策", "Expose developer settings in the settings window.": "在設定視窗中顯示開發者設定。", "Developer Settings": "開發者設定", "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "勾選這個選取盒,代表您同意我們以匿名方式收集資料,用於改善您的使用體驗。您可以在下列位置找到我們會收集哪些資料的相關資訊: ", "<0>Submitting debug logs will help us track down the problem.": "<0>送出除錯紀錄,可幫助我們修正問題。", - "<0>Oops, something's gone wrong.": "<0>喔喔,有些地方怪怪的。" + "<0>Oops, something's gone wrong.": "<0>喔喔,有些地方怪怪的。", + "Your recent calls": "您最近的通話", + "You can't talk at the same time": "您無法在同一時間發言", + "Yes, join call": "是,加入對話", + "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "是否要啟用快捷鍵,例如:開/關麥克風。", + "WebRTC is not supported or is being blocked in this browser.": "此瀏覽器未支援 WebRTC 或 WebRTC 被瀏覽器封鎖。", + "Walkie-talkie call name": "對講機式通話名稱", + "Walkie-talkie call": "即時通話", + "Waiting for other participants…": "等待其他參加者…", + "Waiting for network": "等待網路連線", + "Video call name": "視訊通話姓名", + "Video call": "視訊通話", + "Video": "視訊", + "Version: {{version}}": "版本: {{version}}", + "Username": "使用者名稱", + "User menu": "使用者選單", + "User ID": "使用者 ID", + "Unmute microphone": "取消麥克風靜音", + "Turn on camera": "開啟相機", + "Turn off camera": "關閉相機", + "This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "這會使得發言者的聲音聽起來,像從他們在畫面中的位置傳來(實驗性功能:這可能會影響語音的穩定性。)", + "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy and <6>Terms of Service apply.<9>By clicking \"Register\", you agree to our <12>Terms and conditions": "此網站使用Google 驗證碼技術保護,適用<2>隱私條款 與<6>條款與細則 。<9>按下「註冊」,表示您同意我們的<12>條款與細則", + "This feature is only supported on Firefox.": "只有 Firefox 支援此功能。", + "This call already exists, would you like to join?": "通話已經開始,請問您要加入嗎?", + "Thanks! We'll get right on it.": "謝謝您!我們會盡快處理。", + "Talking…": "對話中…", + "Talk over speaker": "以擴音對話", + "Take me Home": "帶我回主畫面", + "Submitting feedback…": "遞交回饋…", + "Submit feedback": "遞交回覆", + "Stop sharing screen": "停止分享螢幕畫面", + "Spotlight": "聚焦", + "Speaker {{n}}": "發言者{{n}}", + "Speaker": "發言者", + "Spatial audio": "空間音效", + "Single-key keyboard shortcuts": "快捷鍵", + "Sign out": "登出", + "Sign in": "登入", + "Show call inspector": "顯示通話稽查員", + "Share screen": "分享畫面", + "Settings": "設定", + "Sending…": "傳送中…", + "Sending debug logs…": "傳送除錯記錄檔中…", + "Send debug logs": "傳送除錯紀錄", + "Select an option": "選擇一個選項", + "Saving…": "儲存中…", + "Save": "儲存", + "Return to home screen": "回到首頁", + "Remove": "移除", + "Release to stop": "放開以停止", + "Release spacebar key to stop": "放開空白鍵以停止", + "Registering…": "註冊中…", + "Register": "註冊", + "Recaptcha not loaded": "驗證碼未載入", + "Recaptcha dismissed": "略過驗證碼", + "Profile": "個人檔案", + "Press and hold to talk over {{name}}": "與{{name}}對話時,請按住按鍵", + "Press and hold to talk": "請按住按鍵來發言", + "Press and hold spacebar to talk over {{name}}": "與{{name}}對話時,請按住空白鍵", + "Press and hold spacebar to talk": "說話時請按住空白鍵", + "Passwords must match": "密碼必須相符", + "Password": "密碼", + "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}": "有使用者試著加入通話,但他們的軟體版本不相容。這些使用者需要確認已將瀏覽器更新到最新版本:<1>{userLis}", + "Not registered yet? <2>Create an account": "還沒註冊嗎?<2>建立帳號", + "Not now, return to home screen": "現在不行,回到首頁", + "No": "否", + "Mute microphone": "麥克風靜音", + "More menu": "更多選單", + "More": "更多", + "Microphone permissions needed to join the call.": "加入通話前需要取得麥克風的權限。", + "Microphone {{n}}": "麥克風 {{n}}", + "Microphone": "麥克風", + "Login to your account": "登入您的帳號", + "Login": "登入", + "Logging in…": "登入中…", + "Local volume": "您的音量", + "Loading…": "載入中…", + "Loading room…": "載入聊天室…", + "Leave": "離開", + "Join existing call?": "加入已開始的通話嗎?", + "Join call now": "現在加入通話", + "Join call": "加入通話", + "Invite people": "邀請夥伴", + "Invite": "邀請", + "Inspector": "稽查員", + "Incompatible versions!": "不相容版本!", + "Incompatible versions": "不相容版本", + "Include debug logs": "包含除錯紀錄", + "Home": "首頁", + "Having trouble? Help us fix it.": "遇到問題嗎?請讓我們協助您。", + "Grid layout menu": "格框式清單", + "Go": "前往", + "Full screen": "全螢幕", + "Freedom": "自由", + "Fetching group call timed out.": "加入群組對話已逾時。", + "Exit full screen": "退出全螢幕", + "Element Call Home": "Element Call 首頁", + "Download debug logs": "下載偵錯報告", + "Display name": "顯示名稱", + "Developer": "開發者", + "Details": "詳細說明", + "Description (optional)": "描述(選擇性)", + "Debug log request": "請求偵錯報告", + "Debug log": "除錯紀錄", + "Create account": "建立帳號", + "Copy and share this call link": "複製並分享通話連結", + "Copy": "複製", + "Copied!": "已複製!", + "Connection lost": "連線中斷", + "Confirm password": "確認密碼", + "Close": "關閉", + "Change layout": "變更排列", + "Camera/microphone permissions needed to join the call.": "加入通話需要取得相機/麥克風的權限。", + "Camera {{n}}": "相機 {{n}}", + "Camera": "相機", + "Call type menu": "通話類型選單", + "Call link copied": "已複製通話連結", + "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "當您按下「加入通話」,您也同時同意了我們的條款與細則", + "By clicking \"Go\", you agree to our <2>Terms and conditions": "當您按下「前往」,你也同意了我們的條款與細則", + "Avatar": "大頭照", + "Audio": "語音", + "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "這通對話中的另一位使用者遇到了某些問題。為了診斷問題,我們將會建立除錯紀錄。", + "Accept microphone permissions to join the call.": "請授權使用您的麥克風以加入通話。", + "Accept camera/microphone permissions to join the call.": "請授權使用您的相機/麥克風以加入對話。", + "<0>Why not finish by setting up a password to keep your account?<1>You'll be able to keep your name and set an avatar for use on future calls": "<0>何不設定密碼以保留此帳號?<1>您可以保留暱稱並設定頭像,以便未來通話時使用" } From ca1f502e6226e04d1cc182f4c30ee414fdf3488e Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Sat, 4 Mar 2023 18:58:54 +0000 Subject: [PATCH 075/286] Translated using Weblate (Slovak) Currently translated at 100.0% (143 of 143 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/sk/ --- public/locales/sk/app.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/public/locales/sk/app.json b/public/locales/sk/app.json index 2afed738..41ae7ff3 100644 --- a/public/locales/sk/app.json +++ b/public/locales/sk/app.json @@ -119,7 +119,6 @@ "Avatar": "Obrázok", "Audio": "Audio", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ďalší používateľ v tomto hovore má problém. Aby sme mohli lepšie diagnostikovať tieto problémy, chceli by sme získať záznam o ladení.", - "Advanced": "Pokročilé", "Accept camera/microphone permissions to join the call.": "Prijmite povolenia kamery/mikrofónu, aby ste sa mohli pripojiť k hovoru.", "Accept microphone permissions to join the call.": "Prijmite povolenia mikrofónu, aby ste sa mohli pripojiť k hovoru.", "<0>Why not finish by setting up a password to keep your account?<1>You'll be able to keep your name and set an avatar for use on future calls": "<0>Prečo neskončiť nastavením hesla, aby ste si zachovali svoj účet? <1>Budete si môcť ponechať svoje meno a nastaviť obrázok, ktorý sa bude používať pri budúcich hovoroch", @@ -138,5 +137,9 @@ "This feature is only supported on Firefox.": "Táto funkcia je podporovaná len v prehliadači Firefox.", "<0>Submitting debug logs will help us track down the problem.": "<0>Odoslanie záznamov ladenia nám pomôže nájsť problém.", "<0>Oops, something's gone wrong.": "<0>Hups, niečo sa pokazilo.", - "Use the upcoming grid system": "Použiť pripravovaný systém mriežky" + "Use the upcoming grid system": "Použiť pripravovaný systém mriežky", + "Privacy Policy": "Zásady ochrany osobných údajov", + "Expose developer settings in the settings window.": "Zobraziť nastavenia pre vývojárov v okne nastavení.", + "Developer Settings": "Nastavenia pre vývojárov", + "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "Označením tohto políčka súhlasíte so zhromažďovaním anonymných údajov, ktoré používame na zlepšenie vašich skúseností. Viac informácií o tom, ktoré údaje sledujeme, nájdete v našich " } From 7e1d5fff14aaa3905cc90c3a0225d5c81e8e852d Mon Sep 17 00:00:00 2001 From: Weblate Date: Tue, 7 Mar 2023 16:33:45 +0000 Subject: [PATCH 076/286] Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/ --- public/locales/cs/app.json | 1 - public/locales/fa/app.json | 1 - public/locales/pl/app.json | 1 - public/locales/ru/app.json | 1 - public/locales/zh-Hans/app.json | 1 - 5 files changed, 5 deletions(-) diff --git a/public/locales/cs/app.json b/public/locales/cs/app.json index 567a778f..390c33c0 100644 --- a/public/locales/cs/app.json +++ b/public/locales/cs/app.json @@ -130,7 +130,6 @@ "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Kliknutím na \"Připojit se do hovoru\", odsouhlasíte naše <2>Terms and conditions", "By clicking \"Go\", you agree to our <2>Terms and conditions": "Kliknutím na \"Pokračovat\", odsouhlasíte naše <2>Terms and conditions", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Jiný uživatel v tomto hovoru má problémy. Abychom mohli diagnostikovat problém, rádi bychom shromáždili protokoly ladění.", - "Advanced": "Pokročilé", "<0>Why not finish by setting up a password to keep your account?<1>You'll be able to keep your name and set an avatar for use on future calls": "<0>Proč neskončit nastavením hesla, abyste mohli účet použít znovu?<1>Budete si moci nechat své jméno a nastavit si avatar pro budoucí hovory ", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Připojit se<1>Or<2>Zkopírovat odkaz a připojit se později", "<0>Already have an account?<1><0>Log in Or <2>Access as a guest": "<0>Už máte účet?<1><0>Přihlásit se Or <2>Jako host", diff --git a/public/locales/fa/app.json b/public/locales/fa/app.json index 2a89ebd9..6bc8749f 100644 --- a/public/locales/fa/app.json +++ b/public/locales/fa/app.json @@ -131,7 +131,6 @@ "Single-key keyboard shortcuts": "میان‌برهای صفحه‌کلید تک‌کلیده", "Element Call Home": "خانهٔ تماس المنت", "Copy": "رونوشت", - "Advanced": "پیش رفته", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>اکنون به تماس پیوسته<1>یا<2>پیوند تماس را رونوشت کرده و بعداً بپیوندید", "{{name}} (Waiting for video...)": "{{name}} (منتظر تصویر…)", "{{name}} (Connecting...)": "{{name}} (وصل شدن…)" diff --git a/public/locales/pl/app.json b/public/locales/pl/app.json index b8189060..fecfbfbf 100644 --- a/public/locales/pl/app.json +++ b/public/locales/pl/app.json @@ -129,7 +129,6 @@ "This feature is only supported on Firefox.": "Ta funkcjonalność jest dostępna tylko w Firefox.", "Single-key keyboard shortcuts": "Skróty klawiszowe (pojedyncze klawisze)", "Copy": "Kopiuj", - "Advanced": "Zaawansowane", "<0>Submitting debug logs will help us track down the problem.": "<0>Wysłanie logów debuggowania pomoże nam ustalić przyczynę problemu.", "<0>Oops, something's gone wrong.": "<0>Ojej, coś poszło nie tak.", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Dołącz do rozmowy teraz<1>Or<2>Skopiuj link do rozmowy i dołącz później", diff --git a/public/locales/ru/app.json b/public/locales/ru/app.json index 619a0f27..9e510505 100644 --- a/public/locales/ru/app.json +++ b/public/locales/ru/app.json @@ -129,7 +129,6 @@ "{{count}} people connected|one": "{{count}} подключился", "Element Call Home": "Главная Element Call", "Copy": "Копировать", - "Advanced": "Расширенные", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Присоединиться сейчас<1>или<1><2>cкопировать ссылку на звонок и присоединиться позже", "{{name}} (Connecting...)": "{{name}} (Соединение...)", "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Включить горячие клавиши, например 'm' чтобы отключить/включить микрофон.", diff --git a/public/locales/zh-Hans/app.json b/public/locales/zh-Hans/app.json index 290ca9a9..6e0f6af4 100644 --- a/public/locales/zh-Hans/app.json +++ b/public/locales/zh-Hans/app.json @@ -37,7 +37,6 @@ "Sign in": "登录", "Audio": "音频", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "这个通话中的另一个用户出现了问题。为了更好地诊断这些问题,我们想收集调试日志。", - "Advanced": "偏好", "Accept microphone permissions to join the call.": "授予麦克风权限以加入通话。", "Accept camera/microphone permissions to join the call.": "授予摄像头/麦克风权限以加入通话。", "<0>Why not finish by setting up a password to keep your account?<1>You'll be able to keep your name and set an avatar for use on future calls": "<0>为什么不设置一个密码来保留你的账户?<1>你将可以保留你的名字并设置一个头像,以便在未来的通话中使用。", From 4c59638d00f28751466f43f822b472be88d7509a Mon Sep 17 00:00:00 2001 From: Timo K Date: Fri, 10 Mar 2023 10:33:54 +0100 Subject: [PATCH 077/286] otel for call start, end and mute This is send over zipkin. And it uses a posthog exporter to export events to posthog using a _otel prefix --- package.json | 8 + src/analytics/OtelPosthogExporter.ts | 57 ++++++ src/analytics/PosthogAnalytics.ts | 16 ++ src/room/GroupCallView.tsx | 5 +- src/room/useGroupCall.ts | 3 + src/telemetry/otel.ts | 128 ++++++++++++ yarn.lock | 285 +++++++++++++++++++++++++-- 7 files changed, 490 insertions(+), 12 deletions(-) create mode 100644 src/analytics/OtelPosthogExporter.ts create mode 100644 src/telemetry/otel.ts diff --git a/package.json b/package.json index 4242b19c..0adbe947 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,14 @@ "dependencies": { "@juggle/resize-observer": "^3.3.1", "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz", + "@opentelemetry/api": "^1.4.0", + "@opentelemetry/context-zone": "^1.9.1", + "@opentelemetry/exporter-jaeger": "^1.9.1", + "@opentelemetry/exporter-trace-otlp-http": "^0.35.1", + "@opentelemetry/exporter-zipkin": "^1.9.1", + "@opentelemetry/instrumentation-document-load": "^0.31.1", + "@opentelemetry/instrumentation-user-interaction": "^0.32.1", + "@opentelemetry/sdk-trace-web": "^1.9.1", "@react-aria/button": "^3.3.4", "@react-aria/dialog": "^3.1.4", "@react-aria/focus": "^3.5.0", diff --git a/src/analytics/OtelPosthogExporter.ts b/src/analytics/OtelPosthogExporter.ts new file mode 100644 index 00000000..ee72155e --- /dev/null +++ b/src/analytics/OtelPosthogExporter.ts @@ -0,0 +1,57 @@ +import { SpanExporter } from "@opentelemetry/sdk-trace-base"; +import { ReadableSpan } from "@opentelemetry/sdk-trace-base"; +import { ExportResult, ExportResultCode } from "@opentelemetry/core"; + +import { PosthogAnalytics } from "./PosthogAnalytics"; +/** + * This is implementation of {@link SpanExporter} that prints spans to the + * console. This class can be used for diagnostic purposes. + */ +export class PosthogSpanExporter implements SpanExporter { + /** + * Export spans. + * @param spans + * @param resultCallback + */ + async export( + spans: ReadableSpan[], + resultCallback: (result: ExportResult) => void + ): Promise { + console.log("POSTHOGEXPORTER", spans); + for (let i = 0; i < spans.length; i++) { + const span = spans[i]; + const sendInstantly = + span.name == "otel_callEnded" || + span.name == "otel_otherSentInstantlyEventName"; + + await PosthogAnalytics.instance.trackFromSpan( + { eventName: span.name, ...span.attributes }, + { + send_instantly: sendInstantly, + } + ); + resultCallback({ code: ExportResultCode.SUCCESS }); + } + } + /** + * Shutdown the exporter. + */ + shutdown(): Promise { + console.log("POSTHOGEXPORTER shutdown of otelPosthogExporter"); + return new Promise((resolve, _reject) => { + resolve(); + }); + } + /** + * converts span info into more readable format + * @param span + */ + // private _exportInfo; + /** + * Showing spans in console + * @param spans + * @param done + */ + // private _sendSpans; +} +//# sourceMappingURL=ConsoleSpanExporter.d.ts.map diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index e2e8fdae..718a49c2 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -385,6 +385,22 @@ export class PosthogAnalytics { this.capture(eventName, properties, options); } + public async trackFromSpan( + { eventName, ...properties }, + options?: CaptureOptions + ): Promise { + if (this.identificationPromise) { + // only make calls to posthog after the identificaion is done + await this.identificationPromise; + } + if ( + this.anonymity == Anonymity.Disabled || + this.anonymity == Anonymity.Anonymous + ) + return; + this.capture(eventName, properties, options); + } + public startListeningToSettingsChanges(): void { // Listen to account data changes from sync so we can observe changes to relevant flags and update. // This is called - diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 7af31d63..242e0e5e 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -35,6 +35,7 @@ import { useLocationNavigation } from "../useLocationNavigation"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { useMediaHandler } from "../settings/useMediaHandler"; import { findDeviceByName, getDevices } from "../media-utils"; +import { callTracer } from "../telemetry/otel"; declare global { interface Window { @@ -143,7 +144,7 @@ export function GroupCallView({ ]); await groupCall.enter(); - + callTracer.startCall(groupCall.groupCallId); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); @@ -164,6 +165,7 @@ export function GroupCallView({ if (isEmbedded && !preload) { // In embedded mode, bypass the lobby and just enter the call straight away groupCall.enter(); + callTracer.startCall(groupCall.groupCallId); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); @@ -187,6 +189,7 @@ export function GroupCallView({ // In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent, // therefore we want the event to be sent instantly without getting queued/batched. + callTracer.endCall(); const sendInstantly = !!widget; PosthogAnalytics.instance.eventCallEnded.track( groupCall.groupCallId, diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 37484b43..a3250736 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -32,6 +32,7 @@ import { usePageUnload } from "./usePageUnload"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { TranslatedError, translatedError } from "../TranslatedError"; import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget"; +import { callTracer } from "../telemetry/otel"; export enum ConnectionState { EstablishingCall = "establishing call", // call hasn't been established yet @@ -375,6 +376,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { ) { return; } + callTracer.startCall(groupCall.groupCallId); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); @@ -399,6 +401,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { const setMicrophoneMuted = useCallback( (setMuted) => { groupCall.setMicrophoneMuted(setMuted); + callTracer.muteMic(setMuted); PosthogAnalytics.instance.eventMuteMicrophone.track( setMuted, groupCall.groupCallId diff --git a/src/telemetry/otel.ts b/src/telemetry/otel.ts new file mode 100644 index 00000000..c8836692 --- /dev/null +++ b/src/telemetry/otel.ts @@ -0,0 +1,128 @@ +/* document-load.ts|js file - the code is the same for both the languages */ +import { + ConsoleSpanExporter, + SimpleSpanProcessor, +} from "@opentelemetry/sdk-trace-base"; +import { ZipkinExporter } from "@opentelemetry/exporter-zipkin"; +// import { JaegerExporter } from "@opentelemetry/exporter-jaeger"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; +import { WebTracerProvider } from "@opentelemetry/sdk-trace-web"; +import { ZoneContextManager } from "@opentelemetry/context-zone"; +import { registerInstrumentations } from "@opentelemetry/instrumentation"; +import opentelemetry from "@opentelemetry/api"; +import { Resource } from "@opentelemetry/resources"; +import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; + +import { PosthogSpanExporter } from "../analytics/OtelPosthogExporter"; + +const SERVICE_NAME = "element-call"; +// It is really important to set the correct content type here. Otherwise the Jaeger will crash and not accept the zipkin event +// Additionally jaeger needs to be started with zipkin on port 9411 +const optionsZipkin = { + // url: `http://localhost:9411/api/v2/spans`, + // serviceName: SERVICE_NAME, + headers: { + "Content-Type": "application/json", + }, +}; +// We DO NOT use the OTLPTraceExporter. This somehow does not hit the right endpoint and also causes issues with CORS +const collectorOptions = { + // url: `http://localhost:14268/api/v2/spans`, // url is optional and can be omitted - default is http://localhost:4318/v1/traces + headers: { "Access-Control-Allow-Origin": "*" }, // an optional object containing custom headers to be sent with each request + concurrencyLimit: 10, // an optional limit on pending requests +}; +const otlpExporter = new OTLPTraceExporter(collectorOptions); +const consoleExporter = new ConsoleSpanExporter(); +// The zipkin exporter is the actual exporter we need for web based otel applications +const zipkin = new ZipkinExporter(optionsZipkin); +const posthogExporter = new PosthogSpanExporter(); + +// This is how we can make Jaeger show a reaonsable service in the dropdown on the left. +const providerConfig = { + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME, + }), +}; +const provider = new WebTracerProvider(providerConfig); + +provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); +// We can add as many processors and exporters as we want to. The zipkin one is the important one for Jaeger +provider.addSpanProcessor(new SimpleSpanProcessor(posthogExporter)); +provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); +provider.addSpanProcessor(new SimpleSpanProcessor(zipkin)); + +// This is unecassary i think... +provider.register({ + // Changing default contextManager to use ZoneContextManager - supports asynchronous operations - optional + contextManager: new ZoneContextManager(), +}); + +// Registering instrumentations (These are automated span collectors for the Http request during page loading, switching) +registerInstrumentations({ + instrumentations: [ + // new DocumentLoadInstrumentation(), + // new UserInteractionInstrumentation(), + ], +}); + +// This is not the serviceName shown in jaeger +export const tracer = opentelemetry.trace.getTracer( + "my-element-call-otl-tracer" +); + +class CallTracer { + // We create one tracer class for each main context. + // Even if differnt tracer classes overlap in time space, we might want to visulaize them seperately. + // The Call Tracer should only contain spans/events that are relevant to understand the procedure of the individual candidates. + // Another Tracer Class (for example a ConnectionTracer) can contain a very granular list of all steps to connect to a call. + + private callSpan; + private callContext; + private muteSpan?; + public startCall(callId: string) { + // The main context will be set when initiating the main/parent span. + + // Create an initial context with the callId param + const callIdContext = opentelemetry.context + .active() + .setValue(Symbol("callId"), callId); + + // Create the main span that tracks the whole call + this.callSpan = tracer.startSpan("otel_callSpan", undefined, callIdContext); + + // Create a new call based on the callIdContext. This context also has a span assigned to it. + // Other spans can use this context to extract the parent span. + // (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent) + this.callContext = opentelemetry.trace.setSpan( + opentelemetry.context.active(), + this.callSpan + ); + + // Here we start a very short span. This is a hack to trigger the posthog exporter. + // Only ended spans are processed by the exporter. + // We want the exporter to know that a call has started + const startCallSpan = tracer.startSpan( + "otel_startCallSpan", + undefined, + this.callContext + ); + startCallSpan.end(); + } + public muteMic(muteState: boolean) { + if (muteState) { + this.muteSpan = tracer.startSpan( + "otel_muteSpan", + undefined, + this.callContext + ); + } else if (this.muteSpan) { + this.muteSpan.end(); + this.muteSpan = null; + } + } + public endCall() { + this.callSpan?.end(); + } +} + +export const callTracer = new CallTracer(); diff --git a/yarn.lock b/yarn.lock index 7980fa2c..8e518376 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1910,6 +1910,148 @@ mkdirp "^1.0.4" rimraf "^3.0.2" +"@opentelemetry/api@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.4.0.tgz#2c91791a9ba6ca0a0f4aaac5e45d58df13639ac8" + integrity sha512-IgMK9i3sFGNUqPMbjABm0G26g0QCKCUBfglhQ7rQq6WcxbKfEHRcmwsoER4hZcuYqJgkYn2OeuoJIv7Jsftp7g== + +"@opentelemetry/context-zone-peer-dep@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-zone-peer-dep/-/context-zone-peer-dep-1.9.1.tgz#634b1a25eebc68484d3568865ee5a2321b6b020d" + integrity sha512-4qaNi2noNMlT3DhOzXN4qKDiyZFjowj2vnfdtcAHZUwpIP0MQlpE3JYCr+2w7zKGJDfEOp2hg2A9Dkn8TqvzSw== + +"@opentelemetry/context-zone@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-zone/-/context-zone-1.9.1.tgz#1f1c48fb491283ab32320b3d95e542a3a3e86035" + integrity sha512-Kx2n9ftRokgHUAI6CIxsNepCsEP/fggDBH3GT27GdZkqgPYZqBn+nlTS23dB6etjWcSRd0piJnT3OIEnaxyIGA== + dependencies: + "@opentelemetry/context-zone-peer-dep" "1.9.1" + zone.js "^0.11.0" + +"@opentelemetry/core@1.9.1", "@opentelemetry/core@^1.8.0": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.9.1.tgz#e343337e1a7bf30e9a6aef3ef659b9b76379762a" + integrity sha512-6/qon6tw2I8ZaJnHAQUUn4BqhTbTNRS0WP8/bA0ynaX+Uzp/DDbd0NS0Cq6TMlh8+mrlsyqDE7mO50nmv2Yvlg== + dependencies: + "@opentelemetry/semantic-conventions" "1.9.1" + +"@opentelemetry/exporter-jaeger@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-jaeger/-/exporter-jaeger-1.9.1.tgz#941d39c2d425021c734354bbc280a4ae19f95aad" + integrity sha512-6F10NMOtBT3HdxpT0IwYf1BX8RzZB7SpqHTvZsB2vzUvxVAyoLX8+cuo6Ke9sHS9YMqoTA3rER5x9kC6NOxEMQ== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/sdk-trace-base" "1.9.1" + "@opentelemetry/semantic-conventions" "1.9.1" + jaeger-client "^3.15.0" + +"@opentelemetry/exporter-trace-otlp-http@^0.35.1": + version "0.35.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.35.1.tgz#9bf988f91fb145b29a051bce8ff5ef85029ca575" + integrity sha512-EJgAsrvscKsqb/GzF1zS74vM+Z/aQRhrFE7hs/1GK1M9bLixaVyMGwg2pxz1wdYdjxS1mqpHMhXU+VvMvFCw1w== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/otlp-exporter-base" "0.35.1" + "@opentelemetry/otlp-transformer" "0.35.1" + "@opentelemetry/resources" "1.9.1" + "@opentelemetry/sdk-trace-base" "1.9.1" + +"@opentelemetry/exporter-zipkin@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.9.1.tgz#0bcddf2f3bcb1b26b94a090c953996a28087d21f" + integrity sha512-KBgf3w84luP5vWLlrqVFKmbwFK4lXM//t6K7H4nsg576htbz1RpBbQfybADjPdXTjGHqDTtLiC5MC90hxS7Z2w== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/resources" "1.9.1" + "@opentelemetry/sdk-trace-base" "1.9.1" + "@opentelemetry/semantic-conventions" "1.9.1" + +"@opentelemetry/instrumentation-document-load@^0.31.1": + version "0.31.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-document-load/-/instrumentation-document-load-0.31.1.tgz#a535a5d1d71706701d3ff560a700b9dd03e4fb59" + integrity sha512-Ej4EB3m7GXzj4o8zF73amcnqXroN6/QdURjDAOgxN27zvvurR84larzGD7PjqgzzdtV+T7e/0BK07M0I2eA8PQ== + dependencies: + "@opentelemetry/core" "^1.8.0" + "@opentelemetry/instrumentation" "^0.35.1" + "@opentelemetry/sdk-trace-base" "^1.0.0" + "@opentelemetry/sdk-trace-web" "^1.8.0" + "@opentelemetry/semantic-conventions" "^1.0.0" + +"@opentelemetry/instrumentation-user-interaction@^0.32.1": + version "0.32.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-user-interaction/-/instrumentation-user-interaction-0.32.1.tgz#654c0352c2f7d5bb6cc21f07f9ec56f18f2cc854" + integrity sha512-27we7cENzEtO2oCRiEkYG4cFe1v94ybeLvM+5jqNDkZF7UY0GlctCW+jvqf569Z3Gs7yHrakO2sZf4EMEfTFWg== + dependencies: + "@opentelemetry/core" "^1.8.0" + "@opentelemetry/instrumentation" "^0.35.1" + "@opentelemetry/sdk-trace-web" "^1.8.0" + +"@opentelemetry/instrumentation@^0.35.1": + version "0.35.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.35.1.tgz#065bdbc4771137347e648eb4c6c6de6e9e97e4d1" + integrity sha512-EZsvXqxenbRTSNsft6LDcrT4pjAiyZOx3rkDNeqKpwZZe6GmZtsXaZZKuDkJtz9fTjOGjDHjZj9/h80Ya9iIJw== + dependencies: + require-in-the-middle "^5.0.3" + semver "^7.3.2" + shimmer "^1.2.1" + +"@opentelemetry/otlp-exporter-base@0.35.1": + version "0.35.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.35.1.tgz#535166608d5d36e6c959b2857d01245ee3a916b1" + integrity sha512-Sc0buJIs8CfUeQCL/12vDDjBREgsqHdjboBa/kPQDgMf008OBJSM02Ijj6T1TH+QVHl/VHBBEVJF+FTf0EH9Vg== + dependencies: + "@opentelemetry/core" "1.9.1" + +"@opentelemetry/otlp-transformer@0.35.1": + version "0.35.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-transformer/-/otlp-transformer-0.35.1.tgz#d4333b71324b83dbb1b0b3a4cfd769b3e214c6f9" + integrity sha512-c0HXcJ49MKoWSaA49J8PXlVx48CeEFpL0odP6KBkVT+Bw6kAe8JlI3mIezyN05VCDJGtS2I5E6WEsE+DJL1t9A== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/resources" "1.9.1" + "@opentelemetry/sdk-metrics" "1.9.1" + "@opentelemetry/sdk-trace-base" "1.9.1" + +"@opentelemetry/resources@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.9.1.tgz#5ad3d80ba968a3a0e56498ce4bc82a6a01f2682f" + integrity sha512-VqBGbnAfubI+l+yrtYxeLyOoL358JK57btPMJDd3TCOV3mV5TNBmzvOfmesM4NeTyXuGJByd3XvOHvFezLn3rQ== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/semantic-conventions" "1.9.1" + +"@opentelemetry/sdk-metrics@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.9.1.tgz#babc162a81df9884c16b1e67c2dd26ab478f3080" + integrity sha512-AyhKDcA8NuV7o1+9KvzRMxNbATJ8AcrutKilJ6hWSo9R5utnzxgffV4y+Hp4mJn84iXxkv+CBb99GOJ2A5OMzA== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/resources" "1.9.1" + lodash.merge "4.6.2" + +"@opentelemetry/sdk-trace-base@1.9.1", "@opentelemetry/sdk-trace-base@^1.0.0": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.9.1.tgz#c349491b432a7e0ae7316f0b48b2d454d79d2b84" + integrity sha512-Y9gC5M1efhDLYHeeo2MWcDDMmR40z6QpqcWnPCm4Dmh+RHAMf4dnEBBntIe1dDpor686kyU6JV1D29ih1lZpsQ== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/resources" "1.9.1" + "@opentelemetry/semantic-conventions" "1.9.1" + +"@opentelemetry/sdk-trace-web@^1.8.0", "@opentelemetry/sdk-trace-web@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-web/-/sdk-trace-web-1.9.1.tgz#9734c62dfb554336779c0eb4f78bb622d8bde988" + integrity sha512-VCnr8IYW1GYonGF8M3nDqUGFjf2jcL3nlhnNyF3PKGw6OI7xNCBR+65IgW5Va7QhDP0D01jRVJ9oNuTshrVewA== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/sdk-trace-base" "1.9.1" + "@opentelemetry/semantic-conventions" "1.9.1" + +"@opentelemetry/semantic-conventions@1.9.1", "@opentelemetry/semantic-conventions@^1.0.0": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.9.1.tgz#ad3367684a57879392513479e0a436cb2ac46dad" + integrity sha512-oPQdbFDmZvjXk5ZDoBGXG8B4tSB/qW5vQunJWQMFUBp7Xe8O1ByPANueJ+Jzg58esEBegyyxZ7LRmfJr7kFcFg== + "@pmmmwh/react-refresh-webpack-plugin@^0.5.3": version "0.5.7" resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz#58f8217ba70069cc6a73f5d7e05e85b458c150e2" @@ -4164,6 +4306,11 @@ ansi-align@^3.0.0: dependencies: string-width "^4.1.0" +ansi-color@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansi-color/-/ansi-color-0.2.1.tgz#3e75c037475217544ed763a8db5709fa9ae5bf9a" + integrity sha512-bF6xLaZBLpOQzgYUtYEhJx090nPSZk1BQ/q2oyBK9aMMcJHzx9uXGCjI2Y+LebsN4Jwoykr0V9whbPiogdyHoQ== + ansi-colors@^3.0.0: version "3.2.4" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" @@ -4985,6 +5132,16 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +bufrw@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/bufrw/-/bufrw-1.3.0.tgz#28d6cfdaf34300376836310f5c31d57eeb40c8fa" + integrity sha512-jzQnSbdJqhIltU9O5KUiTtljP9ccw2u5ix59McQy4pV2xGhVLhRZIndY8GIrgh5HjXa6+QJ9AQhOd2QWQizJFQ== + dependencies: + ansi-color "^0.2.1" + error "^7.0.0" + hexer "^1.5.0" + xtend "^4.0.0" + builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" @@ -5132,15 +5289,10 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001335, caniuse-lite@^1.0.30001359: - version "1.0.30001363" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001363.tgz#26bec2d606924ba318235944e1193304ea7c4f15" - integrity sha512-HpQhpzTGGPVMnCjIomjt+jvyUu8vNFo3TaDiZ/RcoTrlOq/5+tC8zHdsbgFB6MxmaY+jCpsH09aD80Bb4Ow3Sg== - -caniuse-lite@^1.0.30001400: - version "1.0.30001425" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001425.tgz#52917791a453eb3265143d2cd08d80629e82c735" - integrity sha512-/pzFv0OmNG6W0ym80P3NtapU0QEiDS3VuYAZMGoLLqiC7f6FJFe1MjpQDREGApeenD9wloeytmVDj+JLXPC6qw== +caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001335, caniuse-lite@^1.0.30001359, caniuse-lite@^1.0.30001400: + version "1.0.30001460" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001460.tgz" + integrity sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ== case-sensitive-paths-webpack-plugin@^2.3.0: version "2.4.0" @@ -6901,6 +7053,21 @@ error-stack-parser@^2.0.6: dependencies: stackframe "^1.3.4" +error@7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02" + integrity sha512-UtVv4l5MhijsYUxPJo4390gzfZvAnTHreNnDjnTZaKIiZ/SemXxAhBkYSKtWa5RtBXbLP8tMgn/n0RUa/H7jXw== + dependencies: + string-template "~0.2.1" + xtend "~4.0.0" + +error@^7.0.0: + version "7.2.1" + resolved "https://registry.yarnpkg.com/error/-/error-7.2.1.tgz#eab21a4689b5f684fc83da84a0e390de82d94894" + integrity sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA== + dependencies: + string-template "~0.2.1" + es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.1: version "1.20.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814" @@ -8581,6 +8748,16 @@ heimdalljs@^0.2.6: dependencies: rsvp "~3.2.1" +hexer@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/hexer/-/hexer-1.5.0.tgz#b86ce808598e8a9d1892c571f3cedd86fc9f0653" + integrity sha512-dyrPC8KzBzUJ19QTIo1gXNqIISRXQ0NwteW6OeQHRN4ZuZeHkdODfj0zHBdOlHbRY8GqbqK57C9oWSvQZizFsg== + dependencies: + ansi-color "^0.2.1" + minimist "^1.1.0" + process "^0.10.0" + xtend "^4.0.0" + highlight.js@^10.4.1, highlight.js@~10.7.0: version "10.7.3" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" @@ -9468,6 +9645,17 @@ iterate-value@^1.0.2: es-get-iterator "^1.0.2" iterate-iterator "^1.0.1" +jaeger-client@^3.15.0: + version "3.19.0" + resolved "https://registry.yarnpkg.com/jaeger-client/-/jaeger-client-3.19.0.tgz#9b5bd818ebd24e818616ee0f5cffe1722a53ae6e" + integrity sha512-M0c7cKHmdyEUtjemnJyx/y9uX16XHocL46yQvyqDlPdvAcwPDbHrIbKjQdBqtiE4apQ/9dmr+ZLJYYPGnurgpw== + dependencies: + node-int64 "^0.4.0" + opentracing "^0.14.4" + thriftrw "^3.5.0" + uuid "^8.3.2" + xorshift "^1.1.1" + jest-changed-files@^29.2.0: version "29.2.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.2.0.tgz#b6598daa9803ea6a4dce7968e20ab380ddbee289" @@ -10211,7 +10399,7 @@ lodash.flow@^3.3.0: resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" integrity sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw== -lodash.merge@^4.6.2: +lodash.merge@4.6.2, lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== @@ -10239,6 +10427,11 @@ loglevel@^1.7.1: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114" integrity sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA== +long@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/long/-/long-2.4.0.tgz#9fa180bb1d9500cdc29c4156766a1995e1f4524f" + integrity sha512-ijUtjmO/n2A5PaosNG9ZGDsQ3vxJg7ZW8vsY8Kp0f2yIZWhSJvjmegV7t+9RPQKxKrvj8yKGehhS+po14hPLGQ== + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -10619,6 +10812,11 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimist@^1.1.0: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" @@ -10701,6 +10899,11 @@ mktemp@~0.4.0: resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b" integrity sha512-IXnMcJ6ZyTuhRmJSjzvHSRhlVPiN9Jwc6e59V0bEJ0ba6OBeX2L0E+mRN1QseeOF4mM+F1Rit6Nh7o+rl2Yn/A== +module-details-from-path@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" + integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A== + moment-mini@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/moment-mini/-/moment-mini-2.24.0.tgz#fa68d98f7fe93ae65bf1262f6abb5fb6983d8d18" @@ -11083,6 +11286,11 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +opentracing@^0.14.4: + version "0.14.7" + resolved "https://registry.yarnpkg.com/opentracing/-/opentracing-0.14.7.tgz#25d472bd0296dc0b64d7b94cbc995219031428f5" + integrity sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q== + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -11940,6 +12148,11 @@ process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/process/-/process-0.10.1.tgz#842457cc51cfed72dc775afeeafb8c6034372725" + integrity sha512-dyIett8dgGIZ/TXKUzeYExt7WA6ldDzys9vTDU/cCA9L17Ypme+KzS+NjQCjpn9xsvi/shbMC+yP/BcFMBz0NA== + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -12670,6 +12883,15 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +require-in-the-middle@^5.0.3: + version "5.2.0" + resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz#4b71e3cc7f59977100af9beb76bf2d056a5a6de2" + integrity sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg== + dependencies: + debug "^4.1.1" + module-details-from-path "^1.0.3" + resolve "^1.22.1" + requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" @@ -12714,7 +12936,7 @@ resolve.exports@^1.1.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.3.2: +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1, resolve@^1.3.2: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -13067,6 +13289,11 @@ shelljs@0.8.4: interpret "^1.0.0" rechoir "^0.6.2" +shimmer@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" + integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== + side-channel@^1.0.3, side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -13356,6 +13583,11 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" +string-template@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" + integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== + "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -13677,6 +13909,15 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +thriftrw@^3.5.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/thriftrw/-/thriftrw-3.12.0.tgz#30857847755e7f036b2e0a79d11c9f55075539d9" + integrity sha512-4YZvR4DPEI41n4Opwr4jmrLGG4hndxr7387kzRFIIzxHQjarPusH4lGXrugvgb7TtPrfZVTpZCVe44/xUxowEw== + dependencies: + bufrw "^1.3.0" + error "7.0.2" + long "^2.4.0" + through2-filter@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254" @@ -13866,6 +14107,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== +tslib@^2.3.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -14315,6 +14561,11 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -14833,6 +15084,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xorshift@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/xorshift/-/xorshift-1.2.0.tgz#30a4cdd8e9f8d09d959ed2a88c42a09c660e8148" + integrity sha512-iYgNnGyeeJ4t6U11NpA/QiKy+PXn5Aa3Azg5qkwIFz1tBLllQrjjsk9yzD7IAK0naNU4JxdeDgqW9ov4u/hc4g== + xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -14904,6 +15160,13 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zone.js@^0.11.0: + version "0.11.8" + resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.11.8.tgz#40dea9adc1ad007b5effb2bfed17f350f1f46a21" + integrity sha512-82bctBg2hKcEJ21humWIkXRlLBBmrc3nN7DFh5LGGhcyycO2S7FN8NmdvlcKaGFDNVL4/9kFLmwmInTavdJERA== + dependencies: + tslib "^2.3.0" + zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" From b68d4bf51bb39dabe348bb1b7e70aab0ce39cd1f Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Fri, 10 Mar 2023 14:42:24 +0100 Subject: [PATCH 078/286] Update README.md (#953) Only a tiny typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index de5f1221..511c7dc5 100644 --- a/README.md +++ b/README.md @@ -96,4 +96,4 @@ There are currently two different config files. `.env` holds variables that are ## Translation -Bf you'd like to help translate Element Call, head over to [translate.element.io](https://translate.element.io/engage/element-call/). You're also encouraged to join the [Element Translators](https://matrix.to/#/#translators:element.io) space to discuss and coordinate translation efforts. +If you'd like to help translate Element Call, head over to [translate.element.io](https://translate.element.io/engage/element-call/). You're also encouraged to join the [Element Translators](https://matrix.to/#/#translators:element.io) space to discuss and coordinate translation efforts. From 49b23160f3ad2068124a7b35fef1bb5c434ec658 Mon Sep 17 00:00:00 2001 From: lunarna-gh Date: Fri, 10 Mar 2023 01:32:57 +0000 Subject: [PATCH 079/286] Translated using Weblate (Polish) Currently translated at 97.2% (139 of 143 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/pl/ --- public/locales/pl/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/locales/pl/app.json b/public/locales/pl/app.json index fecfbfbf..21376cdc 100644 --- a/public/locales/pl/app.json +++ b/public/locales/pl/app.json @@ -133,5 +133,9 @@ "<0>Oops, something's gone wrong.": "<0>Ojej, coś poszło nie tak.", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Dołącz do rozmowy teraz<1>Or<2>Skopiuj link do rozmowy i dołącz później", "{{name}} (Waiting for video...)": "{{name}} (Oczekiwanie na wideo...)", - "{{name}} (Connecting...)": "{{name}} (Łączenie...)" + "{{name}} (Connecting...)": "{{name}} (Łączenie...)", + "Expose developer settings in the settings window.": "Wyświetlaj opcje programisty w oknie ustawień.", + "Element Call Home": "Strona główna Element Call", + "Developer Settings": "Opcje programisty", + "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "Zaznaczając to pole, wyrażasz zgodę na gromadzenie anonimowych danych, które wykorzystujemy do poprawy Twoich doświadczeń. Więcej informacji o tym, jakie dane śledzimy, można znaleźć w naszym " } From 971eca59ff9071fe8726fa1855283cf47e162a45 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 13 Mar 2023 18:40:16 -0400 Subject: [PATCH 080/286] Opt into analytics by default during the beta --- public/locales/en-GB/app.json | 4 ++-- src/analytics/AnalyticsNotice.tsx | 14 ++++++++++++++ src/analytics/AnalyticsOptInDescription.tsx | 20 -------------------- src/form/Form.tsx | 4 ++-- src/home/RegisteredView.module.css | 4 ++++ src/home/RegisteredView.tsx | 20 ++++++++------------ src/home/UnauthenticatedView.module.css | 4 ++++ src/home/UnauthenticatedView.tsx | 20 ++++++++------------ src/room/RoomPage.tsx | 7 +++++++ src/settings/SettingsModal.module.css | 4 ++-- src/settings/SettingsModal.tsx | 19 +++++++++++++++---- src/settings/useSetting.ts | 8 +++++++- src/tabs/Tabs.module.css | 4 +++- 13 files changed, 76 insertions(+), 56 deletions(-) create mode 100644 src/analytics/AnalyticsNotice.tsx delete mode 100644 src/analytics/AnalyticsOptInDescription.tsx diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index fe52c85f..389eeb70 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -8,6 +8,7 @@ "{{name}} is talking…": "{{name}} is talking…", "{{names}}, {{name}}": "{{names}}, {{name}}", "{{roomName}} - Walkie-talkie call": "{{roomName}} - Walkie-talkie call", + "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.", "<0>Already have an account?<1><0>Log in Or <2>Access as a guest": "<0>Already have an account?<1><0>Log in Or <2>Access as a guest", "<0>Create an account Or <2>Access as a guest": "<0>Create an account Or <2>Access as a guest", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Join call now<1>Or<2>Copy call link and join later", @@ -21,7 +22,7 @@ "Avatar": "Avatar", "By clicking \"Go\", you agree to our <2>Terms and conditions": "By clicking \"Go\", you agree to our <2>Terms and conditions", "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "By clicking \"Join call now\", you agree to our <2>Terms and conditions", - "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ", + "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy and our <5>Cookie Policy.": "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy and our <5>Cookie Policy.", "Call link copied": "Call link copied", "Call type menu": "Call type menu", "Camera": "Camera", @@ -85,7 +86,6 @@ "Press and hold spacebar to talk over {{name}}": "Press and hold spacebar to talk over {{name}}", "Press and hold to talk": "Press and hold to talk", "Press and hold to talk over {{name}}": "Press and hold to talk over {{name}}", - "Privacy Policy": "Privacy Policy", "Profile": "Profile", "Recaptcha dismissed": "Recaptcha dismissed", "Recaptcha not loaded": "Recaptcha not loaded", diff --git a/src/analytics/AnalyticsNotice.tsx b/src/analytics/AnalyticsNotice.tsx new file mode 100644 index 00000000..feceef76 --- /dev/null +++ b/src/analytics/AnalyticsNotice.tsx @@ -0,0 +1,14 @@ +import React, { FC } from "react"; +import { Trans } from "react-i18next"; + +import { Link } from "../typography/Typography"; + +export const AnalyticsNotice: FC = () => ( + + By participating in this beta, you consent to the collection of anonymous + data, which we use to improve the product. You can find more information + about which data we track in our{" "} + Privacy Policy and our{" "} + Cookie Policy. + +); diff --git a/src/analytics/AnalyticsOptInDescription.tsx b/src/analytics/AnalyticsOptInDescription.tsx deleted file mode 100644 index 46727f5f..00000000 --- a/src/analytics/AnalyticsOptInDescription.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { t } from "i18next"; -import React from "react"; - -import { Link } from "../typography/Typography"; - -export const optInDescription: () => JSX.Element = () => { - return ( - <> - <> - {t( - "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our " - )} - - - <>{t("Privacy Policy")} - - . - - ); -}; diff --git a/src/form/Form.tsx b/src/form/Form.tsx index beea4c86..fd98a62a 100644 --- a/src/form/Form.tsx +++ b/src/form/Form.tsx @@ -15,14 +15,14 @@ limitations under the License. */ import classNames from "classnames"; -import React, { FormEventHandler, forwardRef } from "react"; +import React, { FormEventHandler, forwardRef, ReactNode } from "react"; import styles from "./Form.module.css"; interface FormProps { className: string; onSubmit: FormEventHandler; - children: JSX.Element[]; + children: ReactNode[]; } export const Form = forwardRef( diff --git a/src/home/RegisteredView.module.css b/src/home/RegisteredView.module.css index ae43b5bc..a96bd53b 100644 --- a/src/home/RegisteredView.module.css +++ b/src/home/RegisteredView.module.css @@ -37,3 +37,7 @@ limitations under the License. .recentCallsTitle { margin-bottom: 32px; } + +.notice { + color: var(--secondary-content); +} diff --git a/src/home/RegisteredView.tsx b/src/home/RegisteredView.tsx index a6278c2e..06a6720c 100644 --- a/src/home/RegisteredView.tsx +++ b/src/home/RegisteredView.tsx @@ -39,11 +39,11 @@ import { CallList } from "./CallList"; import { UserMenuContainer } from "../UserMenuContainer"; import { useModalTriggerState } from "../Modal"; import { JoinExistingCallModal } from "./JoinExistingCallModal"; -import { Title } from "../typography/Typography"; +import { Caption, Title } from "../typography/Typography"; import { Form } from "../form/Form"; import { CallType, CallTypeDropdown } from "./CallTypeDropdown"; import { useOptInAnalytics } from "../settings/useSetting"; -import { optInDescription } from "../analytics/AnalyticsOptInDescription"; +import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; interface Props { client: MatrixClient; @@ -54,7 +54,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) { const [callType, setCallType] = useState(CallType.Video); const [loading, setLoading] = useState(false); const [error, setError] = useState(); - const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics(); + const [optInAnalytics] = useOptInAnalytics(); const history = useHistory(); const { t } = useTranslation(); const { modalState, modalProps } = useModalTriggerState(); @@ -144,15 +144,11 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) { {loading ? t("Loading…") : t("Go")} - ) => - setOptInAnalytics(event.target.checked) - } - /> + {optInAnalytics === null && ( + + + + )} {error && ( diff --git a/src/home/UnauthenticatedView.module.css b/src/home/UnauthenticatedView.module.css index bad173ea..49c272bf 100644 --- a/src/home/UnauthenticatedView.module.css +++ b/src/home/UnauthenticatedView.module.css @@ -45,3 +45,7 @@ limitations under the License. display: none; } } + +.notice { + color: var(--secondary-content); +} diff --git a/src/home/UnauthenticatedView.tsx b/src/home/UnauthenticatedView.tsx index 6339e42d..c31637b6 100644 --- a/src/home/UnauthenticatedView.tsx +++ b/src/home/UnauthenticatedView.tsx @@ -39,15 +39,15 @@ import { CallType, CallTypeDropdown } from "./CallTypeDropdown"; import styles from "./UnauthenticatedView.module.css"; import commonStyles from "./common.module.css"; import { generateRandomName } from "../auth/generateRandomName"; +import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; import { useOptInAnalytics } from "../settings/useSetting"; -import { optInDescription } from "../analytics/AnalyticsOptInDescription"; export const UnauthenticatedView: FC = () => { const { setClient } = useClient(); const [callType, setCallType] = useState(CallType.Video); const [loading, setLoading] = useState(false); const [error, setError] = useState(); - const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics(); + const [optInAnalytics] = useOptInAnalytics(); const [privacyPolicyUrl, recaptchaKey, register] = useInteractiveRegistration(); const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey); @@ -155,16 +155,12 @@ export const UnauthenticatedView: FC = () => { autoComplete="off" /> - ) => - setOptInAnalytics(event.target.checked) - } - /> - + {optInAnalytics === null && ( + + + + )} + By clicking "Go", you agree to our{" "} Terms and conditions diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx index f4e460d7..fe918617 100644 --- a/src/room/RoomPage.tsx +++ b/src/room/RoomPage.tsx @@ -27,6 +27,7 @@ import { useUrlParams } from "../UrlParams"; import { MediaHandlerProvider } from "../settings/useMediaHandler"; import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; import { translatedError } from "../TranslatedError"; +import { useOptInAnalytics } from "../settings/useSetting"; export const RoomPage: FC = () => { const { t } = useTranslation(); @@ -46,9 +47,15 @@ export const RoomPage: FC = () => { const roomIdOrAlias = roomId ?? roomAlias; if (!roomIdOrAlias) throw translatedError("No room specified", t); + const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics(); const { registerPasswordlessUser } = useRegisterPasswordlessUser(); const [isRegistering, setIsRegistering] = useState(false); + useEffect(() => { + // During the beta, opt into analytics by default + if (optInAnalytics === null) setOptInAnalytics(true); + }, [optInAnalytics, setOptInAnalytics]); + useEffect(() => { // If we've finished loading, are not already authed and we've been given a display name as // a URL param, automatically register a passwordless user diff --git a/src/settings/SettingsModal.module.css b/src/settings/SettingsModal.module.css index 9b4951b4..1e44dad7 100644 --- a/src/settings/SettingsModal.module.css +++ b/src/settings/SettingsModal.module.css @@ -20,7 +20,7 @@ limitations under the License. } .tabContainer { - margin: 27px 16px; + padding: 27px 20px; } .fieldRowText { @@ -33,5 +33,5 @@ The "Developer" item in the tab bar can be toggled. Without a defined width activating the developer tab makes the tab container jump to the right. */ .tabLabel { - width: 80px; + min-width: 80px; } diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 90a1cb5f..28808315 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -16,7 +16,7 @@ limitations under the License. import React from "react"; import { Item } from "@react-stately/collections"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { Modal } from "../Modal"; import styles from "./SettingsModal.module.css"; @@ -39,8 +39,8 @@ import { import { FieldRow, InputField } from "../input/Input"; import { Button } from "../button"; import { useDownloadDebugLog } from "./submit-rageshake"; -import { Body } from "../typography/Typography"; -import { optInDescription } from "../analytics/AnalyticsOptInDescription"; +import { Body, Caption } from "../typography/Typography"; +import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; interface Props { isOpen: boolean; @@ -71,6 +71,17 @@ export const SettingsModal = (props: Props) => { const downloadDebugLog = useDownloadDebugLog(); + const optInDescription = ( + + + +
+ You may withdraw consent by unchecking this box. If you are currently in + a call, this setting will take effect at the end of the call. +
+ + ); + return ( { id="optInAnalytics" type="checkbox" checked={optInAnalytics} - description={optInDescription()} + description={optInDescription} onChange={(event: React.ChangeEvent) => setOptInAnalytics(event.target.checked) } diff --git a/src/settings/useSetting.ts b/src/settings/useSetting.ts index 756ac74b..0fe5fe24 100644 --- a/src/settings/useSetting.ts +++ b/src/settings/useSetting.ts @@ -87,9 +87,15 @@ export const useSpatialAudio = (): [boolean, (val: boolean) => void] => { }; export const useShowInspector = () => useSetting("show-inspector", false); -export const useOptInAnalytics = () => useSetting("opt-in-analytics", false); + +// null = undecided +export const useOptInAnalytics = () => + useSetting("opt-in-analytics", null); + export const useKeyboardShortcuts = () => useSetting("keyboard-shortcuts", true); + export const useNewGrid = () => useSetting("new-grid", false); + export const useDeveloperSettingsTab = () => useSetting("developer-settings-tab", false); diff --git a/src/tabs/Tabs.module.css b/src/tabs/Tabs.module.css index bad7a0e6..188747cc 100644 --- a/src/tabs/Tabs.module.css +++ b/src/tabs/Tabs.module.css @@ -88,7 +88,9 @@ limitations under the License. .tabContainer { width: 100%; flex-direction: row; - margin: 27px 16px; + padding: 27px 20px; + box-sizing: border-box; + overflow: hidden; } .tabList { From 0cca5ae1741f672a674a5a258bcc4afcdb33a177 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 15 Mar 2023 14:35:10 +0000 Subject: [PATCH 081/286] Slightly evolved but not-yet-working OpenTelemetry More usefully, including docker config for starting a CORS enabled OTLP collector so we don't have to use zipkin. --- config/otel_dev/README.md | 7 + config/otel_dev/collector-gateway.yaml | 38 +++++ config/otel_dev/docker-compose.yaml | 23 +++ src/otel/OTelGroupCallMembership.ts | 206 +++++++++++++++++++++++++ src/otel/otel.ts | 96 ++++++++++++ 5 files changed, 370 insertions(+) create mode 100644 config/otel_dev/README.md create mode 100644 config/otel_dev/collector-gateway.yaml create mode 100644 config/otel_dev/docker-compose.yaml create mode 100644 src/otel/OTelGroupCallMembership.ts create mode 100644 src/otel/otel.ts diff --git a/config/otel_dev/README.md b/config/otel_dev/README.md new file mode 100644 index 00000000..19bf00d4 --- /dev/null +++ b/config/otel_dev/README.md @@ -0,0 +1,7 @@ +# OpenTelemetry Collector for development + +This directory contains a docker compose file that starts a jaeger all-in-one instance +with an in-memory database, along with a standalong OpenTelemetry collector that forwards +traces into the jaeger. Jaeger has a built-in OpenTelemetry collector, but it can't be +configured to send CORS headers so can't be used from a browser. This sets the config on +the collector to send CORS headers. diff --git a/config/otel_dev/collector-gateway.yaml b/config/otel_dev/collector-gateway.yaml new file mode 100644 index 00000000..2c70fb4f --- /dev/null +++ b/config/otel_dev/collector-gateway.yaml @@ -0,0 +1,38 @@ +receivers: + otlp: + protocols: + http: + endpoint: 0.0.0.0:4318 + cors: + allowed_origins: + - "http://*" + allowed_headers: + - "*" +processors: + batch: + timeout: 1s + resource: + attributes: + - key: test.key + value: "test-value" + action: insert +exporters: + logging: + loglevel: info + jaeger: + endpoint: jaeger-all-in-one:14250 + tls: + insecure: true +extensions: + health_check: + pprof: + endpoint: :1888 + zpages: + endpoint: :55679 +service: + extensions: [pprof, zpages, health_check] + pipelines: + traces: + receivers: [otlp] + processors: [batch, resource] + exporters: [logging, jaeger] diff --git a/config/otel_dev/docker-compose.yaml b/config/otel_dev/docker-compose.yaml new file mode 100644 index 00000000..478f80ab --- /dev/null +++ b/config/otel_dev/docker-compose.yaml @@ -0,0 +1,23 @@ +version: "2" +services: + # Jaeger + jaeger-all-in-one: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "14268" + - "14250" + # Collector + collector-gateway: + image: otel/opentelemetry-collector:latest + volumes: + - ./collector-gateway.yaml:/etc/collector-gateway.yaml + command: [ "--config=/etc/collector-gateway.yaml" ] + ports: + - "1888:1888" # pprof extension + - "13133:13133" # health_check extension + - "4317:4317" # OTLP gRPC receiver + - "4318:4318" # OTLP HTTP receiver + - "55670:55679" # zpages extension + depends_on: + - jaeger-all-in-one diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts new file mode 100644 index 00000000..56a8cf8b --- /dev/null +++ b/src/otel/OTelGroupCallMembership.ts @@ -0,0 +1,206 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import opentelemetry, { Context, Span } from "@opentelemetry/api"; +import { + ClientEvent, + GroupCall, + MatrixClient, + MatrixEvent, + RoomStateEvent, +} from "matrix-js-sdk"; +import { CallEvent } from "matrix-js-sdk/src/webrtc/call"; +import { useCallback, useEffect, useState } from "react"; + +import { tracer } from "./otel"; + +/** + * Represent the span of time which we intend to be joined to a group call + */ +export class OTelGroupCallMembership { + private context: Context; + private callMembershipSpan: Span; + + constructor(private groupCall: GroupCall) { + const callIdContext = opentelemetry.context + .active() + .setValue(Symbol("confId"), groupCall.groupCallId); + + // Create the main span that tracks the time we intend to be in the call + this.callMembershipSpan = tracer.startSpan( + "otel_groupCallMembershipSpan", + undefined, + callIdContext + ); + + // Create a new call based on the callIdContext. This context also has a span assigned to it. + // Other spans can use this context to extract the parent span. + // (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent) + this.context = opentelemetry.trace.setSpan( + opentelemetry.context.active(), + this.callMembershipSpan + ); + } + + public onJoinCall() { + // Here we start a very short span. This is a hack to trigger the posthog exporter. + // Only ended spans are processed by the exporter. + // We want the exporter to know that a call has started + const joinCallSpan = tracer.startSpan( + "otel_joinCallSpan", + undefined, + this.context + ); + joinCallSpan.end(); + } + + public onLeaveCall() { + // A very short span to represent us leaving the call + const startCallSpan = tracer.startSpan( + "otel_leaveCallSpan", + undefined, + this.context + ); + startCallSpan.end(); + + // and end the main span to indicate we've left + this.callMembershipSpan.end(); + } + + public onSendStateEvent(stateEvent: MatrixEvent) {} + + public onSendToDeviceEvent(toDeviceEvent: Record) { + const eventType = toDeviceEvent.eventType as string; + if (!eventType.startsWith("m.call")) return; + + const span = tracer.startSpan( + `otel_sendToDeviceEvent_${toDeviceEvent.eventType}`, + undefined, + this.context + ); + + for (const [k, v] of Object.entries(toDeviceEvent)) { + if (["string", "number"].includes(typeof v)) + span.setAttribute(k, v as string | number); + } + } +} + +export const useCallEventInstrumentation = ( + client: MatrixClient, + groupCall: GroupCall +): void => { + const [groupCallSpan, setGroupCallSpan] = useState(); + const [groupCallId, setGroupCallId] = useState(); + + const startChildSpan = useCallback( + (name: string, groupCallId: string): Span => { + const traceId = "7b78c1f568312cb288e55a9bc3c28cc5"; + const spanId = "7d31f3e430d90882"; + + const ctx = opentelemetry.trace.setSpanContext(context.active(), { + traceId, + spanId, + traceFlags: 1, + isRemote: true, + }); + + console.log("LOG context", ctx); + console.log( + "LOG context valid", + trace.isSpanContextValid(trace.getSpan(ctx).spanContext()) + ); + console.log("LOG parent span", trace.getSpan(ctx)); + + return tracer.startSpan(name, undefined, ctx); + }, + [] + ); + + const onUpdateRoomState = useCallback((event?: MatrixEvent) => { + /*const callStateEvent = groupCall.room.currentState.getStateEvents( + "org.matrix.msc3401.call", + groupCall.groupCallId + );*/ + /*const memberStateEvents = groupCall.room.currentState.getStateEvents( + "org.matrix.msc3401.call.member" + );*/ + }, []); + + const onReceivedVoipEvent = (event: MatrixEvent) => {}; + + const onUndecryptableToDevice = (event: MatrixEvent) => {}; + + const onSendVoipEvent = useCallback( + (event: Record) => { + const span = startChildSpan( + `element-call:send-voip-event:${event.eventType}`, + groupCall.groupCallId + ); + span.setAttribute("groupCallId", groupCall.groupCallId); + + console.log("LOG span", span); + + span.end(); + }, + [groupCall.groupCallId, startChildSpan] + ); + + useEffect(() => { + return; + if (groupCallId === groupCall.groupCallId) return; + + console.log("LOG starting span", groupCall.groupCallId, groupCallId); + + groupCallSpan?.end(); + + const newSpan = tracer.startSpan("element-call:group-call"); + newSpan.setAttribute("groupCallId", groupCall.groupCallId); + setGroupCallSpan(newSpan); + setGroupCallId(groupCall.groupCallId); + }, [groupCallSpan, groupCallId, groupCall.groupCallId]); + + useEffect(() => () => { + console.log("LOG ending span"); + + groupCallSpan?.end(); + }); + + useEffect(() => { + client.on(RoomStateEvent.Events, onUpdateRoomState); + //groupCall.on("calls_changed", onCallsChanged); + groupCall.on(CallEvent.SendVoipEvent, onSendVoipEvent); + //client.on("state", onCallsChanged); + //client.on("hangup", onCallHangup); + client.on(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent); + client.on(ClientEvent.UndecryptableToDeviceEvent, onUndecryptableToDevice); + + onUpdateRoomState(); + + return () => { + client.removeListener(RoomStateEvent.Events, onUpdateRoomState); + //groupCall.removeListener("calls_changed", onCallsChanged); + groupCall.removeListener(CallEvent.SendVoipEvent, onSendVoipEvent); + //client.removeListener("state", onCallsChanged); + //client.removeListener("hangup", onCallHangup); + client.removeListener(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent); + client.removeListener( + ClientEvent.UndecryptableToDeviceEvent, + onUndecryptableToDevice + ); + }; + }, [client, groupCall, onSendVoipEvent, onUpdateRoomState]); +}; diff --git a/src/otel/otel.ts b/src/otel/otel.ts new file mode 100644 index 00000000..6f546db3 --- /dev/null +++ b/src/otel/otel.ts @@ -0,0 +1,96 @@ +/* document-load.ts|js file - the code is the same for both the languages */ +import { + ConsoleSpanExporter, + SimpleSpanProcessor, +} from "@opentelemetry/sdk-trace-base"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; +import { WebTracerProvider } from "@opentelemetry/sdk-trace-web"; +import opentelemetry from "@opentelemetry/api"; +import { Context } from "@opentelemetry/api"; +import { Resource } from "@opentelemetry/resources"; +import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; + +import { PosthogSpanExporter } from "../analytics/OtelPosthogExporter"; + +const SERVICE_NAME = "element-call"; + +const otlpExporter = new OTLPTraceExporter(); +const consoleExporter = new ConsoleSpanExporter(); +const posthogExporter = new PosthogSpanExporter(); + +// This is how we can make Jaeger show a reaonsable service in the dropdown on the left. +const providerConfig = { + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME, + }), +}; +const provider = new WebTracerProvider(providerConfig); + +provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); +provider.addSpanProcessor(new SimpleSpanProcessor(posthogExporter)); +provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); + +// This is not the serviceName shown in jaeger +export const tracer = opentelemetry.trace.getTracer( + "my-element-call-otl-tracer" +); + +class CallTracer { + // We create one tracer class for each main context. + // Even if differnt tracer classes overlap in time space, we might want to visulaize them seperately. + // The Call Tracer should only contain spans/events that are relevant to understand the procedure of the individual candidates. + // Another Tracer Class (for example a ConnectionTracer) can contain a very granular list of all steps to connect to a call. + + private callSpan; + private callContext; + private muteSpan?; + + public startGroupCall(groupCallId: string) {} + + public startCall(callId: string): Context { + // The main context will be set when initiating the main/parent span. + + // Create an initial context with the callId param + const callIdContext = opentelemetry.context + .active() + .setValue(Symbol("callId"), callId); + + // Create the main span that tracks the whole call + this.callSpan = tracer.startSpan("otel_callSpan", undefined, callIdContext); + + // Create a new call based on the callIdContext. This context also has a span assigned to it. + // Other spans can use this context to extract the parent span. + // (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent) + this.callContext = opentelemetry.trace.setSpan( + opentelemetry.context.active(), + this.callSpan + ); + + // Here we start a very short span. This is a hack to trigger the posthog exporter. + // Only ended spans are processed by the exporter. + // We want the exporter to know that a call has started + const startCallSpan = tracer.startSpan( + "otel_startCallSpan", + undefined, + this.callContext + ); + startCallSpan.end(); + } + public muteMic(muteState: boolean) { + if (muteState) { + this.muteSpan = tracer.startSpan( + "otel_muteSpan", + undefined, + this.callContext + ); + } else if (this.muteSpan) { + this.muteSpan.end(); + this.muteSpan = null; + } + } + public endCall() { + this.callSpan?.end(); + } +} + +export const callTracer = new CallTracer(); From 1e2cd9776403bbb479e5258a21650ee723f846e6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 15 Mar 2023 14:38:17 +0000 Subject: [PATCH 082/286] Include the arguably-obvious command line --- config/otel_dev/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/otel_dev/README.md b/config/otel_dev/README.md index 19bf00d4..04690c75 100644 --- a/config/otel_dev/README.md +++ b/config/otel_dev/README.md @@ -5,3 +5,5 @@ with an in-memory database, along with a standalong OpenTelemetry collector that traces into the jaeger. Jaeger has a built-in OpenTelemetry collector, but it can't be configured to send CORS headers so can't be used from a browser. This sets the config on the collector to send CORS headers. + +Running `docker compose up` in this directory should be all you need. From c519e13885f72f52c0534f6d85c8af36fb9a7c54 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 15 Mar 2023 16:00:39 +0000 Subject: [PATCH 083/286] Version that does at least send some traces --- package.json | 1 - src/otel/OTelGroupCallMembership.ts | 176 ++++++++-------------------- src/otel/otel.ts | 6 +- src/room/GroupCallInspector.tsx | 23 +++- src/room/GroupCallView.tsx | 4 - src/room/useGroupCall.ts | 3 - src/telemetry/otel.ts | 128 -------------------- yarn.lock | 10 -- 8 files changed, 73 insertions(+), 278 deletions(-) delete mode 100644 src/telemetry/otel.ts diff --git a/package.json b/package.json index 0adbe947..a8f6ff0d 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "@opentelemetry/context-zone": "^1.9.1", "@opentelemetry/exporter-jaeger": "^1.9.1", "@opentelemetry/exporter-trace-otlp-http": "^0.35.1", - "@opentelemetry/exporter-zipkin": "^1.9.1", "@opentelemetry/instrumentation-document-load": "^0.31.1", "@opentelemetry/instrumentation-user-interaction": "^0.32.1", "@opentelemetry/sdk-trace-web": "^1.9.1", diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 56a8cf8b..3ea02660 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -15,18 +15,48 @@ limitations under the License. */ import opentelemetry, { Context, Span } from "@opentelemetry/api"; -import { - ClientEvent, - GroupCall, - MatrixClient, - MatrixEvent, - RoomStateEvent, -} from "matrix-js-sdk"; -import { CallEvent } from "matrix-js-sdk/src/webrtc/call"; -import { useCallback, useEffect, useState } from "react"; +import { GroupCall, MatrixEvent } from "matrix-js-sdk"; +import { VoipEvent } from "matrix-js-sdk/src/webrtc/call"; import { tracer } from "./otel"; +/** + * Recursively sets the contents of a todevice event object as attributes on a span + */ +function setNestedAttributesFromToDeviceEvent(span: Span, event: VoipEvent) { + setSpanEventAttributesRecursive( + span, + event as unknown as Record, // XXX Types + "matrix.", + 0 + ); +} + +function setSpanEventAttributesRecursive( + span: Span, + obj: Record, + prefix: string, + depth: number +) { + if (depth > 10) + throw new Error( + "Depth limit exceeded: aborting VoipEvent recursion. Prefix is " + prefix + ); + + for (const [k, v] of Object.entries(obj)) { + if (["string", "number"].includes(typeof v)) { + span.setAttribute(prefix + k, v as string | number); + } else if (typeof v === "object") { + setSpanEventAttributesRecursive( + span, + v as Record, + prefix + k + ".", + depth + 1 + ); + } + } +} + /** * Represent the span of time which we intend to be joined to a group call */ @@ -34,7 +64,7 @@ export class OTelGroupCallMembership { private context: Context; private callMembershipSpan: Span; - constructor(private groupCall: GroupCall) { + constructor(groupCall: GroupCall) { const callIdContext = opentelemetry.context .active() .setValue(Symbol("confId"), groupCall.groupCallId); @@ -82,125 +112,19 @@ export class OTelGroupCallMembership { public onSendStateEvent(stateEvent: MatrixEvent) {} - public onSendToDeviceEvent(toDeviceEvent: Record) { - const eventType = toDeviceEvent.eventType as string; + public onSendEvent(event: VoipEvent) { + const eventType = event.eventType as string; if (!eventType.startsWith("m.call")) return; - const span = tracer.startSpan( - `otel_sendToDeviceEvent_${toDeviceEvent.eventType}`, - undefined, - this.context - ); + if (event.type === "toDevice") { + const span = tracer.startSpan( + `otel_sendToDeviceEvent_${event.eventType}`, + undefined, + this.context + ); - for (const [k, v] of Object.entries(toDeviceEvent)) { - if (["string", "number"].includes(typeof v)) - span.setAttribute(k, v as string | number); + setNestedAttributesFromToDeviceEvent(span, event); + span.end(); } } } - -export const useCallEventInstrumentation = ( - client: MatrixClient, - groupCall: GroupCall -): void => { - const [groupCallSpan, setGroupCallSpan] = useState(); - const [groupCallId, setGroupCallId] = useState(); - - const startChildSpan = useCallback( - (name: string, groupCallId: string): Span => { - const traceId = "7b78c1f568312cb288e55a9bc3c28cc5"; - const spanId = "7d31f3e430d90882"; - - const ctx = opentelemetry.trace.setSpanContext(context.active(), { - traceId, - spanId, - traceFlags: 1, - isRemote: true, - }); - - console.log("LOG context", ctx); - console.log( - "LOG context valid", - trace.isSpanContextValid(trace.getSpan(ctx).spanContext()) - ); - console.log("LOG parent span", trace.getSpan(ctx)); - - return tracer.startSpan(name, undefined, ctx); - }, - [] - ); - - const onUpdateRoomState = useCallback((event?: MatrixEvent) => { - /*const callStateEvent = groupCall.room.currentState.getStateEvents( - "org.matrix.msc3401.call", - groupCall.groupCallId - );*/ - /*const memberStateEvents = groupCall.room.currentState.getStateEvents( - "org.matrix.msc3401.call.member" - );*/ - }, []); - - const onReceivedVoipEvent = (event: MatrixEvent) => {}; - - const onUndecryptableToDevice = (event: MatrixEvent) => {}; - - const onSendVoipEvent = useCallback( - (event: Record) => { - const span = startChildSpan( - `element-call:send-voip-event:${event.eventType}`, - groupCall.groupCallId - ); - span.setAttribute("groupCallId", groupCall.groupCallId); - - console.log("LOG span", span); - - span.end(); - }, - [groupCall.groupCallId, startChildSpan] - ); - - useEffect(() => { - return; - if (groupCallId === groupCall.groupCallId) return; - - console.log("LOG starting span", groupCall.groupCallId, groupCallId); - - groupCallSpan?.end(); - - const newSpan = tracer.startSpan("element-call:group-call"); - newSpan.setAttribute("groupCallId", groupCall.groupCallId); - setGroupCallSpan(newSpan); - setGroupCallId(groupCall.groupCallId); - }, [groupCallSpan, groupCallId, groupCall.groupCallId]); - - useEffect(() => () => { - console.log("LOG ending span"); - - groupCallSpan?.end(); - }); - - useEffect(() => { - client.on(RoomStateEvent.Events, onUpdateRoomState); - //groupCall.on("calls_changed", onCallsChanged); - groupCall.on(CallEvent.SendVoipEvent, onSendVoipEvent); - //client.on("state", onCallsChanged); - //client.on("hangup", onCallHangup); - client.on(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent); - client.on(ClientEvent.UndecryptableToDeviceEvent, onUndecryptableToDevice); - - onUpdateRoomState(); - - return () => { - client.removeListener(RoomStateEvent.Events, onUpdateRoomState); - //groupCall.removeListener("calls_changed", onCallsChanged); - groupCall.removeListener(CallEvent.SendVoipEvent, onSendVoipEvent); - //client.removeListener("state", onCallsChanged); - //client.removeListener("hangup", onCallHangup); - client.removeListener(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent); - client.removeListener( - ClientEvent.UndecryptableToDeviceEvent, - onUndecryptableToDevice - ); - }; - }, [client, groupCall, onSendVoipEvent, onUpdateRoomState]); -}; diff --git a/src/otel/otel.ts b/src/otel/otel.ts index 6f546db3..ecff690c 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -6,7 +6,6 @@ import { import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; import { WebTracerProvider } from "@opentelemetry/sdk-trace-web"; import opentelemetry from "@opentelemetry/api"; -import { Context } from "@opentelemetry/api"; import { Resource } from "@opentelemetry/resources"; import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; @@ -29,12 +28,14 @@ const provider = new WebTracerProvider(providerConfig); provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); provider.addSpanProcessor(new SimpleSpanProcessor(posthogExporter)); provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); +opentelemetry.trace.setGlobalTracerProvider(provider); // This is not the serviceName shown in jaeger export const tracer = opentelemetry.trace.getTracer( "my-element-call-otl-tracer" ); +/* class CallTracer { // We create one tracer class for each main context. // Even if differnt tracer classes overlap in time space, we might want to visulaize them seperately. @@ -47,7 +48,7 @@ class CallTracer { public startGroupCall(groupCallId: string) {} - public startCall(callId: string): Context { + public startCall(callId: string) { // The main context will be set when initiating the main/parent span. // Create an initial context with the callId param @@ -94,3 +95,4 @@ class CallTracer { } export const callTracer = new CallTracer(); +*/ diff --git a/src/room/GroupCallInspector.tsx b/src/room/GroupCallInspector.tsx index 648a0a1f..a0b16a9f 100644 --- a/src/room/GroupCallInspector.tsx +++ b/src/room/GroupCallInspector.tsx @@ -31,11 +31,12 @@ import { MatrixEvent, IContent } from "matrix-js-sdk/src/models/event"; import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; -import { CallEvent } from "matrix-js-sdk/src/webrtc/call"; +import { CallEvent, VoipEvent } from "matrix-js-sdk/src/webrtc/call"; import styles from "./GroupCallInspector.module.css"; import { SelectInput } from "../input/SelectInput"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; +import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; interface InspectorContextState { eventsByUserId?: { [userId: string]: SequenceDiagramMatrixEvent[] }; @@ -235,7 +236,7 @@ function reducer( action: { type?: CallEvent | ClientEvent | RoomStateEvent; event?: MatrixEvent; - rawEvent?: Record; + rawEvent?: VoipEvent; callStateEvent?: MatrixEvent; memberStateEvents?: MatrixEvent[]; } @@ -355,6 +356,18 @@ function useGroupCallState( groupCall: GroupCall, showPollCallStats: boolean ): InspectorContextState { + const [otelMembership] = useState( + () => new OTelGroupCallMembership(groupCall) + ); + + useEffect(() => { + otelMembership.onJoinCall(); + + return () => { + otelMembership.onLeaveCall(); + }; + }, [otelMembership]); + const [state, dispatch] = useReducer(reducer, { localUserId: client.getUserId(), localSessionId: client.getSessionId(), @@ -387,8 +400,10 @@ function useGroupCallState( dispatch({ type: ClientEvent.ReceivedVoipEvent, event }); } - function onSendVoipEvent(event: Record) { + function onSendVoipEvent(event: VoipEvent) { dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event }); + + otelMembership.onSendEvent(event); } function onUndecryptableToDevice(event: MatrixEvent) { @@ -422,7 +437,7 @@ function useGroupCallState( onUndecryptableToDevice ); }; - }, [client, groupCall]); + }, [client, groupCall, otelMembership]); return state; } diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 242e0e5e..61e1eb5e 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -35,7 +35,6 @@ import { useLocationNavigation } from "../useLocationNavigation"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { useMediaHandler } from "../settings/useMediaHandler"; import { findDeviceByName, getDevices } from "../media-utils"; -import { callTracer } from "../telemetry/otel"; declare global { interface Window { @@ -144,7 +143,6 @@ export function GroupCallView({ ]); await groupCall.enter(); - callTracer.startCall(groupCall.groupCallId); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); @@ -165,7 +163,6 @@ export function GroupCallView({ if (isEmbedded && !preload) { // In embedded mode, bypass the lobby and just enter the call straight away groupCall.enter(); - callTracer.startCall(groupCall.groupCallId); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); @@ -189,7 +186,6 @@ export function GroupCallView({ // In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent, // therefore we want the event to be sent instantly without getting queued/batched. - callTracer.endCall(); const sendInstantly = !!widget; PosthogAnalytics.instance.eventCallEnded.track( groupCall.groupCallId, diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index a3250736..37484b43 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -32,7 +32,6 @@ import { usePageUnload } from "./usePageUnload"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { TranslatedError, translatedError } from "../TranslatedError"; import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget"; -import { callTracer } from "../telemetry/otel"; export enum ConnectionState { EstablishingCall = "establishing call", // call hasn't been established yet @@ -376,7 +375,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { ) { return; } - callTracer.startCall(groupCall.groupCallId); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); @@ -401,7 +399,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { const setMicrophoneMuted = useCallback( (setMuted) => { groupCall.setMicrophoneMuted(setMuted); - callTracer.muteMic(setMuted); PosthogAnalytics.instance.eventMuteMicrophone.track( setMuted, groupCall.groupCallId diff --git a/src/telemetry/otel.ts b/src/telemetry/otel.ts deleted file mode 100644 index c8836692..00000000 --- a/src/telemetry/otel.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* document-load.ts|js file - the code is the same for both the languages */ -import { - ConsoleSpanExporter, - SimpleSpanProcessor, -} from "@opentelemetry/sdk-trace-base"; -import { ZipkinExporter } from "@opentelemetry/exporter-zipkin"; -// import { JaegerExporter } from "@opentelemetry/exporter-jaeger"; -import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; -import { WebTracerProvider } from "@opentelemetry/sdk-trace-web"; -import { ZoneContextManager } from "@opentelemetry/context-zone"; -import { registerInstrumentations } from "@opentelemetry/instrumentation"; -import opentelemetry from "@opentelemetry/api"; -import { Resource } from "@opentelemetry/resources"; -import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; - -import { PosthogSpanExporter } from "../analytics/OtelPosthogExporter"; - -const SERVICE_NAME = "element-call"; -// It is really important to set the correct content type here. Otherwise the Jaeger will crash and not accept the zipkin event -// Additionally jaeger needs to be started with zipkin on port 9411 -const optionsZipkin = { - // url: `http://localhost:9411/api/v2/spans`, - // serviceName: SERVICE_NAME, - headers: { - "Content-Type": "application/json", - }, -}; -// We DO NOT use the OTLPTraceExporter. This somehow does not hit the right endpoint and also causes issues with CORS -const collectorOptions = { - // url: `http://localhost:14268/api/v2/spans`, // url is optional and can be omitted - default is http://localhost:4318/v1/traces - headers: { "Access-Control-Allow-Origin": "*" }, // an optional object containing custom headers to be sent with each request - concurrencyLimit: 10, // an optional limit on pending requests -}; -const otlpExporter = new OTLPTraceExporter(collectorOptions); -const consoleExporter = new ConsoleSpanExporter(); -// The zipkin exporter is the actual exporter we need for web based otel applications -const zipkin = new ZipkinExporter(optionsZipkin); -const posthogExporter = new PosthogSpanExporter(); - -// This is how we can make Jaeger show a reaonsable service in the dropdown on the left. -const providerConfig = { - resource: new Resource({ - [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME, - }), -}; -const provider = new WebTracerProvider(providerConfig); - -provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); -// We can add as many processors and exporters as we want to. The zipkin one is the important one for Jaeger -provider.addSpanProcessor(new SimpleSpanProcessor(posthogExporter)); -provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); -provider.addSpanProcessor(new SimpleSpanProcessor(zipkin)); - -// This is unecassary i think... -provider.register({ - // Changing default contextManager to use ZoneContextManager - supports asynchronous operations - optional - contextManager: new ZoneContextManager(), -}); - -// Registering instrumentations (These are automated span collectors for the Http request during page loading, switching) -registerInstrumentations({ - instrumentations: [ - // new DocumentLoadInstrumentation(), - // new UserInteractionInstrumentation(), - ], -}); - -// This is not the serviceName shown in jaeger -export const tracer = opentelemetry.trace.getTracer( - "my-element-call-otl-tracer" -); - -class CallTracer { - // We create one tracer class for each main context. - // Even if differnt tracer classes overlap in time space, we might want to visulaize them seperately. - // The Call Tracer should only contain spans/events that are relevant to understand the procedure of the individual candidates. - // Another Tracer Class (for example a ConnectionTracer) can contain a very granular list of all steps to connect to a call. - - private callSpan; - private callContext; - private muteSpan?; - public startCall(callId: string) { - // The main context will be set when initiating the main/parent span. - - // Create an initial context with the callId param - const callIdContext = opentelemetry.context - .active() - .setValue(Symbol("callId"), callId); - - // Create the main span that tracks the whole call - this.callSpan = tracer.startSpan("otel_callSpan", undefined, callIdContext); - - // Create a new call based on the callIdContext. This context also has a span assigned to it. - // Other spans can use this context to extract the parent span. - // (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent) - this.callContext = opentelemetry.trace.setSpan( - opentelemetry.context.active(), - this.callSpan - ); - - // Here we start a very short span. This is a hack to trigger the posthog exporter. - // Only ended spans are processed by the exporter. - // We want the exporter to know that a call has started - const startCallSpan = tracer.startSpan( - "otel_startCallSpan", - undefined, - this.callContext - ); - startCallSpan.end(); - } - public muteMic(muteState: boolean) { - if (muteState) { - this.muteSpan = tracer.startSpan( - "otel_muteSpan", - undefined, - this.callContext - ); - } else if (this.muteSpan) { - this.muteSpan.end(); - this.muteSpan = null; - } - } - public endCall() { - this.callSpan?.end(); - } -} - -export const callTracer = new CallTracer(); diff --git a/yarn.lock b/yarn.lock index 8e518376..2662e3df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1956,16 +1956,6 @@ "@opentelemetry/resources" "1.9.1" "@opentelemetry/sdk-trace-base" "1.9.1" -"@opentelemetry/exporter-zipkin@^1.9.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.9.1.tgz#0bcddf2f3bcb1b26b94a090c953996a28087d21f" - integrity sha512-KBgf3w84luP5vWLlrqVFKmbwFK4lXM//t6K7H4nsg576htbz1RpBbQfybADjPdXTjGHqDTtLiC5MC90hxS7Z2w== - dependencies: - "@opentelemetry/core" "1.9.1" - "@opentelemetry/resources" "1.9.1" - "@opentelemetry/sdk-trace-base" "1.9.1" - "@opentelemetry/semantic-conventions" "1.9.1" - "@opentelemetry/instrumentation-document-load@^0.31.1": version "0.31.1" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-document-load/-/instrumentation-document-load-0.31.1.tgz#a535a5d1d71706701d3ff560a700b9dd03e4fb59" From 22d2404370c2ce1c72b6da3742c01f4c95df535b Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 15 Mar 2023 16:04:15 +0000 Subject: [PATCH 084/286] Prettier --- config/otel_dev/collector-gateway.yaml | 66 +++++++++++++------------- config/otel_dev/docker-compose.yaml | 42 ++++++++-------- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/config/otel_dev/collector-gateway.yaml b/config/otel_dev/collector-gateway.yaml index 2c70fb4f..6cc85a04 100644 --- a/config/otel_dev/collector-gateway.yaml +++ b/config/otel_dev/collector-gateway.yaml @@ -1,38 +1,38 @@ receivers: - otlp: - protocols: - http: - endpoint: 0.0.0.0:4318 - cors: - allowed_origins: - - "http://*" - allowed_headers: - - "*" + otlp: + protocols: + http: + endpoint: 0.0.0.0:4318 + cors: + allowed_origins: + - "http://*" + allowed_headers: + - "*" processors: - batch: - timeout: 1s - resource: - attributes: - - key: test.key - value: "test-value" - action: insert + batch: + timeout: 1s + resource: + attributes: + - key: test.key + value: "test-value" + action: insert exporters: - logging: - loglevel: info - jaeger: - endpoint: jaeger-all-in-one:14250 - tls: - insecure: true + logging: + loglevel: info + jaeger: + endpoint: jaeger-all-in-one:14250 + tls: + insecure: true extensions: - health_check: - pprof: - endpoint: :1888 - zpages: - endpoint: :55679 + health_check: + pprof: + endpoint: :1888 + zpages: + endpoint: :55679 service: - extensions: [pprof, zpages, health_check] - pipelines: - traces: - receivers: [otlp] - processors: [batch, resource] - exporters: [logging, jaeger] + extensions: [pprof, zpages, health_check] + pipelines: + traces: + receivers: [otlp] + processors: [batch, resource] + exporters: [logging, jaeger] diff --git a/config/otel_dev/docker-compose.yaml b/config/otel_dev/docker-compose.yaml index 478f80ab..40de4a52 100644 --- a/config/otel_dev/docker-compose.yaml +++ b/config/otel_dev/docker-compose.yaml @@ -1,23 +1,23 @@ version: "2" services: - # Jaeger - jaeger-all-in-one: - image: jaegertracing/all-in-one:latest - ports: - - "16686:16686" - - "14268" - - "14250" - # Collector - collector-gateway: - image: otel/opentelemetry-collector:latest - volumes: - - ./collector-gateway.yaml:/etc/collector-gateway.yaml - command: [ "--config=/etc/collector-gateway.yaml" ] - ports: - - "1888:1888" # pprof extension - - "13133:13133" # health_check extension - - "4317:4317" # OTLP gRPC receiver - - "4318:4318" # OTLP HTTP receiver - - "55670:55679" # zpages extension - depends_on: - - jaeger-all-in-one + # Jaeger + jaeger-all-in-one: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "14268" + - "14250" + # Collector + collector-gateway: + image: otel/opentelemetry-collector:latest + volumes: + - ./collector-gateway.yaml:/etc/collector-gateway.yaml + command: ["--config=/etc/collector-gateway.yaml"] + ports: + - "1888:1888" # pprof extension + - "13133:13133" # health_check extension + - "4317:4317" # OTLP gRPC receiver + - "4318:4318" # OTLP HTTP receiver + - "55670:55679" # zpages extension + depends_on: + - jaeger-all-in-one From 31450219c87636677bcf8d1b5f60431e7cbe3918 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 16 Mar 2023 14:41:55 +0000 Subject: [PATCH 085/286] More work on opentelemetry event reporting Moastly a re-org to avoid new contexts over React component unmounts/ remounts. --- src/analytics/OtelPosthogExporter.ts | 16 +++++++++++++ src/otel/OTelGroupCallMembership.ts | 35 ++++++++++++++-------------- src/otel/otel.ts | 17 +++++++++++++- src/room/GroupCallInspector.tsx | 22 +++++------------ src/room/GroupCallView.tsx | 5 +++- src/room/InCallView.tsx | 4 ++++ src/room/PTTCallView.tsx | 4 ++++ src/room/useGroupCall.ts | 28 ++++++++++++++++++++-- 8 files changed, 93 insertions(+), 38 deletions(-) diff --git a/src/analytics/OtelPosthogExporter.ts b/src/analytics/OtelPosthogExporter.ts index ee72155e..d4f8b536 100644 --- a/src/analytics/OtelPosthogExporter.ts +++ b/src/analytics/OtelPosthogExporter.ts @@ -1,3 +1,19 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import { SpanExporter } from "@opentelemetry/sdk-trace-base"; import { ReadableSpan } from "@opentelemetry/sdk-trace-base"; import { ExportResult, ExportResultCode } from "@opentelemetry/core"; diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 3ea02660..d8570b7b 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -15,7 +15,7 @@ limitations under the License. */ import opentelemetry, { Context, Span } from "@opentelemetry/api"; -import { GroupCall, MatrixEvent } from "matrix-js-sdk"; +import { GroupCall, MatrixClient, MatrixEvent } from "matrix-js-sdk"; import { VoipEvent } from "matrix-js-sdk/src/webrtc/call"; import { tracer } from "./otel"; @@ -27,7 +27,7 @@ function setNestedAttributesFromToDeviceEvent(span: Span, event: VoipEvent) { setSpanEventAttributesRecursive( span, event as unknown as Record, // XXX Types - "matrix.", + "matrix.event.", 0 ); } @@ -64,28 +64,27 @@ export class OTelGroupCallMembership { private context: Context; private callMembershipSpan: Span; - constructor(groupCall: GroupCall) { - const callIdContext = opentelemetry.context - .active() - .setValue(Symbol("confId"), groupCall.groupCallId); + constructor(groupCall: GroupCall, client: MatrixClient) { + // Create a new call based on the callIdContext. This context also has a span assigned to it. + // Other spans can use this context to extract the parent span. + // (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent) + const myMember = groupCall.room.getMember(client.getUserId()); + this.context = opentelemetry.trace + .setSpan(opentelemetry.context.active(), this.callMembershipSpan) + .setValue(Symbol("confId"), groupCall.groupCallId) + .setValue(Symbol("matrix.userId"), client.getUserId()) + .setValue(Symbol("matrix.displayName"), myMember.name); + } + + public onJoinCall() { // Create the main span that tracks the time we intend to be in the call this.callMembershipSpan = tracer.startSpan( "otel_groupCallMembershipSpan", undefined, - callIdContext + this.context ); - // Create a new call based on the callIdContext. This context also has a span assigned to it. - // Other spans can use this context to extract the parent span. - // (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent) - this.context = opentelemetry.trace.setSpan( - opentelemetry.context.active(), - this.callMembershipSpan - ); - } - - public onJoinCall() { // Here we start a very short span. This is a hack to trigger the posthog exporter. // Only ended spans are processed by the exporter. // We want the exporter to know that a call has started @@ -107,7 +106,7 @@ export class OTelGroupCallMembership { startCallSpan.end(); // and end the main span to indicate we've left - this.callMembershipSpan.end(); + if (this.callMembershipSpan) this.callMembershipSpan.end(); } public onSendStateEvent(stateEvent: MatrixEvent) {} diff --git a/src/otel/otel.ts b/src/otel/otel.ts index ecff690c..f7facbae 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -1,4 +1,19 @@ -/* document-load.ts|js file - the code is the same for both the languages */ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import { ConsoleSpanExporter, SimpleSpanProcessor, diff --git a/src/room/GroupCallInspector.tsx b/src/room/GroupCallInspector.tsx index a0b16a9f..18c57520 100644 --- a/src/room/GroupCallInspector.tsx +++ b/src/room/GroupCallInspector.tsx @@ -354,20 +354,8 @@ function reducer( function useGroupCallState( client: MatrixClient, groupCall: GroupCall, - showPollCallStats: boolean + otelGroupCallMembership: OTelGroupCallMembership ): InspectorContextState { - const [otelMembership] = useState( - () => new OTelGroupCallMembership(groupCall) - ); - - useEffect(() => { - otelMembership.onJoinCall(); - - return () => { - otelMembership.onLeaveCall(); - }; - }, [otelMembership]); - const [state, dispatch] = useReducer(reducer, { localUserId: client.getUserId(), localSessionId: client.getSessionId(), @@ -403,7 +391,7 @@ function useGroupCallState( function onSendVoipEvent(event: VoipEvent) { dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event }); - otelMembership.onSendEvent(event); + otelGroupCallMembership.onSendEvent(event); } function onUndecryptableToDevice(event: MatrixEvent) { @@ -437,7 +425,7 @@ function useGroupCallState( onUndecryptableToDevice ); }; - }, [client, groupCall, otelMembership]); + }, [client, groupCall, otelGroupCallMembership]); return state; } @@ -445,17 +433,19 @@ function useGroupCallState( interface GroupCallInspectorProps { client: MatrixClient; groupCall: GroupCall; + otelGroupCallMembership: OTelGroupCallMembership; show: boolean; } export function GroupCallInspector({ client, groupCall, + otelGroupCallMembership, show, }: GroupCallInspectorProps) { const [currentTab, setCurrentTab] = useState("sequence-diagrams"); const [selectedUserId, setSelectedUserId] = useState(); - const state = useGroupCallState(client, groupCall, show); + const state = useGroupCallState(client, groupCall, otelGroupCallMembership); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_, setState] = useContext(InspectorContext); diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 61e1eb5e..ad011efb 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -81,7 +81,8 @@ export function GroupCallView({ screenshareFeeds, participants, unencryptedEventsFromUsers, - } = useGroupCall(groupCall); + otelGroupCallMembership, + } = useGroupCall(groupCall, client); const { t } = useTranslation(); const { setAudioInput, setVideoInput } = useMediaHandler(); @@ -237,6 +238,7 @@ export function GroupCallView({ onLeave={onLeave} isEmbedded={isEmbedded} hideHeader={hideHeader} + otelGroupCallMembership={otelGroupCallMembership} /> ); } else { @@ -261,6 +263,7 @@ export function GroupCallView({ roomIdOrAlias={roomIdOrAlias} unencryptedEventsFromUsers={unencryptedEventsFromUsers} hideHeader={hideHeader} + otelGroupCallMembership={otelGroupCallMembership} /> ); } diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 48b1cbf1..8639cbae 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -73,6 +73,7 @@ import { TileDescriptor } from "../video-grid/TileDescriptor"; import { AudioSink } from "../video-grid/AudioSink"; import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; import { NewVideoGrid } from "../video-grid/NewVideoGrid"; +import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); // There is currently a bug in Safari our our code with cloning and sending MediaStreams @@ -100,6 +101,7 @@ interface Props { roomIdOrAlias: string; unencryptedEventsFromUsers: Set; hideHeader: boolean; + otelGroupCallMembership: OTelGroupCallMembership; } export function InCallView({ @@ -122,6 +124,7 @@ export function InCallView({ roomIdOrAlias, unencryptedEventsFromUsers, hideHeader, + otelGroupCallMembership, }: Props) { const { t } = useTranslation(); usePreventScroll(); @@ -429,6 +432,7 @@ export function InCallView({ {rageshakeRequestModalState.isOpen && ( diff --git a/src/room/PTTCallView.tsx b/src/room/PTTCallView.tsx index a25f3371..27816409 100644 --- a/src/room/PTTCallView.tsx +++ b/src/room/PTTCallView.tsx @@ -44,6 +44,7 @@ import { GroupCallInspector } from "./GroupCallInspector"; import { OverflowMenu } from "./OverflowMenu"; import { Size } from "../Avatar"; import { ParticipantInfo } from "./useGroupCall"; +import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; function getPromptText( networkWaiting: boolean, @@ -106,6 +107,7 @@ interface Props { onLeave: () => void; isEmbedded: boolean; hideHeader: boolean; + otelGroupCallMembership: OTelGroupCallMembership; } export const PTTCallView: React.FC = ({ @@ -119,6 +121,7 @@ export const PTTCallView: React.FC = ({ onLeave, isEmbedded, hideHeader, + otelGroupCallMembership, }) => { const { t } = useTranslation(); const { modalState: inviteModalState, modalProps: inviteModalProps } = @@ -192,6 +195,7 @@ export const PTTCallView: React.FC = ({ >; hasLocalParticipant: boolean; unencryptedEventsFromUsers: Set; + otelGroupCallMembership: OTelGroupCallMembership; } interface State { @@ -84,6 +87,13 @@ interface State { hasLocalParticipant: boolean; } +// This is a bit of a hack, but we keep the opentelemetry tracker object at the file +// level so that it doesn't pop in & out of existence as react mounts & unmounts +// components. The right solution is probably for this to live in the js-sdk and have +// the same lifetime as groupcalls themselves. +let groupCallOTelMembership: OTelGroupCallMembership; +let groupCallOTelMembershipGroupCallId: string; + function getParticipants( groupCall: GroupCall ): Map> { @@ -112,7 +122,10 @@ function getParticipants( return participants; } -export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { +export function useGroupCall( + groupCall: GroupCall, + client: MatrixClient +): UseGroupCallReturnType { const [ { state, @@ -146,6 +159,11 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { hasLocalParticipant: false, }); + if (groupCallOTelMembershipGroupCallId !== groupCall.groupCallId) { + groupCallOTelMembership = new OTelGroupCallMembership(groupCall, client); + groupCallOTelMembershipGroupCallId = groupCall.groupCallId; + } + const [unencryptedEventsFromUsers, addUnencryptedEventUser] = useReducer( (state: Set, newVal: string) => { return new Set(state).add(newVal); @@ -383,9 +401,14 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { console.error(error); updateState({ error }); }); + + groupCallOTelMembership.onJoinCall(); }, [groupCall, updateState]); - const leave = useCallback(() => groupCall.leave(), [groupCall]); + const leave = useCallback(() => { + groupCallOTelMembership.onLeaveCall(); + groupCall.leave(); + }, [groupCall]); const toggleLocalVideoMuted = useCallback(() => { const toggleToMute = !groupCall.isLocalVideoMuted(); @@ -525,5 +548,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { participants, hasLocalParticipant, unencryptedEventsFromUsers, + otelGroupCallMembership: groupCallOTelMembership, }; } From 521b0a857aa90ffdd4f162370bd6779acd482cc3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 16 Mar 2023 18:08:28 +0000 Subject: [PATCH 086/286] Send spans for state events --- src/otel/OTelGroupCallMembership.ts | 31 ++++++++++++++++++++++++++--- src/room/GroupCallInspector.tsx | 2 ++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index d8570b7b..5a8dae36 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -23,7 +23,7 @@ import { tracer } from "./otel"; /** * Recursively sets the contents of a todevice event object as attributes on a span */ -function setNestedAttributesFromToDeviceEvent(span: Span, event: VoipEvent) { +function setNestedAttributesFromEvent(span: Span, event: VoipEvent) { setSpanEventAttributesRecursive( span, event as unknown as Record, // XXX Types @@ -109,7 +109,23 @@ export class OTelGroupCallMembership { if (this.callMembershipSpan) this.callMembershipSpan.end(); } - public onSendStateEvent(stateEvent: MatrixEvent) {} + public onUpdateRoomState(event: MatrixEvent) { + if ( + !event || + (!event.getType().startsWith("m.call") && + !event.getType().startsWith("org.matrix.msc3401.call")) + ) + return; + + const span = tracer.startSpan( + `otel_onRoomStateEvent_${event.getType()}`, + undefined, + this.context + ); + + setNestedAttributesFromEvent(span, event.getContent()); + span.end(); + } public onSendEvent(event: VoipEvent) { const eventType = event.eventType as string; @@ -122,7 +138,16 @@ export class OTelGroupCallMembership { this.context ); - setNestedAttributesFromToDeviceEvent(span, event); + setNestedAttributesFromEvent(span, event); + span.end(); + } else if (event.type === "sendEvent") { + const span = tracer.startSpan( + `otel_sendToRoomEvent_${event.eventType}`, + undefined, + this.context + ); + + setNestedAttributesFromEvent(span, event); span.end(); } } diff --git a/src/room/GroupCallInspector.tsx b/src/room/GroupCallInspector.tsx index 18c57520..e1e12ea8 100644 --- a/src/room/GroupCallInspector.tsx +++ b/src/room/GroupCallInspector.tsx @@ -382,6 +382,8 @@ function useGroupCallState( callStateEvent, memberStateEvents, }); + + otelGroupCallMembership.onUpdateRoomState(event); } function onReceivedVoipEvent(event: MatrixEvent) { From 1bf1813a77f284b832c402818e279c89b6049f3e Mon Sep 17 00:00:00 2001 From: alariej Date: Fri, 17 Mar 2023 17:20:16 +0100 Subject: [PATCH 087/286] Fix for Android WebView, which does not support navigator.mediaSession --- src/room/useGroupCall.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 60f2d67d..d6c785a1 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -189,7 +189,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { ]; for (const mediaAction of mediaActions) { - navigator.mediaSession.setActionHandler( + navigator.mediaSession?.setActionHandler( mediaAction, doNothingMediaActionCallback ); @@ -197,7 +197,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { return () => { for (const mediaAction of mediaActions) { - navigator.mediaSession.setActionHandler(mediaAction, null); + navigator.mediaSession?.setActionHandler(mediaAction, null); } }; }, [doNothingMediaActionCallback]); From f8f5d2011d207c2c5e1add80b6c61b6fe3a28ae3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Mar 2023 17:01:59 +0000 Subject: [PATCH 088/286] Add CORS to jaeger query endpoint and make spans nested Adds an nginx in front of the query endpoint so we can use stalk without faffing with browser extension to bypass CORS. Also make the spans correctly have the call membership span as parent, which they didn't because we hadn't set the span at the point we made the context. --- config/otel_dev/README.md | 9 ++++++++ config/otel_dev/docker-compose.yaml | 6 +++++ src/otel/OTelGroupCallMembership.ts | 35 ++++++++++++++++------------- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/config/otel_dev/README.md b/config/otel_dev/README.md index 04690c75..8fe102d0 100644 --- a/config/otel_dev/README.md +++ b/config/otel_dev/README.md @@ -6,4 +6,13 @@ traces into the jaeger. Jaeger has a built-in OpenTelemetry collector, but it ca configured to send CORS headers so can't be used from a browser. This sets the config on the collector to send CORS headers. +This also adds an nginx to add CORS headers to the jaeger query endpoint, such that it can +be used from webapps like stalk (https://deniz.co/stalk/). The CORS enabled endpoint is +exposed on port 16687. To use stalk, you should simply be able to navigate to it and add +http://127.0.0.1:16687/api as a data source. + +(Yes, we could enable the OTLP collector in jaeger all-in-one and passed this through +the nginx to enable CORS too, rather than running a separate collector. There's no reason +it's done this way other than that I'd already set up the separate collector.) + Running `docker compose up` in this directory should be all you need. diff --git a/config/otel_dev/docker-compose.yaml b/config/otel_dev/docker-compose.yaml index 40de4a52..0b43abed 100644 --- a/config/otel_dev/docker-compose.yaml +++ b/config/otel_dev/docker-compose.yaml @@ -21,3 +21,9 @@ services: - "55670:55679" # zpages extension depends_on: - jaeger-all-in-one + nginx: + image: nginxinc/nginx-unprivileged:latest + volumes: + - ./nginx_otel.conf:/etc/nginx/conf.d/default.conf:ro + ports: + - "16687:8080" diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 5a8dae36..886b4ec8 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -15,7 +15,12 @@ limitations under the License. */ import opentelemetry, { Context, Span } from "@opentelemetry/api"; -import { GroupCall, MatrixClient, MatrixEvent } from "matrix-js-sdk"; +import { + GroupCall, + MatrixClient, + MatrixEvent, + RoomMember, +} from "matrix-js-sdk"; import { VoipEvent } from "matrix-js-sdk/src/webrtc/call"; import { tracer } from "./otel"; @@ -63,27 +68,27 @@ function setSpanEventAttributesRecursive( export class OTelGroupCallMembership { private context: Context; private callMembershipSpan: Span; + private myUserId: string; + private myMember: RoomMember; - constructor(groupCall: GroupCall, client: MatrixClient) { + constructor(private groupCall: GroupCall, client: MatrixClient) { + this.myUserId = client.getUserId(); + this.myMember = groupCall.room.getMember(client.getUserId()); + } + + public onJoinCall() { // Create a new call based on the callIdContext. This context also has a span assigned to it. // Other spans can use this context to extract the parent span. // (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent) - const myMember = groupCall.room.getMember(client.getUserId()); + // Create the main span that tracks the time we intend to be in the call + this.callMembershipSpan = tracer.startSpan("otel_groupCallMembershipSpan"); + this.context = opentelemetry.trace .setSpan(opentelemetry.context.active(), this.callMembershipSpan) - .setValue(Symbol("confId"), groupCall.groupCallId) - .setValue(Symbol("matrix.userId"), client.getUserId()) - .setValue(Symbol("matrix.displayName"), myMember.name); - } - - public onJoinCall() { - // Create the main span that tracks the time we intend to be in the call - this.callMembershipSpan = tracer.startSpan( - "otel_groupCallMembershipSpan", - undefined, - this.context - ); + .setValue(Symbol("confId"), this.groupCall.groupCallId) + .setValue(Symbol("matrix.userId"), this.myUserId) + .setValue(Symbol("matrix.displayName"), this.myMember.name); // Here we start a very short span. This is a hack to trigger the posthog exporter. // Only ended spans are processed by the exporter. From 2d91b43a7d78e3c6e35a4630d48e8069215b7e44 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Mar 2023 19:03:43 +0000 Subject: [PATCH 089/286] Set attributes on the root span Setting them on the context doesn't actually make them show up in jaeger, it's just a way to propagate the info around between different things. --- src/otel/OTelGroupCallMembership.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 886b4ec8..ba06090f 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -84,11 +84,20 @@ export class OTelGroupCallMembership { // Create the main span that tracks the time we intend to be in the call this.callMembershipSpan = tracer.startSpan("otel_groupCallMembershipSpan"); - this.context = opentelemetry.trace - .setSpan(opentelemetry.context.active(), this.callMembershipSpan) - .setValue(Symbol("confId"), this.groupCall.groupCallId) - .setValue(Symbol("matrix.userId"), this.myUserId) - .setValue(Symbol("matrix.displayName"), this.myMember.name); + this.callMembershipSpan.setAttribute( + "matrix.confId", + this.groupCall.groupCallId + ); + this.callMembershipSpan.setAttribute("matrix.userId", this.myUserId); + this.callMembershipSpan.setAttribute( + "matrix.displayName", + this.myMember.name + ); + + this.context = opentelemetry.trace.setSpan( + opentelemetry.context.active(), + this.callMembershipSpan + ); // Here we start a very short span. This is a hack to trigger the posthog exporter. // Only ended spans are processed by the exporter. From 63ede0b51aa86d0649fb0dd8262ca2ba9b706231 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Mar 2023 19:26:23 +0000 Subject: [PATCH 090/286] Version using events for call joins / leaves and matrix events This is probably conceptually nicer although isn't quite as nice in the jaeger / stalk UI. Also this may no loger work with the posthog exporter (unsure what it will do with events on spans). --- src/otel/OTelGroupCallMembership.ts | 78 +++++++++++------------------ 1 file changed, 28 insertions(+), 50 deletions(-) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index ba06090f..a4e3bfb1 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import opentelemetry, { Context, Span } from "@opentelemetry/api"; +import opentelemetry, { Span, Attributes } from "@opentelemetry/api"; import { GroupCall, MatrixClient, @@ -26,20 +26,25 @@ import { VoipEvent } from "matrix-js-sdk/src/webrtc/call"; import { tracer } from "./otel"; /** - * Recursively sets the contents of a todevice event object as attributes on a span + * Flattens out an object into a single layer with components + * of the key separated by dots */ -function setNestedAttributesFromEvent(span: Span, event: VoipEvent) { - setSpanEventAttributesRecursive( - span, +function flattenVoipEvent(event: VoipEvent): Attributes { + const flatObject = {}; + + flattenVoipEventRecursive( event as unknown as Record, // XXX Types + flatObject, "matrix.event.", 0 ); + + return flatObject; } -function setSpanEventAttributesRecursive( - span: Span, +function flattenVoipEventRecursive( obj: Record, + flatObject: Record, prefix: string, depth: number ) { @@ -50,11 +55,11 @@ function setSpanEventAttributesRecursive( for (const [k, v] of Object.entries(obj)) { if (["string", "number"].includes(typeof v)) { - span.setAttribute(prefix + k, v as string | number); + flatObject[prefix + k] = v; } else if (typeof v === "object") { - setSpanEventAttributesRecursive( - span, + flattenVoipEventRecursive( v as Record, + flatObject, prefix + k + ".", depth + 1 ); @@ -66,7 +71,6 @@ function setSpanEventAttributesRecursive( * Represent the span of time which we intend to be joined to a group call */ export class OTelGroupCallMembership { - private context: Context; private callMembershipSpan: Span; private myUserId: string; private myMember: RoomMember; @@ -83,7 +87,6 @@ export class OTelGroupCallMembership { // Create the main span that tracks the time we intend to be in the call this.callMembershipSpan = tracer.startSpan("otel_groupCallMembershipSpan"); - this.callMembershipSpan.setAttribute( "matrix.confId", this.groupCall.groupCallId @@ -94,30 +97,16 @@ export class OTelGroupCallMembership { this.myMember.name ); - this.context = opentelemetry.trace.setSpan( + opentelemetry.trace.setSpan( opentelemetry.context.active(), this.callMembershipSpan ); - // Here we start a very short span. This is a hack to trigger the posthog exporter. - // Only ended spans are processed by the exporter. - // We want the exporter to know that a call has started - const joinCallSpan = tracer.startSpan( - "otel_joinCallSpan", - undefined, - this.context - ); - joinCallSpan.end(); + this.callMembershipSpan.addEvent("matrix.joinCall"); } public onLeaveCall() { - // A very short span to represent us leaving the call - const startCallSpan = tracer.startSpan( - "otel_leaveCallSpan", - undefined, - this.context - ); - startCallSpan.end(); + this.callMembershipSpan.addEvent("matrix.leaveCall"); // and end the main span to indicate we've left if (this.callMembershipSpan) this.callMembershipSpan.end(); @@ -128,17 +117,14 @@ export class OTelGroupCallMembership { !event || (!event.getType().startsWith("m.call") && !event.getType().startsWith("org.matrix.msc3401.call")) - ) + ) { return; + } - const span = tracer.startSpan( + this.callMembershipSpan.addEvent( `otel_onRoomStateEvent_${event.getType()}`, - undefined, - this.context + flattenVoipEvent(event.getContent()) ); - - setNestedAttributesFromEvent(span, event.getContent()); - span.end(); } public onSendEvent(event: VoipEvent) { @@ -146,23 +132,15 @@ export class OTelGroupCallMembership { if (!eventType.startsWith("m.call")) return; if (event.type === "toDevice") { - const span = tracer.startSpan( - `otel_sendToDeviceEvent_${event.eventType}`, - undefined, - this.context + this.callMembershipSpan.addEvent( + `matrix.sendToDeviceEvent_${event.eventType}`, + flattenVoipEvent(event) ); - - setNestedAttributesFromEvent(span, event); - span.end(); } else if (event.type === "sendEvent") { - const span = tracer.startSpan( - `otel_sendToRoomEvent_${event.eventType}`, - undefined, - this.context + this.callMembershipSpan.addEvent( + `matrix.sendToRoomEvent_${event.eventType}`, + flattenVoipEvent(event) ); - - setNestedAttributesFromEvent(span, event); - span.end(); } } } From e7a7cf3eb8080b3dbe2067339ff3216bb68bc55d Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 20 Mar 2023 13:30:21 +0000 Subject: [PATCH 091/286] Export events to posthog too --- src/analytics/OtelPosthogExporter.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/analytics/OtelPosthogExporter.ts b/src/analytics/OtelPosthogExporter.ts index d4f8b536..3850e13b 100644 --- a/src/analytics/OtelPosthogExporter.ts +++ b/src/analytics/OtelPosthogExporter.ts @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { SpanExporter } from "@opentelemetry/sdk-trace-base"; -import { ReadableSpan } from "@opentelemetry/sdk-trace-base"; +import { SpanExporter, ReadableSpan } from "@opentelemetry/sdk-trace-base"; import { ExportResult, ExportResultCode } from "@opentelemetry/core"; import { PosthogAnalytics } from "./PosthogAnalytics"; @@ -34,11 +33,23 @@ export class PosthogSpanExporter implements SpanExporter { resultCallback: (result: ExportResult) => void ): Promise { console.log("POSTHOGEXPORTER", spans); - for (let i = 0; i < spans.length; i++) { - const span = spans[i]; - const sendInstantly = - span.name == "otel_callEnded" || - span.name == "otel_otherSentInstantlyEventName"; + for (const span of spans) { + const sendInstantly = [ + "otel_callEnded", + "otel_otherSentInstantlyEventName", + ].includes(span.name); + + for (const spanEvent of span.events) { + await PosthogAnalytics.instance.trackFromSpan( + { + eventName: spanEvent.name, + ...spanEvent.attributes, + }, + { + send_instantly: sendInstantly, + } + ); + } await PosthogAnalytics.instance.trackFromSpan( { eventName: span.name, ...span.attributes }, From ef9934ce6b6574fb2300551d25aedafd66dde427 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 20 Mar 2023 13:53:07 +0000 Subject: [PATCH 092/286] Commit nginx config file --- config/otel_dev/nginx_otel.conf | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 config/otel_dev/nginx_otel.conf diff --git a/config/otel_dev/nginx_otel.conf b/config/otel_dev/nginx_otel.conf new file mode 100644 index 00000000..90d69707 --- /dev/null +++ b/config/otel_dev/nginx_otel.conf @@ -0,0 +1,16 @@ +server { + listen 8080; + server_name localhost; + + location / { + proxy_pass http://jaeger-all-in-one:16686/; + add_header Access-Control-Allow-Origin *; + + if ($request_method = OPTIONS) { + add_header Access-Control-Allow-Origin *; + add_header Content-Type text/plain; + add_header Content-Length 0; + return 204; + } + } +} From 6b36604c8417879aacf0ae6a082b7dd30b34046d Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 20 Mar 2023 19:17:50 +0000 Subject: [PATCH 093/286] Update js-sdk --- package.json | 2 +- src/otel/OTelGroupCallMembership.ts | 43 ++++++++++++++++--- src/otel/otel.ts | 64 +---------------------------- src/room/useGroupCall.ts | 6 +++ yarn.lock | 16 ++++---- 5 files changed, 53 insertions(+), 78 deletions(-) diff --git a/package.json b/package.json index a8f6ff0d..a32fd8c9 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#64197bf4db6486d77708125d7fb2e8d7fe001f14", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#23837266fca5ee799b51a722f7b8eefb2f5ac140", "matrix-widget-api": "^1.0.0", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index a4e3bfb1..65e5d6ac 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -15,6 +15,7 @@ limitations under the License. */ import opentelemetry, { Span, Attributes } from "@opentelemetry/api"; +import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; import { GroupCall, MatrixClient, @@ -23,7 +24,7 @@ import { } from "matrix-js-sdk"; import { VoipEvent } from "matrix-js-sdk/src/webrtc/call"; -import { tracer } from "./otel"; +import { provider, tracer } from "./otel"; /** * Flattens out an object into a single layer with components @@ -78,15 +79,15 @@ export class OTelGroupCallMembership { constructor(private groupCall: GroupCall, client: MatrixClient) { this.myUserId = client.getUserId(); this.myMember = groupCall.room.getMember(client.getUserId()); + + provider.resource.attributes[ + SemanticResourceAttributes.SERVICE_NAME + ] = `element-call-${this.myUserId}-${client.getDeviceId()}`; } public onJoinCall() { - // Create a new call based on the callIdContext. This context also has a span assigned to it. - // Other spans can use this context to extract the parent span. - // (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent) - // Create the main span that tracks the time we intend to be in the call - this.callMembershipSpan = tracer.startSpan("otel_groupCallMembershipSpan"); + this.callMembershipSpan = tracer.startSpan("matrix.groupCallMembership"); this.callMembershipSpan.setAttribute( "matrix.confId", this.groupCall.groupCallId @@ -143,4 +144,34 @@ export class OTelGroupCallMembership { ); } } + + public onToggleMicrophoneMuted(newValue: boolean) { + this.callMembershipSpan.addEvent("matrix.toggleMicMuted", { + "matrix.microphone.muted": newValue, + }); + } + + public onSetMicrophoneMuted(setMuted: boolean) { + this.callMembershipSpan.addEvent("matrix.setMicMuted", { + "matrix.microphone.muted": setMuted, + }); + } + + public onToggleLocalVideoMuted(newValue: boolean) { + this.callMembershipSpan.addEvent("matrix.toggleVidMuted", { + "matrix.video.muted": newValue, + }); + } + + public onSetLocalVideoMuted(setMuted: boolean) { + this.callMembershipSpan.addEvent("matrix.setVidMuted", { + "matrix.video.muted": setMuted, + }); + } + + public onToggleScreensharing(newValue: boolean) { + this.callMembershipSpan.addEvent("matrix.setVidMuted", { + "matrix.screensharing.enabled": newValue, + }); + } } diff --git a/src/otel/otel.ts b/src/otel/otel.ts index f7facbae..95e81970 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -38,7 +38,7 @@ const providerConfig = { [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME, }), }; -const provider = new WebTracerProvider(providerConfig); +export const provider = new WebTracerProvider(providerConfig); provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); provider.addSpanProcessor(new SimpleSpanProcessor(posthogExporter)); @@ -49,65 +49,3 @@ opentelemetry.trace.setGlobalTracerProvider(provider); export const tracer = opentelemetry.trace.getTracer( "my-element-call-otl-tracer" ); - -/* -class CallTracer { - // We create one tracer class for each main context. - // Even if differnt tracer classes overlap in time space, we might want to visulaize them seperately. - // The Call Tracer should only contain spans/events that are relevant to understand the procedure of the individual candidates. - // Another Tracer Class (for example a ConnectionTracer) can contain a very granular list of all steps to connect to a call. - - private callSpan; - private callContext; - private muteSpan?; - - public startGroupCall(groupCallId: string) {} - - public startCall(callId: string) { - // The main context will be set when initiating the main/parent span. - - // Create an initial context with the callId param - const callIdContext = opentelemetry.context - .active() - .setValue(Symbol("callId"), callId); - - // Create the main span that tracks the whole call - this.callSpan = tracer.startSpan("otel_callSpan", undefined, callIdContext); - - // Create a new call based on the callIdContext. This context also has a span assigned to it. - // Other spans can use this context to extract the parent span. - // (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent) - this.callContext = opentelemetry.trace.setSpan( - opentelemetry.context.active(), - this.callSpan - ); - - // Here we start a very short span. This is a hack to trigger the posthog exporter. - // Only ended spans are processed by the exporter. - // We want the exporter to know that a call has started - const startCallSpan = tracer.startSpan( - "otel_startCallSpan", - undefined, - this.callContext - ); - startCallSpan.end(); - } - public muteMic(muteState: boolean) { - if (muteState) { - this.muteSpan = tracer.startSpan( - "otel_muteSpan", - undefined, - this.callContext - ); - } else if (this.muteSpan) { - this.muteSpan.end(); - this.muteSpan = null; - } - } - public endCall() { - this.callSpan?.end(); - } -} - -export const callTracer = new CallTracer(); -*/ diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 0311ed97..44db2124 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -413,6 +413,8 @@ export function useGroupCall( const toggleLocalVideoMuted = useCallback(() => { const toggleToMute = !groupCall.isLocalVideoMuted(); groupCall.setLocalVideoMuted(toggleToMute); + groupCallOTelMembership.onToggleLocalVideoMuted(toggleToMute); + // TODO: These explict posthog calls should be unnecessary now with the posthog otel exporter? PosthogAnalytics.instance.eventMuteCamera.track( toggleToMute, groupCall.groupCallId @@ -422,6 +424,7 @@ export function useGroupCall( const setMicrophoneMuted = useCallback( (setMuted) => { groupCall.setMicrophoneMuted(setMuted); + groupCallOTelMembership.onSetMicrophoneMuted(setMuted); PosthogAnalytics.instance.eventMuteMicrophone.track( setMuted, groupCall.groupCallId @@ -432,10 +435,13 @@ export function useGroupCall( const toggleMicrophoneMuted = useCallback(() => { const toggleToMute = !groupCall.isMicrophoneMuted(); + groupCallOTelMembership.onToggleMicrophoneMuted(toggleToMute); setMicrophoneMuted(toggleToMute); }, [groupCall, setMicrophoneMuted]); const toggleScreensharing = useCallback(async () => { + groupCallOTelMembership.onToggleScreensharing(!groupCall.isScreensharing); + if (!groupCall.isScreensharing()) { // toggling on updateState({ requestingScreenshare: true }); diff --git a/yarn.lock b/yarn.lock index 2662e3df..ddb3fc68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1821,10 +1821,10 @@ resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0" integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw== -"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.2": - version "0.1.0-alpha.2" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.2.tgz#a09d0fea858e817da971a3c9f904632ef7b49eb6" - integrity sha512-oVkBCh9YP7H9i4gAoQbZzswniczfo/aIptNa4dxRi4Ff9lSvUCFv6Hvzi7C+90c0/PWZLXjIDTIAWZYmwyd2fA== +"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.3": + version "0.1.0-alpha.5" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.5.tgz#60ede2c43b9d808ba8cf46085a3b347b290d9658" + integrity sha512-2KjAgWNGfuGLNjJwsrs6gGX157vmcTfNrA4u249utgnMPbJl7QwuUqh1bGxQ0PpK06yvZjgPlkna0lTbuwtuQw== "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": version "3.2.14" @@ -10545,12 +10545,12 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#64197bf4db6486d77708125d7fb2e8d7fe001f14": - version "23.1.1" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/64197bf4db6486d77708125d7fb2e8d7fe001f14" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#23837266fca5ee799b51a722f7b8eefb2f5ac140": + version "23.5.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/23837266fca5ee799b51a722f7b8eefb2f5ac140" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.2" + "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.3" another-json "^0.2.0" bs58 "^5.0.0" content-type "^1.0.4" From 6696af9b3ff5eb113dbef1530ecbb23a7166c2cf Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 20 Mar 2023 19:29:19 +0000 Subject: [PATCH 094/286] Experiment to try & stop vite OOMing --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index eee14064..654d6099 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -23,6 +23,7 @@ jobs: SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} SENTRY_URL: ${{ secrets.SENTRY_URL }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + NODE_OPTIONS: "--max-old-space-size=16384" - name: Upload Artifact uses: actions/upload-artifact@v2 with: From 359e055314123cbd0eea6d46fc1811c80868f6de Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 21 Mar 2023 12:13:51 +0000 Subject: [PATCH 095/286] Make callMembershipSpan optional --- src/otel/OTelGroupCallMembership.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 65e5d6ac..8d9f03f9 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -72,7 +72,7 @@ function flattenVoipEventRecursive( * Represent the span of time which we intend to be joined to a group call */ export class OTelGroupCallMembership { - private callMembershipSpan: Span; + private callMembershipSpan?: Span; private myUserId: string; private myMember: RoomMember; @@ -103,11 +103,11 @@ export class OTelGroupCallMembership { this.callMembershipSpan ); - this.callMembershipSpan.addEvent("matrix.joinCall"); + this.callMembershipSpan?.addEvent("matrix.joinCall"); } public onLeaveCall() { - this.callMembershipSpan.addEvent("matrix.leaveCall"); + this.callMembershipSpan?.addEvent("matrix.leaveCall"); // and end the main span to indicate we've left if (this.callMembershipSpan) this.callMembershipSpan.end(); @@ -122,7 +122,7 @@ export class OTelGroupCallMembership { return; } - this.callMembershipSpan.addEvent( + this.callMembershipSpan?.addEvent( `otel_onRoomStateEvent_${event.getType()}`, flattenVoipEvent(event.getContent()) ); @@ -133,12 +133,12 @@ export class OTelGroupCallMembership { if (!eventType.startsWith("m.call")) return; if (event.type === "toDevice") { - this.callMembershipSpan.addEvent( + this.callMembershipSpan?.addEvent( `matrix.sendToDeviceEvent_${event.eventType}`, flattenVoipEvent(event) ); } else if (event.type === "sendEvent") { - this.callMembershipSpan.addEvent( + this.callMembershipSpan?.addEvent( `matrix.sendToRoomEvent_${event.eventType}`, flattenVoipEvent(event) ); @@ -146,31 +146,31 @@ export class OTelGroupCallMembership { } public onToggleMicrophoneMuted(newValue: boolean) { - this.callMembershipSpan.addEvent("matrix.toggleMicMuted", { + this.callMembershipSpan?.addEvent("matrix.toggleMicMuted", { "matrix.microphone.muted": newValue, }); } public onSetMicrophoneMuted(setMuted: boolean) { - this.callMembershipSpan.addEvent("matrix.setMicMuted", { + this.callMembershipSpan?.addEvent("matrix.setMicMuted", { "matrix.microphone.muted": setMuted, }); } public onToggleLocalVideoMuted(newValue: boolean) { - this.callMembershipSpan.addEvent("matrix.toggleVidMuted", { + this.callMembershipSpan?.addEvent("matrix.toggleVidMuted", { "matrix.video.muted": newValue, }); } public onSetLocalVideoMuted(setMuted: boolean) { - this.callMembershipSpan.addEvent("matrix.setVidMuted", { + this.callMembershipSpan?.addEvent("matrix.setVidMuted", { "matrix.video.muted": setMuted, }); } public onToggleScreensharing(newValue: boolean) { - this.callMembershipSpan.addEvent("matrix.setVidMuted", { + this.callMembershipSpan?.addEvent("matrix.setVidMuted", { "matrix.screensharing.enabled": newValue, }); } From 3d6ae3fbc39ff892403e7fccf38c867ac4113172 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2023 11:55:21 +0000 Subject: [PATCH 096/286] Enable/disable opentelemetry based on config/user preference Add config to set collector URL, obey the same analytics setting as posthog. Also refactor into a class to make it easier to manage. --- src/config/ConfigOptions.ts | 7 +++ src/otel/OTelGroupCallMembership.ts | 9 ++- src/otel/otel.ts | 87 ++++++++++++++++++++++------- src/room/GroupCallInspector.tsx | 4 +- src/room/useGroupCall.ts | 24 +++++--- 5 files changed, 98 insertions(+), 33 deletions(-) diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index 5899d45f..cfaaf771 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -36,6 +36,13 @@ export interface ConfigOptions { submit_url: string; }; + /** + * Controls whether to to send OpenTelemetry debugging data to collector + */ + opentelemetry?: { + collector_url: string; + }; + // Describes the default homeserver to use. The same format as Element Web // (without identity servers as we don't use them). default_server_config?: { diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 8d9f03f9..764249f3 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -24,7 +24,7 @@ import { } from "matrix-js-sdk"; import { VoipEvent } from "matrix-js-sdk/src/webrtc/call"; -import { provider, tracer } from "./otel"; +import { ElementCallOpenTelemetry } from "./otel"; /** * Flattens out an object into a single layer with components @@ -80,14 +80,17 @@ export class OTelGroupCallMembership { this.myUserId = client.getUserId(); this.myMember = groupCall.room.getMember(client.getUserId()); - provider.resource.attributes[ + ElementCallOpenTelemetry.instance.provider.resource.attributes[ SemanticResourceAttributes.SERVICE_NAME ] = `element-call-${this.myUserId}-${client.getDeviceId()}`; } public onJoinCall() { // Create the main span that tracks the time we intend to be in the call - this.callMembershipSpan = tracer.startSpan("matrix.groupCallMembership"); + this.callMembershipSpan = + ElementCallOpenTelemetry.instance.tracer.startSpan( + "matrix.groupCallMembership" + ); this.callMembershipSpan.setAttribute( "matrix.confId", this.groupCall.groupCallId diff --git a/src/otel/otel.ts b/src/otel/otel.ts index 95e81970..301c0777 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -20,32 +20,79 @@ import { } from "@opentelemetry/sdk-trace-base"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; import { WebTracerProvider } from "@opentelemetry/sdk-trace-web"; -import opentelemetry from "@opentelemetry/api"; +import opentelemetry, { Tracer } from "@opentelemetry/api"; import { Resource } from "@opentelemetry/resources"; import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; +import { logger } from "@sentry/utils"; import { PosthogSpanExporter } from "../analytics/OtelPosthogExporter"; +import { Anonymity } from "../analytics/PosthogAnalytics"; +import { Config } from "../config/Config"; +import { getSetting, settingsBus } from "../settings/useSetting"; -const SERVICE_NAME = "element-call"; +const SERVICE_NAME_BASE = "element-call"; -const otlpExporter = new OTLPTraceExporter(); -const consoleExporter = new ConsoleSpanExporter(); -const posthogExporter = new PosthogSpanExporter(); +let sharedInstance: ElementCallOpenTelemetry; -// This is how we can make Jaeger show a reaonsable service in the dropdown on the left. -const providerConfig = { - resource: new Resource({ - [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME, - }), -}; -export const provider = new WebTracerProvider(providerConfig); +export class ElementCallOpenTelemetry { + private _provider: WebTracerProvider; + private _tracer: Tracer; + private _anonymity: Anonymity; -provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); -provider.addSpanProcessor(new SimpleSpanProcessor(posthogExporter)); -provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); -opentelemetry.trace.setGlobalTracerProvider(provider); + static get instance(): ElementCallOpenTelemetry { + return sharedInstance; + } -// This is not the serviceName shown in jaeger -export const tracer = opentelemetry.trace.getTracer( - "my-element-call-otl-tracer" -); + constructor(collectorUrl: string) { + const otlpExporter = new OTLPTraceExporter({ + url: collectorUrl, + }); + const consoleExporter = new ConsoleSpanExporter(); + const posthogExporter = new PosthogSpanExporter(); + + // This is how we can make Jaeger show a reaonsable service in the dropdown on the left. + const providerConfig = { + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: `${SERVICE_NAME_BASE}-unauthenticated`, + }), + }; + this._provider = new WebTracerProvider(providerConfig); + + this._provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); + this._provider.addSpanProcessor(new SimpleSpanProcessor(posthogExporter)); + this._provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); + opentelemetry.trace.setGlobalTracerProvider(this._provider); + + this._tracer = opentelemetry.trace.getTracer( + // This is not the serviceName shown in jaeger + "my-element-call-otl-tracer" + ); + } + + public get tracer(): Tracer { + return this._tracer; + } + + public get provider(): WebTracerProvider { + return this._provider; + } + + public get anonymity(): Anonymity { + return this._anonymity; + } +} + +function recheckOTelEnabledStatus(optInAnalayticsEnabled: boolean): void { + if (optInAnalayticsEnabled && !sharedInstance) { + logger.info("Starting OpenTelemetry debug reporting"); + sharedInstance = new ElementCallOpenTelemetry( + Config.get().opentelemetry?.collector_url + ); + } else if (!optInAnalayticsEnabled && sharedInstance) { + logger.info("Stopping OpenTelemetry debug reporting"); + sharedInstance = undefined; + } +} + +settingsBus.on("opt-in-analytics", recheckOTelEnabledStatus); +recheckOTelEnabledStatus(getSetting("opt-in-analytics", false)); diff --git a/src/room/GroupCallInspector.tsx b/src/room/GroupCallInspector.tsx index e1e12ea8..c058297a 100644 --- a/src/room/GroupCallInspector.tsx +++ b/src/room/GroupCallInspector.tsx @@ -383,7 +383,7 @@ function useGroupCallState( memberStateEvents, }); - otelGroupCallMembership.onUpdateRoomState(event); + otelGroupCallMembership?.onUpdateRoomState(event); } function onReceivedVoipEvent(event: MatrixEvent) { @@ -393,7 +393,7 @@ function useGroupCallState( function onSendVoipEvent(event: VoipEvent) { dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event }); - otelGroupCallMembership.onSendEvent(event); + otelGroupCallMembership?.onSendEvent(event); } function onUndecryptableToDevice(event: MatrixEvent) { diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 7d3f888c..b8dfc0c0 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -28,12 +28,14 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { useTranslation } from "react-i18next"; import { IWidgetApiRequest } from "matrix-widget-api"; import { MatrixClient } from "matrix-js-sdk"; +import { logger } from "@sentry/utils"; import { usePageUnload } from "./usePageUnload"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { TranslatedError, translatedError } from "../TranslatedError"; import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget"; import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; +import { ElementCallOpenTelemetry } from "../otel/otel"; export enum ConnectionState { EstablishingCall = "establishing call", // call hasn't been established yet @@ -172,8 +174,14 @@ export function useGroupCall( }); if (groupCallOTelMembershipGroupCallId !== groupCall.groupCallId) { - groupCallOTelMembership = new OTelGroupCallMembership(groupCall, client); - groupCallOTelMembershipGroupCallId = groupCall.groupCallId; + // If the user disables analytics, this will stay around until they leave the call + // so analytics will be disabled once they leave. + if (ElementCallOpenTelemetry.instance) { + groupCallOTelMembership = new OTelGroupCallMembership(groupCall, client); + groupCallOTelMembershipGroupCallId = groupCall.groupCallId; + } else { + groupCallOTelMembership = undefined; + } } const [unencryptedEventsFromUsers, addUnencryptedEventUser] = useReducer( @@ -414,18 +422,18 @@ export function useGroupCall( updateState({ error }); }); - groupCallOTelMembership.onJoinCall(); + groupCallOTelMembership?.onJoinCall(); }, [groupCall, updateState]); const leave = useCallback(() => { - groupCallOTelMembership.onLeaveCall(); + groupCallOTelMembership?.onLeaveCall(); groupCall.leave(); }, [groupCall]); const toggleLocalVideoMuted = useCallback(() => { const toggleToMute = !groupCall.isLocalVideoMuted(); groupCall.setLocalVideoMuted(toggleToMute); - groupCallOTelMembership.onToggleLocalVideoMuted(toggleToMute); + groupCallOTelMembership?.onToggleLocalVideoMuted(toggleToMute); // TODO: These explict posthog calls should be unnecessary now with the posthog otel exporter? PosthogAnalytics.instance.eventMuteCamera.track( toggleToMute, @@ -436,7 +444,7 @@ export function useGroupCall( const setMicrophoneMuted = useCallback( (setMuted) => { groupCall.setMicrophoneMuted(setMuted); - groupCallOTelMembership.onSetMicrophoneMuted(setMuted); + groupCallOTelMembership?.onSetMicrophoneMuted(setMuted); PosthogAnalytics.instance.eventMuteMicrophone.track( setMuted, groupCall.groupCallId @@ -447,12 +455,12 @@ export function useGroupCall( const toggleMicrophoneMuted = useCallback(() => { const toggleToMute = !groupCall.isMicrophoneMuted(); - groupCallOTelMembership.onToggleMicrophoneMuted(toggleToMute); + groupCallOTelMembership?.onToggleMicrophoneMuted(toggleToMute); setMicrophoneMuted(toggleToMute); }, [groupCall, setMicrophoneMuted]); const toggleScreensharing = useCallback(async () => { - groupCallOTelMembership.onToggleScreensharing(!groupCall.isScreensharing); + groupCallOTelMembership?.onToggleScreensharing(!groupCall.isScreensharing); if (!groupCall.isScreensharing()) { // toggling on From f6fb65be49c280be7ef0a4d4c8e3e28ae169121a Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2023 11:58:41 +0000 Subject: [PATCH 097/286] Remove odd source mapping comment & unused commented code --- src/analytics/OtelPosthogExporter.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/analytics/OtelPosthogExporter.ts b/src/analytics/OtelPosthogExporter.ts index 3850e13b..4fbfa909 100644 --- a/src/analytics/OtelPosthogExporter.ts +++ b/src/analytics/OtelPosthogExporter.ts @@ -69,16 +69,4 @@ export class PosthogSpanExporter implements SpanExporter { resolve(); }); } - /** - * converts span info into more readable format - * @param span - */ - // private _exportInfo; - /** - * Showing spans in console - * @param spans - * @param done - */ - // private _sendSpans; } -//# sourceMappingURL=ConsoleSpanExporter.d.ts.map From 9c0adfd32ebed5ccaed4955581db887460580fe6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2023 12:00:34 +0000 Subject: [PATCH 098/286] Unused import --- src/room/useGroupCall.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index b8dfc0c0..0b54c82f 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -28,7 +28,6 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { useTranslation } from "react-i18next"; import { IWidgetApiRequest } from "matrix-widget-api"; import { MatrixClient } from "matrix-js-sdk"; -import { logger } from "@sentry/utils"; import { usePageUnload } from "./usePageUnload"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; From ec88907981f3d517fe5fa5db4172c7d010093167 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2023 12:04:15 +0000 Subject: [PATCH 099/286] Comment the max old space workaround which seems to be working (so far) --- .github/workflows/build.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 654d6099..ba27e267 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -23,6 +23,8 @@ jobs: SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} SENTRY_URL: ${{ secrets.SENTRY_URL }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + # This appears to be necessary to stop Vite from OOMing + # https://github.com/vitejs/vite/issues/2433 NODE_OPTIONS: "--max-old-space-size=16384" - name: Upload Artifact uses: actions/upload-artifact@v2 From 48493a96e1707769ee168f4e0621d52bfd890fce Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Mar 2023 12:41:33 +0000 Subject: [PATCH 100/286] Wait until config is loaded to load otel --- src/initializer.tsx | 11 +++++++++++ src/otel/otel.ts | 8 +++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/initializer.tsx b/src/initializer.tsx index e10a4ec5..37e659e7 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -23,6 +23,7 @@ import * as Sentry from "@sentry/react"; import { getUrlParams } from "./UrlParams"; import { Config } from "./config/Config"; +import { ElementCallOpenTelemetry } from "./otel/otel"; enum LoadState { None, @@ -35,6 +36,7 @@ class DependencyLoadStates { // olm: LoadState = LoadState.None; config: LoadState = LoadState.None; sentry: LoadState = LoadState.None; + openTelemetry: LoadState = LoadState.None; allDepsAreLoaded() { return !Object.values(this).some((s) => s !== LoadState.Loaded); @@ -209,6 +211,15 @@ export class Initializer { this.loadStates.sentry = LoadState.Loaded; } + // OpenTelemetry (also only after config loaded) + if ( + this.loadStates.openTelemetry === LoadState.None && + this.loadStates.config === LoadState.Loaded + ) { + ElementCallOpenTelemetry.globalInit(); + this.loadStates.openTelemetry = LoadState.Loaded; + } + if (this.loadStates.allDepsAreLoaded()) { // resolve if there is no dependency that is not loaded resolve(); diff --git a/src/otel/otel.ts b/src/otel/otel.ts index 301c0777..25de3acf 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -39,6 +39,11 @@ export class ElementCallOpenTelemetry { private _tracer: Tracer; private _anonymity: Anonymity; + static globalInit(): void { + settingsBus.on("opt-in-analytics", recheckOTelEnabledStatus); + recheckOTelEnabledStatus(getSetting("opt-in-analytics", false)); + } + static get instance(): ElementCallOpenTelemetry { return sharedInstance; } @@ -93,6 +98,3 @@ function recheckOTelEnabledStatus(optInAnalayticsEnabled: boolean): void { sharedInstance = undefined; } } - -settingsBus.on("opt-in-analytics", recheckOTelEnabledStatus); -recheckOTelEnabledStatus(getSetting("opt-in-analytics", false)); From 698bea93e3eaefb788012d4ce4f51c062e1f4675 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 22 Mar 2023 11:33:50 -0400 Subject: [PATCH 101/286] Update matrix-widget-api --- package.json | 2 +- yarn.lock | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3b6368da..f7e2263c 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#8cbbdaa239e449848e8874f041ef1879c1956696", - "matrix-widget-api": "^1.0.0", + "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", "pako": "^2.0.4", diff --git a/yarn.lock b/yarn.lock index 3b6c84f7..cd1d7efd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10387,6 +10387,14 @@ matrix-widget-api@^1.0.0: "@types/events" "^3.0.0" events "^3.2.0" +matrix-widget-api@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.3.1.tgz#e38f404c76bb15c113909505c1c1a5b4d781c2f5" + integrity sha512-+rN6vGvnXm+fn0uq9r2KWSL/aPtehD6ObC50jYmUcEfgo8CUpf9eUurmjbRlwZkWq3XHXFuKQBUCI9UzqWg37Q== + dependencies: + "@types/events" "^3.0.0" + events "^3.2.0" + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" From 76c02773017791fb7ca931f89d7150620a3628f5 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 22 Mar 2023 11:41:41 -0400 Subject: [PATCH 102/286] Update matrix-js-sdk --- package.json | 2 +- src/room/GroupCallInspector.tsx | 6 +++--- yarn.lock | 26 +++++++++----------------- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index f7e2263c..8a95ba77 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#8cbbdaa239e449848e8874f041ef1879c1956696", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#f795577e14d5e56b2f57d4b9a686d832c5210e3d", "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/src/room/GroupCallInspector.tsx b/src/room/GroupCallInspector.tsx index 648a0a1f..399f8d72 100644 --- a/src/room/GroupCallInspector.tsx +++ b/src/room/GroupCallInspector.tsx @@ -31,7 +31,7 @@ import { MatrixEvent, IContent } from "matrix-js-sdk/src/models/event"; import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; -import { CallEvent } from "matrix-js-sdk/src/webrtc/call"; +import { CallEvent, VoipEvent } from "matrix-js-sdk/src/webrtc/call"; import styles from "./GroupCallInspector.module.css"; import { SelectInput } from "../input/SelectInput"; @@ -235,7 +235,7 @@ function reducer( action: { type?: CallEvent | ClientEvent | RoomStateEvent; event?: MatrixEvent; - rawEvent?: Record; + rawEvent?: VoipEvent; callStateEvent?: MatrixEvent; memberStateEvents?: MatrixEvent[]; } @@ -387,7 +387,7 @@ function useGroupCallState( dispatch({ type: ClientEvent.ReceivedVoipEvent, event }); } - function onSendVoipEvent(event: Record) { + function onSendVoipEvent(event: VoipEvent) { dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event }); } diff --git a/yarn.lock b/yarn.lock index cd1d7efd..1852bfea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1821,10 +1821,10 @@ resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0" integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw== -"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.3": - version "0.1.0-alpha.4" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.4.tgz#1b20294e0354c3dcc9c7dc810d883198a4042f04" - integrity sha512-mdaDKrw3P5ZVCpq0ioW0pV6ihviDEbS8ZH36kpt9stLKHwwDSopPogE6CkQhi0B1jn1yBUtOYi32mBV/zcOR7g== +"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.5": + version "0.1.0-alpha.5" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.5.tgz#60ede2c43b9d808ba8cf46085a3b347b290d9658" + integrity sha512-2KjAgWNGfuGLNjJwsrs6gGX157vmcTfNrA4u249utgnMPbJl7QwuUqh1bGxQ0PpK06yvZjgPlkna0lTbuwtuQw== "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": version "3.2.14" @@ -10362,31 +10362,23 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#8cbbdaa239e449848e8874f041ef1879c1956696": - version "23.4.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8cbbdaa239e449848e8874f041ef1879c1956696" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#f795577e14d5e56b2f57d4b9a686d832c5210e3d": + version "23.5.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f795577e14d5e56b2f57d4b9a686d832c5210e3d" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.3" + "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.5" another-json "^0.2.0" bs58 "^5.0.0" content-type "^1.0.4" loglevel "^1.7.1" matrix-events-sdk "0.0.1" - matrix-widget-api "^1.0.0" + matrix-widget-api "^1.3.1" p-retry "4" sdp-transform "^2.14.1" unhomoglyph "^1.0.6" uuid "9" -matrix-widget-api@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.1.1.tgz#d3fec45033d0cbc14387a38ba92dac4dbb1be962" - integrity sha512-gNSgmgSwvOsOcWK9k2+tOhEMYBiIMwX95vMZu0JqY7apkM02xrOzUBuPRProzN8CnbIALH7e3GAhatF6QCNvtA== - dependencies: - "@types/events" "^3.0.0" - events "^3.2.0" - matrix-widget-api@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.3.1.tgz#e38f404c76bb15c113909505c1c1a5b4d781c2f5" From 313ebe258e20686319fe98234aea5f4ce5390b02 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 22 Mar 2023 14:23:26 -0400 Subject: [PATCH 103/286] Add end-to-end audio observability This reports via OpenTelemetry when particular participants are speaking, as an easy way to observe the delivery of audio in calls. --- src/otel/OTelGroupCallMembership.ts | 45 ++++++++++++++++++++++---- src/room/InCallView.tsx | 2 +- src/video-grid/AudioSink.tsx | 8 ++++- src/video-grid/useCallFeed.ts | 50 +++++++++++++++++------------ 4 files changed, 76 insertions(+), 29 deletions(-) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 764249f3..3a00d43b 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import opentelemetry, { Span, Attributes } from "@opentelemetry/api"; +import opentelemetry, { Span, Attributes, Context } from "@opentelemetry/api"; import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; import { GroupCall, @@ -73,8 +73,10 @@ function flattenVoipEventRecursive( */ export class OTelGroupCallMembership { private callMembershipSpan?: Span; + private callMembershipContext?: Context; private myUserId: string; private myMember: RoomMember; + private readonly speakingSpans = new Map>(); constructor(private groupCall: GroupCall, client: MatrixClient) { this.myUserId = client.getUserId(); @@ -101,7 +103,7 @@ export class OTelGroupCallMembership { this.myMember.name ); - opentelemetry.trace.setSpan( + this.callMembershipContext = opentelemetry.trace.setSpan( opentelemetry.context.active(), this.callMembershipSpan ); @@ -110,10 +112,11 @@ export class OTelGroupCallMembership { } public onLeaveCall() { - this.callMembershipSpan?.addEvent("matrix.leaveCall"); - - // and end the main span to indicate we've left - if (this.callMembershipSpan) this.callMembershipSpan.end(); + this.callMembershipSpan!.addEvent("matrix.leaveCall"); + // and end the span to indicate we've left + this.callMembershipSpan!.end(); + this.callMembershipSpan = undefined; + this.callMembershipContext = undefined; } public onUpdateRoomState(event: MatrixEvent) { @@ -177,4 +180,34 @@ export class OTelGroupCallMembership { "matrix.screensharing.enabled": newValue, }); } + + public onSpeaking(member: RoomMember, deviceId: string, speaking: boolean) { + if (speaking) { + // Ensure that there's an audio activity span for this speaker + let deviceMap = this.speakingSpans.get(member); + if (deviceMap === undefined) { + deviceMap = new Map(); + this.speakingSpans.set(member, deviceMap); + } + + if (!deviceMap.has(deviceId)) { + const span = ElementCallOpenTelemetry.instance.tracer.startSpan( + "matrix.audioActivity", + undefined, + this.callMembershipContext + ); + span.setAttribute("matrix.userId", member.userId); + span.setAttribute("matrix.displayName", member.rawDisplayName); + + deviceMap.set(deviceId, span); + } + } else { + // End the audio activity span for this speaker, if any + const deviceMap = this.speakingSpans.get(member); + deviceMap?.get(deviceId)?.end(); + deviceMap?.delete(deviceId); + + if (deviceMap?.size === 0) this.speakingSpans.delete(member); + } + } } diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 2e0e932c..cf1e4dc2 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -360,11 +360,11 @@ export function InCallView({ const audioElements: JSX.Element[] = []; if (!spatialAudio || maximisedParticipant) { for (const item of items) { - if (item.isLocal) continue; // We don't want to render own audio audioElements.push( ); diff --git a/src/video-grid/AudioSink.tsx b/src/video-grid/AudioSink.tsx index 24019e4a..3787cdc7 100644 --- a/src/video-grid/AudioSink.tsx +++ b/src/video-grid/AudioSink.tsx @@ -16,6 +16,7 @@ limitations under the License. import React from "react"; +import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; import { TileDescriptor } from "./TileDescriptor"; import { useCallFeed } from "./useCallFeed"; import { useMediaStream } from "./useMediaStream"; @@ -23,6 +24,7 @@ import { useMediaStream } from "./useMediaStream"; interface Props { tileDescriptor: TileDescriptor; audioOutput: string; + otelGroupCallMembership: OTelGroupCallMembership; } // Renders and diff --git a/src/home/UnauthenticatedView.tsx b/src/home/UnauthenticatedView.tsx index c31637b6..be20d702 100644 --- a/src/home/UnauthenticatedView.tsx +++ b/src/home/UnauthenticatedView.tsx @@ -142,6 +142,7 @@ export const UnauthenticatedView: FC = () => { type="text" required autoComplete="off" + data-testid="home_callName" /> @@ -152,6 +153,7 @@ export const UnauthenticatedView: FC = () => { placeholder={t("Display name")} type="text" required + data-testid="home_displayName" autoComplete="off" /> @@ -171,7 +173,7 @@ export const UnauthenticatedView: FC = () => { )} -
diff --git a/src/icons/VideoMuted.svg b/src/icons/VideoMuted.svg index 188ed08b..20ecf0fd 100644 --- a/src/icons/VideoMuted.svg +++ b/src/icons/VideoMuted.svg @@ -1,4 +1,4 @@ - + diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index a1a69618..2e726ccf 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -137,6 +137,7 @@ export function LobbyView({ size="lg" disabled={state !== GroupCallState.LocalCallFeedInitialized} onPress={onEnter} + data-testid="lobby_joinCall" > Join call now diff --git a/src/room/VideoPreview.tsx b/src/room/VideoPreview.tsx index 0e92cc5d..6325bda0 100644 --- a/src/room/VideoPreview.tsx +++ b/src/room/VideoPreview.tsx @@ -64,7 +64,7 @@ export function VideoPreview({ return (
-
))} -
- + settingsModalState.open()} /> {!isEmbedded && } inviteModalState.open()} />
@@ -265,7 +259,7 @@ export const PTTCallView: React.FC = ({
))} = ({
+ {settingsModalState.isOpen && ( + + )} {inviteModalState.isOpen && showControls && ( )} diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx index fe918617..21ebc634 100644 --- a/src/room/RoomPage.tsx +++ b/src/room/RoomPage.tsx @@ -1,5 +1,5 @@ /* -Copyright 2021-2022 New Vector Ltd +Copyright 2021-2023 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import { RoomAuthView } from "./RoomAuthView"; import { GroupCallLoader } from "./GroupCallLoader"; import { GroupCallView } from "./GroupCallView"; import { useUrlParams } from "../UrlParams"; -import { MediaHandlerProvider } from "../settings/useMediaHandler"; import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; import { translatedError } from "../TranslatedError"; import { useOptInAnalytics } from "../settings/useSetting"; @@ -101,15 +100,13 @@ export const RoomPage: FC = () => { } return ( - - - {groupCallView} - - + + {groupCallView} + ); }; diff --git a/src/room/VideoPreview.tsx b/src/room/VideoPreview.tsx index 0e92cc5d..a7fc4983 100644 --- a/src/room/VideoPreview.tsx +++ b/src/room/VideoPreview.tsx @@ -1,5 +1,5 @@ /* -Copyright 2022 New Vector Ltd +Copyright 2022 - 2023 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,21 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { useCallback } from "react"; import useMeasure from "react-use-measure"; import { ResizeObserver } from "@juggle/resize-observer"; import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { useTranslation } from "react-i18next"; +import { OverlayTriggerState } from "@react-stately/overlays"; -import { MicButton, VideoButton } from "../button"; +import { MicButton, SettingsButton, VideoButton } from "../button"; import { useMediaStream } from "../video-grid/useMediaStream"; -import { OverflowMenu } from "./OverflowMenu"; import { Avatar } from "../Avatar"; import { useProfile } from "../profile/useProfile"; import styles from "./VideoPreview.module.css"; import { Body } from "../typography/Typography"; import { useModalTriggerState } from "../Modal"; +import { SettingsModal } from "../settings/SettingsModal"; interface Props { client: MatrixClient; @@ -59,8 +60,20 @@ export function VideoPreview({ const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver }); const avatarSize = (previewBounds.height - 66) / 2; - const { modalState: feedbackModalState, modalProps: feedbackModalProps } = - useModalTriggerState(); + const { + modalState: settingsModalState, + modalProps: settingsModalProps, + }: { + modalState: OverlayTriggerState; + modalProps: { + isOpen: boolean; + onClose: () => void; + }; + } = useModalTriggerState(); + + const openSettings = useCallback(() => { + settingsModalState.open(); + }, [settingsModalState]); return (
@@ -95,17 +108,13 @@ export function VideoPreview({ muted={localVideoMuted} onPress={toggleLocalVideoMuted} /> - +
)} + {settingsModalState.isOpen && ( + + )}
); } diff --git a/src/settings/FeedbackSettingsTab.tsx b/src/settings/FeedbackSettingsTab.tsx new file mode 100644 index 00000000..13834905 --- /dev/null +++ b/src/settings/FeedbackSettingsTab.tsx @@ -0,0 +1,93 @@ +/* +Copyright 2022 - 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useCallback } from "react"; +import { randomString } from "matrix-js-sdk/src/randomstring"; +import { useTranslation } from "react-i18next"; + +import { Button } from "../button"; +import { FieldRow, InputField, ErrorMessage } from "../input/Input"; +import { useSubmitRageshake, useRageshakeRequest } from "./submit-rageshake"; +import { Body } from "../typography/Typography"; + +interface Props { + roomId?: string; +} + +export function FeedbackSettingsTab({ roomId }: Props) { + const { t } = useTranslation(); + const { submitRageshake, sending, error } = useSubmitRageshake(); + const sendRageshakeRequest = useRageshakeRequest(); + + const onSubmitFeedback = useCallback( + (e) => { + e.preventDefault(); + const data = new FormData(e.target); + const descriptionData = data.get("description"); + const description = + typeof descriptionData === "string" ? descriptionData : ""; + const sendLogs = Boolean(data.get("sendLogs")); + const rageshakeRequestId = randomString(16); + + submitRageshake({ + description, + sendLogs, + rageshakeRequestId, + roomId, + }); + + if (roomId && sendLogs) { + sendRageshakeRequest(roomId, rageshakeRequestId); + } + }, + [submitRageshake, roomId, sendRageshakeRequest] + ); + + return ( +
+ {t("Having trouble? Help us fix it.")} +
+ + + + + + + {error && ( + + + + )} + + + +
+
+ ); +} diff --git a/src/profile/ProfileModal.module.css b/src/settings/ProfileSettingsTab.module.css similarity index 93% rename from src/profile/ProfileModal.module.css rename to src/settings/ProfileSettingsTab.module.css index 268fa932..4b80e49a 100644 --- a/src/profile/ProfileModal.module.css +++ b/src/settings/ProfileSettingsTab.module.css @@ -1,5 +1,5 @@ /* -Copyright 2022 New Vector Ltd +Copyright 2022 - 2023 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/profile/ProfileModal.tsx b/src/settings/ProfileSettingsTab.tsx similarity index 50% rename from src/profile/ProfileModal.tsx rename to src/settings/ProfileSettingsTab.tsx index adb1e2bd..3afd7531 100644 --- a/src/profile/ProfileModal.tsx +++ b/src/settings/ProfileSettingsTab.tsx @@ -1,5 +1,5 @@ /* -Copyright 2022 New Vector Ltd +Copyright 2022 - 2023 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,27 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ChangeEvent, useCallback, useEffect, useState } from "react"; +import React, { ChangeEvent, useCallback, useState } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { useTranslation } from "react-i18next"; import { Button } from "../button"; -import { useProfile } from "./useProfile"; +import { useProfile } from "../profile/useProfile"; import { FieldRow, InputField, ErrorMessage } from "../input/Input"; -import { Modal, ModalContent } from "../Modal"; import { AvatarInputField } from "../input/AvatarInputField"; -import styles from "./ProfileModal.module.css"; +import styles from "./ProfileSettingsTab.module.css"; interface Props { client: MatrixClient; - onClose: () => void; - [rest: string]: unknown; } -export function ProfileModal({ client, ...rest }: Props) { - const { onClose } = rest; +export function ProfileSettingsTab({ client }: Props) { const { t } = useTranslation(); const { - success, error, loading, displayName: initialDisplayName, @@ -78,64 +73,51 @@ export function ProfileModal({ client, ...rest }: Props) { [saveProfile, removeAvatar] ); - useEffect(() => { - if (success) { - onClose(); - } - }, [success, onClose]); - return ( - - -
- - - - - - - - - - {error && ( - - - - )} - - - - -
-
-
+
+ + + + + + + + + + {error && ( + + + + )} + + + +
); } diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index b126bb87..d2b59969 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -1,5 +1,5 @@ /* -Copyright 2022 New Vector Ltd +Copyright 2022 - 2023 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,9 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { useCallback, useState } from "react"; import { Item } from "@react-stately/collections"; import { Trans, useTranslation } from "react-i18next"; +import { MatrixClient } from "matrix-js-sdk"; import { Modal } from "../Modal"; import styles from "./SettingsModal.module.css"; @@ -25,6 +26,8 @@ import { ReactComponent as AudioIcon } from "../icons/Audio.svg"; import { ReactComponent as VideoIcon } from "../icons/Video.svg"; import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg"; import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg"; +import { ReactComponent as UserIcon } from "../icons/User.svg"; +import { ReactComponent as FeedbackIcon } from "../icons/Feedback.svg"; import { SelectInput } from "../input/SelectInput"; import { useMediaHandler } from "./useMediaHandler"; import { @@ -39,9 +42,14 @@ import { Button } from "../button"; import { useDownloadDebugLog } from "./submit-rageshake"; import { Body, Caption } from "../typography/Typography"; import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; +import { ProfileSettingsTab } from "./ProfileSettingsTab"; +import { FeedbackSettingsTab } from "./FeedbackSettingsTab"; interface Props { isOpen: boolean; + client: MatrixClient; + roomId?: string; + defaultTab?: string; onClose: () => void; } @@ -68,6 +76,15 @@ export const SettingsModal = (props: Props) => { const downloadDebugLog = useDownloadDebugLog(); + const [selectedTab, setSelectedTab] = useState(); + + const onSelectedTabChanged = useCallback( + (tab) => { + setSelectedTab(tab); + }, + [setSelectedTab] + ); + const optInDescription = ( @@ -87,8 +104,13 @@ export const SettingsModal = (props: Props) => { className={styles.settingsModal} {...props} > - + @@ -145,6 +167,7 @@ export const SettingsModal = (props: Props) => { @@ -167,6 +190,29 @@ export const SettingsModal = (props: Props) => { + + {t("Profile")} + + } + > + + + + + {t("feedback")} + + } + > + + + @@ -174,18 +220,10 @@ export const SettingsModal = (props: Props) => { } > -

Analytics

- - ) => - setOptInAnalytics(event.target.checked) - } - /> - +

Developer

+

+ Version: {(import.meta.env.VITE_APP_VERSION as string) || "dev"} +

{ } /> +

Analytics

+ + ) => + setOptInAnalytics(event.target.checked) + } + /> +
{developerSettingsTab && ( diff --git a/src/settings/useMediaHandler.tsx b/src/settings/useMediaHandler.tsx index ce3953ef..6f491c84 100644 --- a/src/settings/useMediaHandler.tsx +++ b/src/settings/useMediaHandler.tsx @@ -1,5 +1,5 @@ /* -Copyright 2022 New Vector Ltd +Copyright 2022 - 2023 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,23 +15,7 @@ limitations under the License. */ /* eslint-disable @typescript-eslint/ban-ts-comment */ -/* -Copyright 2022 New Vector Ltd -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { MatrixClient } from "matrix-js-sdk/src/client"; import { MediaHandlerEvent } from "matrix-js-sdk/src/webrtc/mediaHandler"; import React, { useState, @@ -43,6 +27,8 @@ import React, { ReactNode, } from "react"; +import { useClient } from "../ClientContext"; + export interface MediaHandlerContextInterface { audioInput: string; audioInputs: MediaDeviceInfo[]; @@ -89,10 +75,10 @@ function updateMediaPreferences(newPreferences: MediaPreferences): void { ); } interface Props { - client: MatrixClient; children: ReactNode; } -export function MediaHandlerProvider({ client, children }: Props): JSX.Element { +export function MediaHandlerProvider({ children }: Props): JSX.Element { + const { client } = useClient(); const [ { audioInput, @@ -104,19 +90,21 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element { }, setState, ] = useState(() => { - const mediaPreferences = getMediaPreferences(); - const mediaHandler = client.getMediaHandler(); + const mediaHandler = client?.getMediaHandler(); - mediaHandler.restoreMediaSettings( - mediaPreferences?.audioInput, - mediaPreferences?.videoInput - ); + if (mediaHandler) { + const mediaPreferences = getMediaPreferences(); + mediaHandler?.restoreMediaSettings( + mediaPreferences?.audioInput, + mediaPreferences?.videoInput + ); + } return { // @ts-ignore, ignore that audioInput is a private members of mediaHandler - audioInput: mediaHandler.audioInput, + audioInput: mediaHandler?.audioInput, // @ts-ignore, ignore that videoInput is a private members of mediaHandler - videoInput: mediaHandler.videoInput, + videoInput: mediaHandler?.videoInput, audioOutput: undefined, audioInputs: [], videoInputs: [], @@ -125,6 +113,8 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element { }); useEffect(() => { + if (!client) return; + const mediaHandler = client.getMediaHandler(); function updateDevices(): void { From f11e1fac6bbe7db9d9260c0418f2d543ce2a41e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 5 May 2023 12:04:48 +0200 Subject: [PATCH 210/286] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- public/locales/en-GB/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index 6e91a53e..13e29a8a 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -47,6 +47,7 @@ "Element Call Home": "Element Call Home", "Exit full screen": "Exit full screen", "Expose developer settings in the settings window.": "Expose developer settings in the settings window.", + "feedback": "feedback", "Fetching group call timed out.": "Fetching group call timed out.", "Freedom": "Freedom", "Full screen": "Full screen", @@ -74,7 +75,6 @@ "Microphone {{n}}": "Microphone {{n}}", "Microphone permissions needed to join the call.": "Microphone permissions needed to join the call.", "More": "More", - "More menu": "More menu", "Mute microphone": "Mute microphone", "No": "No", "Not now, return to home screen": "Not now, return to home screen", From b19150bbbaad6e239d3e917e940709bf92474fcd Mon Sep 17 00:00:00 2001 From: Michael Kaye <1917473+michaelkaye@users.noreply.github.com> Date: Fri, 5 May 2023 11:46:05 +0100 Subject: [PATCH 211/286] RegisteredView needs data tags as not reused from UnregisteredView. --- src/home/RegisteredView.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/home/RegisteredView.tsx b/src/home/RegisteredView.tsx index 06a6720c..41559d6f 100644 --- a/src/home/RegisteredView.tsx +++ b/src/home/RegisteredView.tsx @@ -133,6 +133,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) { type="text" required autoComplete="off" + data-testid="home_callName" /> From 41817800409f52f8e12e41c6e5cb5123826dc2ea Mon Sep 17 00:00:00 2001 From: Enrico Schwendig Date: Fri, 5 May 2023 16:14:19 +0200 Subject: [PATCH 212/286] change stats interval to 10s (#1038) --- src/room/useLoadGroupCall.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index 8347fb11..57175537 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -32,7 +32,7 @@ import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils"; import { translatedError } from "../TranslatedError"; import { widget } from "../widget"; -const STATS_COLLECT_INTERVAL_TIME_MS = 30000; +const STATS_COLLECT_INTERVAL_TIME_MS = 10000; export interface GroupCallLoadState { loading: boolean; From d4f0300c82fa3f1fa5c9f2013287f764656af9f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 5 May 2023 19:17:49 +0200 Subject: [PATCH 213/286] Match designs Co-authored-by: Robin --- src/settings/FeedbackSettingsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/FeedbackSettingsTab.tsx b/src/settings/FeedbackSettingsTab.tsx index 13834905..79272ec9 100644 --- a/src/settings/FeedbackSettingsTab.tsx +++ b/src/settings/FeedbackSettingsTab.tsx @@ -84,7 +84,7 @@ export function FeedbackSettingsTab({ roomId }: Props) { )} From 93a47e7009d4a02d8f52e96ba44a29d183881cbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 5 May 2023 19:17:59 +0200 Subject: [PATCH 214/286] Fix casing Co-authored-by: Robin --- src/settings/SettingsModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index d2b59969..9fec9dea 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -205,7 +205,7 @@ export const SettingsModal = (props: Props) => { title={ <> - {t("feedback")} + {t("Feedback")} } > From 57e79862a5608f4d01f1a395a0b7470109ae6539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 5 May 2023 19:18:34 +0200 Subject: [PATCH 215/286] User ID -> Username Co-authored-by: Robin --- src/settings/ProfileSettingsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/ProfileSettingsTab.tsx b/src/settings/ProfileSettingsTab.tsx index 3afd7531..07fc8e48 100644 --- a/src/settings/ProfileSettingsTab.tsx +++ b/src/settings/ProfileSettingsTab.tsx @@ -89,7 +89,7 @@ export function ProfileSettingsTab({ client }: Props) { Date: Fri, 5 May 2023 19:29:11 +0200 Subject: [PATCH 216/286] Feedback copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/settings/FeedbackSettingsTab.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/settings/FeedbackSettingsTab.tsx b/src/settings/FeedbackSettingsTab.tsx index 79272ec9..53ce576e 100644 --- a/src/settings/FeedbackSettingsTab.tsx +++ b/src/settings/FeedbackSettingsTab.tsx @@ -22,6 +22,7 @@ import { Button } from "../button"; import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { useSubmitRageshake, useRageshakeRequest } from "./submit-rageshake"; import { Body } from "../typography/Typography"; +import styles from "../input/SelectInput.module.css"; interface Props { roomId?: string; @@ -58,13 +59,18 @@ export function FeedbackSettingsTab({ roomId }: Props) { return (
- {t("Having trouble? Help us fix it.")} +

{t("Submit feedback")}

+ + {t( + "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below." + )} +
From 6cad89b20ca71e556e566cb8240f372701094f6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 5 May 2023 19:36:23 +0200 Subject: [PATCH 217/286] Add success message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/settings/FeedbackSettingsTab.tsx | 37 +++++++++++++++------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/settings/FeedbackSettingsTab.tsx b/src/settings/FeedbackSettingsTab.tsx index 53ce576e..da40678f 100644 --- a/src/settings/FeedbackSettingsTab.tsx +++ b/src/settings/FeedbackSettingsTab.tsx @@ -30,7 +30,7 @@ interface Props { export function FeedbackSettingsTab({ roomId }: Props) { const { t } = useTranslation(); - const { submitRageshake, sending, error } = useSubmitRageshake(); + const { submitRageshake, sending, sent, error } = useSubmitRageshake(); const sendRageshakeRequest = useRageshakeRequest(); const onSubmitFeedback = useCallback( @@ -72,27 +72,30 @@ export function FeedbackSettingsTab({ roomId }: Props) { name="description" label={t("Your feedback")} type="textarea" + disabled={sending || sent} /> - - - - {error && ( + {sent ? ( + {t("Thanks, we received your feedback!")} + ) : ( - + + {error && ( + + + + )} + )} - - -
); From 6dc26392d7efd4bdfc81a42efd0734254959723c Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 9 May 2023 20:33:49 +0200 Subject: [PATCH 218/286] add audio concealment to posthog exporter --- src/analytics/PosthogSpanProcessor.ts | 2 ++ src/otel/otel.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/analytics/PosthogSpanProcessor.ts b/src/analytics/PosthogSpanProcessor.ts index e9e287a2..a76ab308 100644 --- a/src/analytics/PosthogSpanProcessor.ts +++ b/src/analytics/PosthogSpanProcessor.ts @@ -124,6 +124,7 @@ export class PosthogSpanProcessor implements SpanProcessor { const audioReceived = `${attributes["matrix.stats.summary.percentageReceivedAudioMedia"]}`; const maxJitter = `${attributes["matrix.stats.summary.maxJitter"]}`; const maxPacketLoss = `${attributes["matrix.stats.summary.maxPacketLoss"]}`; + const audioConcealmentRatio = `${attributes["matrix.stats.summary.audioConcealmentRatio"]}`; PosthogAnalytics.instance.trackEvent( { eventName: "MediaReceived", @@ -133,6 +134,7 @@ export class PosthogSpanProcessor implements SpanProcessor { videoReceived: videoReceived, maxJitter: maxJitter, maxPacketLoss: maxPacketLoss, + audioConcealmentRatio: audioConcealmentRatio, }, // Send instantly because the window might be closing { send_instantly: true } diff --git a/src/otel/otel.ts b/src/otel/otel.ts index e30e84a0..8fc80805 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -42,7 +42,7 @@ export class ElementCallOpenTelemetry { const config = Config.get(); // we always enable opentelemetry in general. We only enable the OTLP // collector if a URL is defined (and in future if another setting is defined) - // The posthog exporter is always enabled, posthog reporting is enabled or disabled + // Posthog reporting is enabled or disabled // within the posthog code. const shouldEnableOtlp = Boolean(config.opentelemetry?.collector_url); From e3d5c84b17f6682910bbc00a7f986a2d295dd6a0 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 10 May 2023 10:53:52 -0400 Subject: [PATCH 219/286] Update react-spring Just in case this has a chance of fixing https://github.com/vector-im/element-call/issues/960 --- yarn.lock | 70 +++++++++++++++++++++++++++---------------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8c973269..0898d15a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2264,51 +2264,51 @@ "@react-aria/utils" "^3.13.1" clsx "^1.1.1" -"@react-spring/animated@~9.4.5": - version "9.4.5" - resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.4.5.tgz#dd9921c716a4f4a3ed29491e0c0c9f8ca0eb1a54" - integrity sha512-KWqrtvJSMx6Fj9nMJkhTwM9r6LIriExDRV6YHZV9HKQsaolUFppgkOXpC+rsL1JEtEvKv6EkLLmSqHTnuYjiIA== +"@react-spring/animated@~9.7.2": + version "9.7.2" + resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.7.2.tgz#0119db8075e91d693ec45c42575541e01b104a70" + integrity sha512-ipvleJ99ipqlnHkz5qhSsgf/ny5aW0ZG8Q+/2Oj9cI7LCc7COdnrSO6V/v8MAX3JOoQNzfz6dye2s5Pt5jGaIA== dependencies: - "@react-spring/shared" "~9.4.5" - "@react-spring/types" "~9.4.5" + "@react-spring/shared" "~9.7.2" + "@react-spring/types" "~9.7.2" -"@react-spring/core@~9.4.5": - version "9.4.5" - resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.4.5.tgz#4616e1adc18dd10f5731f100ebdbe9518b89ba3c" - integrity sha512-83u3FzfQmGMJFwZLAJSwF24/ZJctwUkWtyPD7KYtNagrFeQKUH1I05ZuhmCmqW+2w1KDW1SFWQ43RawqfXKiiQ== +"@react-spring/core@~9.7.2": + version "9.7.2" + resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.7.2.tgz#804ebadee45a6adff00886454d6f1c5d97ee219d" + integrity sha512-fF512edZT/gKVCA90ZRxfw1DmELeVwiL4OC2J6bMUlNr707C0h4QRoec6DjzG27uLX2MvS1CEatf9KRjwZR9/w== dependencies: - "@react-spring/animated" "~9.4.5" - "@react-spring/rafz" "~9.4.5" - "@react-spring/shared" "~9.4.5" - "@react-spring/types" "~9.4.5" + "@react-spring/animated" "~9.7.2" + "@react-spring/rafz" "~9.7.2" + "@react-spring/shared" "~9.7.2" + "@react-spring/types" "~9.7.2" -"@react-spring/rafz@~9.4.5": - version "9.4.5" - resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.4.5.tgz#84f809f287f2a66bbfbc66195db340482f886bd7" - integrity sha512-swGsutMwvnoyTRxvqhfJBtGM8Ipx6ks0RkIpNX9F/U7XmyPvBMGd3GgX/mqxZUpdlsuI1zr/jiYw+GXZxAlLcQ== +"@react-spring/rafz@~9.7.2": + version "9.7.2" + resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.7.2.tgz#77e7088c215e05cf893851cd87ceb40d89f2a7d7" + integrity sha512-kDWMYDQto3+flkrX3vy6DU/l9pxQ4TVW91DglQEc11iDc7shF4+WVDRJvOVLX+xoMP7zyag1dMvlIgvQ+dvA/A== -"@react-spring/shared@~9.4.5": - version "9.4.5" - resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.4.5.tgz#4c3ad817bca547984fb1539204d752a412a6d829" - integrity sha512-JhMh3nFKsqyag0KM5IIM8BQANGscTdd0mMv3BXsUiMZrcjQTskyfnv5qxEeGWbJGGar52qr5kHuBHtCjQOzniA== +"@react-spring/shared@~9.7.2": + version "9.7.2" + resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.7.2.tgz#b8485617bdcc9f6348b245922051fb534e07c566" + integrity sha512-6U9qkno+9DxlH5nSltnPs+kU6tYKf0bPLURX2te13aGel8YqgcpFYp5Av8DcN2x3sukinAsmzHUS/FRsdZMMBA== dependencies: - "@react-spring/rafz" "~9.4.5" - "@react-spring/types" "~9.4.5" + "@react-spring/rafz" "~9.7.2" + "@react-spring/types" "~9.7.2" -"@react-spring/types@~9.4.5": - version "9.4.5" - resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.4.5.tgz#9c71e5ff866b5484a7ef3db822bf6c10e77bdd8c" - integrity sha512-mpRIamoHwql0ogxEUh9yr4TP0xU5CWyZxVQeccGkHHF8kPMErtDXJlxyo0lj+telRF35XNihtPTWoflqtyARmg== +"@react-spring/types@~9.7.2": + version "9.7.2" + resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.7.2.tgz#e04dd72755d88b0e3163ba143ecd8ba78b68a5b0" + integrity sha512-GEflx2Ex/TKVMHq5g5MxQDNNPNhqg+4Db9m7+vGTm8ttZiyga7YQUF24shgRNebKIjahqCuei16SZga8h1pe4g== "@react-spring/web@^9.4.4": - version "9.4.5" - resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.4.5.tgz#b92f05b87cdc0963a59ee149e677dcaff09f680e" - integrity sha512-NGAkOtKmOzDEctL7MzRlQGv24sRce++0xAY7KlcxmeVkR7LRSGkoXHaIfm9ObzxPMcPHQYQhf3+X9jepIFNHQA== + version "9.7.2" + resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.7.2.tgz#76e53dd24033764c3062f9927f88b0f3194688d4" + integrity sha512-7qNc7/5KShu2D05x7o2Ols2nUE7mCKfKLaY2Ix70xPMfTle1sZisoQMBFgV9w/fSLZlHZHV9P0uWJqEXQnbV4Q== dependencies: - "@react-spring/animated" "~9.4.5" - "@react-spring/core" "~9.4.5" - "@react-spring/shared" "~9.4.5" - "@react-spring/types" "~9.4.5" + "@react-spring/animated" "~9.7.2" + "@react-spring/core" "~9.7.2" + "@react-spring/shared" "~9.7.2" + "@react-spring/types" "~9.7.2" "@react-stately/collections@^3.3.4", "@react-stately/collections@^3.4.1": version "3.4.1" From e925e7e0601d995c6506273000d30881ae70ce54 Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 10 May 2023 17:58:05 +0200 Subject: [PATCH 220/286] peer connections count in posthog media summary --- src/analytics/PosthogSpanProcessor.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/analytics/PosthogSpanProcessor.ts b/src/analytics/PosthogSpanProcessor.ts index e9e287a2..45659239 100644 --- a/src/analytics/PosthogSpanProcessor.ts +++ b/src/analytics/PosthogSpanProcessor.ts @@ -124,6 +124,7 @@ export class PosthogSpanProcessor implements SpanProcessor { const audioReceived = `${attributes["matrix.stats.summary.percentageReceivedAudioMedia"]}`; const maxJitter = `${attributes["matrix.stats.summary.maxJitter"]}`; const maxPacketLoss = `${attributes["matrix.stats.summary.maxPacketLoss"]}`; + const peerConnections = `${attributes["matrix.stats.summary.peerConnections"]}`; PosthogAnalytics.instance.trackEvent( { eventName: "MediaReceived", @@ -133,6 +134,7 @@ export class PosthogSpanProcessor implements SpanProcessor { videoReceived: videoReceived, maxJitter: maxJitter, maxPacketLoss: maxPacketLoss, + peerConnections: peerConnections, }, // Send instantly because the window might be closing { send_instantly: true } From 099dcd28c73f09312e712a04ad9d2b4cebe6e54d Mon Sep 17 00:00:00 2001 From: Michael Kaye <1917473+michaelkaye@users.noreply.github.com> Date: Thu, 11 May 2023 14:29:01 +0100 Subject: [PATCH 221/286] Test tags for invite links; joining call by UR:; getting call name. --- src/Header.tsx | 2 +- src/Modal.tsx | 1 + src/room/InviteModal.tsx | 1 + src/room/LobbyView.tsx | 1 + src/room/OverflowMenu.tsx | 4 ++-- src/room/RoomAuthView.tsx | 3 ++- 6 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Header.tsx b/src/Header.tsx index 7ccf8666..5a4f7f5e 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -134,7 +134,7 @@ export function RoomHeaderInfo({ roomName, avatarUrl }: RoomHeaderInfo) { />
- {roomName} + {roomName} ); } diff --git a/src/Modal.tsx b/src/Modal.tsx index 2665e116..6734e885 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -92,6 +92,7 @@ export function Modal({ {...closeButtonProps} ref={closeButtonRef} className={styles.closeButton} + data-testid="modal_close" title={t("Close")} > diff --git a/src/room/InviteModal.tsx b/src/room/InviteModal.tsx index a7f7ba52..9db8fdd9 100644 --- a/src/room/InviteModal.tsx +++ b/src/room/InviteModal.tsx @@ -41,6 +41,7 @@ export const InviteModal: FC = ({ roomIdOrAlias, ...rest }) => { diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index 2e726ccf..d05a848b 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -147,6 +147,7 @@ export function LobbyView({ value={getRoomUrl(roomIdOrAlias)} className={styles.copyButton} copiedMessage={t("Call link copied")} + data-testid="lobby_inviteLink" > Copy call link and join later diff --git a/src/room/OverflowMenu.tsx b/src/room/OverflowMenu.tsx index 0920f2d4..cfd5fb99 100644 --- a/src/room/OverflowMenu.tsx +++ b/src/room/OverflowMenu.tsx @@ -102,7 +102,7 @@ export function OverflowMenu({ <> - @@ -111,7 +111,7 @@ export function OverflowMenu({ {showInvite && ( - {t("Invite people")} + {t("Invite people")} )} diff --git a/src/room/RoomAuthView.tsx b/src/room/RoomAuthView.tsx index 11b20b8e..c3ccf0a3 100644 --- a/src/room/RoomAuthView.tsx +++ b/src/room/RoomAuthView.tsx @@ -74,6 +74,7 @@ export function RoomAuthView() { name="displayName" label={t("Display name")} placeholder={t("Display name")} + data-testid="joincall_displayName" type="text" required autoComplete="off" @@ -90,7 +91,7 @@ export function RoomAuthView() { )} -
From 9587dd7352fd7294e8bf366069b072f7532b18bc Mon Sep 17 00:00:00 2001 From: Michael Kaye <1917473+michaelkaye@users.noreply.github.com> Date: Thu, 11 May 2023 15:16:17 +0100 Subject: [PATCH 222/286] Prettier --- src/Header.tsx | 4 +++- src/room/RoomAuthView.tsx | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Header.tsx b/src/Header.tsx index 5a4f7f5e..410f3488 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -134,7 +134,9 @@ export function RoomHeaderInfo({ roomName, avatarUrl }: RoomHeaderInfo) { />
- {roomName} + + {roomName} + ); } diff --git a/src/room/RoomAuthView.tsx b/src/room/RoomAuthView.tsx index c3ccf0a3..12adcc1f 100644 --- a/src/room/RoomAuthView.tsx +++ b/src/room/RoomAuthView.tsx @@ -91,7 +91,12 @@ export function RoomAuthView() { )} -
From d148a81f91927009b70c451ac0ad973212e83cd7 Mon Sep 17 00:00:00 2001 From: Enrico Schwendig Date: Thu, 11 May 2023 16:47:05 +0200 Subject: [PATCH 223/286] Reconnect on network switch (#1029) --- package.json | 2 +- yarn.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index ca31832b..35425caa 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#261bc81554580b442769a65ceed2b154178fbe1c", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#90e8336797480ff0e512d7c76ba6f420c2c3a2d5", "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/yarn.lock b/yarn.lock index 8c973269..98740625 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1821,10 +1821,10 @@ resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0" integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw== -"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.7": - version "0.1.0-alpha.7" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.7.tgz#136375b84fd8a7e698f70fc969f668e541a61313" - integrity sha512-sQEG9cSfNji5NYBf5h7j5IxYVO0dwtAKoetaVyR+LhIXz/Su7zyEE3EwlAWAeJOFdAV/vZ5LTNyh39xADuNlTg== +"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.8": + version "0.1.0-alpha.8" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.8.tgz#18dd8e7fb56602d2999d8a502b49e902a2bb3782" + integrity sha512-hdmbbGXKrN6JNo3wdBaR5Zs3lXlzllT3U43ViNTlabB3nKkOZQnEAN/Isv+4EQSgz1+8897veI9Q8sqlQX22oA== "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": version "3.2.14" @@ -10550,12 +10550,12 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#261bc81554580b442769a65ceed2b154178fbe1c": - version "25.0.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/261bc81554580b442769a65ceed2b154178fbe1c" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#90e8336797480ff0e512d7c76ba6f420c2c3a2d5": + version "25.1.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/90e8336797480ff0e512d7c76ba6f420c2c3a2d5" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.7" + "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.8" another-json "^0.2.0" bs58 "^5.0.0" content-type "^1.0.4" From d270756443740877a7d1ecc77d7fee5d26ca319b Mon Sep 17 00:00:00 2001 From: Michael Kaye <1917473+michaelkaye@users.noreply.github.com> Date: Fri, 12 May 2023 16:25:24 +0100 Subject: [PATCH 224/286] Useful to be able to hang up rather than close the window. --- src/room/InCallView.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 198edb25..63035470 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -419,7 +419,9 @@ export function InCallView({ } } - buttons.push(); + buttons.push( + + ); footer =
{buttons}
; } From 2df8488c20422c4da3fb36d6619051231d2dfbf9 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 12 May 2023 11:43:17 -0400 Subject: [PATCH 225/286] Cap the size of the local tile in 1:1 calls So that it doesn't cover up too much of the remote tile at small window sizes --- src/video-grid/VideoGrid.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/video-grid/VideoGrid.tsx b/src/video-grid/VideoGrid.tsx index 522924f8..99b9df60 100644 --- a/src/video-grid/VideoGrid.tsx +++ b/src/video-grid/VideoGrid.tsx @@ -172,8 +172,16 @@ function getOneOnOneLayoutTilePositions( const gridAspectRatio = gridWidth / gridHeight; const smallPip = gridAspectRatio < 1 || gridWidth < 700; - const pipWidth = smallPip ? 114 : 230; - const pipHeight = smallPip ? 163 : 155; + const maxPipWidth = smallPip ? 114 : 230; + const maxPipHeight = smallPip ? 163 : 155; + // Cap the PiP size at 1/3 the remote tile size, preserving aspect ratio + const pipScaleFactor = Math.min( + 1, + remotePosition.width / 3 / maxPipWidth, + remotePosition.height / 3 / maxPipHeight + ); + const pipWidth = maxPipWidth * pipScaleFactor; + const pipHeight = maxPipHeight * pipScaleFactor; const pipGap = getPipGap(gridAspectRatio, gridWidth); const pipMinX = remotePosition.x + pipGap; From 7c5c4d18706c6efa01b21c7c327d74450be749ba Mon Sep 17 00:00:00 2001 From: Timo K Date: Fri, 12 May 2023 18:24:19 +0200 Subject: [PATCH 226/286] rename to percentage --- src/analytics/PosthogSpanProcessor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/analytics/PosthogSpanProcessor.ts b/src/analytics/PosthogSpanProcessor.ts index a76ab308..8f883391 100644 --- a/src/analytics/PosthogSpanProcessor.ts +++ b/src/analytics/PosthogSpanProcessor.ts @@ -124,7 +124,7 @@ export class PosthogSpanProcessor implements SpanProcessor { const audioReceived = `${attributes["matrix.stats.summary.percentageReceivedAudioMedia"]}`; const maxJitter = `${attributes["matrix.stats.summary.maxJitter"]}`; const maxPacketLoss = `${attributes["matrix.stats.summary.maxPacketLoss"]}`; - const audioConcealmentRatio = `${attributes["matrix.stats.summary.audioConcealmentRatio"]}`; + const percentageConcealedAudio = `${attributes["matrix.stats.summary.percentageConcealedAudio"]}`; PosthogAnalytics.instance.trackEvent( { eventName: "MediaReceived", @@ -134,7 +134,7 @@ export class PosthogSpanProcessor implements SpanProcessor { videoReceived: videoReceived, maxJitter: maxJitter, maxPacketLoss: maxPacketLoss, - audioConcealmentRatio: audioConcealmentRatio, + percentageConcealedAudio: percentageConcealedAudio, }, // Send instantly because the window might be closing { send_instantly: true } From dc15fbc8c9d7b67e14c7a1bcc29cc2be9f681dcd Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 12 May 2023 14:10:34 -0400 Subject: [PATCH 227/286] Update matrix-widget-api To fix a TypeScript error I'm seeing when running the linter locally --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 98740625..04448078 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10568,9 +10568,9 @@ matrix-events-sdk@0.0.1: uuid "9" matrix-widget-api@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.3.1.tgz#e38f404c76bb15c113909505c1c1a5b4d781c2f5" - integrity sha512-+rN6vGvnXm+fn0uq9r2KWSL/aPtehD6ObC50jYmUcEfgo8CUpf9eUurmjbRlwZkWq3XHXFuKQBUCI9UzqWg37Q== + version "1.4.0" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.4.0.tgz#e426ec16a013897f3a4a9c2bff423f54ab0ba745" + integrity sha512-dw0dRylGQzDUoiaY/g5xx1tBbS7aoov31PRtFMAvG58/4uerYllV9Gfou7w+I1aglwB6hihTREzKltVjARWV6A== dependencies: "@types/events" "^3.0.0" events "^3.2.0" From 440c617738f097bb86bb58a2ca1c865475ea903a Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 12 May 2023 14:04:23 -0400 Subject: [PATCH 228/286] Use a more noticeable shadow when displaying one tile on top another So that the local tile in 1:1 calls is more discernable against the background, especially when both participants have their video off --- src/video-grid/NewVideoGrid.tsx | 1 + src/video-grid/VideoGrid.tsx | 13 +++++++++++-- src/video-grid/VideoTile.module.css | 2 ++ src/video-grid/VideoTile.tsx | 7 ++++--- src/video-grid/VideoTileContainer.tsx | 1 + 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index a32c6b48..d8c45bf1 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -245,6 +245,7 @@ export const NewVideoGrid: FC = ({ opacity: 0, scale: 0, shadow: 1, + shadowSpread: 0, zIndex: 1, x, y, diff --git a/src/video-grid/VideoGrid.tsx b/src/video-grid/VideoGrid.tsx index 522924f8..94545f56 100644 --- a/src/video-grid/VideoGrid.tsx +++ b/src/video-grid/VideoGrid.tsx @@ -51,6 +51,7 @@ export interface TileSpring { opacity: number; scale: number; shadow: number; + shadowSpread: number; zIndex: number; x: number; y: number; @@ -892,6 +893,8 @@ export function VideoGrid({ // Whether the tile positions were valid at the time of the previous // animation const tilePositionsWereValid = tilePositionsValid.current; + const oneOnOneLayout = + tiles.length === 2 && !tiles.some((t) => t.presenter || t.focused); return (tileIndex: number) => { const tile = tiles[tileIndex]; @@ -911,12 +914,14 @@ export function VideoGrid({ opacity: 1, zIndex: 2, shadow: 15, + shadowSpread: 0, immediate: (key: string) => disableAnimations || key === "zIndex" || key === "x" || key === "y" || - key === "shadow", + key === "shadow" || + key === "shadowSpread", from: { shadow: 0, scale: 0, @@ -974,10 +979,14 @@ export function VideoGrid({ opacity: remove ? 0 : 1, zIndex: tilePosition.zIndex, shadow: 1, + shadowSpread: oneOnOneLayout && tile.item.isLocal ? 1 : 0, from, reset, immediate: (key: string) => - disableAnimations || key === "zIndex" || key === "shadow", + disableAnimations || + key === "zIndex" || + key === "shadow" || + key === "shadowSpread", // If we just stopped dragging a tile, give it time for the // animation to settle before pushing its z-index back down delay: (key: string) => (key === "zIndex" ? 500 : 0), diff --git a/src/video-grid/VideoTile.module.css b/src/video-grid/VideoTile.module.css index a83d1f0f..c2b9a632 100644 --- a/src/video-grid/VideoTile.module.css +++ b/src/video-grid/VideoTile.module.css @@ -22,6 +22,8 @@ limitations under the License. height: var(--tileHeight); --tileRadius: 8px; border-radius: var(--tileRadius); + box-shadow: rgba(0, 0, 0, 0.5) 0px var(--tileShadow) + calc(2 * var(--tileShadow)) var(--tileShadowSpread); overflow: hidden; cursor: pointer; diff --git a/src/video-grid/VideoTile.tsx b/src/video-grid/VideoTile.tsx index e3d26ad3..2c0c9d76 100644 --- a/src/video-grid/VideoTile.tsx +++ b/src/video-grid/VideoTile.tsx @@ -47,6 +47,7 @@ interface Props { opacity?: SpringValue; scale?: SpringValue; shadow?: SpringValue; + shadowSpread?: SpringValue; zIndex?: SpringValue; x?: SpringValue; y?: SpringValue; @@ -79,6 +80,7 @@ export const VideoTile = forwardRef( opacity, scale, shadow, + shadowSpread, zIndex, x, y, @@ -141,9 +143,6 @@ export const VideoTile = forwardRef( style={{ opacity, scale, - boxShadow: shadow?.to( - (s) => `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px 0px` - ), zIndex, x, y, @@ -152,6 +151,8 @@ export const VideoTile = forwardRef( // but React's types say no "--tileWidth": width?.to((w) => `${w}px`), "--tileHeight": height?.to((h) => `${h}px`), + "--tileShadow": shadow?.to((s) => `${s}px`), + "--tileShadowSpread": shadowSpread?.to((s) => `${s}px`), }} ref={ref as ForwardedRef} data-testid="videoTile" diff --git a/src/video-grid/VideoTileContainer.tsx b/src/video-grid/VideoTileContainer.tsx index c3a4c611..f3f50597 100644 --- a/src/video-grid/VideoTileContainer.tsx +++ b/src/video-grid/VideoTileContainer.tsx @@ -47,6 +47,7 @@ interface Props { opacity?: SpringValue; scale?: SpringValue; shadow?: SpringValue; + shadowSpread?: SpringValue; zIndex?: SpringValue; x?: SpringValue; y?: SpringValue; From a74733f6bc2de54038cd08e393641553c2c29698 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 12 May 2023 14:32:16 -0400 Subject: [PATCH 229/286] Show audio mute status next to people's names Previously we were showing a combination of audio and video status icons on people's name badges, which meant there was no way to tell whether someone who had their video off was muted or not. The designs call for only microphone icons to be shown here. --- src/video-grid/VideoTile.module.css | 10 ++++++---- src/video-grid/VideoTile.tsx | 5 ++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/video-grid/VideoTile.module.css b/src/video-grid/VideoTile.module.css index a83d1f0f..db2e50b4 100644 --- a/src/video-grid/VideoTile.module.css +++ b/src/video-grid/VideoTile.module.css @@ -83,6 +83,12 @@ limitations under the License. z-index: 1; } +.infoBubble > svg { + height: 16px; + width: 16px; + margin-right: 4px; +} + .toolbar { position: absolute; top: 0; @@ -126,10 +132,6 @@ limitations under the License. bottom: 16px; } -.memberName > * { - margin-right: 6px; -} - .memberName > :last-child { margin-right: 0px; } diff --git a/src/video-grid/VideoTile.tsx b/src/video-grid/VideoTile.tsx index e3d26ad3..005e10cb 100644 --- a/src/video-grid/VideoTile.tsx +++ b/src/video-grid/VideoTile.tsx @@ -20,8 +20,8 @@ import classNames from "classnames"; import { useTranslation } from "react-i18next"; import styles from "./VideoTile.module.css"; +import { ReactComponent as MicIcon } from "../icons/Mic.svg"; import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg"; -import { ReactComponent as VideoMutedIcon } from "../icons/VideoMuted.svg"; import { AudioButton, FullscreenButton } from "../button/Button"; import { ConnectionState } from "../room/useGroupCall"; @@ -178,9 +178,8 @@ export const VideoTile = forwardRef( Mute state is currently sent over to-device messages, which aren't quite real-time, so this is an important kludge to make sure no one appears muted when they've clearly begun talking. */ - audioMuted && !videoMuted && !speaking && + speaking || !audioMuted ? : } - {videoMuted && } {caption} From caf90d851e0eb2105a5a50d93d6ad5984508dc03 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 12 May 2023 14:49:55 -0400 Subject: [PATCH 230/286] Subtly animate active speaker indicators A light touch of animation here is consistent with what the designs call for, and what we've done with the toolbars on video tiles. --- src/video-grid/VideoTile.module.css | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/video-grid/VideoTile.module.css b/src/video-grid/VideoTile.module.css index a83d1f0f..5ec3aae3 100644 --- a/src/video-grid/VideoTile.module.css +++ b/src/video-grid/VideoTile.module.css @@ -45,7 +45,7 @@ limitations under the License. transform: scaleX(-1); } -.videoTile.speaking::after { +.videoTile::after { position: absolute; top: -1px; left: -1px; @@ -54,6 +54,12 @@ limitations under the License. content: ""; border-radius: var(--tileRadius); box-shadow: inset 0 0 0 4px var(--accent) !important; + opacity: 0; + transition: opacity ease 0.15s; +} + +.videoTile.speaking::after { + opacity: 1; } .videoTile.maximised { From fcb923f6dbd0e163101da3d33a09c5db64077207 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 13 May 2023 14:11:59 -0400 Subject: [PATCH 231/286] Update matrix-js-sdk --- package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 35425caa..07f3e43c 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#90e8336797480ff0e512d7c76ba6f420c2c3a2d5", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#aaae55736f5a1295aae249a391489514cab12b64", "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/yarn.lock b/yarn.lock index a7bce4e7..8306694e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10550,9 +10550,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#90e8336797480ff0e512d7c76ba6f420c2c3a2d5": +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#aaae55736f5a1295aae249a391489514cab12b64": version "25.1.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/90e8336797480ff0e512d7c76ba6f420c2c3a2d5" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/aaae55736f5a1295aae249a391489514cab12b64" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.8" From 7d454645d0690dcd737c4d2874e564693cc698f6 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 13 May 2023 14:27:01 -0400 Subject: [PATCH 232/286] Fix types --- test/otel/ObjectFlattener-test.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/test/otel/ObjectFlattener-test.ts b/test/otel/ObjectFlattener-test.ts index 84fce347..ac001ebb 100644 --- a/test/otel/ObjectFlattener-test.ts +++ b/test/otel/ObjectFlattener-test.ts @@ -1,3 +1,8 @@ +import { GroupCallStatsReport } from "matrix-js-sdk/src/webrtc/groupCall"; +import { + AudioConcealment, + ConnectionStatsReport, +} from "matrix-js-sdk/src/webrtc/stats/statsReport"; import { ObjectFlattener } from "../../src/otel/ObjectFlattener"; /* @@ -16,7 +21,12 @@ See the License for the specific language governing permissions and limitations under the License. */ describe("ObjectFlattener", () => { - const statsReport = { + const noConcealment: AudioConcealment = { + concealedAudio: 0, + totalAudioDuration: 0, + }; + + const statsReport: GroupCallStatsReport = { report: { bandwidth: { upload: 426, download: 0 }, bitrate: { @@ -92,8 +102,14 @@ describe("ObjectFlattener", () => { rtt: null, }, ], + audioConcealment: new Map([ + ["REMOTE_AUDIO_TRACK_ID", noConcealment], + ["REMOTE_VIDEO_TRACK_ID", noConcealment], + ]), + totalAudioConcealment: noConcealment, }, }; + describe("on flattenObjectRecursive", () => { it("should flatter an Map object", () => { const flatObject = {}; From 047fc822d67a9dda7c58349aa59a9597b31605fb Mon Sep 17 00:00:00 2001 From: raspin0 Date: Sat, 13 May 2023 00:23:35 +0000 Subject: [PATCH 233/286] Translated using Weblate (Polish) Currently translated at 100.0% (141 of 141 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/pl/ --- public/locales/pl/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/locales/pl/app.json b/public/locales/pl/app.json index 7359a561..d55018ae 100644 --- a/public/locales/pl/app.json +++ b/public/locales/pl/app.json @@ -99,7 +99,7 @@ "Debug log request": "Prośba o dzienniki debugowania", "Debug log": "Dzienniki debugowania", "Create account": "Utwórz konto", - "Copy and share this call link": "Skopiuj i podziel się linkiem do połączenia", + "Copy and share this call link": "Skopiuj i udostępnij link do rozmowy", "Copied!": "Skopiowano!", "Connection lost": "Połączenie utracone", "Confirm password": "Potwierdź hasło", @@ -129,7 +129,7 @@ "Copy": "Kopiuj", "<0>Submitting debug logs will help us track down the problem.": "<0>Wysłanie logów debuggowania pomoże nam ustalić przyczynę problemu.", "<0>Oops, something's gone wrong.": "<0>Ojej, coś poszło nie tak.", - "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Dołącz do rozmowy teraz<1>Or<2>Skopiuj link do rozmowy i dołącz później", + "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Dołącz do rozmowy już teraz<1>Or<2>Skopiuj link do rozmowy i dołącz później", "{{name}} (Waiting for video...)": "{{name}} (Oczekiwanie na wideo...)", "{{name}} (Connecting...)": "{{name}} (Łączenie...)", "Expose developer settings in the settings window.": "Wyświetlaj opcje programisty w oknie ustawień.", From b0ba366a2cfb0bd63fd61e8f4c0697fe6c771d2c Mon Sep 17 00:00:00 2001 From: Weblate Date: Sun, 14 May 2023 00:34:03 +0000 Subject: [PATCH 234/286] Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/ --- public/locales/bg/app.json | 1 - public/locales/cs/app.json | 1 - public/locales/de/app.json | 1 - public/locales/el/app.json | 1 - public/locales/es/app.json | 1 - public/locales/et/app.json | 1 - public/locales/fa/app.json | 1 - public/locales/fr/app.json | 1 - public/locales/id/app.json | 1 - public/locales/ja/app.json | 1 - public/locales/pl/app.json | 1 - public/locales/ru/app.json | 1 - public/locales/sk/app.json | 1 - public/locales/tr/app.json | 1 - public/locales/uk/app.json | 1 - public/locales/zh-Hans/app.json | 1 - public/locales/zh-Hant/app.json | 1 - 17 files changed, 17 deletions(-) diff --git a/public/locales/bg/app.json b/public/locales/bg/app.json index 97f42aaf..ec37ef1d 100644 --- a/public/locales/bg/app.json +++ b/public/locales/bg/app.json @@ -46,7 +46,6 @@ "Join call now": "Влез в разговора сега", "Join existing call?": "Присъединяване към съществуващ разговор?", "Leave": "Напусни", - "Loading room…": "Напускане на стаята…", "Loading…": "Зареждане…", "Local volume": "Локална сила на звука", "Logging in…": "Влизане…", diff --git a/public/locales/cs/app.json b/public/locales/cs/app.json index 9bcdbc4f..43abeeb5 100644 --- a/public/locales/cs/app.json +++ b/public/locales/cs/app.json @@ -71,7 +71,6 @@ "Logging in…": "Přihlašování se…", "Local volume": "Lokální hlasitost", "Loading…": "Načítání…", - "Loading room…": "Načítání místnosti…", "Leave": "Opustit hovor", "Join call now": "Připojit se k hovoru", "Join call": "Připojit se k hovoru", diff --git a/public/locales/de/app.json b/public/locales/de/app.json index d738e8cb..9ecac302 100644 --- a/public/locales/de/app.json +++ b/public/locales/de/app.json @@ -45,7 +45,6 @@ "Join call now": "Anruf beitreten", "Join existing call?": "An bestehendem Anruf teilnehmen?", "Leave": "Verlassen", - "Loading room…": "Lade Raum …", "Loading…": "Lade …", "Local volume": "Lokale Lautstärke", "Logging in…": "Anmelden …", diff --git a/public/locales/el/app.json b/public/locales/el/app.json index 41913bd1..7facbe32 100644 --- a/public/locales/el/app.json +++ b/public/locales/el/app.json @@ -69,7 +69,6 @@ "Microphone": "Μικρόφωνο", "Login": "Σύνδεση", "Loading…": "Φόρτωση…", - "Loading room…": "Φόρτωση δωματίου…", "Leave": "Αποχώρηση", "Join existing call?": "Συμμετοχή στην υπάρχουσα κλήση;", "Join call now": "Συμμετοχή στην κλήση τώρα", diff --git a/public/locales/es/app.json b/public/locales/es/app.json index 3fa0cd8e..de72a5d9 100644 --- a/public/locales/es/app.json +++ b/public/locales/es/app.json @@ -77,7 +77,6 @@ "Logging in…": "Iniciando sesión…", "Local volume": "Volumen local", "Loading…": "Cargando…", - "Loading room…": "Cargando sala…", "Leave": "Abandonar", "Join existing call?": "¿Unirse a llamada existente?", "Join call now": "Unirse a la llamada ahora", diff --git a/public/locales/et/app.json b/public/locales/et/app.json index 0267ec18..9c9bab6e 100644 --- a/public/locales/et/app.json +++ b/public/locales/et/app.json @@ -70,7 +70,6 @@ "Logging in…": "Sisselogimine …", "Local volume": "Kohalik helitugevus", "Loading…": "Laadimine …", - "Loading room…": "Ruumi laadimine …", "Leave": "Lahku", "Join existing call?": "Liitu juba käimasoleva kõnega?", "Join call now": "Kõnega liitumine", diff --git a/public/locales/fa/app.json b/public/locales/fa/app.json index e59506f6..7ecc3b7b 100644 --- a/public/locales/fa/app.json +++ b/public/locales/fa/app.json @@ -21,7 +21,6 @@ "Login to your account": "به حساب کاربری خود وارد شوید", "Login": "ورود", "Loading…": "بارگزاری…", - "Loading room…": "بارگزاری اتاق…", "Leave": "خروج", "Join existing call?": "پیوست به تماس؟", "Join call now": "الان به تماس بپیوند", diff --git a/public/locales/fr/app.json b/public/locales/fr/app.json index 8c6fafe2..4e682c7d 100644 --- a/public/locales/fr/app.json +++ b/public/locales/fr/app.json @@ -43,7 +43,6 @@ "Join call now": "Rejoindre l’appel maintenant", "Join existing call?": "Rejoindre un appel existant ?", "Leave": "Partir", - "Loading room…": "Chargement du salon…", "Loading…": "Chargement…", "Local volume": "Volume local", "Logging in…": "Connexion…", diff --git a/public/locales/id/app.json b/public/locales/id/app.json index 9102c2be..b81a57fe 100644 --- a/public/locales/id/app.json +++ b/public/locales/id/app.json @@ -46,7 +46,6 @@ "Join call now": "Bergabung ke panggilan sekarang", "Join existing call?": "Bergabung ke panggilan yang sudah ada?", "Leave": "Keluar", - "Loading room…": "Memuat ruangan…", "Loading…": "Memuat…", "Local volume": "Volume lokal", "Logging in…": "Memasuki…", diff --git a/public/locales/ja/app.json b/public/locales/ja/app.json index 0550c780..44fae025 100644 --- a/public/locales/ja/app.json +++ b/public/locales/ja/app.json @@ -53,7 +53,6 @@ "Login": "ログイン", "Logging in…": "ログインしています…", "Loading…": "読み込んでいます…", - "Loading room…": "ルームを読み込んでいます…", "Leave": "退出", "Version: {{version}}": "バージョン:{{version}}", "Username": "ユーザー名", diff --git a/public/locales/pl/app.json b/public/locales/pl/app.json index d55018ae..9ba41b08 100644 --- a/public/locales/pl/app.json +++ b/public/locales/pl/app.json @@ -73,7 +73,6 @@ "Logging in…": "Logowanie…", "Local volume": "Lokalna głośność", "Loading…": "Ładowanie…", - "Loading room…": "Ładowanie pokoju…", "Leave": "Opuść", "Join existing call?": "Dołączyć do istniejącego połączenia?", "Join call now": "Dołącz do połączenia teraz", diff --git a/public/locales/ru/app.json b/public/locales/ru/app.json index ad201107..5aa99c2c 100644 --- a/public/locales/ru/app.json +++ b/public/locales/ru/app.json @@ -86,7 +86,6 @@ "Login to your account": "Войдите в свой аккаунт", "Login": "Вход", "Loading…": "Загрузка…", - "Loading room…": "Загрузка комнаты…", "Leave": "Покинуть", "Join existing call?": "Присоединиться к существующему звонку?", "Join call now": "Присоединиться сейчас", diff --git a/public/locales/sk/app.json b/public/locales/sk/app.json index 8f6a9237..ce0d4ee0 100644 --- a/public/locales/sk/app.json +++ b/public/locales/sk/app.json @@ -52,7 +52,6 @@ "Login": "Prihlásiť sa", "Logging in…": "Prihlasovanie…", "Loading…": "Načítanie…", - "Loading room…": "Načítanie miestnosti…", "Leave": "Opustiť", "Join existing call?": "Pripojiť sa k existujúcemu hovoru?", "Join call now": "Pripojiť sa k hovoru teraz", diff --git a/public/locales/tr/app.json b/public/locales/tr/app.json index 79f21f6e..5cf079ef 100644 --- a/public/locales/tr/app.json +++ b/public/locales/tr/app.json @@ -43,7 +43,6 @@ "Join call now": "Aramaya katıl", "Join existing call?": "Mevcut aramaya katıl?", "Leave": "Çık", - "Loading room…": "Oda yükleniyor…", "Loading…": "Yükleniyor…", "Local volume": "Yerel ses seviyesi", "Logging in…": "Giriliyor…", diff --git a/public/locales/uk/app.json b/public/locales/uk/app.json index 8017e1ad..42751cc3 100644 --- a/public/locales/uk/app.json +++ b/public/locales/uk/app.json @@ -72,7 +72,6 @@ "Login": "Увійти", "Logging in…": "Вхід…", "Local volume": "Локальна гучність", - "Loading room…": "Завантаження кімнати…", "Leave": "Вийти", "Join existing call?": "Приєднатися до наявного виклику?", "Join call now": "Приєднатися до виклику зараз", diff --git a/public/locales/zh-Hans/app.json b/public/locales/zh-Hans/app.json index ee4612ec..25ba5826 100644 --- a/public/locales/zh-Hans/app.json +++ b/public/locales/zh-Hans/app.json @@ -90,7 +90,6 @@ "Logging in…": "登录中……", "Local volume": "本地音量", "Loading…": "加载中……", - "Loading room…": "加载房间中……", "Leave": "离开", "Join existing call?": "加入现有的通话?", "Join call now": "现在加入通话", diff --git a/public/locales/zh-Hant/app.json b/public/locales/zh-Hant/app.json index c57e2109..e18e3d74 100644 --- a/public/locales/zh-Hant/app.json +++ b/public/locales/zh-Hant/app.json @@ -90,7 +90,6 @@ "Logging in…": "登入中…", "Local volume": "您的音量", "Loading…": "載入中…", - "Loading room…": "載入聊天室…", "Leave": "離開", "Join existing call?": "加入已開始的通話嗎?", "Join call now": "現在加入通話", From 207554f06778af69b9de2e0838ca55d0c70d7be6 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sun, 14 May 2023 01:32:11 -0400 Subject: [PATCH 235/286] Fix tests --- test/otel/ObjectFlattener-test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/otel/ObjectFlattener-test.ts b/test/otel/ObjectFlattener-test.ts index ac001ebb..e3709d20 100644 --- a/test/otel/ObjectFlattener-test.ts +++ b/test/otel/ObjectFlattener-test.ts @@ -214,6 +214,12 @@ describe("ObjectFlattener", () => { "matrix.stats.conn.transport.1.remoteCandidateType": "srfx", "matrix.stats.conn.transport.1.networkType": "ethernet", "matrix.stats.conn.transport.1.rtt": "null", + "matrix.stats.conn.audioConcealment.REMOTE_AUDIO_TRACK_ID.concealedAudio": 0, + "matrix.stats.conn.audioConcealment.REMOTE_AUDIO_TRACK_ID.totalAudioDuration": 0, + "matrix.stats.conn.audioConcealment.REMOTE_VIDEO_TRACK_ID.concealedAudio": 0, + "matrix.stats.conn.audioConcealment.REMOTE_VIDEO_TRACK_ID.totalAudioDuration": 0, + "matrix.stats.conn.totalAudioConcealment.concealedAudio": 0, + "matrix.stats.conn.totalAudioConcealment.totalAudioDuration": 0, }); }); }); From f0f2ffe9726a01f85db816ec32e4579f8bf7d981 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Sun, 14 May 2023 16:36:41 +0200 Subject: [PATCH 236/286] add audio concealment to posthog exporter (#1042) * add audio concealment to posthog exporter --- src/analytics/PosthogSpanProcessor.ts | 2 ++ src/otel/otel.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/analytics/PosthogSpanProcessor.ts b/src/analytics/PosthogSpanProcessor.ts index e9e287a2..8f883391 100644 --- a/src/analytics/PosthogSpanProcessor.ts +++ b/src/analytics/PosthogSpanProcessor.ts @@ -124,6 +124,7 @@ export class PosthogSpanProcessor implements SpanProcessor { const audioReceived = `${attributes["matrix.stats.summary.percentageReceivedAudioMedia"]}`; const maxJitter = `${attributes["matrix.stats.summary.maxJitter"]}`; const maxPacketLoss = `${attributes["matrix.stats.summary.maxPacketLoss"]}`; + const percentageConcealedAudio = `${attributes["matrix.stats.summary.percentageConcealedAudio"]}`; PosthogAnalytics.instance.trackEvent( { eventName: "MediaReceived", @@ -133,6 +134,7 @@ export class PosthogSpanProcessor implements SpanProcessor { videoReceived: videoReceived, maxJitter: maxJitter, maxPacketLoss: maxPacketLoss, + percentageConcealedAudio: percentageConcealedAudio, }, // Send instantly because the window might be closing { send_instantly: true } diff --git a/src/otel/otel.ts b/src/otel/otel.ts index e30e84a0..8fc80805 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -42,7 +42,7 @@ export class ElementCallOpenTelemetry { const config = Config.get(); // we always enable opentelemetry in general. We only enable the OTLP // collector if a URL is defined (and in future if another setting is defined) - // The posthog exporter is always enabled, posthog reporting is enabled or disabled + // Posthog reporting is enabled or disabled // within the posthog code. const shouldEnableOtlp = Boolean(config.opentelemetry?.collector_url); From 172ff266a6ee14ae0fea892b5f4c3f82b0cbc380 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Mon, 15 May 2023 10:55:36 +0200 Subject: [PATCH 237/286] Peer connections count in posthog media summary (#1046) --- src/analytics/PosthogSpanProcessor.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/analytics/PosthogSpanProcessor.ts b/src/analytics/PosthogSpanProcessor.ts index 8f883391..1edb5a2f 100644 --- a/src/analytics/PosthogSpanProcessor.ts +++ b/src/analytics/PosthogSpanProcessor.ts @@ -124,6 +124,7 @@ export class PosthogSpanProcessor implements SpanProcessor { const audioReceived = `${attributes["matrix.stats.summary.percentageReceivedAudioMedia"]}`; const maxJitter = `${attributes["matrix.stats.summary.maxJitter"]}`; const maxPacketLoss = `${attributes["matrix.stats.summary.maxPacketLoss"]}`; + const peerConnections = `${attributes["matrix.stats.summary.peerConnections"]}`; const percentageConcealedAudio = `${attributes["matrix.stats.summary.percentageConcealedAudio"]}`; PosthogAnalytics.instance.trackEvent( { @@ -134,6 +135,7 @@ export class PosthogSpanProcessor implements SpanProcessor { videoReceived: videoReceived, maxJitter: maxJitter, maxPacketLoss: maxPacketLoss, + peerConnections: peerConnections, percentageConcealedAudio: percentageConcealedAudio, }, // Send instantly because the window might be closing From 01f20bf6c07015fdb63b94a4b5cdfd5a764e2892 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 15 May 2023 16:21:00 +0200 Subject: [PATCH 238/286] update matrix-js-sdk --- package.json | 2 +- yarn.lock | 20 ++++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 07f3e43c..7e132263 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#aaae55736f5a1295aae249a391489514cab12b64", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#af38021d282641d0ec0cb7e6d294f404f554bed9", "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/yarn.lock b/yarn.lock index 8306694e..0a77e1d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1209,13 +1209,20 @@ core-js-pure "^3.20.2" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.18.3", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.6.tgz#6a1ef59f838debd670421f8c7f2cbb8da9751580" integrity sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ== dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.5": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200" + integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q== + dependencies: + regenerator-runtime "^0.13.11" + "@babel/runtime@^7.13.9", "@babel/runtime@^7.9.2": version "7.19.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.4.tgz#a42f814502ee467d55b38dd1c256f53a7b885c78" @@ -10550,9 +10557,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#aaae55736f5a1295aae249a391489514cab12b64": +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#af38021d282641d0ec0cb7e6d294f404f554bed9": version "25.1.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/aaae55736f5a1295aae249a391489514cab12b64" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/af38021d282641d0ec0cb7e6d294f404f554bed9" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.8" @@ -12702,7 +12709,12 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7: +regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + +regenerator-runtime@^0.13.7: version "0.13.9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== From abdb85226ff41a8a063f87b4cef0f8f7671a8e49 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 15 May 2023 11:04:57 -0400 Subject: [PATCH 239/286] Update matrix-js-sdk --- package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 7e132263..cc593a0e 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#af38021d282641d0ec0cb7e6d294f404f554bed9", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#fcbc195fbe4170251b87f03a69c8dc5bfccfd5ac", "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/yarn.lock b/yarn.lock index b9722def..71620d55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10557,9 +10557,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#af38021d282641d0ec0cb7e6d294f404f554bed9": +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#fcbc195fbe4170251b87f03a69c8dc5bfccfd5ac": version "25.1.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/af38021d282641d0ec0cb7e6d294f404f554bed9" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fcbc195fbe4170251b87f03a69c8dc5bfccfd5ac" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.8" From f1ee3604de8be06e32bb16663aa0d5e1fcceab41 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 15 May 2023 22:03:26 -0400 Subject: [PATCH 240/286] Make Element Call work in Firefox's resist fingerprinting mode This one is gonna take some explaining: When in resist fingerprinting mode, Firefox exhibits some funny behavior: when we ask for the the list of media devices, it gives us fake device IDs. But when the js-sdk requests a stream for any of those devices, Firefox associates the stream with the real device ID. Now, in order to get the names of devices included in their metadata when you query the device list, you need to be holding a stream. For this reason, useMediaHandler was set up to reload the device list whenever matrix-js-sdk got a new local stream. But because of the inconsistency in device IDs, it would enter an infinite cycle telling matrix-js-sdk to request a stream for the fake device ID, but with matrix-js-sdk always responding with the real device ID. I already wasn't happy with useMediaHandler's use of @ts-ignore comments to inspect private js-sdk fields, and in the meantime we've come up with a simpler function for requesting device names, so I decided to refactor useMediaHandler to use it instead. Importantly, it doesn't break in resist fingerprinting mode. This created a new UX issue though: now, when on the lobby screen, useMediaHandler would request microphone access so it could get device names, followed immediately by a *second* pop-up for the lobby screen to request camera access. That's 1 pop-up too many, so I changed useMediaHandler to only request device names when a component is mounted that actually wants to show them. Currently, the settings modal is the only such component, and users normally only open it *after* granting full audio/video access, so this solution works out quite nicely. --- src/media-utils.ts | 2 +- src/room/GroupCallView.tsx | 4 +- src/settings/SettingsModal.tsx | 2 + src/settings/useMediaHandler.tsx | 203 +++++++++++++++---------------- 4 files changed, 104 insertions(+), 107 deletions(-) diff --git a/src/media-utils.ts b/src/media-utils.ts index 382360b0..30bade9c 100644 --- a/src/media-utils.ts +++ b/src/media-utils.ts @@ -47,7 +47,7 @@ export async function findDeviceByName( * * @return The available media devices */ -export async function getDevices(): Promise { +export async function getNamedDevices(): Promise { // First get the devices without their labels, to learn what kinds of streams // we can request let devices: MediaDeviceInfo[]; diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 1f208623..722686ca 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -34,7 +34,7 @@ import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler"; import { useLocationNavigation } from "../useLocationNavigation"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { useMediaHandler } from "../settings/useMediaHandler"; -import { findDeviceByName, getDevices } from "../media-utils"; +import { findDeviceByName, getNamedDevices } from "../media-utils"; declare global { interface Window { @@ -102,7 +102,7 @@ export function GroupCallView({ // Get the available devices so we can match the selected device // to its ID. This involves getting a media stream (see docs on // the function) so we only do it once and re-use the result. - const devices = await getDevices(); + const devices = await getNamedDevices(); const { audioInput, videoInput } = ev.detail .data as unknown as JoinCallData; diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index b126bb87..3b9db2ed 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -57,7 +57,9 @@ export const SettingsModal = (props: Props) => { audioOutput, audioOutputs, setAudioOutput, + useDeviceNames, } = useMediaHandler(); + useDeviceNames(); const [spatialAudio, setSpatialAudio] = useSpatialAudio(); const [showInspector, setShowInspector] = useShowInspector(); diff --git a/src/settings/useMediaHandler.tsx b/src/settings/useMediaHandler.tsx index ce3953ef..6bc3e565 100644 --- a/src/settings/useMediaHandler.tsx +++ b/src/settings/useMediaHandler.tsx @@ -32,7 +32,6 @@ limitations under the License. */ import { MatrixClient } from "matrix-js-sdk/src/client"; -import { MediaHandlerEvent } from "matrix-js-sdk/src/webrtc/mediaHandler"; import React, { useState, useEffect, @@ -41,18 +40,26 @@ import React, { useContext, createContext, ReactNode, + useRef, } from "react"; +import { getNamedDevices } from "../media-utils"; + export interface MediaHandlerContextInterface { - audioInput: string; + audioInput: string | undefined; audioInputs: MediaDeviceInfo[]; setAudioInput: (deviceId: string) => void; - videoInput: string; + videoInput: string | undefined; videoInputs: MediaDeviceInfo[]; setVideoInput: (deviceId: string) => void; - audioOutput: string; + audioOutput: string | undefined; audioOutputs: MediaDeviceInfo[]; setAudioOutput: (deviceId: string) => void; + /** + * A hook which requests for devices to be named. This requires media + * permissions. + */ + useDeviceNames: () => void; } const MediaHandlerContext = @@ -70,10 +77,10 @@ function getMediaPreferences(): MediaPreferences { try { return JSON.parse(mediaPreferences); } catch (e) { - return undefined; + return {}; } } else { - return undefined; + return {}; } } @@ -103,112 +110,98 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element { audioOutputs, }, setState, - ] = useState(() => { - const mediaPreferences = getMediaPreferences(); - const mediaHandler = client.getMediaHandler(); + ] = useState(() => ({ + audioInput: undefined as string | undefined, + videoInput: undefined as string | undefined, + audioOutput: undefined as string | undefined, + audioInputs: [] as MediaDeviceInfo[], + videoInputs: [] as MediaDeviceInfo[], + audioOutputs: [] as MediaDeviceInfo[], + })); - mediaHandler.restoreMediaSettings( - mediaPreferences?.audioInput, - mediaPreferences?.videoInput - ); + // A ref counting the number of components currently mounted that want + // to know device names + const numComponentsWantingNames = useRef(0); - return { - // @ts-ignore, ignore that audioInput is a private members of mediaHandler - audioInput: mediaHandler.audioInput, - // @ts-ignore, ignore that videoInput is a private members of mediaHandler - videoInput: mediaHandler.videoInput, - audioOutput: undefined, - audioInputs: [], - videoInputs: [], - audioOutputs: [], - }; - }); + const updateDevices = useCallback( + async (initial: boolean) => { + // Only request device names if components actually want them, because it + // could trigger an extra permission pop-up + const devices = await (numComponentsWantingNames.current > 0 + ? getNamedDevices() + : navigator.mediaDevices.enumerateDevices()); + const mediaPreferences = getMediaPreferences(); + + const audioInputs = devices.filter((d) => d.kind === "audioinput"); + const videoInputs = devices.filter((d) => d.kind === "videoinput"); + const audioOutputs = devices.filter((d) => d.kind === "audiooutput"); + + const audioInput = ( + mediaPreferences.audioInput === undefined + ? audioInputs.at(0) + : audioInputs.find( + (d) => d.deviceId === mediaPreferences.audioInput + ) ?? audioInputs.at(0) + )?.deviceId; + const videoInput = ( + mediaPreferences.videoInput === undefined + ? videoInputs.at(0) + : videoInputs.find( + (d) => d.deviceId === mediaPreferences.videoInput + ) ?? videoInputs.at(0) + )?.deviceId; + const audioOutput = + mediaPreferences.audioOutput === undefined + ? undefined + : audioOutputs.find( + (d) => d.deviceId === mediaPreferences.audioOutput + )?.deviceId; + + updateMediaPreferences({ audioInput, videoInput, audioOutput }); + setState({ + audioInput, + videoInput, + audioOutput, + audioInputs, + videoInputs, + audioOutputs, + }); + + if ( + initial || + audioInput !== mediaPreferences.audioInput || + videoInput !== mediaPreferences.videoInput + ) { + client.getMediaHandler().setMediaInputs(audioInput, videoInput); + } + }, + [client, setState] + ); + + const useDeviceNames = useCallback(() => { + // This is a little weird from React's perspective as it looks like a + // dynamic hook, but it works + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + numComponentsWantingNames.current++; + if (numComponentsWantingNames.current === 1) updateDevices(false); + return () => void numComponentsWantingNames.current--; + }, []); + }, [updateDevices]); useEffect(() => { - const mediaHandler = client.getMediaHandler(); - - function updateDevices(): void { - navigator.mediaDevices.enumerateDevices().then((devices) => { - const mediaPreferences = getMediaPreferences(); - - const audioInputs = devices.filter( - (device) => device.kind === "audioinput" - ); - const audioConnected = audioInputs.some( - // @ts-ignore - (device) => device.deviceId === mediaHandler.audioInput - ); - // @ts-ignore - let audioInput = mediaHandler.audioInput; - - if (!audioConnected && audioInputs.length > 0) { - audioInput = audioInputs[0].deviceId; - } - - const videoInputs = devices.filter( - (device) => device.kind === "videoinput" - ); - const videoConnected = videoInputs.some( - // @ts-ignore - (device) => device.deviceId === mediaHandler.videoInput - ); - - // @ts-ignore - let videoInput = mediaHandler.videoInput; - - if (!videoConnected && videoInputs.length > 0) { - videoInput = videoInputs[0].deviceId; - } - - const audioOutputs = devices.filter( - (device) => device.kind === "audiooutput" - ); - let audioOutput = undefined; - - if ( - mediaPreferences && - audioOutputs.some( - (device) => device.deviceId === mediaPreferences.audioOutput - ) - ) { - audioOutput = mediaPreferences.audioOutput; - } - - if ( - // @ts-ignore - (mediaHandler.videoInput && mediaHandler.videoInput !== videoInput) || - // @ts-ignore - mediaHandler.audioInput !== audioInput - ) { - mediaHandler.setMediaInputs(audioInput, videoInput); - } - - updateMediaPreferences({ audioInput, videoInput, audioOutput }); - - setState({ - audioInput, - videoInput, - audioOutput, - audioInputs, - videoInputs, - audioOutputs, - }); - }); - } - updateDevices(); - - mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, updateDevices); - navigator.mediaDevices.addEventListener("devicechange", updateDevices); + updateDevices(true); + const onDeviceChange = () => updateDevices(false); + navigator.mediaDevices.addEventListener("devicechange", onDeviceChange); return () => { - mediaHandler.removeListener( - MediaHandlerEvent.LocalStreamsChanged, - updateDevices + navigator.mediaDevices.removeEventListener( + "devicechange", + onDeviceChange ); - navigator.mediaDevices.removeEventListener("devicechange", updateDevices); - mediaHandler.stopAllStreams(); + client.getMediaHandler().stopAllStreams(); }; - }, [client]); + }, [client, updateDevices]); const setAudioInput: (deviceId: string) => void = useCallback( (deviceId: string) => { @@ -245,6 +238,7 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element { audioOutput, audioOutputs, setAudioOutput, + useDeviceNames, }), [ audioInput, @@ -256,6 +250,7 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element { audioOutput, audioOutputs, setAudioOutput, + useDeviceNames, ] ); From 0114db7d2d124e13d960623b8a475a67e50fe22e Mon Sep 17 00:00:00 2001 From: Enrico Schwendig Date: Wed, 17 May 2023 18:00:37 +0200 Subject: [PATCH 241/286] update matrix-js-sdk (#1067) --- package.json | 2 +- yarn.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index cc593a0e..8d9773bb 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#fcbc195fbe4170251b87f03a69c8dc5bfccfd5ac", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#a7b1dcaf9514b2e424a387e266c6f383a5909927", "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/yarn.lock b/yarn.lock index 71620d55..2e8e7b4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1828,10 +1828,10 @@ resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0" integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw== -"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.8": - version "0.1.0-alpha.8" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.8.tgz#18dd8e7fb56602d2999d8a502b49e902a2bb3782" - integrity sha512-hdmbbGXKrN6JNo3wdBaR5Zs3lXlzllT3U43ViNTlabB3nKkOZQnEAN/Isv+4EQSgz1+8897veI9Q8sqlQX22oA== +"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.9": + version "0.1.0-alpha.9" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.9.tgz#00bc266781502641a661858a5a521dd4d95275fc" + integrity sha512-g5cjpFwA9h0CbEGoAqNVI2QcyDsbI8FHoLo9+OXWHIezEKITsSv78mc5ilIwN+2YpmVlH0KNeQWTHw4vi0BMnw== "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": version "3.2.14" @@ -10557,12 +10557,12 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#fcbc195fbe4170251b87f03a69c8dc5bfccfd5ac": - version "25.1.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fcbc195fbe4170251b87f03a69c8dc5bfccfd5ac" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#a7b1dcaf9514b2e424a387e266c6f383a5909927": + version "25.1.1" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/a7b1dcaf9514b2e424a387e266c6f383a5909927" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.8" + "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.9" another-json "^0.2.0" bs58 "^5.0.0" content-type "^1.0.4" From c01e36363965c28ad979896c3c0a9b51935557e3 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 17 May 2023 13:59:15 -0400 Subject: [PATCH 242/286] Hide the rageshake request modal when the window is too small --- src/room/InCallView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 198edb25..2e37d38a 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -451,7 +451,7 @@ export function InCallView({ otelGroupCallMembership={otelGroupCallMembership} show={showInspector} /> - {rageshakeRequestModalState.isOpen && ( + {rageshakeRequestModalState.isOpen && !noControls && ( Date: Tue, 16 May 2023 21:51:34 +0000 Subject: [PATCH 243/286] Translated using Weblate (Polish) Currently translated at 100.0% (140 of 140 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/pl/ --- public/locales/pl/app.json | 46 +++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/public/locales/pl/app.json b/public/locales/pl/app.json index 9ba41b08..ada961a1 100644 --- a/public/locales/pl/app.json +++ b/public/locales/pl/app.json @@ -1,8 +1,8 @@ { "More menu": "Menu \"więcej\"", "Login": "Zaloguj się", - "Go": "Kontynuuj", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "Klikając \"Kontynuuj\", wyrażasz zgodę na nasze <2>Warunki", + "Go": "Przejdź", + "By clicking \"Go\", you agree to our <2>Terms and conditions": "Klikając \"Kontynuuj\", wyrażasz zgodę na nasze <2>Zasady i warunki", "{{count}} people connected|other": "{{count}} osób połączonych", "Your recent calls": "Twoje ostatnie połączenia", "You can't talk at the same time": "Nie możesz mówić w tym samym czasie", @@ -26,7 +26,7 @@ "This call already exists, would you like to join?": "Te połączenie już istnieje, czy chcesz do niego dołączyć?", "Thanks! We'll get right on it.": "Dziękujemy! Zaraz się tym zajmiemy.", "Talking…": "Mówienie…", - "Take me Home": "Zabierz mnie do ekranu startowego", + "Take me Home": "Zabierz mnie do strony głównej", "Submitting feedback…": "Przesyłanie opinii…", "Submit feedback": "Prześlij opinię", "Stop sharing screen": "Zatrzymaj udostępnianie ekranu", @@ -45,10 +45,10 @@ "Select an option": "Wybierz opcję", "Saving…": "Zapisywanie…", "Save": "Zapisz", - "Return to home screen": "Powróć do ekranu domowego", + "Return to home screen": "Powróć do strony głównej", "Remove": "Usuń", - "Release to stop": "Puść przycisk, aby przestać", - "Release spacebar key to stop": "Puść spację, aby przestać", + "Release to stop": "Puść przycisk, aby zatrzymać", + "Release spacebar key to stop": "Puść spację, aby zatrzymać", "Registering…": "Rejestrowanie…", "Register": "Zarejestruj", "Recaptcha not loaded": "Recaptcha nie została załadowana", @@ -58,7 +58,7 @@ "Press and hold to talk": "Przytrzymaj, aby mówić", "Press and hold spacebar to talk over {{name}}": "Przytrzymaj spację, aby mówić wraz z {{name}}", "Press and hold spacebar to talk": "Przytrzymaj spację, aby mówić", - "Passwords must match": "Hasła muszą być identyczne", + "Passwords must match": "Hasła muszą pasować", "Password": "Hasło", "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}": "Inni użytkownicy próbują dołączyć do tego połączenia przy użyciu niekompatybilnych wersji. Powinni oni upewnić się, że odświeżyli stronę w swoich przeglądarkach:<1>{userLis}", "Not registered yet? <2>Create an account": "Nie masz konta? <2>Utwórz je", @@ -71,7 +71,7 @@ "Microphone": "Mikrofon", "Login to your account": "Zaloguj się do swojego konta", "Logging in…": "Logowanie…", - "Local volume": "Lokalna głośność", + "Local volume": "Głośność lokalna", "Loading…": "Ładowanie…", "Leave": "Opuść", "Join existing call?": "Dołączyć do istniejącego połączenia?", @@ -86,15 +86,15 @@ "Home": "Strona domowa", "Having trouble? Help us fix it.": "Masz problem? Pomóż nam go naprawić.", "Grid layout menu": "Menu układu siatki", - "Full screen": "Pełen ekran", + "Full screen": "Pełny ekran", "Freedom": "Wolność", "Fetching group call timed out.": "Przekroczono limit czasu na uzyskanie połączenia grupowego.", - "Exit full screen": "Zamknij pełny ekran", + "Exit full screen": "Opuść pełny ekran", "Download debug logs": "Pobierz dzienniki debugowania", - "Display name": "Wyświetlana nazwa", - "Developer": "Deweloper", + "Display name": "Nazwa wyświetlana", + "Developer": "Programista", "Details": "Szczegóły", - "Description (optional)": "Opis (opcjonalny)", + "Description (optional)": "Opis (opcjonalne)", "Debug log request": "Prośba o dzienniki debugowania", "Debug log": "Dzienniki debugowania", "Create account": "Utwórz konto", @@ -104,20 +104,20 @@ "Confirm password": "Potwierdź hasło", "Close": "Zamknij", "Change layout": "Zmień układ", - "Camera/microphone permissions needed to join the call.": "Aby dołączyć do tego połączenia, potrzebne są uprawnienia do kamery/mikrofonu.", + "Camera/microphone permissions needed to join the call.": "Wymagane są uprawnienia do kamery/mikrofonu, aby dołączyć do rozmowy.", "Camera {{n}}": "Kamera {{n}}", "Camera": "Kamera", - "Call type menu": "Menu rodzaju połączenia", + "Call type menu": "Menu typu połączenia", "Call link copied": "Skopiowano link do połączenia", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Klikając \"Dołącz do rozmowy\", wyrażasz zgodę na nasze <2>Warunki", + "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Klikając \"Dołącz do rozmowy\", wyrażasz zgodę na nasze <2>Zasady i warunki", "Avatar": "Awatar", "Audio": "Dźwięk", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Inny użytkownik w tym połączeniu napotkał problem. Aby lepiej zdiagnozować tę usterkę, chcielibyśmy zebrać dzienniki debugowania.", - "Accept microphone permissions to join the call.": "Przyznaj uprawnienia do mikrofonu aby dołączyć do połączenia.", - "Accept camera/microphone permissions to join the call.": "Przyznaj uprawnienia do kamery/mikrofonu aby dołączyć do połączenia.", + "Accept microphone permissions to join the call.": "Akceptuj uprawnienia mikrofonu, aby dołączyć do połączenia.", + "Accept camera/microphone permissions to join the call.": "Akceptuj uprawnienia kamery/mikrofonu, aby dołączyć do połączenia.", "<0>Why not finish by setting up a password to keep your account?<1>You'll be able to keep your name and set an avatar for use on future calls": "<0>Może zechcesz ustawić hasło, aby zachować swoje konto?<1>Będziesz w stanie utrzymać swoją nazwę i ustawić awatar do wyświetlania podczas połączeń w przyszłości", - "<0>Create an account Or <2>Access as a guest": "<0>Utwórz konto Albo <2>Dołącz jako gość", - "<0>Already have an account?<1><0>Log in Or <2>Access as a guest": "<0>Masz już konto?<1><0>Zaloguj się Albo <2>Dołącz jako gość", + "<0>Create an account Or <2>Access as a guest": "<0>Utwórz konto lub <2>Dołącz jako gość", + "<0>Already have an account?<1><0>Log in Or <2>Access as a guest": "<0>Masz już konto?<1><0>Zaloguj się lub <2>Dołącz jako gość", "{{roomName}} - Walkie-talkie call": "{{roomName}} - połączenie walkie-talkie", "{{names}}, {{name}}": "{{names}}, {{name}}", "{{name}} is talking…": "{{name}} mówi…", @@ -126,12 +126,12 @@ "{{count}} people connected|one": "{{count}} osoba połączona", "This feature is only supported on Firefox.": "Ta funkcjonalność jest dostępna tylko w Firefox.", "Copy": "Kopiuj", - "<0>Submitting debug logs will help us track down the problem.": "<0>Wysłanie logów debuggowania pomoże nam ustalić przyczynę problemu.", + "<0>Submitting debug logs will help us track down the problem.": "<0>Wysłanie dzienników debuggowania pomoże nam ustalić przyczynę problemu.", "<0>Oops, something's gone wrong.": "<0>Ojej, coś poszło nie tak.", - "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Dołącz do rozmowy już teraz<1>Or<2>Skopiuj link do rozmowy i dołącz później", + "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Dołącz do rozmowy już teraz<1>lub<2>Skopiuj link do rozmowy i dołącz później", "{{name}} (Waiting for video...)": "{{name}} (Oczekiwanie na wideo...)", "{{name}} (Connecting...)": "{{name}} (Łączenie...)", - "Expose developer settings in the settings window.": "Wyświetlaj opcje programisty w oknie ustawień.", + "Expose developer settings in the settings window.": "Wyświetl opcje programisty w oknie ustawień.", "Element Call Home": "Strona główna Element Call", "Developer Settings": "Opcje programisty", "Talk over speaker": "Rozmowa przez głośnik", From cf1a7f2e21b915e5e3f2603e16edb12b111cd436 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 22 May 2023 12:54:26 -0400 Subject: [PATCH 244/286] Match settings modal to design nuances better --- src/Modal.module.css | 4 ++-- src/button/Button.module.css | 4 ++-- src/index.css | 8 +++++++- src/input/SelectInput.module.css | 2 -- src/settings/SettingsModal.module.css | 13 ++++--------- src/settings/SettingsModal.tsx | 2 +- src/tabs/Tabs.module.css | 13 +++++++++---- 7 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/Modal.module.css b/src/Modal.module.css index 30c8af04..5143f8b5 100644 --- a/src/Modal.module.css +++ b/src/Modal.module.css @@ -40,7 +40,7 @@ limitations under the License. .modalHeader { display: flex; justify-content: space-between; - padding: 34px 34px 0 34px; + padding: 34px 32px 0 32px; } .modalHeader h3 { @@ -72,7 +72,7 @@ limitations under the License. .modalHeader { display: flex; justify-content: space-between; - padding: 24px 24px 0 24px; + padding: 32px 20px 0 20px; } .modal.mobileFullScreen { diff --git a/src/button/Button.module.css b/src/button/Button.module.css index eb8f0b1b..b19db46f 100644 --- a/src/button/Button.module.css +++ b/src/button/Button.module.css @@ -39,10 +39,10 @@ limitations under the License. .secondaryHangup, .button, .copyButton { - padding: 7px 15px; + padding: 8px 20px; border-radius: 8px; font-size: var(--font-size-body); - font-weight: 700; + font-weight: 600; } .button { diff --git a/src/index.css b/src/index.css index fbc9d1a6..f9e00ef3 100644 --- a/src/index.css +++ b/src/index.css @@ -180,10 +180,16 @@ h2 { /* Subtitle */ h3 { - font-weight: 400; + font-weight: 600; font-size: var(--font-size-subtitle); } +/* Body Semi Bold */ +h4 { + font-weight: 600; + font-size: var(--font-size-body); +} + h1, h2, h3 { diff --git a/src/input/SelectInput.module.css b/src/input/SelectInput.module.css index 727ede0a..086be826 100644 --- a/src/input/SelectInput.module.css +++ b/src/input/SelectInput.module.css @@ -22,8 +22,6 @@ limitations under the License. } .label { - font-weight: 600; - font-size: var(--font-size-subtitle); margin-top: 0; margin-bottom: 12px; } diff --git a/src/settings/SettingsModal.module.css b/src/settings/SettingsModal.module.css index 1e44dad7..864d7954 100644 --- a/src/settings/SettingsModal.module.css +++ b/src/settings/SettingsModal.module.css @@ -19,6 +19,10 @@ limitations under the License. height: 480px; } +.settingsModal p { + color: var(--secondary-content); +} + .tabContainer { padding: 27px 20px; } @@ -26,12 +30,3 @@ limitations under the License. .fieldRowText { margin-bottom: 0; } - -/* -This style guarantees a fixed width of the tab bar in the settings window. -The "Developer" item in the tab bar can be toggled. -Without a defined width activating the developer tab makes the tab container jump to the right. -*/ -.tabLabel { - min-width: 80px; -} diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index b3d25a54..03dd2acf 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -195,7 +195,7 @@ export const SettingsModal = (props: Props) => { key="profile" title={ <> - + {t("Profile")} } diff --git a/src/tabs/Tabs.module.css b/src/tabs/Tabs.module.css index 188747cc..9ba61048 100644 --- a/src/tabs/Tabs.module.css +++ b/src/tabs/Tabs.module.css @@ -25,12 +25,14 @@ limitations under the License. list-style: none; padding: 0; margin: 0 auto 24px auto; + gap: 16px; + overflow: scroll; + max-width: 100%; } .tab { - max-width: 190px; - min-width: fit-content; height: 32px; + box-sizing: border-box; border-radius: 8px; background-color: transparent; display: flex; @@ -38,6 +40,7 @@ limitations under the License. padding: 0 8px; border: none; cursor: pointer; + font-size: var(--font-size-body); } .tab > * { @@ -78,17 +81,18 @@ limitations under the License. @media (min-width: 800px) { .tab { + width: 200px; padding: 0 16px; } .tab > * { - margin: 0 16px 0 0; + margin: 0 12px 0 0; } .tabContainer { width: 100%; flex-direction: row; - padding: 27px 20px; + padding: 20px 18px; box-sizing: border-box; overflow: hidden; } @@ -96,6 +100,7 @@ limitations under the License. .tabList { flex-direction: column; margin-bottom: 0; + gap: 0; } .tabPanel { From 69099772e02ec161b1bfe0401c57ff344d94a6ea Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 22 May 2023 13:44:53 -0400 Subject: [PATCH 245/286] Make settings button icon size match designs --- src/button/Button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/button/Button.tsx b/src/button/Button.tsx index 1ac79216..289f2b9a 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -238,7 +238,7 @@ export function SettingsButton({ return ( ); From 6560d9eb1a62a1403b059528801c0b0272b4b0ea Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 22 May 2023 13:51:05 -0400 Subject: [PATCH 246/286] Make remove avatar button target area larger --- src/input/AvatarInputField.module.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/input/AvatarInputField.module.css b/src/input/AvatarInputField.module.css index 14620425..626d1109 100644 --- a/src/input/AvatarInputField.module.css +++ b/src/input/AvatarInputField.module.css @@ -54,4 +54,6 @@ limitations under the License. .removeButton { color: var(--accent); + font-size: var(--font-size-caption); + padding: 6px 0; } From 85380c814231df2309787754c32cdc53d1b809c7 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 22 May 2023 13:59:18 -0400 Subject: [PATCH 247/286] Make width of profile tab conform to designs --- src/settings/ProfileSettingsTab.module.css | 6 ++++++ src/settings/ProfileSettingsTab.tsx | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/settings/ProfileSettingsTab.module.css b/src/settings/ProfileSettingsTab.module.css index 4b80e49a..e85a00a5 100644 --- a/src/settings/ProfileSettingsTab.module.css +++ b/src/settings/ProfileSettingsTab.module.css @@ -14,6 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +.content { + width: 100%; + max-width: 350px; + align-self: center; +} + .avatarFieldRow { justify-content: center; } diff --git a/src/settings/ProfileSettingsTab.tsx b/src/settings/ProfileSettingsTab.tsx index a6451336..7e325685 100644 --- a/src/settings/ProfileSettingsTab.tsx +++ b/src/settings/ProfileSettingsTab.tsx @@ -74,7 +74,7 @@ export function ProfileSettingsTab({ client }: Props) { ); return ( -
+ Date: Mon, 22 May 2023 14:33:20 -0400 Subject: [PATCH 248/286] Make the profile form autosave --- src/input/Input.tsx | 1 + src/settings/ProfileSettingsTab.tsx | 84 +++++++++++++---------------- src/settings/useMediaHandler.tsx | 2 +- 3 files changed, 38 insertions(+), 49 deletions(-) diff --git a/src/input/Input.tsx b/src/input/Input.tsx index afecd6ae..95d3fc54 100644 --- a/src/input/Input.tsx +++ b/src/input/Input.tsx @@ -72,6 +72,7 @@ interface InputFieldProps { autoCorrect?: string; autoCapitalize?: string; value?: string; + defaultValue?: string; placeholder?: string; defaultChecked?: boolean; onChange?: (event: ChangeEvent) => void; diff --git a/src/settings/ProfileSettingsTab.tsx b/src/settings/ProfileSettingsTab.tsx index 7e325685..023a7cd2 100644 --- a/src/settings/ProfileSettingsTab.tsx +++ b/src/settings/ProfileSettingsTab.tsx @@ -14,11 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ChangeEvent, useCallback, useState } from "react"; +import React, { useCallback, useEffect, useRef } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { useTranslation } from "react-i18next"; -import { Button } from "../button"; import { useProfile } from "../profile/useProfile"; import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { AvatarInputField } from "../input/AvatarInputField"; @@ -29,52 +28,47 @@ interface Props { } export function ProfileSettingsTab({ client }: Props) { const { t } = useTranslation(); - const { - error, - loading, - displayName: initialDisplayName, - avatarUrl, - saveProfile, - } = useProfile(client); - const [displayName, setDisplayName] = useState(initialDisplayName || ""); - const [removeAvatar, setRemoveAvatar] = useState(false); + const { error, displayName, avatarUrl, saveProfile } = useProfile(client); - const onRemoveAvatar = useCallback(() => { - setRemoveAvatar(true); + const formRef = useRef(null); + + const formChanged = useRef(false); + const onFormChange = useCallback(() => { + formChanged.current = true; }, []); - const onChangeDisplayName = useCallback( - (e: ChangeEvent) => { - setDisplayName(e.target.value); - }, - [setDisplayName] - ); + const removeAvatar = useRef(false); + const onRemoveAvatar = useCallback(() => { + removeAvatar.current = true; + formChanged.current = true; + }, []); - const onSubmit = useCallback( - (e) => { - e.preventDefault(); - const data = new FormData(e.target); - const displayNameDataEntry = data.get("displayName"); - const avatar: File | string = data.get("avatar"); + useEffect(() => { + const form = formRef.current!; + return () => { + if (formChanged.current) { + const data = new FormData(form); + const displayNameDataEntry = data.get("displayName"); + const avatar = data.get("avatar"); - const avatarSize = - typeof avatar == "string" ? avatar.length : avatar.size; - const displayName = - typeof displayNameDataEntry == "string" - ? displayNameDataEntry - : displayNameDataEntry.name; + const avatarSize = + typeof avatar == "string" ? avatar.length : avatar?.size ?? 0; + const displayName = + typeof displayNameDataEntry == "string" + ? displayNameDataEntry + : displayNameDataEntry?.name ?? null; - saveProfile({ - displayName, - avatar: avatar && avatarSize > 0 ? avatar : undefined, - removeAvatar: removeAvatar && (!avatar || avatarSize === 0), - }); - }, - [saveProfile, removeAvatar] - ); + saveProfile({ + displayName, + avatar: avatar && avatarSize > 0 ? avatar : undefined, + removeAvatar: removeAvatar.current && (!avatar || avatarSize === 0), + }); + } + }; + }, [saveProfile]); return ( - + @@ -104,8 +98,7 @@ export function ProfileSettingsTab({ client }: Props) { required autoComplete="off" placeholder={t("Display name")} - value={displayName} - onChange={onChangeDisplayName} + defaultValue={displayName} data-testid="profile_displayname" /> @@ -114,11 +107,6 @@ export function ProfileSettingsTab({ client }: Props) { )} - - -
); } diff --git a/src/settings/useMediaHandler.tsx b/src/settings/useMediaHandler.tsx index bf62eeb2..0d84996e 100644 --- a/src/settings/useMediaHandler.tsx +++ b/src/settings/useMediaHandler.tsx @@ -25,8 +25,8 @@ import React, { ReactNode, useRef, } from "react"; -import { useClient } from "../ClientContext"; +import { useClient } from "../ClientContext"; import { getNamedDevices } from "../media-utils"; export interface MediaHandlerContextInterface { From dc8d0fd81bf261451a728121ab5c2b2cfff67d6c Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 22 May 2023 14:34:37 -0400 Subject: [PATCH 249/286] Update strings --- public/locales/en-GB/app.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index 79d59fdb..ccd111b8 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -38,7 +38,6 @@ "Create account": "Create account", "Debug log": "Debug log", "Debug log request": "Debug log request", - "Description (optional)": "Description (optional)", "Details": "Details", "Developer": "Developer", "Developer Settings": "Developer Settings", @@ -47,14 +46,14 @@ "Element Call Home": "Element Call Home", "Exit full screen": "Exit full screen", "Expose developer settings in the settings window.": "Expose developer settings in the settings window.", - "feedback": "feedback", + "Feedback": "Feedback", "Fetching group call timed out.": "Fetching group call timed out.", "Freedom": "Freedom", "Full screen": "Full screen", "Go": "Go", "Grid layout menu": "Grid layout menu", - "Having trouble? Help us fix it.": "Having trouble? Help us fix it.", "Home": "Home", + "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.", "Include debug logs": "Include debug logs", "Incompatible versions": "Incompatible versions", "Incompatible versions!": "Incompatible versions!", @@ -94,8 +93,6 @@ "Release to stop": "Release to stop", "Remove": "Remove", "Return to home screen": "Return to home screen", - "Save": "Save", - "Saving…": "Saving…", "Select an option": "Select an option", "Send debug logs": "Send debug logs", "Sending debug logs…": "Sending debug logs…", @@ -110,11 +107,13 @@ "Speaker {{n}}": "Speaker {{n}}", "Spotlight": "Spotlight", "Stop sharing screen": "Stop sharing screen", + "Submit": "Submit", "Submit feedback": "Submit feedback", - "Submitting feedback…": "Submitting feedback…", + "Submitting…": "Submitting…", "Take me Home": "Take me Home", "Talk over speaker": "Talk over speaker", "Talking…": "Talking…", + "Thanks, we received your feedback!": "Thanks, we received your feedback!", "Thanks! We'll get right on it.": "Thanks! We'll get right on it.", "This call already exists, would you like to join?": "This call already exists, would you like to join?", "This feature is only supported on Firefox.": "This feature is only supported on Firefox.", @@ -124,7 +123,6 @@ "Turn on camera": "Turn on camera", "Unmute microphone": "Unmute microphone", "Use the upcoming grid system": "Use the upcoming grid system", - "User ID": "User ID", "User menu": "User menu", "Username": "Username", "Version: {{version}}": "Version: {{version}}", @@ -138,5 +136,6 @@ "WebRTC is not supported or is being blocked in this browser.": "WebRTC is not supported or is being blocked in this browser.", "Yes, join call": "Yes, join call", "You can't talk at the same time": "You can't talk at the same time", + "Your feedback": "Your feedback", "Your recent calls": "Your recent calls" } From 9c2f4be17c2272f4fdb4492c5b85844f84ad3657 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 22 May 2023 15:30:29 -0400 Subject: [PATCH 250/286] Bring back the rageshake request modal --- src/room/InCallView.tsx | 15 ++++++++++++++- src/room/PTTCallView.tsx | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index d27dd2cf..3446d4ac 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -75,6 +75,8 @@ import { NewVideoGrid } from "../video-grid/NewVideoGrid"; import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; import { SettingsModal } from "../settings/SettingsModal"; import { InviteModal } from "./InviteModal"; +import { useRageshakeRequestModal } from "../settings/submit-rageshake"; +import { RageshakeRequestModal } from "./RageshakeRequestModal"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); // There is currently a bug in Safari our our code with cloning and sending MediaStreams @@ -340,6 +342,11 @@ export function InCallView({ ); }; + const { + modalState: rageshakeRequestModalState, + modalProps: rageshakeRequestModalProps, + } = useRageshakeRequestModal(groupCall.room.roomId); + const { modalState: settingsModalState, modalProps: settingsModalProps, @@ -469,10 +476,16 @@ export function InCallView({ otelGroupCallMembership={otelGroupCallMembership} show={showInspector} /> + {rageshakeRequestModalState.isOpen && !noControls && ( + + )} {settingsModalState.isOpen && ( )} diff --git a/src/room/PTTCallView.tsx b/src/room/PTTCallView.tsx index af5437d0..44b4d74e 100644 --- a/src/room/PTTCallView.tsx +++ b/src/room/PTTCallView.tsx @@ -309,7 +309,7 @@ export const PTTCallView: React.FC = ({ {settingsModalState.isOpen && ( )} From 3c118f0cf73bcf7cfae8f0a2ceb65483712ba466 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 22 May 2023 15:44:39 -0400 Subject: [PATCH 251/286] Add a comment --- src/settings/ProfileSettingsTab.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/settings/ProfileSettingsTab.tsx b/src/settings/ProfileSettingsTab.tsx index 023a7cd2..409c0e6e 100644 --- a/src/settings/ProfileSettingsTab.tsx +++ b/src/settings/ProfileSettingsTab.tsx @@ -45,6 +45,7 @@ export function ProfileSettingsTab({ client }: Props) { useEffect(() => { const form = formRef.current!; + // Auto-save when the user dismisses this component return () => { if (formChanged.current) { const data = new FormData(form); From 99f06b0322e3e3f25ea97e91928607119ef38b1e Mon Sep 17 00:00:00 2001 From: Avery Date: Wed, 31 May 2023 22:11:17 +0000 Subject: [PATCH 252/286] Translated using Weblate (Spanish) Currently translated at 100.0% (140 of 140 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/es/ --- public/locales/es/app.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/locales/es/app.json b/public/locales/es/app.json index de72a5d9..b671eee7 100644 --- a/public/locales/es/app.json +++ b/public/locales/es/app.json @@ -135,5 +135,8 @@ "<0>Submitting debug logs will help us track down the problem.": "<0>Subir los registros de depuración nos ayudará a encontrar el problema.", "<0>Oops, something's gone wrong.": "<0>Ups, algo ha salido mal.", "Expose developer settings in the settings window.": "Muestra los ajustes de desarrollador en la ventana de ajustes.", - "Developer Settings": "Ajustes de desarrollador" + "Developer Settings": "Ajustes de desarrollador", + "Use the upcoming grid system": "Utilizar el próximo sistema de cuadrícula", + "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy and our <5>Cookie Policy.": "Al participar en esta beta, consientes a la recogida de datos anónimos, los cuales usaremos para mejorar el producto. Puedes encontrar más información sobre que datos recogemos en nuestra <2>Política de privacidad y en nuestra <5>Política sobre Cookies.", + "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>Puedes retirar tu consentimiento desmarcando esta casilla. Si estás en una llamada, este ajuste se aplicará al final de esta." } From aa519f3c67a280a436954e43d5edfbf78e17302a Mon Sep 17 00:00:00 2001 From: joemama Date: Wed, 31 May 2023 17:39:41 +0000 Subject: [PATCH 253/286] Translated using Weblate (Chinese (Simplified)) Currently translated at 95.7% (134 of 140 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hans/ --- public/locales/zh-Hans/app.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/locales/zh-Hans/app.json b/public/locales/zh-Hans/app.json index 25ba5826..382dc38d 100644 --- a/public/locales/zh-Hans/app.json +++ b/public/locales/zh-Hans/app.json @@ -130,5 +130,7 @@ "Call type menu": "通话类型菜单", "Call link copied": "链接已复制", "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "点击“现在加入”则表示同意我们的<2>条款与条件<2>", - "Avatar": "头像" + "Avatar": "头像", + "<0>Oops, something's gone wrong.": "Hello", + "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "Hi" } From 3f8848981d49c3f86ffe56c85b9c8c82d137d3ef Mon Sep 17 00:00:00 2001 From: Weblate Date: Thu, 1 Jun 2023 23:07:30 +0000 Subject: [PATCH 254/286] Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/ --- public/locales/bg/app.json | 6 ------ public/locales/cs/app.json | 6 ------ public/locales/de/app.json | 6 ------ public/locales/el/app.json | 4 ---- public/locales/es/app.json | 6 ------ public/locales/et/app.json | 6 ------ public/locales/fa/app.json | 7 ------- public/locales/fr/app.json | 6 ------ public/locales/id/app.json | 6 ------ public/locales/ja/app.json | 5 ----- public/locales/pl/app.json | 6 ------ public/locales/ru/app.json | 7 ------- public/locales/sk/app.json | 6 ------ public/locales/tr/app.json | 5 ----- public/locales/uk/app.json | 6 ------ public/locales/zh-Hans/app.json | 6 ------ public/locales/zh-Hant/app.json | 6 ------ 17 files changed, 100 deletions(-) diff --git a/public/locales/bg/app.json b/public/locales/bg/app.json index ec37ef1d..b0eaff4a 100644 --- a/public/locales/bg/app.json +++ b/public/locales/bg/app.json @@ -23,7 +23,6 @@ "Create account": "Създай акаунт", "Debug log": "Debug логове", "Debug log request": "Заявка за debug логове", - "Description (optional)": "Описание (незадължително)", "Details": "Детайли", "Developer": "Разработчик", "Display name": "Име/псевдоним", @@ -34,7 +33,6 @@ "Full screen": "Цял екран", "Go": "Напред", "Grid layout menu": "Меню \"решетков изглед\"", - "Having trouble? Help us fix it.": "Имате проблем? Помогнете да го поправим.", "Home": "Начало", "Include debug logs": "Включи debug логове", "Incompatible versions": "Несъвместими версии", @@ -55,7 +53,6 @@ "Microphone permissions needed to join the call.": "Необходими са разрешения за микрофона за да можете да се присъедините в разговора.", "Microphone {{n}}": "Микрофон {{n}}", "More": "Още", - "More menu": "Мено \"още\"", "Mute microphone": "Заглуши микрофона", "No": "Не", "Not now, return to home screen": "Не сега, върни се на началния екран", @@ -76,7 +73,6 @@ "Release to stop": "Отпуснете за да спрете", "Remove": "Премахни", "Return to home screen": "Връщане на началния екран", - "Save": "Запази", "Saving…": "Запазване…", "Select an option": "Изберете опция", "Send debug logs": "Изпратете debug логове", @@ -92,7 +88,6 @@ "Spotlight": "Прожектор", "Stop sharing screen": "Спри споделянето на екрана", "Submit feedback": "Изпрати обратна връзка", - "Submitting feedback…": "Изпращане на обратна връзка…", "Take me Home": "Отиди в Начало", "Talk over speaker": "Говорете заедно с говорителя", "Talking…": "Говорене…", @@ -103,7 +98,6 @@ "Turn off camera": "Изключи камерата", "Turn on camera": "Включи камерата", "Unmute microphone": "Включи микрофона", - "User ID": "Потребителски идентификатор", "User menu": "Потребителско меню", "Username": "Потребителско име", "Version: {{version}}": "Версия: {{version}}", diff --git a/public/locales/cs/app.json b/public/locales/cs/app.json index 43abeeb5..143203ff 100644 --- a/public/locales/cs/app.json +++ b/public/locales/cs/app.json @@ -26,14 +26,12 @@ "Version: {{version}}": "Verze: {{version}}", "Username": "Uživatelské jméno", "User menu": "Uživatelské menu", - "User ID": "ID uživatele", "Unmute microphone": "Zapnout mikrofon", "Turn on camera": "Zapnout kameru", "Turn off camera": "Vypnout kameru", "This call already exists, would you like to join?": "Tento hovor již existuje, chcete se připojit?", "Thanks! We'll get right on it.": "Děkujeme! Hned se na to vrhneme.", "Take me Home": "Domovská obrazovka", - "Submitting feedback…": "Odesílání zpětné vazby…", "Submit feedback": "Dát feedback", "Stop sharing screen": "Zastavit sdílení obrazovek", "Speaker {{n}}": "Reproduktor {{n}}", @@ -48,7 +46,6 @@ "Sending debug logs…": "Posílání ladícího záznamu…", "Send debug logs": "Poslat ladící záznam", "Select an option": "Vyberte možnost", - "Saving…": "Ukládání…", "Save": "Uložit", "Return to home screen": "Vrátit se na domácí obrazovku", "Remove": "Odstranit", @@ -102,11 +99,9 @@ "Press and hold spacebar to talk over {{name}}": "Zmáčkněte a držte mezerník, abyste mluvili přes {{name}}", "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}": "Ostatní uživatelé se pokoušejí připojit k tomuto hovoru s nekompatibilních verzí. Tito uživatelé by se měli ujistit, že stránku načetli znovu:<1>{userLis}", "Not registered yet? <2>Create an account": "Nejste registrovaní? <2>Vytvořit účet", - "More menu": "Další možnosti", "Join existing call?": "Připojit se k existujícimu hovoru?", "Include debug logs": "Zahrnout ladící záznamy", "Home": "Domov", - "Having trouble? Help us fix it.": "Máte problémy? Pomozte nám je spravit.", "Grid layout menu": "Menu rozložení", "Go": "Pokračovat", "Full screen": "Zvětšit na celou obrazovku", @@ -118,7 +113,6 @@ "Display name": "Zobrazované jméno", "Developer": "Vývojář", "Details": "Detaily", - "Description (optional)": "Popis (nepovinný)", "Debug log request": "Žádost o protokoly ladění", "Debug log": "Protokoly ladění", "Create account": "Vytvořit účet", diff --git a/public/locales/de/app.json b/public/locales/de/app.json index 9ecac302..f9769612 100644 --- a/public/locales/de/app.json +++ b/public/locales/de/app.json @@ -23,7 +23,6 @@ "Create account": "Konto erstellen", "Debug log": "Debug-Protokoll", "Debug log request": "Debug-Log Anfrage", - "Description (optional)": "Beschreibung (optional)", "Details": "Details", "Developer": "Entwickler", "Display name": "Anzeigename", @@ -33,7 +32,6 @@ "Full screen": "Vollbild", "Go": "Los geht’s", "Grid layout menu": "Grid-Layout-Menü", - "Having trouble? Help us fix it.": "Du hast ein Problem? Hilf uns, es zu beheben.", "Home": "Startseite", "Include debug logs": "Debug-Protokolle einschließen", "Incompatible versions": "Inkompatible Versionen", @@ -54,7 +52,6 @@ "Microphone permissions needed to join the call.": "Mikrofon-Berechtigung ist erforderlich, um dem Anruf beizutreten.", "Microphone {{n}}": "Mikrofon {{n}}", "More": "Mehr", - "More menu": "Weiteres Menü", "Mute microphone": "Mikrofon stummschalten", "No": "Nein", "Not now, return to home screen": "Nicht jetzt, zurück zum Startbildschirm", @@ -75,7 +72,6 @@ "Release to stop": "Loslassen zum Stoppen", "Remove": "Entfernen", "Return to home screen": "Zurück zum Startbildschirm", - "Save": "Speichern", "Saving…": "Speichere …", "Select an option": "Wähle eine Option", "Send debug logs": "Debug-Logs senden", @@ -91,7 +87,6 @@ "Spotlight": "Rampenlicht", "Stop sharing screen": "Beenden der Bildschirmfreigabe", "Submit feedback": "Rückmeldung geben", - "Submitting feedback…": "Sende Rückmeldung …", "Take me Home": "Zurück zur Startseite", "Talk over speaker": "Aktiven Sprecher verdrängen und sprechen", "Talking…": "Sprechen …", @@ -102,7 +97,6 @@ "Turn off camera": "Kamera ausschalten", "Turn on camera": "Kamera einschalten", "Unmute microphone": "Mikrofon aktivieren", - "User ID": "Benutzer-ID", "User menu": "Benutzermenü", "Username": "Benutzername", "Version: {{version}}": "Version: {{version}}", diff --git a/public/locales/el/app.json b/public/locales/el/app.json index 7facbe32..831cab1f 100644 --- a/public/locales/el/app.json +++ b/public/locales/el/app.json @@ -9,7 +9,6 @@ "Share screen": "Κοινή χρήση οθόνης", "Sending…": "Αποστολή…", "Select an option": "Επιλέξτε μια επιλογή", - "Saving…": "Αποθήκευση…", "Remove": "Αφαίρεση", "Registering…": "Εγγραφή…", "Press and hold to talk": "Πατήστε παρατεταμένα για να μιλήσετε", @@ -52,7 +51,6 @@ "Spatial audio": "Χωρικός ήχος", "Sign out": "Αποσύνδεση", "Settings": "Ρυθμίσεις", - "Save": "Αποθήκευση", "Return to home screen": "Επιστροφή στην αρχική οθόνη", "Register": "Εγγραφή", "Profile": "Προφίλ", @@ -62,7 +60,6 @@ "Not now, return to home screen": "Όχι τώρα, επιστροφή στην αρχική οθόνη", "No": "Όχι", "Mute microphone": "Σίγαση μικροφώνου", - "More menu": "Μενού περισσότερα", "More": "Περισσότερα", "Microphone permissions needed to join the call.": "Απαιτούνται δικαιώματα μικροφώνου για συμμετοχή στην κλήση.", "Microphone {{n}}": "Μικρόφωνο {{n}}", @@ -77,7 +74,6 @@ "Full screen": "Πλήρη οθόνη", "Exit full screen": "Έξοδος από πλήρη οθόνη", "Details": "Λεπτομέρειες", - "Description (optional)": "Περιγραφή (προαιρετική)", "Create account": "Δημιουργία λογαριασμού", "Copy and share this call link": "Αντιγράψτε και μοιραστείτε αυτόν τον σύνδεσμο κλήσης", "Copy": "Αντιγραφή", diff --git a/public/locales/es/app.json b/public/locales/es/app.json index b671eee7..011cb4ab 100644 --- a/public/locales/es/app.json +++ b/public/locales/es/app.json @@ -23,7 +23,6 @@ "Version: {{version}}": "Versión: {{version}}", "Username": "Nombre de usuario", "User menu": "Menú de usuario", - "User ID": "ID de usuario", "Unmute microphone": "Desilenciar el micrófono", "Turn on camera": "Encender la cámara", "Turn off camera": "Apagar la cámara", @@ -33,7 +32,6 @@ "Talking…": "Hablando…", "Talk over speaker": "Hablar por encima", "Take me Home": "Volver al inicio", - "Submitting feedback…": "Enviando comentarios…", "Submit feedback": "Enviar comentarios", "Stop sharing screen": "Dejar de compartir pantalla", "Spotlight": "Foco", @@ -49,7 +47,6 @@ "Sending debug logs…": "Enviando registros de depuración…", "Send debug logs": "Enviar registros de depuración", "Select an option": "Selecciona una opción", - "Saving…": "Guardando…", "Save": "Guardar", "Return to home screen": "Volver a la pantalla de inicio", "Remove": "Eliminar", @@ -68,7 +65,6 @@ "Not now, return to home screen": "Ahora no, volver a la pantalla de inicio", "No": "No", "Mute microphone": "Silenciar micrófono", - "More menu": "Menú Más", "More": "Más", "Microphone permissions needed to join the call.": "Se necesitan permisos del micrófono para unirse a la llamada.", "Microphone {{n}}": "Micrófono {{n}}", @@ -88,7 +84,6 @@ "Incompatible versions": "Versiones incompatibles", "Include debug logs": "Incluir registros de depuración", "Home": "Inicio", - "Having trouble? Help us fix it.": "¿Tienes problemas? Ayúdanos a resolverlos.", "Grid layout menu": "Menú de distribución de cuadrícula", "Go": "Comenzar", "Full screen": "Pantalla completa", @@ -99,7 +94,6 @@ "Display name": "Nombre a mostrar", "Developer": "Desarrollador", "Details": "Detalles", - "Description (optional)": "Descripción (opcional)", "Debug log request": "Petición de registros de depuración", "Debug log": "Registro de depuración", "Create account": "Crear cuenta", diff --git a/public/locales/et/app.json b/public/locales/et/app.json index 9c9bab6e..683f658f 100644 --- a/public/locales/et/app.json +++ b/public/locales/et/app.json @@ -20,7 +20,6 @@ "Incompatible versions": "Ühildumatud versioonid", "Include debug logs": "Lisa veatuvastuslogid", "Home": "Avavaatesse", - "Having trouble? Help us fix it.": "Kas on probleeme? Aita meil asja parandada.", "Grid layout menu": "Ruudustikvaate menüü", "Go": "Jätka", "Full screen": "Täisekraan", @@ -31,7 +30,6 @@ "Display name": "Kuvatav nimi", "Developer": "Arendaja", "Details": "Täpsemalt", - "Description (optional)": "Kirjeldus (valikuline)", "Debug log request": "Veaotsingulogi päring", "Debug log": "Veaotsingulogi", "Create account": "Loo konto", @@ -60,7 +58,6 @@ "Mute microphone": "Summuta mikrofon", "Your recent calls": "Hiljutised kõned", "You can't talk at the same time": "Üheaegselt ei saa rääkida", - "More menu": "Rohkem valikuid", "More": "Rohkem", "Microphone permissions needed to join the call.": "Kõnega liitumiseks on vaja lubada mikrofoni kasutamine.", "Microphone {{n}}": "Mikrofon {{n}}", @@ -74,10 +71,8 @@ "Join existing call?": "Liitu juba käimasoleva kõnega?", "Join call now": "Kõnega liitumine", "Join call": "Kõnega liitumine", - "User ID": "Kasutajatunnus", "Turn on camera": "Lülita kaamera sisse", "Turn off camera": "Lülita kaamera välja", - "Submitting feedback…": "Tagasiside saatmine…", "Take me Home": "Mine avalehele", "Submit feedback": "Jaga tagasisidet", "Stop sharing screen": "Lõpeta ekraani jagamine", @@ -94,7 +89,6 @@ "Sending debug logs…": "Veaotsingulogide saatmine…", "Send debug logs": "Saada veaotsingulogid", "Select an option": "Vali oma eelistus", - "Saving…": "Salvestamine…", "Save": "Salvesta", "Return to home screen": "Tagasi avalehele", "Remove": "Eemalda", diff --git a/public/locales/fa/app.json b/public/locales/fa/app.json index 7ecc3b7b..28c1193e 100644 --- a/public/locales/fa/app.json +++ b/public/locales/fa/app.json @@ -3,7 +3,6 @@ "Video call": "تماس تصویری", "Video": "ویدیو", "Username": "نام کاربری", - "User ID": "آی دی کاربر", "Turn on camera": "روشن کردن دوربین", "Turn off camera": "خاموش کردن دوربین", "Take me Home": "مرا به خانه ببر", @@ -11,7 +10,6 @@ "Sign out": "خروج", "Sign in": "ورود", "Settings": "تنظیمات", - "Save": "ذخیره", "Profile": "پروفایل", "Password": "رمز عبور", "No": "خیر", @@ -36,7 +34,6 @@ "Display name": "نام نمایشی", "Developer": "توسعه دهنده", "Details": "جزئیات", - "Description (optional)": "توضیحات (اختیاری)", "Debug log request": "درخواست لاگ عیب‌یابی", "Debug log": "لاگ عیب‌یابی", "Create account": "ساخت حساب کاربری", @@ -80,7 +77,6 @@ "Sending debug logs…": "در حال ارسال باگ‌های عیب‌یابی…", "Send debug logs": "ارسال لاگ‌های عیب‌یابی", "Select an option": "یک گزینه را انتخاب کنید", - "Saving…": "در حال ذخیره…", "Return to home screen": "برگشت به صفحه اصلی", "Remove": "حذف", "Release to stop": "برای توقف رها کنید", @@ -97,12 +93,10 @@ "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}": "کاربران دیگر تلاش می‌کنند با ورژن‌های ناسازگار به مکالمه بپیوندند. این کاربران باید از بروزرسانی مرورگرشان اطمینان داشته باشند:<1>{userLis}", "Not registered yet? <2>Create an account": "هنوز ثبت‌نام نکرده‌اید؟ <2>ساخت حساب کاربری", "Not now, return to home screen": "الان نه، به صفحه اصلی برگردید", - "More menu": "تنظیمات بیشتر", "Microphone permissions needed to join the call.": "برای پیوستن به مکالمه دسترسی به میکروفون نیاز است.", "Microphone {{n}}": "میکروفون {{n}}", "Logging in…": "ورود…", "Include debug logs": "شامل لاگ‌های عیب‌یابی", - "Having trouble? Help us fix it.": "با مشکلی رو به رو شدید؟ به ما کمک کنید رفعش کنیم.", "Grid layout menu": "منوی طرح‌بندی شبکه‌ای", "Fetching group call timed out.": "زمان اتصال به مکالمه گروهی تمام شد.", "You can't talk at the same time": "نمی توانید هم‌زمان صحبت کنید", @@ -122,7 +116,6 @@ "Thanks! We'll get right on it.": "با تشکر! ما به درستی آن را انجام خواهیم داد.", "Talking…": "در حال صحبت کردن…", "Talk over speaker": "روی بلندگو صحبت کنید", - "Submitting feedback…": "در حال ارسال بازخورد…", "Submit feedback": "بازخورد ارائه دهید", "Stop sharing screen": "توقف اشتراک‌گذاری صفحه نمایش", "Spatial audio": "صدای جهت‌دار", diff --git a/public/locales/fr/app.json b/public/locales/fr/app.json index 4e682c7d..83380d9f 100644 --- a/public/locales/fr/app.json +++ b/public/locales/fr/app.json @@ -22,7 +22,6 @@ "Create account": "Créer un compte", "Debug log": "Journal de débogage", "Debug log request": "Demande d’un journal de débogage", - "Description (optional)": "Description (facultatif)", "Details": "Informations", "Developer": "Développeur", "Display name": "Nom d’affichage", @@ -32,7 +31,6 @@ "Full screen": "Plein écran", "Go": "Commencer", "Grid layout menu": "Menu en grille", - "Having trouble? Help us fix it.": "Un problème ? Aidez nous à le résoudre.", "Home": "Accueil", "Include debug logs": "Inclure les journaux de débogage", "Incompatible versions": "Versions incompatibles", @@ -52,7 +50,6 @@ "Microphone permissions needed to join the call.": "Accès au microphone requis pour rejoindre l’appel.", "Microphone {{n}}": "Microphone {{n}}", "More": "Plus", - "More menu": "Menu plus", "Mute microphone": "Couper le micro", "No": "Non", "Not now, return to home screen": "Pas maintenant, retourner à l’accueil", @@ -73,7 +70,6 @@ "Release to stop": "Relâcher pour arrêter", "Remove": "Supprimer", "Return to home screen": "Retour à l’accueil", - "Save": "Enregistrer", "Saving…": "Enregistrement…", "Select an option": "Sélectionnez une option", "Send debug logs": "Envoyer les journaux de débogage", @@ -87,7 +83,6 @@ "Spotlight": "Premier plan", "Stop sharing screen": "Arrêter le partage d’écran", "Submit feedback": "Envoyer des retours", - "Submitting feedback…": "Envoi des retours…", "Take me Home": "Retouner à l’accueil", "Talk over speaker": "Parler par dessus l’intervenant", "Thanks! We'll get right on it.": "Merci ! Nous allons nous y attaquer.", @@ -114,7 +109,6 @@ "Version: {{version}}": "Version : {{version}}", "Username": "Nom d’utilisateur", "User menu": "Menu utilisateur", - "User ID": "Identifiant utilisateur", "Unmute microphone": "Allumer le micro", "Turn on camera": "Allumer la caméra", "Turn off camera": "Couper la caméra", diff --git a/public/locales/id/app.json b/public/locales/id/app.json index b81a57fe..0bafaaee 100644 --- a/public/locales/id/app.json +++ b/public/locales/id/app.json @@ -23,7 +23,6 @@ "Create account": "Buat akun", "Debug log": "Catatan pengawakutuan", "Debug log request": "Permintaan catatan pengawakutuan", - "Description (optional)": "Deskripsi (opsional)", "Details": "Detail", "Developer": "Pengembang", "Display name": "Nama tampilan", @@ -34,7 +33,6 @@ "Full screen": "Layar penuh", "Go": "Bergabung", "Grid layout menu": "Menu tata letak kisi", - "Having trouble? Help us fix it.": "Mengalami masalah? Bantu kami memperbaikinya.", "Home": "Beranda", "Include debug logs": "Termasuk catatan pengawakutuan", "Incompatible versions": "Versi tidak kompatibel", @@ -55,7 +53,6 @@ "Microphone permissions needed to join the call.": "Izin mikrofon dibutuhkan untuk bergabung ke panggilan ini.", "Microphone {{n}}": "Mikrofon {{n}}", "More": "Lainnya", - "More menu": "Menu lainnya", "Mute microphone": "Bisukan mikrofon", "No": "Tidak", "Not now, return to home screen": "Tidak sekarang, kembali ke layar beranda", @@ -76,7 +73,6 @@ "Release to stop": "Lepaskan untuk berhenti", "Remove": "Hapus", "Return to home screen": "Kembali ke layar beranda", - "Save": "Simpan", "Saving…": "Menyimpan…", "Select an option": "Pilih sebuah opsi", "Send debug logs": "Kirim catatan pengawakutuan", @@ -92,7 +88,6 @@ "Spotlight": "Sorotan", "Stop sharing screen": "Berhenti membagikan layar", "Submit feedback": "Kirim masukan", - "Submitting feedback…": "Mengirimkan masukan…", "Take me Home": "Bawa saya ke Beranda", "Talk over speaker": "Bicara pada pembicara", "Talking…": "Berbicara…", @@ -103,7 +98,6 @@ "Turn off camera": "Matikan kamera", "Turn on camera": "Nyalakan kamera", "Unmute microphone": "Suarakan mikrofon", - "User ID": "ID pengguna", "User menu": "Menu pengguna", "Username": "Nama pengguna", "Version: {{version}}": "Versi: {{version}}", diff --git a/public/locales/ja/app.json b/public/locales/ja/app.json index 44fae025..577faac3 100644 --- a/public/locales/ja/app.json +++ b/public/locales/ja/app.json @@ -24,10 +24,8 @@ "Copied!": "コピーしました!", "Copy and share this call link": "通話リンクをコピーし共有", "Copy": "コピー", - "Description (optional)": "概要(任意)", "Debug log": "デバッグログ", "Create account": "アカウントを作成", - "Having trouble? Help us fix it.": "問題が起きましたか?修正にご協力ください。", "Go": "続行", "Fetching group call timed out.": "グループ通話の取得がタイムアウトしました。", "Element Call Home": "Element Call ホーム", @@ -57,11 +55,9 @@ "Version: {{version}}": "バージョン:{{version}}", "Username": "ユーザー名", "User menu": "ユーザーメニュー", - "User ID": "ユーザーID", "Unmute microphone": "マイクのミュートを解除", "Turn on camera": "カメラをつける", "Turn off camera": "カメラを切る", - "Submitting feedback…": "フィードバックを送信しています…", "Submit feedback": "フィードバックを送信", "Stop sharing screen": "画面共有を停止", "Spotlight": "スポットライト", @@ -72,7 +68,6 @@ "Settings": "設定", "Sending…": "送信しています…", "Sending debug logs…": "デバッグログを送信しています…", - "Saving…": "保存しています…", "Save": "保存", "Return to home screen": "ホーム画面に戻る", "Registering…": "登録しています…", diff --git a/public/locales/pl/app.json b/public/locales/pl/app.json index ada961a1..9ef51443 100644 --- a/public/locales/pl/app.json +++ b/public/locales/pl/app.json @@ -1,5 +1,4 @@ { - "More menu": "Menu \"więcej\"", "Login": "Zaloguj się", "Go": "Przejdź", "By clicking \"Go\", you agree to our <2>Terms and conditions": "Klikając \"Kontynuuj\", wyrażasz zgodę na nasze <2>Zasady i warunki", @@ -18,7 +17,6 @@ "Version: {{version}}": "Wersja: {{version}}", "Username": "Nazwa użytkownika", "User menu": "Menu użytkownika", - "User ID": "ID użytkownika", "Unmute microphone": "Wyłącz wyciszenie mikrofonu", "Turn on camera": "Włącz kamerę", "Turn off camera": "Wyłącz kamerę", @@ -27,7 +25,6 @@ "Thanks! We'll get right on it.": "Dziękujemy! Zaraz się tym zajmiemy.", "Talking…": "Mówienie…", "Take me Home": "Zabierz mnie do strony głównej", - "Submitting feedback…": "Przesyłanie opinii…", "Submit feedback": "Prześlij opinię", "Stop sharing screen": "Zatrzymaj udostępnianie ekranu", "Spotlight": "Centrum uwagi", @@ -43,7 +40,6 @@ "Sending debug logs…": "Wysyłanie dzienników debugowania…", "Send debug logs": "Wyślij dzienniki debugowania", "Select an option": "Wybierz opcję", - "Saving…": "Zapisywanie…", "Save": "Zapisz", "Return to home screen": "Powróć do strony głównej", "Remove": "Usuń", @@ -84,7 +80,6 @@ "Incompatible versions": "Niekompatybilne wersje", "Include debug logs": "Dołącz dzienniki debugowania", "Home": "Strona domowa", - "Having trouble? Help us fix it.": "Masz problem? Pomóż nam go naprawić.", "Grid layout menu": "Menu układu siatki", "Full screen": "Pełny ekran", "Freedom": "Wolność", @@ -94,7 +89,6 @@ "Display name": "Nazwa wyświetlana", "Developer": "Programista", "Details": "Szczegóły", - "Description (optional)": "Opis (opcjonalne)", "Debug log request": "Prośba o dzienniki debugowania", "Debug log": "Dzienniki debugowania", "Create account": "Utwórz konto", diff --git a/public/locales/ru/app.json b/public/locales/ru/app.json index 5aa99c2c..593df4f4 100644 --- a/public/locales/ru/app.json +++ b/public/locales/ru/app.json @@ -1,6 +1,5 @@ { "Register": "Зарегистрироваться", - "Saving…": "Сохранение…", "Registering…": "Регистрация…", "Logging in…": "Вход…", "{{names}}, {{name}}": "{{names}}, {{name}}", @@ -10,7 +9,6 @@ "This call already exists, would you like to join?": "Этот звонок уже существует, хотите присоединиться?", "Thanks! We'll get right on it.": "Спасибо! Мы учтём ваш отзыв.", "Talking…": "Говорите…", - "Submitting feedback…": "Отправка отзыва…", "Submit feedback": "Отправить отзыв", "Sending debug logs…": "Отправка журнала отладки…", "Select an option": "Выберите вариант", @@ -38,7 +36,6 @@ "Version: {{version}}": "Версия: {{version}}", "Username": "Имя пользователя", "User menu": "Меню пользователя", - "User ID": "ID пользователя", "Unmute microphone": "Включить микрофон", "Turn on camera": "Включить камеру", "Turn off camera": "Отключить камеру", @@ -57,7 +54,6 @@ "Sending…": "Отправка…", "Local volume": "Местная громкость", "Call type menu": "Меню \"Тип звонка\"", - "More menu": "Полное меню", "{{roomName}} - Walkie-talkie call": "{{roomName}} - Звонок-рация", "Include debug logs": "Приложить журнал отладки", "Download debug logs": "Скачать журнал отладки", @@ -65,7 +61,6 @@ "Debug log": "Журнал отладки", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "У одного из участников звонка есть неполадки. Чтобы лучше диагностировать похожие проблемы, нам нужен журнал отладки.", "Send debug logs": "Отправить журнал отладки", - "Save": "Сохранить", "Return to home screen": "Вернуться в Начало", "Remove": "Удалить", "Recaptcha not loaded": "Невозможно начать проверку", @@ -96,7 +91,6 @@ "Incompatible versions!": "Несовместимые версии!", "Incompatible versions": "Несовместимые версии", "Home": "Начало", - "Having trouble? Help us fix it.": "Есть проблема? Помогите нам её устранить.", "Go": "Далее", "Full screen": "Полноэкранный режим", "Freedom": "Свобода", @@ -105,7 +99,6 @@ "Display name": "Видимое имя", "Developer": "Разработчику", "Details": "Подробности", - "Description (optional)": "Описание (необязательно)", "Create account": "Создать аккаунт", "Copy and share this call link": "Скопируйте и поделитесь этой ссылкой на звонок", "Copied!": "Скопировано!", diff --git a/public/locales/sk/app.json b/public/locales/sk/app.json index ce0d4ee0..e8b2ec4b 100644 --- a/public/locales/sk/app.json +++ b/public/locales/sk/app.json @@ -12,7 +12,6 @@ "Talking…": "Rozprávanie…", "Talk over speaker": "Hovor cez reproduktor", "Take me Home": "Zober ma domov", - "Submitting feedback…": "Odosielanie spätnej väzby…", "Submit feedback": "Odoslať spätnú väzbu", "Stop sharing screen": "Zastaviť zdieľanie obrazovky", "Show call inspector": "Zobraziť inšpektora hovorov", @@ -21,7 +20,6 @@ "Sending debug logs…": "Odosielanie záznamov o ladení…", "Send debug logs": "Odoslať záznamy o ladení", "Select an option": "Vyberte možnosť", - "Saving…": "Ukladanie…", "Save": "Uložiť", "Return to home screen": "Návrat na domovskú obrazovku", "Remove": "Odstrániť", @@ -43,7 +41,6 @@ "Not now, return to home screen": "Teraz nie, vrátiť sa na domovskú obrazovku", "No": "Nie", "Mute microphone": "Stlmiť mikrofón", - "More menu": "Ponuka viac", "More": "Viac", "Microphone permissions needed to join the call.": "Povolenie mikrofónu je potrebné na pripojenie k hovoru.", "Microphone {{n}}": "Mikrofón {{n}}", @@ -62,7 +59,6 @@ "Incompatible versions!": "Nekompatibilné verzie!", "Incompatible versions": "Nekompatibilné verzie", "Home": "Domov", - "Having trouble? Help us fix it.": "Máte problém? Pomôžte nám ho opraviť.", "Grid layout menu": "Ponuka rozloženia mriežky", "Go": "Prejsť", "Full screen": "Zobrazenie na celú obrazovku", @@ -80,7 +76,6 @@ "Version: {{version}}": "Verzia: {{version}}", "Username": "Meno používateľa", "User menu": "Používateľské menu", - "User ID": "ID používateľa", "Unmute microphone": "Zrušiť stlmenie mikrofónu", "Turn on camera": "Zapnúť kameru", "Turn off camera": "Vypnúť kameru", @@ -95,7 +90,6 @@ "Display name": "Zobrazované meno", "Developer": "Vývojár", "Details": "Podrobnosti", - "Description (optional)": "Popis (voliteľné)", "Debug log request": "Žiadosť o záznam ladenia", "Debug log": "Záznam o ladení", "Create account": "Vytvoriť účet", diff --git a/public/locales/tr/app.json b/public/locales/tr/app.json index 5cf079ef..e60ed7f3 100644 --- a/public/locales/tr/app.json +++ b/public/locales/tr/app.json @@ -21,7 +21,6 @@ "Create account": "Hesap aç", "Debug log": "Hata ayıklama kütüğü", "Debug log request": "Hata ayıklama kütük istemi", - "Description (optional)": "Tanım (isteğe bağlı)", "Details": "Ayrıntı", "Developer": "Geliştirici", "Display name": "Ekran adı", @@ -32,7 +31,6 @@ "Full screen": "Tam ekran", "Go": "Git", "Grid layout menu": "Izgara plan menü", - "Having trouble? Help us fix it.": "Sorun mu var? Çözmemize yardım edin.", "Home": "Ev", "Include debug logs": "Hata ayıklama kütüğünü dahil et", "Incompatible versions": "Uyumsuz sürümler", @@ -52,7 +50,6 @@ "Microphone permissions needed to join the call.": "Aramaya katılmak için mikrofon erişim izni gerek.", "Microphone {{n}}": "{{n}}. mikrofon", "More": "Daha", - "More menu": "Daha fazla", "Mute microphone": "Mikrofonu kapat", "No": "Hayır", "Not now, return to home screen": "Şimdi değil, ev ekranına dön", @@ -70,7 +67,6 @@ "Release to stop": "Kesmek için bırakın", "Remove": "Çıkar", "Return to home screen": "Ev ekranına geri dön", - "Save": "Kaydet", "Saving…": "Kaydediliyor…", "Select an option": "Bir seçenek seç", "Send debug logs": "Hata ayıklama kütüğünü gönder", @@ -83,7 +79,6 @@ "Spatial audio": "Uzamsal ses", "Stop sharing screen": "Ekran paylaşmayı terk et", "Submit feedback": "Geri bildirim ver", - "Submitting feedback…": "Geri bildirimler gönderiliyor…", "Take me Home": "Ev ekranına gir", "Talking…": "Konuşuyor…", "Thanks! We'll get right on it.": "Sağol! Bununla ilgileneceğiz.", diff --git a/public/locales/uk/app.json b/public/locales/uk/app.json index 42751cc3..75ce8628 100644 --- a/public/locales/uk/app.json +++ b/public/locales/uk/app.json @@ -14,7 +14,6 @@ "Version: {{version}}": "Версія: {{version}}", "Username": "Ім'я користувача", "User menu": "Меню користувача", - "User ID": "ID користувача", "Unmute microphone": "Увімкнути мікрофон", "Turn on camera": "Увімкнути камеру", "Turn off camera": "Вимкнути камеру", @@ -25,7 +24,6 @@ "Talking…": "Говоріть…", "Talk over speaker": "Говорити через динамік", "Take me Home": "Перейти до Домівки", - "Submitting feedback…": "Надсилання відгуку…", "Submit feedback": "Надіслати відгук", "Stop sharing screen": "Припинити показ екрана", "Spotlight": "У центрі уваги", @@ -41,7 +39,6 @@ "Sending debug logs…": "Надсилання журналу налагодження…", "Send debug logs": "Надіслати журнал налагодження", "Select an option": "Вибрати опцію", - "Saving…": "Збереження…", "Save": "Зберегти", "Return to home screen": "Повернутися на екран домівки", "Remove": "Вилучити", @@ -63,7 +60,6 @@ "Not now, return to home screen": "Не зараз, повернутися на екран домівки", "No": "Ні", "Mute microphone": "Заглушити мікрофон", - "More menu": "Усе меню", "More": "Докладніше", "Microphone permissions needed to join the call.": "Для участі у виклику необхідний дозвіл на користування мікрофоном.", "Microphone {{n}}": "Мікрофон {{n}}", @@ -83,7 +79,6 @@ "Incompatible versions": "Несумісні версії", "Include debug logs": "Долучити журнали налагодження", "Home": "Домівка", - "Having trouble? Help us fix it.": "Проблеми? Допоможіть нам це виправити.", "Grid layout menu": "Меню у вигляді сітки", "Go": "Далі", "Full screen": "Повноекранний режим", @@ -94,7 +89,6 @@ "Display name": "Показуване ім'я", "Developer": "Розробнику", "Details": "Подробиці", - "Description (optional)": "Опис (необов'язково)", "Debug log request": "Запит журналу налагодження", "Debug log": "Журнал налагодження", "Create account": "Створити обліковий запис", diff --git a/public/locales/zh-Hans/app.json b/public/locales/zh-Hans/app.json index 382dc38d..49916972 100644 --- a/public/locales/zh-Hans/app.json +++ b/public/locales/zh-Hans/app.json @@ -13,7 +13,6 @@ "Version: {{version}}": "版本:{{version}}", "Username": "用户名", "User menu": "用户菜单", - "User ID": "用户ID", "Unmute microphone": "取消麦克风静音", "Turn on camera": "开启摄像头", "Turn off camera": "关闭摄像头", @@ -24,7 +23,6 @@ "Talking…": "正在发言……", "Talk over speaker": "通过扬声器发言", "Take me Home": "返回主页", - "Submitting feedback…": "正在提交反馈……", "Submit feedback": "提交反馈", "Stop sharing screen": "停止屏幕共享", "Spotlight": "聚焦模式", @@ -58,7 +56,6 @@ "Sending debug logs…": "正在发送调试日志……", "Send debug logs": "发送调试日志", "Select an option": "选择一个选项", - "Saving…": "正在保存……", "Save": "保存", "Return to home screen": "返回主页", "Remove": "移除", @@ -80,7 +77,6 @@ "Not now, return to home screen": "暂不,先返回主页", "No": "否", "Mute microphone": "麦克风静音", - "More menu": "更多", "More": "更多", "Microphone permissions needed to join the call.": "加入通话需要麦克风权限。", "Microphone {{n}}": "麦克风 {{n}}", @@ -100,7 +96,6 @@ "Incompatible versions": "不兼容版本", "Include debug logs": "包含调试日志", "Home": "主页", - "Having trouble? Help us fix it.": "遇到麻烦?帮助我们解决问题。", "Grid layout menu": "网格布局菜单", "Go": "开始", "Full screen": "全屏", @@ -112,7 +107,6 @@ "Display name": "显示名称", "Developer": "开发者", "Details": "详情", - "Description (optional)": "描述(可选)", "Debug log request": "调试日志请求", "Debug log": "调试日志", "Create account": "创建账户", diff --git a/public/locales/zh-Hant/app.json b/public/locales/zh-Hant/app.json index e18e3d74..c10b096a 100644 --- a/public/locales/zh-Hant/app.json +++ b/public/locales/zh-Hant/app.json @@ -30,7 +30,6 @@ "Version: {{version}}": "版本: {{version}}", "Username": "使用者名稱", "User menu": "使用者選單", - "User ID": "使用者 ID", "Unmute microphone": "取消麥克風靜音", "Turn on camera": "開啟相機", "Turn off camera": "關閉相機", @@ -42,7 +41,6 @@ "Talking…": "對話中…", "Talk over speaker": "以擴音對話", "Take me Home": "帶我回主畫面", - "Submitting feedback…": "遞交回饋…", "Submit feedback": "遞交回覆", "Stop sharing screen": "停止分享螢幕畫面", "Spotlight": "聚焦", @@ -58,7 +56,6 @@ "Sending debug logs…": "傳送除錯記錄檔中…", "Send debug logs": "傳送除錯紀錄", "Select an option": "選擇一個選項", - "Saving…": "儲存中…", "Save": "儲存", "Return to home screen": "回到首頁", "Remove": "移除", @@ -80,7 +77,6 @@ "Not now, return to home screen": "現在不行,回到首頁", "No": "否", "Mute microphone": "麥克風靜音", - "More menu": "更多選單", "More": "更多", "Microphone permissions needed to join the call.": "加入通話前需要取得麥克風的權限。", "Microphone {{n}}": "麥克風 {{n}}", @@ -101,7 +97,6 @@ "Incompatible versions": "不相容版本", "Include debug logs": "包含除錯紀錄", "Home": "首頁", - "Having trouble? Help us fix it.": "遇到問題嗎?請讓我們協助您。", "Grid layout menu": "格框式清單", "Go": "前往", "Full screen": "全螢幕", @@ -113,7 +108,6 @@ "Display name": "顯示名稱", "Developer": "開發者", "Details": "詳細說明", - "Description (optional)": "描述(選擇性)", "Debug log request": "請求偵錯報告", "Debug log": "除錯紀錄", "Create account": "建立帳號", From af4f27cbbfb1e771c5cdc283404590d4ec225bb3 Mon Sep 17 00:00:00 2001 From: Vri Date: Fri, 2 Jun 2023 11:56:45 +0000 Subject: [PATCH 255/286] Translated using Weblate (German) Currently translated at 96.4% (134 of 139 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/de/ --- public/locales/de/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/locales/de/app.json b/public/locales/de/app.json index f9769612..560f74f1 100644 --- a/public/locales/de/app.json +++ b/public/locales/de/app.json @@ -72,7 +72,6 @@ "Release to stop": "Loslassen zum Stoppen", "Remove": "Entfernen", "Return to home screen": "Zurück zum Startbildschirm", - "Saving…": "Speichere …", "Select an option": "Wähle eine Option", "Send debug logs": "Debug-Logs senden", "Sending…": "Senden …", @@ -132,5 +131,6 @@ "Expose developer settings in the settings window.": "Zeige die Entwicklereinstellungen im Einstellungsfenster.", "Developer Settings": "Entwicklereinstellungen", "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy and our <5>Cookie Policy.": "Mit der Teilnahme an der Beta akzeptierst du die Sammlung von anonymen Daten, die wir zur Verbesserung des Produkts verwenden. Weitere Informationen zu den von uns erhobenen Daten findest du in unserer <2>Datenschutzerklärung und unseren <5>Cookie-Richtlinien.", - "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>Du kannst deine Zustimmung durch Abwählen dieses Kästchens zurückziehen. Falls du dich aktuell in einem Anruf befindest, wird diese Einstellung nach dem Ende des Anrufs wirksam." + "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>Du kannst deine Zustimmung durch Abwählen dieses Kästchens zurückziehen. Falls du dich aktuell in einem Anruf befindest, wird diese Einstellung nach dem Ende des Anrufs wirksam.", + "Feedback": "Rückmeldung" } From a9b2ca01a7162cb6df89019cbe80b4b7ddf170bf Mon Sep 17 00:00:00 2001 From: Glandos Date: Fri, 2 Jun 2023 07:11:33 +0000 Subject: [PATCH 256/286] Translated using Weblate (French) Currently translated at 100.0% (139 of 139 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/fr/ --- public/locales/fr/app.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/locales/fr/app.json b/public/locales/fr/app.json index 83380d9f..de11558a 100644 --- a/public/locales/fr/app.json +++ b/public/locales/fr/app.json @@ -70,7 +70,6 @@ "Release to stop": "Relâcher pour arrêter", "Remove": "Supprimer", "Return to home screen": "Retour à l’accueil", - "Saving…": "Enregistrement…", "Select an option": "Sélectionnez une option", "Send debug logs": "Envoyer les journaux de débogage", "Sending…": "Envoi…", @@ -132,5 +131,11 @@ "Expose developer settings in the settings window.": "Affiche les paramètres développeurs dans la fenêtre des paramètres.", "Developer Settings": "Paramètres développeurs", "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy and our <5>Cookie Policy.": "En participant à cette beta, vous consentez à la collecte de données anonymes, qui seront utilisées pour améliorer le produit. Vous trouverez plus d’informations sur les données collectées dans notre <2>Politique de vie privée et notre <5>Politique de cookies.", - "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>Vous pouvez retirer votre consentement en décochant cette case. Si vous êtes actuellement en communication, ce paramètre prendra effet à la fin de l’appel." + "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>Vous pouvez retirer votre consentement en décochant cette case. Si vous êtes actuellement en communication, ce paramètre prendra effet à la fin de l’appel.", + "Your feedback": "Votre commentaire", + "Thanks, we received your feedback!": "Merci, nous avons reçu vos commentaires !", + "Submitting…": "Envoi…", + "Submit": "Envoyer", + "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Si vous rencontrez des problèmes, ou vous voulez simplement faire un commentaire, veuillez nous envoyer une courte description ci-dessous.", + "Feedback": "Commentaires" } From 3db081440ea2395462f742fb085b3be8232f5a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Fri, 2 Jun 2023 07:04:01 +0000 Subject: [PATCH 257/286] Translated using Weblate (Estonian) Currently translated at 100.0% (139 of 139 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/et/ --- public/locales/et/app.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/locales/et/app.json b/public/locales/et/app.json index 683f658f..c6ac16f0 100644 --- a/public/locales/et/app.json +++ b/public/locales/et/app.json @@ -89,7 +89,6 @@ "Sending debug logs…": "Veaotsingulogide saatmine…", "Send debug logs": "Saada veaotsingulogid", "Select an option": "Vali oma eelistus", - "Save": "Salvesta", "Return to home screen": "Tagasi avalehele", "Remove": "Eemalda", "Release to stop": "Peatamiseks vabasta klahv", @@ -132,5 +131,11 @@ "Expose developer settings in the settings window.": "Näita seadistuste aknas arendajale vajalikke seadeid.", "Developer Settings": "Arendaja seadistused", "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy and our <5>Cookie Policy.": "Nõustudes selle beetaversiooni kasutamisega sa nõustud ka toote arendamiseks kasutatavate anonüümsete andmete kogumisega. Täpsemat teavet kogutavate andmete kohta leiad meie <2>Privaatsuspoliitikast ja meie <5>Küpsiste kasutamise reeglitest.", - "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>Sa võid selle valiku eelmaldamisega alati oma nõusoleku tagasi võtta. Kui sul parasjagu on kõne pooleli, siis seadistuste muudatus jõustub pärast kõne lõppu." + "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>Sa võid selle valiku eelmaldamisega alati oma nõusoleku tagasi võtta. Kui sul parasjagu on kõne pooleli, siis seadistuste muudatus jõustub pärast kõne lõppu.", + "Your feedback": "Sinu tagasiside", + "Thanks, we received your feedback!": "Tänud, me oleme sinu tagasiside kätte saanud!", + "Submitting…": "Saadan…", + "Submit": "Saada", + "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Kui selle rakenduse kasutamisel tekib sul probleeme või lihtsalt soovid oma arvamust avaldada, siis palun täida alljärgnev lühike kirjeldus.", + "Feedback": "Tagasiside" } From 0e50679db5c8d7d80eb6246ea9a0e43d4e509483 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 2 Jun 2023 14:45:21 +0000 Subject: [PATCH 258/286] Translated using Weblate (Chinese (Simplified)) Currently translated at 89.9% (125 of 139 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hans/ --- public/locales/zh-Hans/app.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/public/locales/zh-Hans/app.json b/public/locales/zh-Hans/app.json index 49916972..0b494e1f 100644 --- a/public/locales/zh-Hans/app.json +++ b/public/locales/zh-Hans/app.json @@ -56,7 +56,6 @@ "Sending debug logs…": "正在发送调试日志……", "Send debug logs": "发送调试日志", "Select an option": "选择一个选项", - "Save": "保存", "Return to home screen": "返回主页", "Remove": "移除", "Release to stop": "松开后停止", @@ -125,6 +124,6 @@ "Call link copied": "链接已复制", "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "点击“现在加入”则表示同意我们的<2>条款与条件<2>", "Avatar": "头像", - "<0>Oops, something's gone wrong.": "Hello", - "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "Hi" + "<0>Oops, something's gone wrong.": "", + "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "" } From ce3d315d5074f0fb1edc62374f80db2e18f90ec7 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Fri, 2 Jun 2023 02:23:27 +0000 Subject: [PATCH 259/286] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (139 of 139 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hant/ --- public/locales/zh-Hant/app.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/locales/zh-Hant/app.json b/public/locales/zh-Hant/app.json index c10b096a..e81cd773 100644 --- a/public/locales/zh-Hant/app.json +++ b/public/locales/zh-Hant/app.json @@ -56,7 +56,6 @@ "Sending debug logs…": "傳送除錯記錄檔中…", "Send debug logs": "傳送除錯紀錄", "Select an option": "選擇一個選項", - "Save": "儲存", "Return to home screen": "回到首頁", "Remove": "移除", "Release to stop": "放開以停止", @@ -132,5 +131,11 @@ "Accept camera/microphone permissions to join the call.": "請授權使用您的相機/麥克風以加入對話。", "<0>Why not finish by setting up a password to keep your account?<1>You'll be able to keep your name and set an avatar for use on future calls": "<0>何不設定密碼以保留此帳號?<1>您可以保留暱稱並設定頭像,以便未來通話時使用", "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy and our <5>Cookie Policy.": "參與此測試版即表示您同意蒐集匿名資料,我們使用這些資料來改進產品。您可以在我們的<2>隱私政策與我們的 <5>Cookie 政策 中找到關於我們追蹤哪些資料的更多資訊。", - "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>您可以透過取消核取此方塊來撤回同意。若您目前正在通話中,此設定將在通話結束時生效。" + "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>您可以透過取消核取此方塊來撤回同意。若您目前正在通話中,此設定將在通話結束時生效。", + "Your feedback": "您的回饋", + "Thanks, we received your feedback!": "感謝,我們已經收到您的回饋了!", + "Submitting…": "正在遞交……", + "Submit": "遞交", + "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "若您遇到問題或只是想提供一些回饋,請在下方傳送簡短說明給我們。", + "Feedback": "回饋" } From 00c44fb38a9349f29f919b47f7e9018b35242b3a Mon Sep 17 00:00:00 2001 From: Vri Date: Sat, 3 Jun 2023 12:30:00 +0000 Subject: [PATCH 260/286] Translated using Weblate (German) Currently translated at 100.0% (139 of 139 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/de/ --- public/locales/de/app.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/locales/de/app.json b/public/locales/de/app.json index 560f74f1..119ad31f 100644 --- a/public/locales/de/app.json +++ b/public/locales/de/app.json @@ -132,5 +132,10 @@ "Developer Settings": "Entwicklereinstellungen", "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy and our <5>Cookie Policy.": "Mit der Teilnahme an der Beta akzeptierst du die Sammlung von anonymen Daten, die wir zur Verbesserung des Produkts verwenden. Weitere Informationen zu den von uns erhobenen Daten findest du in unserer <2>Datenschutzerklärung und unseren <5>Cookie-Richtlinien.", "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>Du kannst deine Zustimmung durch Abwählen dieses Kästchens zurückziehen. Falls du dich aktuell in einem Anruf befindest, wird diese Einstellung nach dem Ende des Anrufs wirksam.", - "Feedback": "Rückmeldung" + "Feedback": "Rückmeldung", + "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Falls du auf Probleme stößt oder einfach nur eine Rückmeldung geben möchtest, sende uns bitte eine kurze Beschreibung.", + "Your feedback": "Deine Rückmeldung", + "Thanks, we received your feedback!": "Danke, wir haben deine Rückmeldung erhalten!", + "Submitting…": "Sende …", + "Submit": "Absenden" } From 48cf604bd1b8137a718fa1bdef4cd8954aa93c98 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Fri, 2 Jun 2023 17:30:17 +0000 Subject: [PATCH 261/286] Translated using Weblate (Ukrainian) Currently translated at 100.0% (139 of 139 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/ --- public/locales/uk/app.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/locales/uk/app.json b/public/locales/uk/app.json index 75ce8628..b02bcf8c 100644 --- a/public/locales/uk/app.json +++ b/public/locales/uk/app.json @@ -39,7 +39,6 @@ "Sending debug logs…": "Надсилання журналу налагодження…", "Send debug logs": "Надіслати журнал налагодження", "Select an option": "Вибрати опцію", - "Save": "Зберегти", "Return to home screen": "Повернутися на екран домівки", "Remove": "Вилучити", "Release to stop": "Відпустіть, щоб закінчити", @@ -132,5 +131,11 @@ "Expose developer settings in the settings window.": "Відкрийте налаштування розробника у вікні налаштувань.", "Developer Settings": "Налаштування розробника", "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy and our <5>Cookie Policy.": "Користуючись дочасним доступом, ви даєте згоду на збір анонімних даних, які ми використовуємо для вдосконалення продукту. Ви можете знайти більше інформації про те, які дані ми відстежуємо в нашій <2>Політиці Приватності і нашій <5>Політиці про куки.", - "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>Ви можете відкликати згоду, прибравши цей прапорець. Якщо ви зараз розмовляєте, це налаштування застосується після завершення виклику." + "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>Ви можете відкликати згоду, прибравши цей прапорець. Якщо ви зараз розмовляєте, це налаштування застосується після завершення виклику.", + "Your feedback": "Ваш відгук", + "Thanks, we received your feedback!": "Дякуємо, ми отримали ваш відгук!", + "Submitting…": "Надсилання…", + "Submit": "Надіслати", + "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Якщо у вас виникли проблеми або ви просто хочете залишити відгук, надішліть нам короткий опис нижче.", + "Feedback": "Відгук" } From a0da11ea7871b99d7055d3f492d536c43d6df779 Mon Sep 17 00:00:00 2001 From: phardyle Date: Sat, 3 Jun 2023 13:03:01 +0000 Subject: [PATCH 262/286] Translated using Weblate (Chinese (Simplified)) Currently translated at 90.6% (126 of 139 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/zh_Hans/ --- public/locales/zh-Hans/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/zh-Hans/app.json b/public/locales/zh-Hans/app.json index 0b494e1f..82bf38d7 100644 --- a/public/locales/zh-Hans/app.json +++ b/public/locales/zh-Hans/app.json @@ -124,6 +124,6 @@ "Call link copied": "链接已复制", "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "点击“现在加入”则表示同意我们的<2>条款与条件<2>", "Avatar": "头像", - "<0>Oops, something's gone wrong.": "", + "<0>Oops, something's gone wrong.": "<0>哎哟,出问题了。", "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "" } From 9f924aef64f6c47ed326214b0cadab2f1936c20e Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Fri, 2 Jun 2023 20:32:18 +0000 Subject: [PATCH 263/286] Translated using Weblate (Slovak) Currently translated at 100.0% (139 of 139 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/sk/ --- public/locales/sk/app.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/locales/sk/app.json b/public/locales/sk/app.json index e8b2ec4b..407680de 100644 --- a/public/locales/sk/app.json +++ b/public/locales/sk/app.json @@ -20,7 +20,6 @@ "Sending debug logs…": "Odosielanie záznamov o ladení…", "Send debug logs": "Odoslať záznamy o ladení", "Select an option": "Vyberte možnosť", - "Save": "Uložiť", "Return to home screen": "Návrat na domovskú obrazovku", "Remove": "Odstrániť", "Release spacebar key to stop": "Pustite medzerník pre ukončenie", @@ -132,5 +131,11 @@ "Expose developer settings in the settings window.": "Zobraziť nastavenia pre vývojárov v okne nastavení.", "Developer Settings": "Nastavenia pre vývojárov", "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy and our <5>Cookie Policy.": "Účasťou v tejto beta verzii súhlasíte so zhromažďovaním anonymných údajov, ktoré použijeme na zlepšenie produktu. Viac informácií o tom, ktoré údaje sledujeme, nájdete v našich <2>Zásadách ochrany osobných údajov a <5>Zásadách používania súborov cookie.", - "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>Súhlas môžete odvolať zrušením označenia tohto políčka. Ak práve prebieha hovor, toto nastavenie nadobudne platnosť po skončení hovoru." + "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>Súhlas môžete odvolať zrušením označenia tohto políčka. Ak práve prebieha hovor, toto nastavenie nadobudne platnosť po skončení hovoru.", + "Your feedback": "Vaša spätná väzba", + "Thanks, we received your feedback!": "Ďakujeme, dostali sme vašu spätnú väzbu!", + "Submitting…": "Odosielanie…", + "Submit": "Odoslať", + "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Ak máte problémy alebo jednoducho chcete poskytnúť spätnú väzbu, pošlite nám krátky popis nižšie.", + "Feedback": "Spätná väzba" } From 5af7c9e7c7de875e38b0d3506cd30fcf2f8688fc Mon Sep 17 00:00:00 2001 From: Weblate Date: Sun, 4 Jun 2023 13:07:31 +0000 Subject: [PATCH 264/286] Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/ --- public/locales/bg/app.json | 1 - public/locales/cs/app.json | 1 - public/locales/es/app.json | 1 - public/locales/id/app.json | 1 - public/locales/ja/app.json | 1 - public/locales/pl/app.json | 1 - public/locales/tr/app.json | 1 - 7 files changed, 7 deletions(-) diff --git a/public/locales/bg/app.json b/public/locales/bg/app.json index b0eaff4a..a62b6b06 100644 --- a/public/locales/bg/app.json +++ b/public/locales/bg/app.json @@ -73,7 +73,6 @@ "Release to stop": "Отпуснете за да спрете", "Remove": "Премахни", "Return to home screen": "Връщане на началния екран", - "Saving…": "Запазване…", "Select an option": "Изберете опция", "Send debug logs": "Изпратете debug логове", "Sending…": "Изпращане…", diff --git a/public/locales/cs/app.json b/public/locales/cs/app.json index 143203ff..dabaa042 100644 --- a/public/locales/cs/app.json +++ b/public/locales/cs/app.json @@ -46,7 +46,6 @@ "Sending debug logs…": "Posílání ladícího záznamu…", "Send debug logs": "Poslat ladící záznam", "Select an option": "Vyberte možnost", - "Save": "Uložit", "Return to home screen": "Vrátit se na domácí obrazovku", "Remove": "Odstranit", "Registering…": "Registrování…", diff --git a/public/locales/es/app.json b/public/locales/es/app.json index 011cb4ab..104fee3a 100644 --- a/public/locales/es/app.json +++ b/public/locales/es/app.json @@ -47,7 +47,6 @@ "Sending debug logs…": "Enviando registros de depuración…", "Send debug logs": "Enviar registros de depuración", "Select an option": "Selecciona una opción", - "Save": "Guardar", "Return to home screen": "Volver a la pantalla de inicio", "Remove": "Eliminar", "Release to stop": "Suelta para parar", diff --git a/public/locales/id/app.json b/public/locales/id/app.json index 0bafaaee..6d6ba5d8 100644 --- a/public/locales/id/app.json +++ b/public/locales/id/app.json @@ -73,7 +73,6 @@ "Release to stop": "Lepaskan untuk berhenti", "Remove": "Hapus", "Return to home screen": "Kembali ke layar beranda", - "Saving…": "Menyimpan…", "Select an option": "Pilih sebuah opsi", "Send debug logs": "Kirim catatan pengawakutuan", "Sending…": "Mengirimkan…", diff --git a/public/locales/ja/app.json b/public/locales/ja/app.json index 577faac3..69f7411f 100644 --- a/public/locales/ja/app.json +++ b/public/locales/ja/app.json @@ -68,7 +68,6 @@ "Settings": "設定", "Sending…": "送信しています…", "Sending debug logs…": "デバッグログを送信しています…", - "Save": "保存", "Return to home screen": "ホーム画面に戻る", "Registering…": "登録しています…", "Register": "登録", diff --git a/public/locales/pl/app.json b/public/locales/pl/app.json index 9ef51443..32b5e09a 100644 --- a/public/locales/pl/app.json +++ b/public/locales/pl/app.json @@ -40,7 +40,6 @@ "Sending debug logs…": "Wysyłanie dzienników debugowania…", "Send debug logs": "Wyślij dzienniki debugowania", "Select an option": "Wybierz opcję", - "Save": "Zapisz", "Return to home screen": "Powróć do strony głównej", "Remove": "Usuń", "Release to stop": "Puść przycisk, aby zatrzymać", diff --git a/public/locales/tr/app.json b/public/locales/tr/app.json index e60ed7f3..f1870e3a 100644 --- a/public/locales/tr/app.json +++ b/public/locales/tr/app.json @@ -67,7 +67,6 @@ "Release to stop": "Kesmek için bırakın", "Remove": "Çıkar", "Return to home screen": "Ev ekranına geri dön", - "Saving…": "Kaydediliyor…", "Select an option": "Bir seçenek seç", "Send debug logs": "Hata ayıklama kütüğünü gönder", "Sending…": "Gönderiliyor…", From e129e90dd893a480afa718bee9180dbe1c5bbed8 Mon Sep 17 00:00:00 2001 From: Someone Date: Mon, 5 Jun 2023 04:16:05 +0000 Subject: [PATCH 265/286] Added translation using Weblate (Vietnamese) --- public/locales/vi/app.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 public/locales/vi/app.json diff --git a/public/locales/vi/app.json b/public/locales/vi/app.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/public/locales/vi/app.json @@ -0,0 +1 @@ +{} From 5ef0486eff9955af440255c5a812395b7b6a806f Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 5 Jun 2023 15:52:05 -0400 Subject: [PATCH 266/286] Add a URL parameter for allowing fallback ICE servers --- src/UrlParams.ts | 6 ++++++ src/widget.ts | 10 +++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/UrlParams.ts b/src/UrlParams.ts index df62aefa..27b99cce 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -79,6 +79,11 @@ export interface UrlParams { * The Posthog analytics ID. It is only available if the user has given consent for sharing telemetry in element web. */ analyticsID: string | null; + /** + * Whether the app is allowed to use fallback STUN servers for ICE in case the + * user's homeserver doesn't provide any. + */ + allowIceFallback: boolean; } /** @@ -135,6 +140,7 @@ export const getUrlParams = ( fonts: getAllParams("font"), fontScale: Number.isNaN(fontScale) ? null : fontScale, analyticsID: getParam("analyticsID"), + allowIceFallback: hasParam("allowIceFallback"), }; }; diff --git a/src/widget.ts b/src/widget.ts index 6a38f9dd..0bee23f0 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -101,7 +101,14 @@ export const widget: WidgetHelpers | null = (() => { // We need to do this now rather than later because it has capabilities to // request, and is responsible for starting the transport (should it be?) - const { roomId, userId, deviceId, baseUrl, e2eEnabled } = getUrlParams(); + const { + roomId, + userId, + deviceId, + baseUrl, + e2eEnabled, + allowIceFallback, + } = getUrlParams(); if (!roomId) throw new Error("Room ID must be supplied"); if (!userId) throw new Error("User ID must be supplied"); if (!deviceId) throw new Error("Device ID must be supplied"); @@ -148,6 +155,7 @@ export const widget: WidgetHelpers | null = (() => { deviceId, timelineSupport: true, useE2eForGroupCall: e2eEnabled, + fallbackICEServerAllowed: allowIceFallback, } ); const clientPromise = client.startClient().then(() => client); From f0a6f5919e421fff88c7795fb8d6d3a4e21f4119 Mon Sep 17 00:00:00 2001 From: Enrico Schwendig Date: Tue, 6 Jun 2023 08:28:53 +0200 Subject: [PATCH 267/286] move webrtc etc. events from groupCall to matrix.call span (#1080) * add new linked span for connection stats * move stats span under call span and add user attribute * Update matrix-js-sdk --- package.json | 2 +- src/otel/OTelGroupCallMembership.ts | 111 ++++++++------- src/otel/ObjectFlattener.ts | 12 +- test/otel/ObjectFlattener-test.ts | 205 ++++++++++++++++------------ yarn.lock | 4 +- 5 files changed, 184 insertions(+), 150 deletions(-) diff --git a/package.json b/package.json index 8d9773bb..0d5325b5 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#a7b1dcaf9514b2e424a387e266c6f383a5909927", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#e70a1a1effe59e6754f9a10cc2df8eef81638c7d", "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 60ae2b34..a8d93b6a 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -333,22 +333,70 @@ export class OTelGroupCallMembership { public onConnectionStatsReport( statsReport: GroupCallStatsReport ) { - if (!ElementCallOpenTelemetry.instance) return; - - const type = OTelStatsReportType.ConnectionReport; - const data = - ObjectFlattener.flattenConnectionStatsReportObject(statsReport); - this.buildStatsEventSpan({ type, data }); + this.buildCallStatsSpan( + OTelStatsReportType.ConnectionReport, + statsReport.report + ); } public onByteSentStatsReport( statsReport: GroupCallStatsReport ) { - if (!ElementCallOpenTelemetry.instance) return; + this.buildCallStatsSpan( + OTelStatsReportType.ByteSentReport, + statsReport.report + ); + } - const type = OTelStatsReportType.ByteSentReport; - const data = ObjectFlattener.flattenByteSentStatsReportObject(statsReport); - this.buildStatsEventSpan({ type, data }); + public buildCallStatsSpan( + type: OTelStatsReportType, + report: ByteSentStatsReport | ConnectionStatsReport + ): void { + if (!ElementCallOpenTelemetry.instance) return; + let call: OTelCall | undefined; + const callId = report?.callId; + + if (callId) { + call = this.callsByCallId.get(callId); + } + + if (!call) { + this.callMembershipSpan?.addEvent(type + "_unknown_callid", { + "call.callId": callId, + "call.opponentMemberId": report.opponentMemberId + ? report.opponentMemberId + : "unknown", + }); + logger.error(`Received ${type} with unknown call ID: ${callId}`); + return; + } + const data = ObjectFlattener.flattenReportObject(type, report); + const ctx = opentelemetry.trace.setSpan( + opentelemetry.context.active(), + call.span + ); + + const options = { + links: [ + { + context: call.span.spanContext(), + }, + ], + }; + + const span = ElementCallOpenTelemetry.instance.tracer.startSpan( + type, + options, + ctx + ); + + span.setAttribute("matrix.callId", callId); + span.setAttribute( + "matrix.opponentMemberId", + report.opponentMemberId ? report.opponentMemberId : "unknown" + ); + span.addEvent("matrix.call.connection_stats_event", data); + span.end(); } public onSummaryStatsReport( @@ -381,45 +429,6 @@ export class OTelGroupCallMembership { span.end(); } } - - private buildStatsEventSpan(event: OTelStatsReportEvent): void { - // @ TODO: fix this - Because on multiple calls we receive multiple stats report spans. - // This could be break if stats arrived in same time from different call objects. - if (this.statsReportSpan.span === undefined && this.callMembershipSpan) { - const ctx = setSpan( - opentelemetry.context.active(), - this.callMembershipSpan - ); - this.statsReportSpan.span = - ElementCallOpenTelemetry.instance?.tracer.startSpan( - "matrix.groupCallMembership.statsReport", - undefined, - ctx - ); - if (this.statsReportSpan.span === undefined) { - return; - } - this.statsReportSpan.span.setAttribute( - "matrix.confId", - this.groupCall.groupCallId - ); - this.statsReportSpan.span.setAttribute("matrix.userId", this.myUserId); - this.statsReportSpan.span.setAttribute( - "matrix.displayName", - this.myMember ? this.myMember.name : "unknown-name" - ); - - this.statsReportSpan.span.addEvent(event.type, event.data); - this.statsReportSpan.stats.push(event); - } else if ( - this.statsReportSpan.span !== undefined && - this.callMembershipSpan - ) { - this.statsReportSpan.span.addEvent(event.type, event.data); - this.statsReportSpan.span.end(); - this.statsReportSpan = { span: undefined, stats: [] }; - } - } } interface OTelStatsReportEvent { @@ -428,7 +437,7 @@ interface OTelStatsReportEvent { } enum OTelStatsReportType { - ConnectionReport = "matrix.stats.connection", - ByteSentReport = "matrix.stats.byteSent", + ConnectionReport = "matrix.call.stats.connection", + ByteSentReport = "matrix.call.stats.byteSent", SummaryReport = "matrix.stats.summary", } diff --git a/src/otel/ObjectFlattener.ts b/src/otel/ObjectFlattener.ts index 321c45ee..652f9056 100644 --- a/src/otel/ObjectFlattener.ts +++ b/src/otel/ObjectFlattener.ts @@ -23,16 +23,12 @@ import { } from "matrix-js-sdk/src/webrtc/stats/statsReport"; export class ObjectFlattener { - public static flattenConnectionStatsReportObject( - statsReport: GroupCallStatsReport + public static flattenReportObject( + prefix: string, + report: ConnectionStatsReport | ByteSentStatsReport ): Attributes { const flatObject = {}; - ObjectFlattener.flattenObjectRecursive( - statsReport.report, - flatObject, - "matrix.stats.conn.", - 0 - ); + ObjectFlattener.flattenObjectRecursive(report, flatObject, `${prefix}.`, 0); return flatObject; } diff --git a/test/otel/ObjectFlattener-test.ts b/test/otel/ObjectFlattener-test.ts index e3709d20..b2fdce37 100644 --- a/test/otel/ObjectFlattener-test.ts +++ b/test/otel/ObjectFlattener-test.ts @@ -1,6 +1,7 @@ import { GroupCallStatsReport } from "matrix-js-sdk/src/webrtc/groupCall"; import { AudioConcealment, + ByteSentStatsReport, ConnectionStatsReport, } from "matrix-js-sdk/src/webrtc/stats/statsReport"; import { ObjectFlattener } from "../../src/otel/ObjectFlattener"; @@ -28,6 +29,8 @@ describe("ObjectFlattener", () => { const statsReport: GroupCallStatsReport = { report: { + callId: "callId", + opponentMemberId: "opponentMemberId", bandwidth: { upload: 426, download: 0 }, bitrate: { upload: 426, @@ -116,21 +119,25 @@ describe("ObjectFlattener", () => { ObjectFlattener.flattenObjectRecursive( statsReport.report.resolution, flatObject, - "matrix.stats.conn.resolution.", + "matrix.call.stats.connection.resolution.", 0 ); expect(flatObject).toEqual({ - "matrix.stats.conn.resolution.local.LOCAL_AUDIO_TRACK_ID.height": -1, - "matrix.stats.conn.resolution.local.LOCAL_AUDIO_TRACK_ID.width": -1, + "matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.height": + -1, + "matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.width": + -1, - "matrix.stats.conn.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460, - "matrix.stats.conn.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780, + "matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460, + "matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780, - "matrix.stats.conn.resolution.remote.REMOTE_AUDIO_TRACK_ID.height": -1, - "matrix.stats.conn.resolution.remote.REMOTE_AUDIO_TRACK_ID.width": -1, + "matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.height": + -1, + "matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.width": + -1, - "matrix.stats.conn.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960, - "matrix.stats.conn.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080, + "matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960, + "matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080, }); }); it("should flatter an Array object", () => { @@ -138,106 +145,128 @@ describe("ObjectFlattener", () => { ObjectFlattener.flattenObjectRecursive( statsReport.report.transport, flatObject, - "matrix.stats.conn.transport.", + "matrix.call.stats.connection.transport.", 0 ); expect(flatObject).toEqual({ - "matrix.stats.conn.transport.0.ip": "ff11::5fa:abcd:999c:c5c5:50000", - "matrix.stats.conn.transport.0.type": "udp", - "matrix.stats.conn.transport.0.localIp": + "matrix.call.stats.connection.transport.0.ip": + "ff11::5fa:abcd:999c:c5c5:50000", + "matrix.call.stats.connection.transport.0.type": "udp", + "matrix.call.stats.connection.transport.0.localIp": "2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000", - "matrix.stats.conn.transport.0.isFocus": true, - "matrix.stats.conn.transport.0.localCandidateType": "host", - "matrix.stats.conn.transport.0.remoteCandidateType": "host", - "matrix.stats.conn.transport.0.networkType": "ethernet", - "matrix.stats.conn.transport.0.rtt": "NaN", - "matrix.stats.conn.transport.1.ip": "10.10.10.2:22222", - "matrix.stats.conn.transport.1.type": "tcp", - "matrix.stats.conn.transport.1.localIp": "10.10.10.100:33333", - "matrix.stats.conn.transport.1.isFocus": true, - "matrix.stats.conn.transport.1.localCandidateType": "srfx", - "matrix.stats.conn.transport.1.remoteCandidateType": "srfx", - "matrix.stats.conn.transport.1.networkType": "ethernet", - "matrix.stats.conn.transport.1.rtt": "null", + "matrix.call.stats.connection.transport.0.isFocus": true, + "matrix.call.stats.connection.transport.0.localCandidateType": "host", + "matrix.call.stats.connection.transport.0.remoteCandidateType": "host", + "matrix.call.stats.connection.transport.0.networkType": "ethernet", + "matrix.call.stats.connection.transport.0.rtt": "NaN", + "matrix.call.stats.connection.transport.1.ip": "10.10.10.2:22222", + "matrix.call.stats.connection.transport.1.type": "tcp", + "matrix.call.stats.connection.transport.1.localIp": + "10.10.10.100:33333", + "matrix.call.stats.connection.transport.1.isFocus": true, + "matrix.call.stats.connection.transport.1.localCandidateType": "srfx", + "matrix.call.stats.connection.transport.1.remoteCandidateType": "srfx", + "matrix.call.stats.connection.transport.1.networkType": "ethernet", + "matrix.call.stats.connection.transport.1.rtt": "null", }); }); }); - describe("on flattenConnectionStatsReportObject", () => { + describe("on flattenReportObject Connection Stats", () => { it("should flatten a Report to otel Attributes Object", () => { expect( - ObjectFlattener.flattenConnectionStatsReportObject(statsReport) + ObjectFlattener.flattenReportObject( + "matrix.call.stats.connection", + statsReport.report + ) ).toEqual({ - "matrix.stats.conn.bandwidth.download": 0, - "matrix.stats.conn.bandwidth.upload": 426, - "matrix.stats.conn.bitrate.audio.download": 0, - "matrix.stats.conn.bitrate.audio.upload": 124, - "matrix.stats.conn.bitrate.download": 0, - "matrix.stats.conn.bitrate.upload": 426, - "matrix.stats.conn.bitrate.video.download": 0, - "matrix.stats.conn.bitrate.video.upload": 302, - "matrix.stats.conn.codec.local.LOCAL_AUDIO_TRACK_ID": "opus", - "matrix.stats.conn.codec.local.LOCAL_VIDEO_TRACK_ID": "v8", - "matrix.stats.conn.codec.remote.REMOTE_AUDIO_TRACK_ID": "opus", - "matrix.stats.conn.codec.remote.REMOTE_VIDEO_TRACK_ID": "v9", - "matrix.stats.conn.framerate.local.LOCAL_AUDIO_TRACK_ID": 0, - "matrix.stats.conn.framerate.local.LOCAL_VIDEO_TRACK_ID": 30, - "matrix.stats.conn.framerate.remote.REMOTE_AUDIO_TRACK_ID": 0, - "matrix.stats.conn.framerate.remote.REMOTE_VIDEO_TRACK_ID": 60, - "matrix.stats.conn.jitter.REMOTE_AUDIO_TRACK_ID": 2, - "matrix.stats.conn.jitter.REMOTE_VIDEO_TRACK_ID": 50, - "matrix.stats.conn.packetLoss.download": 0, - "matrix.stats.conn.packetLoss.total": 0, - "matrix.stats.conn.packetLoss.upload": 0, - "matrix.stats.conn.resolution.local.LOCAL_AUDIO_TRACK_ID.height": -1, - "matrix.stats.conn.resolution.local.LOCAL_AUDIO_TRACK_ID.width": -1, - "matrix.stats.conn.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460, - "matrix.stats.conn.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780, - "matrix.stats.conn.resolution.remote.REMOTE_AUDIO_TRACK_ID.height": -1, - "matrix.stats.conn.resolution.remote.REMOTE_AUDIO_TRACK_ID.width": -1, - "matrix.stats.conn.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960, - "matrix.stats.conn.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080, - "matrix.stats.conn.transport.0.ip": "ff11::5fa:abcd:999c:c5c5:50000", - "matrix.stats.conn.transport.0.type": "udp", - "matrix.stats.conn.transport.0.localIp": + "matrix.call.stats.connection.callId": "callId", + "matrix.call.stats.connection.opponentMemberId": "opponentMemberId", + "matrix.call.stats.connection.bandwidth.download": 0, + "matrix.call.stats.connection.bandwidth.upload": 426, + "matrix.call.stats.connection.bitrate.audio.download": 0, + "matrix.call.stats.connection.bitrate.audio.upload": 124, + "matrix.call.stats.connection.bitrate.download": 0, + "matrix.call.stats.connection.bitrate.upload": 426, + "matrix.call.stats.connection.bitrate.video.download": 0, + "matrix.call.stats.connection.bitrate.video.upload": 302, + "matrix.call.stats.connection.codec.local.LOCAL_AUDIO_TRACK_ID": "opus", + "matrix.call.stats.connection.codec.local.LOCAL_VIDEO_TRACK_ID": "v8", + "matrix.call.stats.connection.codec.remote.REMOTE_AUDIO_TRACK_ID": + "opus", + "matrix.call.stats.connection.codec.remote.REMOTE_VIDEO_TRACK_ID": "v9", + "matrix.call.stats.connection.framerate.local.LOCAL_AUDIO_TRACK_ID": 0, + "matrix.call.stats.connection.framerate.local.LOCAL_VIDEO_TRACK_ID": 30, + "matrix.call.stats.connection.framerate.remote.REMOTE_AUDIO_TRACK_ID": 0, + "matrix.call.stats.connection.framerate.remote.REMOTE_VIDEO_TRACK_ID": 60, + "matrix.call.stats.connection.jitter.REMOTE_AUDIO_TRACK_ID": 2, + "matrix.call.stats.connection.jitter.REMOTE_VIDEO_TRACK_ID": 50, + "matrix.call.stats.connection.packetLoss.download": 0, + "matrix.call.stats.connection.packetLoss.total": 0, + "matrix.call.stats.connection.packetLoss.upload": 0, + "matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.height": + -1, + "matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.width": + -1, + "matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460, + "matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780, + "matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.height": + -1, + "matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.width": + -1, + "matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960, + "matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080, + "matrix.call.stats.connection.transport.0.ip": + "ff11::5fa:abcd:999c:c5c5:50000", + "matrix.call.stats.connection.transport.0.type": "udp", + "matrix.call.stats.connection.transport.0.localIp": "2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000", - "matrix.stats.conn.transport.0.isFocus": true, - "matrix.stats.conn.transport.0.localCandidateType": "host", - "matrix.stats.conn.transport.0.remoteCandidateType": "host", - "matrix.stats.conn.transport.0.networkType": "ethernet", - "matrix.stats.conn.transport.0.rtt": "NaN", - "matrix.stats.conn.transport.1.ip": "10.10.10.2:22222", - "matrix.stats.conn.transport.1.type": "tcp", - "matrix.stats.conn.transport.1.localIp": "10.10.10.100:33333", - "matrix.stats.conn.transport.1.isFocus": true, - "matrix.stats.conn.transport.1.localCandidateType": "srfx", - "matrix.stats.conn.transport.1.remoteCandidateType": "srfx", - "matrix.stats.conn.transport.1.networkType": "ethernet", - "matrix.stats.conn.transport.1.rtt": "null", - "matrix.stats.conn.audioConcealment.REMOTE_AUDIO_TRACK_ID.concealedAudio": 0, - "matrix.stats.conn.audioConcealment.REMOTE_AUDIO_TRACK_ID.totalAudioDuration": 0, - "matrix.stats.conn.audioConcealment.REMOTE_VIDEO_TRACK_ID.concealedAudio": 0, - "matrix.stats.conn.audioConcealment.REMOTE_VIDEO_TRACK_ID.totalAudioDuration": 0, - "matrix.stats.conn.totalAudioConcealment.concealedAudio": 0, - "matrix.stats.conn.totalAudioConcealment.totalAudioDuration": 0, + "matrix.call.stats.connection.transport.0.isFocus": true, + "matrix.call.stats.connection.transport.0.localCandidateType": "host", + "matrix.call.stats.connection.transport.0.remoteCandidateType": "host", + "matrix.call.stats.connection.transport.0.networkType": "ethernet", + "matrix.call.stats.connection.transport.0.rtt": "NaN", + "matrix.call.stats.connection.transport.1.ip": "10.10.10.2:22222", + "matrix.call.stats.connection.transport.1.type": "tcp", + "matrix.call.stats.connection.transport.1.localIp": + "10.10.10.100:33333", + "matrix.call.stats.connection.transport.1.isFocus": true, + "matrix.call.stats.connection.transport.1.localCandidateType": "srfx", + "matrix.call.stats.connection.transport.1.remoteCandidateType": "srfx", + "matrix.call.stats.connection.transport.1.networkType": "ethernet", + "matrix.call.stats.connection.transport.1.rtt": "null", + "matrix.call.stats.connection.audioConcealment.REMOTE_AUDIO_TRACK_ID.concealedAudio": 0, + "matrix.call.stats.connection.audioConcealment.REMOTE_AUDIO_TRACK_ID.totalAudioDuration": 0, + "matrix.call.stats.connection.audioConcealment.REMOTE_VIDEO_TRACK_ID.concealedAudio": 0, + "matrix.call.stats.connection.audioConcealment.REMOTE_VIDEO_TRACK_ID.totalAudioDuration": 0, + "matrix.call.stats.connection.totalAudioConcealment.concealedAudio": 0, + "matrix.call.stats.connection.totalAudioConcealment.totalAudioDuration": 0, }); }); }); describe("on flattenByteSendStatsReportObject", () => { - const byteSent = { - report: new Map([ - ["4aa92608-04c6-428e-8312-93e17602a959", 132093], - ["a08e4237-ee30-4015-a932-b676aec894b1", 913448], - ]), - }; + const byteSentStatsReport = new Map< + string, + number + >() as ByteSentStatsReport; + byteSentStatsReport.callId = "callId"; + byteSentStatsReport.opponentMemberId = "opponentMemberId"; + byteSentStatsReport.set("4aa92608-04c6-428e-8312-93e17602a959", 132093); + byteSentStatsReport.set("a08e4237-ee30-4015-a932-b676aec894b1", 913448); + it("should flatten a Report to otel Attributes Object", () => { expect( - ObjectFlattener.flattenByteSentStatsReportObject(byteSent) + ObjectFlattener.flattenReportObject( + "matrix.call.stats.bytesSend", + byteSentStatsReport + ) ).toEqual({ - "matrix.stats.bytesSent.4aa92608-04c6-428e-8312-93e17602a959": 132093, - "matrix.stats.bytesSent.a08e4237-ee30-4015-a932-b676aec894b1": 913448, + "matrix.call.stats.bytesSend.4aa92608-04c6-428e-8312-93e17602a959": 132093, + "matrix.call.stats.bytesSend.a08e4237-ee30-4015-a932-b676aec894b1": 913448, }); + expect(byteSentStatsReport.callId).toEqual("callId"); + expect(byteSentStatsReport.opponentMemberId).toEqual("opponentMemberId"); }); }); }); diff --git a/yarn.lock b/yarn.lock index 2e8e7b4c..c9c9a44e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10557,9 +10557,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#a7b1dcaf9514b2e424a387e266c6f383a5909927": +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#e70a1a1effe59e6754f9a10cc2df8eef81638c7d": version "25.1.1" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/a7b1dcaf9514b2e424a387e266c6f383a5909927" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e70a1a1effe59e6754f9a10cc2df8eef81638c7d" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.9" From 66a582dd5f9a5a4e1a34965d312243cbd3c9adb7 Mon Sep 17 00:00:00 2001 From: Linerly Date: Mon, 5 Jun 2023 06:21:30 +0000 Subject: [PATCH 268/286] Translated using Weblate (Indonesian) Currently translated at 100.0% (139 of 139 strings) Translation: Element Call/element-call Translate-URL: http://translate.element.io/projects/element-call/element-call/id/ --- public/locales/id/app.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/public/locales/id/app.json b/public/locales/id/app.json index 6d6ba5d8..85b141d0 100644 --- a/public/locales/id/app.json +++ b/public/locales/id/app.json @@ -131,5 +131,11 @@ "Expose developer settings in the settings window.": "Ekspos pengaturan pengembang dalam jendela pengaturan.", "Developer Settings": "Pengaturan Pengembang", "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy and our <5>Cookie Policy.": "Dengan bergabung dalam beta ini, Anda mengizinkan kami untuk mengumpulkan data anonim, yang kami gunakan untuk meningkatkan produk ini. Anda dapat mempelajari lebih lanjut tentang data apa yang kami lacak dalam <2>Kebijakan Privasi dan <5>Kebijakan Kuki kami.", - "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>Anda dapat mengurungkan kembali izin dengan mencentang kotak ini. Jika Anda saat ini dalam panggilan, pengaturan ini akan diterapkan di akhir panggilan." + "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>Anda dapat mengurungkan kembali izin dengan mencentang kotak ini. Jika Anda saat ini dalam panggilan, pengaturan ini akan diterapkan di akhir panggilan.", + "Feedback": "Masukan", + "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Jika Anda mengalami masalah atau hanya ingin memberikan masukan, silakan kirimkan kami deskripsi pendek di bawah.", + "Submit": "Kirim", + "Submitting…": "Mengirim", + "Thanks, we received your feedback!": "Terima kasih, kami telah menerima masukan Anda!", + "Your feedback": "Masukan Anda" } From cc2402e61c7dd9cd47baede2b62b289506ae29f7 Mon Sep 17 00:00:00 2001 From: Someone Date: Mon, 5 Jun 2023 04:21:23 +0000 Subject: [PATCH 269/286] Translated using Weblate (Vietnamese) Currently translated at 25.8% (36 of 139 strings) Translation: Element Call/element-call Translate-URL: http://translate.element.io/projects/element-call/element-call/vi/ --- public/locales/vi/app.json | 39 +++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/public/locales/vi/app.json b/public/locales/vi/app.json index 0967ef42..81961343 100644 --- a/public/locales/vi/app.json +++ b/public/locales/vi/app.json @@ -1 +1,38 @@ -{} +{ + "Login": "Đăng nhập", + "{{count}} people connected|other": "{{count}} người đã kết nối", + "{{name}} (Waiting for video...)": "{{name}} (Đang đợi truyền hình...)", + "Join call": "Tham gia cuộc gọi", + "Mute microphone": "Tắt micrô", + "Password": "Mật khẩu", + "Settings": "Cài đặt", + "Sending…": "Đang gửi…", + "Sign in": "Đăng nhập", + "Submit": "Gửi", + "Video call name": "Tên cuộc gọi truyền hình", + "Video call": "Gọi truyền hình", + "Video": "Truyền hình", + "Username": "Tên người dùng", + "Yes, join call": "Vâng, tham gia cuộc gọi", + "Your feedback": "Phản hồi của bạn", + "{{count}} people connected|one": "{{count}} người đã kết nối", + "{{displayName}}, your call is now ended": "{{displayName}}, cuộc gọi của bạn đã kết thúc", + "{{name}} (Connecting...)": "{{name}} (Đang kết nối...)", + "Your recent calls": "Cuộc gọi gần đây", + "You can't talk at the same time": "Bạn không thể nói cùng thời điểm", + "WebRTC is not supported or is being blocked in this browser.": "WebRTC không được hỗ trợ hay bị chặn trong trình duyệt này.", + "Waiting for network": "Đang đợi kết nối mạng", + "Waiting for other participants…": "Đang đợi những người khác…", + "Version: {{version}}": "Phiên bản: {{version}}", + "Turn on camera": "Bật máy quay", + "Turn off camera": "Tắt máy quay", + "Submit feedback": "Gửi phản hồi", + "Stop sharing screen": "Ngừng chia sẻ màn hình", + "Speaker": "Loa", + "Sign out": "Đăng xuất", + "Share screen": "Chia sẻ màn hình", + "No": "Không", + "Invite people": "Mời mọi người", + "Join call now": "Tham gia cuộc gọi", + "Create account": "Tạo tài khoản" +} From eff8847586fbe617f6a1b4d30033aaf2f22cb675 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Wed, 7 Jun 2023 15:59:42 +0200 Subject: [PATCH 270/286] add splitbrain params to MediaReceived event (#1089) Signed-off-by: Timo K --- src/analytics/PosthogSpanProcessor.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/analytics/PosthogSpanProcessor.ts b/src/analytics/PosthogSpanProcessor.ts index 1edb5a2f..effa6436 100644 --- a/src/analytics/PosthogSpanProcessor.ts +++ b/src/analytics/PosthogSpanProcessor.ts @@ -126,6 +126,11 @@ export class PosthogSpanProcessor implements SpanProcessor { const maxPacketLoss = `${attributes["matrix.stats.summary.maxPacketLoss"]}`; const peerConnections = `${attributes["matrix.stats.summary.peerConnections"]}`; const percentageConcealedAudio = `${attributes["matrix.stats.summary.percentageConcealedAudio"]}`; + const opponentUsersInCall = `${attributes["matrix.stats.summary.opponentUsersInCall"]}`; + const opponentDevicesInCall = `${attributes["matrix.stats.summary.opponentDevicesInCall"]}`; + const diffDevicesToPeerConnections = `${attributes["matrix.stats.summary.diffDevicesToPeerConnections"]}`; + const ratioPeerConnectionToDevices = `${attributes["matrix.stats.summary.ratioPeerConnectionToDevices"]}`; + PosthogAnalytics.instance.trackEvent( { eventName: "MediaReceived", @@ -137,6 +142,10 @@ export class PosthogSpanProcessor implements SpanProcessor { maxPacketLoss: maxPacketLoss, peerConnections: peerConnections, percentageConcealedAudio: percentageConcealedAudio, + opponentUsersInCall: opponentUsersInCall, + opponentDevicesInCall: opponentDevicesInCall, + diffDevicesToPeerConnections: diffDevicesToPeerConnections, + ratioPeerConnectionToDevices: ratioPeerConnectionToDevices, }, // Send instantly because the window might be closing { send_instantly: true } From 2a6981c58dd57183cc878145b44ea1b2e7b0416c Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Wed, 7 Jun 2023 16:22:44 +0200 Subject: [PATCH 271/286] Add quality survey at the end of the call (#1084) Signed-off-by: Timo K Co-authored-by: Robin --- public/locales/en-GB/app.json | 7 +- src/analytics/PosthogAnalytics.ts | 2 + src/analytics/PosthogEvents.ts | 18 ++++ src/icons/StarSelected.svg | 3 + src/icons/StarUnselected.svg | 4 + src/input/FeedbackInput.module.css | 23 ++++ src/input/StarRatingInput.module.css | 41 ++++++++ src/input/StarRatingInput.tsx | 85 +++++++++++++++ src/room/CallEndedView.module.css | 15 ++- src/room/CallEndedView.tsx | 151 ++++++++++++++++++++++----- src/room/GroupCallView.tsx | 16 ++- src/settings/FeedbackSettingsTab.tsx | 3 + 12 files changed, 338 insertions(+), 30 deletions(-) create mode 100644 src/icons/StarSelected.svg create mode 100644 src/icons/StarUnselected.svg create mode 100644 src/input/FeedbackInput.module.css create mode 100644 src/input/StarRatingInput.module.css create mode 100644 src/input/StarRatingInput.tsx diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index ccd111b8..987402a0 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -1,7 +1,9 @@ { "{{count}} people connected|one": "{{count}} person connected", "{{count}} people connected|other": "{{count}} people connected", - "{{displayName}}, your call is now ended": "{{displayName}}, your call is now ended", + "{{count}} stars|one": "{{count}} star", + "{{count}} stars|other": "{{count}} stars", + "{{displayName}}, your call has ended.": "{{displayName}}, your call has ended.", "{{name}} (Connecting...)": "{{name}} (Connecting...)", "{{name}} (Waiting for video...)": "{{name}} (Waiting for video...)", "{{name}} is presenting": "{{name}} is presenting", @@ -14,6 +16,8 @@ "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Join call now<1>Or<2>Copy call link and join later", "<0>Oops, something's gone wrong.": "<0>Oops, something's gone wrong.", "<0>Submitting debug logs will help us track down the problem.": "<0>Submitting debug logs will help us track down the problem.", + "<0>Thanks for your feedback!": "<0>Thanks for your feedback!", + "<0>We'd love to hear your feedback so we can improve your experience.": "<0>We'd love to hear your feedback so we can improve your experience.", "<0>Why not finish by setting up a password to keep your account?<1>You'll be able to keep your name and set an avatar for use on future calls": "<0>Why not finish by setting up a password to keep your account?<1>You'll be able to keep your name and set an avatar for use on future calls", "Accept camera/microphone permissions to join the call.": "Accept camera/microphone permissions to join the call.", "Accept microphone permissions to join the call.": "Accept microphone permissions to join the call.", @@ -53,6 +57,7 @@ "Go": "Go", "Grid layout menu": "Grid layout menu", "Home": "Home", + "How did it go?": "How did it go?", "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.", "Include debug logs": "Include debug logs", "Incompatible versions": "Incompatible versions", diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index ed8ada35..fad315b1 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -29,6 +29,7 @@ import { MuteCameraTracker, MuteMicrophoneTracker, UndecryptableToDeviceEventTracker, + QualitySurveyEventTracker, } from "./PosthogEvents"; import { Config } from "../config/Config"; import { getUrlParams } from "../UrlParams"; @@ -431,4 +432,5 @@ export class PosthogAnalytics { public eventMuteMicrophone = new MuteMicrophoneTracker(); public eventMuteCamera = new MuteCameraTracker(); public eventUndecryptableToDevice = new UndecryptableToDeviceEventTracker(); + public eventQualitySurvey = new QualitySurveyEventTracker(); } diff --git a/src/analytics/PosthogEvents.ts b/src/analytics/PosthogEvents.ts index aa8aa329..f2fecb4e 100644 --- a/src/analytics/PosthogEvents.ts +++ b/src/analytics/PosthogEvents.ts @@ -163,3 +163,21 @@ export class UndecryptableToDeviceEventTracker { }); } } + +interface QualitySurveyEvent { + eventName: "QualitySurvey"; + callId: string; + feedbackText: string; + stars: number; +} + +export class QualitySurveyEventTracker { + track(callId: string, feedbackText: string, stars: number) { + PosthogAnalytics.instance.trackEvent({ + eventName: "QualitySurvey", + callId, + feedbackText, + stars, + }); + } +} diff --git a/src/icons/StarSelected.svg b/src/icons/StarSelected.svg new file mode 100644 index 00000000..69a8ce80 --- /dev/null +++ b/src/icons/StarSelected.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/icons/StarUnselected.svg b/src/icons/StarUnselected.svg new file mode 100644 index 00000000..be281947 --- /dev/null +++ b/src/icons/StarUnselected.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/input/FeedbackInput.module.css b/src/input/FeedbackInput.module.css new file mode 100644 index 00000000..75647939 --- /dev/null +++ b/src/input/FeedbackInput.module.css @@ -0,0 +1,23 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.feedback textarea { + height: 75px; + border-radius: 8px; +} +.feedback { + border-radius: 8px; +} diff --git a/src/input/StarRatingInput.module.css b/src/input/StarRatingInput.module.css new file mode 100644 index 00000000..08a65d11 --- /dev/null +++ b/src/input/StarRatingInput.module.css @@ -0,0 +1,41 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.starIcon { + cursor: pointer; +} + +.starRating { + display: flex; + justify-content: center; + flex: 1; +} + +.inputContainer { + display: inline-block; +} + +.hideElement { + border: 0; + clip-path: content-box; + height: 0px; + width: 0px; + margin: -1px; + overflow: hidden; + padding: 0; + width: 1px; + display: inline-block; +} diff --git a/src/input/StarRatingInput.tsx b/src/input/StarRatingInput.tsx new file mode 100644 index 00000000..34f01202 --- /dev/null +++ b/src/input/StarRatingInput.tsx @@ -0,0 +1,85 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; + +import styles from "./StarRatingInput.module.css"; +import { ReactComponent as StarSelected } from "../icons/StarSelected.svg"; +import { ReactComponent as StarUnselected } from "../icons/StarUnselected.svg"; + +interface Props { + starCount: number; + onChange: (stars: number) => void; + required?: boolean; +} + +export function StarRatingInput({ + starCount, + onChange, + required, +}: Props): JSX.Element { + const [rating, setRating] = useState(0); + const [hover, setHover] = useState(0); + const { t } = useTranslation(); + return ( +
+ {[...Array(starCount)].map((_star, index) => { + index += 1; + return ( +
setHover(index)} + onMouseLeave={() => setHover(rating)} + key={index} + > + { + setRating(index); + onChange(index); + }} + required + /> + + +
+ ); + })} +
+ ); +} diff --git a/src/room/CallEndedView.module.css b/src/room/CallEndedView.module.css index dcf11f04..fd9bad99 100644 --- a/src/room/CallEndedView.module.css +++ b/src/room/CallEndedView.module.css @@ -17,20 +17,31 @@ limitations under the License. .headline { text-align: center; margin-bottom: 60px; + white-space: pre; } .callEndedContent { text-align: center; - max-width: 360px; + max-width: 450px; +} +.callEndedContent p { + font-size: var(--font-size-subtitle); } - .callEndedContent h3 { margin-bottom: 32px; } .callEndedButton { + margin-top: 54px; + margin-left: 30px; + margin-right: 30px !important; +} + +.submitButton { width: 100%; margin-top: 54px; + margin-left: 30px; + margin-right: 30px !important; } .container { diff --git a/src/room/CallEndedView.tsx b/src/room/CallEndedView.tsx index 7543d669..e36d06c3 100644 --- a/src/room/CallEndedView.tsx +++ b/src/room/CallEndedView.tsx @@ -14,19 +14,130 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { FormEventHandler, useCallback, useState } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { Trans, useTranslation } from "react-i18next"; +import { useHistory } from "react-router-dom"; import styles from "./CallEndedView.module.css"; -import { LinkButton } from "../button"; +import feedbackStyle from "../input/FeedbackInput.module.css"; +import { Button, LinkButton } from "../button"; import { useProfile } from "../profile/useProfile"; -import { Subtitle, Body, Link, Headline } from "../typography/Typography"; +import { Body, Link, Headline } from "../typography/Typography"; import { Header, HeaderLogo, LeftNav, RightNav } from "../Header"; +import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; +import { FieldRow, InputField } from "../input/Input"; +import { StarRatingInput } from "../input/StarRatingInput"; -export function CallEndedView({ client }: { client: MatrixClient }) { +export function CallEndedView({ + client, + isPasswordlessUser, + endedCallId, +}: { + client: MatrixClient; + isPasswordlessUser: boolean; + endedCallId: string; +}) { const { t } = useTranslation(); + const history = useHistory(); + const { displayName } = useProfile(client); + const [surveySubmitted, setSurverySubmitted] = useState(false); + const [starRating, setStarRating] = useState(0); + const [submitting, setSubmitting] = useState(false); + const [submitDone, setSubmitDone] = useState(false); + const submitSurvery: FormEventHandler = useCallback( + (e) => { + e.preventDefault(); + const data = new FormData(e.target as HTMLFormElement); + const feedbackText = data.get("feedbackText") as string; + + PosthogAnalytics.instance.eventQualitySurvey.track( + endedCallId, + feedbackText, + starRating + ); + + setSubmitting(true); + + setTimeout(() => { + setSubmitDone(true); + + setTimeout(() => { + if (isPasswordlessUser) { + // setting this renders the callEndedView with the invitation to create an account + setSurverySubmitted(true); + } else { + // if the user already has an account immediately go back to the home screen + history.push("/"); + } + }, 1000); + }, 1000); + }, + [endedCallId, history, isPasswordlessUser, starRating] + ); + const createAccountDialog = isPasswordlessUser && ( +
+ +

Why not finish by setting up a password to keep your account?

+

+ You'll be able to keep your name and set an avatar for use on future + calls +

+
+ + {t("Create account")} + +
+ ); + + const qualitySurveyDialog = ( +
+ +

+ We'd love to hear your feedback so we can improve your experience. +

+
+
+ + + + + + {" "} + + {submitDone ? ( + +

Thanks for your feedback!

+
+ ) : ( + + )} +
+
+
+ ); return ( <> @@ -39,27 +150,19 @@ export function CallEndedView({ client }: { client: MatrixClient }) {
- {t("{{displayName}}, your call is now ended", { displayName })} + {surveySubmitted + ? t("{{displayName}}, your call has ended.", { + displayName, + }) + : t("{{displayName}}, your call has ended.", { + displayName, + }) + + "\n" + + t("How did it go?")} -
- - - Why not finish by setting up a password to keep your account? - - - You'll be able to keep your name and set an avatar for use on - future calls - - - - {t("Create account")} - -
+ {!surveySubmitted && PosthogAnalytics.instance.isEnabled() + ? qualitySurveyDialog + : createAccountDialog}
diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 722686ca..595bc3fd 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -203,7 +203,11 @@ export function GroupCallView({ widget.api.transport.send(ElementWidgetActions.HangupCall, {}); } - if (!isPasswordlessUser && !isEmbedded) { + if ( + !isPasswordlessUser && + !isEmbedded && + !PosthogAnalytics.instance.isEnabled() + ) { history.push("/"); } }, [groupCall, leave, isPasswordlessUser, isEmbedded, history]); @@ -268,8 +272,14 @@ export function GroupCallView({ ); } } else if (left) { - if (isPasswordlessUser) { - return ; + if (isPasswordlessUser || PosthogAnalytics.instance.isEnabled()) { + return ( + + ); } else { // If the user is a regular user, we'll have sent them back to the homepage, // so just sit here & do nothing: otherwise we would (briefly) mount the diff --git a/src/settings/FeedbackSettingsTab.tsx b/src/settings/FeedbackSettingsTab.tsx index da40678f..b57f0f78 100644 --- a/src/settings/FeedbackSettingsTab.tsx +++ b/src/settings/FeedbackSettingsTab.tsx @@ -23,6 +23,7 @@ import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { useSubmitRageshake, useRageshakeRequest } from "./submit-rageshake"; import { Body } from "../typography/Typography"; import styles from "../input/SelectInput.module.css"; +import feedbackStyles from "../input/FeedbackInput.module.css"; interface Props { roomId?: string; @@ -68,9 +69,11 @@ export function FeedbackSettingsTab({ roomId }: Props) {
From 8f8dd5f803d247451520aea6e58dc501c45ead35 Mon Sep 17 00:00:00 2001 From: Enrico Schwendig Date: Wed, 7 Jun 2023 16:40:47 +0200 Subject: [PATCH 272/286] Display active tracks in OTel metrics (#1085) * Add track, feed and transceiver spans under call span --- package.json | 2 +- src/otel/OTelCall.ts | 78 +++++++++++++++++++ src/otel/OTelCallAbstractMediaStreamSpan.ts | 62 +++++++++++++++ src/otel/OTelCallFeedMediaStreamSpan.ts | 57 ++++++++++++++ src/otel/OTelCallMediaStreamTrackSpan.ts | 62 +++++++++++++++ .../OTelCallTransceiverMediaStreamSpan.ts | 54 +++++++++++++ src/otel/OTelGroupCallMembership.ts | 33 +++++++- src/room/useGroupCall.ts | 16 ++++ yarn.lock | 6 +- 9 files changed, 365 insertions(+), 5 deletions(-) create mode 100644 src/otel/OTelCallAbstractMediaStreamSpan.ts create mode 100644 src/otel/OTelCallFeedMediaStreamSpan.ts create mode 100644 src/otel/OTelCallMediaStreamTrackSpan.ts create mode 100644 src/otel/OTelCallTransceiverMediaStreamSpan.ts diff --git a/package.json b/package.json index 0d5325b5..9923b6b3 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#e70a1a1effe59e6754f9a10cc2df8eef81638c7d", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#3cfad3cdeb7b19b8e0e7015784efd803cb9542f1", "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/src/otel/OTelCall.ts b/src/otel/OTelCall.ts index 79cc38d5..eae1f347 100644 --- a/src/otel/OTelCall.ts +++ b/src/otel/OTelCall.ts @@ -17,13 +17,33 @@ limitations under the License. import { Span } from "@opentelemetry/api"; import { MatrixCall } from "matrix-js-sdk"; import { CallEvent } from "matrix-js-sdk/src/webrtc/call"; +import { + TransceiverStats, + CallFeedStats, +} from "matrix-js-sdk/src/webrtc/stats/statsReport"; import { ObjectFlattener } from "./ObjectFlattener"; +import { ElementCallOpenTelemetry } from "./otel"; +import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan"; +import { OTelCallTransceiverMediaStreamSpan } from "./OTelCallTransceiverMediaStreamSpan"; +import { OTelCallFeedMediaStreamSpan } from "./OTelCallFeedMediaStreamSpan"; + +type StreamId = string; +type MID = string; /** * Tracks an individual call within a group call, either to a full-mesh peer or a focus */ export class OTelCall { + private readonly trackFeedSpan = new Map< + StreamId, + OTelCallAbstractMediaStreamSpan + >(); + private readonly trackTransceiverSpan = new Map< + MID, + OTelCallAbstractMediaStreamSpan + >(); + constructor( public userId: string, public deviceId: string, @@ -116,4 +136,62 @@ export class OTelCall { this.span.addEvent("matrix.call.iceCandidateError", flatObject); }; + + public onCallFeedStats(callFeeds: CallFeedStats[]): void { + let prvFeeds: StreamId[] = [...this.trackFeedSpan.keys()]; + + callFeeds.forEach((feed) => { + if (!this.trackFeedSpan.has(feed.stream)) { + this.trackFeedSpan.set( + feed.stream, + new OTelCallFeedMediaStreamSpan( + ElementCallOpenTelemetry.instance, + this.span, + feed + ) + ); + } + this.trackFeedSpan.get(feed.stream)?.update(feed); + prvFeeds = prvFeeds.filter((prvStreamId) => prvStreamId !== feed.stream); + }); + + prvFeeds.forEach((prvStreamId) => { + this.trackFeedSpan.get(prvStreamId)?.end(); + this.trackFeedSpan.delete(prvStreamId); + }); + } + + public onTransceiverStats(transceiverStats: TransceiverStats[]): void { + let prvTransSpan: MID[] = [...this.trackTransceiverSpan.keys()]; + + transceiverStats.forEach((transStats) => { + if (!this.trackTransceiverSpan.has(transStats.mid)) { + this.trackTransceiverSpan.set( + transStats.mid, + new OTelCallTransceiverMediaStreamSpan( + ElementCallOpenTelemetry.instance, + this.span, + transStats + ) + ); + } + this.trackTransceiverSpan.get(transStats.mid)?.update(transStats); + prvTransSpan = prvTransSpan.filter( + (prvStreamId) => prvStreamId !== transStats.mid + ); + }); + + prvTransSpan.forEach((prvMID) => { + this.trackTransceiverSpan.get(prvMID)?.end(); + this.trackTransceiverSpan.delete(prvMID); + }); + } + + public end(): void { + this.trackFeedSpan.forEach((feedSpan) => feedSpan.end()); + this.trackTransceiverSpan.forEach((transceiverSpan) => + transceiverSpan.end() + ); + this.span.end(); + } } diff --git a/src/otel/OTelCallAbstractMediaStreamSpan.ts b/src/otel/OTelCallAbstractMediaStreamSpan.ts new file mode 100644 index 00000000..aa77051d --- /dev/null +++ b/src/otel/OTelCallAbstractMediaStreamSpan.ts @@ -0,0 +1,62 @@ +import opentelemetry, { Span } from "@opentelemetry/api"; +import { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport"; + +import { ElementCallOpenTelemetry } from "./otel"; +import { OTelCallMediaStreamTrackSpan } from "./OTelCallMediaStreamTrackSpan"; + +type TrackId = string; + +export abstract class OTelCallAbstractMediaStreamSpan { + protected readonly trackSpans = new Map< + TrackId, + OTelCallMediaStreamTrackSpan + >(); + public readonly span; + + public constructor( + readonly oTel: ElementCallOpenTelemetry, + readonly callSpan: Span, + protected readonly type: string + ) { + const ctx = opentelemetry.trace.setSpan( + opentelemetry.context.active(), + callSpan + ); + const options = { + links: [ + { + context: callSpan.spanContext(), + }, + ], + }; + this.span = oTel.tracer.startSpan(this.type, options, ctx); + } + + protected upsertTrackSpans(tracks: TrackStats[]) { + let prvTracks: TrackId[] = [...this.trackSpans.keys()]; + tracks.forEach((t) => { + if (!this.trackSpans.has(t.id)) { + this.trackSpans.set( + t.id, + new OTelCallMediaStreamTrackSpan(this.oTel, this.span, t) + ); + } + this.trackSpans.get(t.id)?.update(t); + prvTracks = prvTracks.filter((prvTrackId) => prvTrackId !== t.id); + }); + + prvTracks.forEach((prvTrackId) => { + this.trackSpans.get(prvTrackId)?.end(); + this.trackSpans.delete(prvTrackId); + }); + } + + public abstract update(data: Object): void; + + public end(): void { + this.trackSpans.forEach((tSpan) => { + tSpan.end(); + }); + this.span.end(); + } +} diff --git a/src/otel/OTelCallFeedMediaStreamSpan.ts b/src/otel/OTelCallFeedMediaStreamSpan.ts new file mode 100644 index 00000000..6023fa65 --- /dev/null +++ b/src/otel/OTelCallFeedMediaStreamSpan.ts @@ -0,0 +1,57 @@ +import { Span } from "@opentelemetry/api"; +import { + CallFeedStats, + TrackStats, +} from "matrix-js-sdk/src/webrtc/stats/statsReport"; + +import { ElementCallOpenTelemetry } from "./otel"; +import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan"; + +export class OTelCallFeedMediaStreamSpan extends OTelCallAbstractMediaStreamSpan { + private readonly prev: { isAudioMuted: boolean; isVideoMuted: boolean }; + + constructor( + readonly oTel: ElementCallOpenTelemetry, + readonly callSpan: Span, + callFeed: CallFeedStats + ) { + const postFix = + callFeed.type === "local" && callFeed.prefix === "from-call-feed" + ? "(clone)" + : ""; + super(oTel, callSpan, `matrix.call.feed.${callFeed.type}${postFix}`); + this.span.setAttribute("feed.streamId", callFeed.stream); + this.span.setAttribute("feed.type", callFeed.type); + this.span.setAttribute("feed.readFrom", callFeed.prefix); + this.span.setAttribute("feed.purpose", callFeed.purpose); + this.prev = { + isAudioMuted: callFeed.isAudioMuted, + isVideoMuted: callFeed.isVideoMuted, + }; + this.span.addEvent("matrix.call.feed.initState", this.prev); + } + + public update(callFeed: CallFeedStats): void { + if (this.prev.isAudioMuted !== callFeed.isAudioMuted) { + this.span.addEvent("matrix.call.feed.audioMuted", { + isAudioMuted: callFeed.isAudioMuted, + }); + this.prev.isAudioMuted = callFeed.isAudioMuted; + } + if (this.prev.isVideoMuted !== callFeed.isVideoMuted) { + this.span.addEvent("matrix.call.feed.isVideoMuted", { + isVideoMuted: callFeed.isVideoMuted, + }); + this.prev.isVideoMuted = callFeed.isVideoMuted; + } + + const trackStats: TrackStats[] = []; + if (callFeed.video) { + trackStats.push(callFeed.video); + } + if (callFeed.audio) { + trackStats.push(callFeed.audio); + } + this.upsertTrackSpans(trackStats); + } +} diff --git a/src/otel/OTelCallMediaStreamTrackSpan.ts b/src/otel/OTelCallMediaStreamTrackSpan.ts new file mode 100644 index 00000000..935e22fc --- /dev/null +++ b/src/otel/OTelCallMediaStreamTrackSpan.ts @@ -0,0 +1,62 @@ +import { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport"; +import opentelemetry, { Span } from "@opentelemetry/api"; + +import { ElementCallOpenTelemetry } from "./otel"; + +export class OTelCallMediaStreamTrackSpan { + private readonly span: Span; + private prev: TrackStats; + + public constructor( + readonly oTel: ElementCallOpenTelemetry, + readonly streamSpan: Span, + data: TrackStats + ) { + const ctx = opentelemetry.trace.setSpan( + opentelemetry.context.active(), + streamSpan + ); + const options = { + links: [ + { + context: streamSpan.spanContext(), + }, + ], + }; + const type = `matrix.call.track.${data.label}.${data.kind}`; + this.span = oTel.tracer.startSpan(type, options, ctx); + this.span.setAttribute("track.trackId", data.id); + this.span.setAttribute("track.kind", data.kind); + this.span.setAttribute("track.constrainDeviceId", data.constrainDeviceId); + this.span.setAttribute("track.settingDeviceId", data.settingDeviceId); + this.span.setAttribute("track.label", data.label); + + this.span.addEvent("matrix.call.track.initState", { + readyState: data.readyState, + muted: data.muted, + enabled: data.enabled, + }); + this.prev = data; + } + + public update(data: TrackStats): void { + if (this.prev.muted !== data.muted) { + this.span.addEvent("matrix.call.track.muted", { muted: data.muted }); + } + if (this.prev.enabled !== data.enabled) { + this.span.addEvent("matrix.call.track.enabled", { + enabled: data.enabled, + }); + } + if (this.prev.readyState !== data.readyState) { + this.span.addEvent("matrix.call.track.readyState", { + readyState: data.readyState, + }); + } + this.prev = data; + } + + public end(): void { + this.span.end(); + } +} diff --git a/src/otel/OTelCallTransceiverMediaStreamSpan.ts b/src/otel/OTelCallTransceiverMediaStreamSpan.ts new file mode 100644 index 00000000..97006cd8 --- /dev/null +++ b/src/otel/OTelCallTransceiverMediaStreamSpan.ts @@ -0,0 +1,54 @@ +import { Span } from "@opentelemetry/api"; +import { + TrackStats, + TransceiverStats, +} from "matrix-js-sdk/src/webrtc/stats/statsReport"; + +import { ElementCallOpenTelemetry } from "./otel"; +import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan"; + +export class OTelCallTransceiverMediaStreamSpan extends OTelCallAbstractMediaStreamSpan { + private readonly prev: { + direction: string; + currentDirection: string; + }; + + constructor( + readonly oTel: ElementCallOpenTelemetry, + readonly callSpan: Span, + stats: TransceiverStats + ) { + super(oTel, callSpan, `matrix.call.transceiver.${stats.mid}`); + this.span.setAttribute("transceiver.mid", stats.mid); + + this.prev = { + direction: stats.direction, + currentDirection: stats.currentDirection, + }; + this.span.addEvent("matrix.call.transceiver.initState", this.prev); + } + + public update(stats: TransceiverStats): void { + if (this.prev.currentDirection !== stats.currentDirection) { + this.span.addEvent("matrix.call.transceiver.currentDirection", { + currentDirection: stats.currentDirection, + }); + this.prev.currentDirection = stats.currentDirection; + } + if (this.prev.direction !== stats.direction) { + this.span.addEvent("matrix.call.transceiver.direction", { + direction: stats.direction, + }); + this.prev.direction = stats.direction; + } + + const trackStats: TrackStats[] = []; + if (stats.sender) { + trackStats.push(stats.sender); + } + if (stats.receiver) { + trackStats.push(stats.receiver); + } + this.upsertTrackSpans(trackStats); + } +} diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index a8d93b6a..fa1e164e 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -38,6 +38,7 @@ import { ConnectionStatsReport, ByteSentStatsReport, SummaryStatsReport, + CallFeedReport, } from "matrix-js-sdk/src/webrtc/stats/statsReport"; import { setSpan } from "@opentelemetry/api/build/esm/trace/context-utils"; @@ -174,7 +175,7 @@ export class OTelGroupCallMembership { userCalls.get(callTrackingInfo.deviceId).callId !== callTrackingInfo.call.callId ) { - callTrackingInfo.span.end(); + callTrackingInfo.end(); this.callsByCallId.delete(callTrackingInfo.call.callId); } } @@ -330,6 +331,35 @@ export class OTelGroupCallMembership { }); } + public onCallFeedStatsReport(report: GroupCallStatsReport) { + if (!ElementCallOpenTelemetry.instance) return; + let call: OTelCall | undefined; + const callId = report.report?.callId; + + if (callId) { + call = this.callsByCallId.get(callId); + } + + if (!call) { + this.callMembershipSpan?.addEvent( + OTelStatsReportType.CallFeedReport + "_unknown_callId", + { + "call.callId": callId, + "call.opponentMemberId": report.report?.opponentMemberId + ? report.report?.opponentMemberId + : "unknown", + } + ); + logger.error( + `Received ${OTelStatsReportType.CallFeedReport} with unknown call ID: ${callId}` + ); + return; + } else { + call.onCallFeedStats(report.report.callFeeds); + call.onTransceiverStats(report.report.transceiver); + } + } + public onConnectionStatsReport( statsReport: GroupCallStatsReport ) { @@ -440,4 +470,5 @@ enum OTelStatsReportType { ConnectionReport = "matrix.call.stats.connection", ByteSentReport = "matrix.call.stats.byteSent", SummaryReport = "matrix.stats.summary", + CallFeedReport = "matrix.stats.call_feed", } diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 0126e1cc..153bb186 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -34,6 +34,7 @@ import { ByteSentStatsReport, ConnectionStatsReport, SummaryStatsReport, + CallFeedReport, } from "matrix-js-sdk/src/webrtc/stats/statsReport"; import { usePageUnload } from "./usePageUnload"; @@ -363,6 +364,12 @@ export function useGroupCall( groupCallOTelMembership?.onSummaryStatsReport(report); } + function onCallFeedStatsReport( + report: GroupCallStatsReport + ): void { + groupCallOTelMembership?.onCallFeedStatsReport(report); + } + groupCall.on(GroupCallEvent.GroupCallStateChanged, onGroupCallStateChanged); groupCall.on(GroupCallEvent.UserMediaFeedsChanged, onUserMediaFeedsChanged); groupCall.on( @@ -387,6 +394,11 @@ export function useGroupCall( onByteSentStatsReport ); groupCall.on(GroupCallStatsReportEvent.SummaryStats, onSummaryStatsReport); + groupCall.on( + GroupCallStatsReportEvent.CallFeedStats, + onCallFeedStatsReport + ); + groupCall.room.currentState.on( RoomStateEvent.Update, checkForParallelCalls @@ -450,6 +462,10 @@ export function useGroupCall( GroupCallStatsReportEvent.SummaryStats, onSummaryStatsReport ); + groupCall.removeListener( + GroupCallStatsReportEvent.CallFeedStats, + onCallFeedStatsReport + ); groupCall.room.currentState.off( RoomStateEvent.Update, checkForParallelCalls diff --git a/yarn.lock b/yarn.lock index c9c9a44e..1b5602a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10557,9 +10557,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#e70a1a1effe59e6754f9a10cc2df8eef81638c7d": - version "25.1.1" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e70a1a1effe59e6754f9a10cc2df8eef81638c7d" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#3cfad3cdeb7b19b8e0e7015784efd803cb9542f1": + version "26.0.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/3cfad3cdeb7b19b8e0e7015784efd803cb9542f1" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.9" From efc25fd4ec7cc127c02e79d1a682c28d9a986890 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Wed, 7 Jun 2023 17:12:24 +0200 Subject: [PATCH 273/286] hotfix Quality survey button interaction (#1091) Signed-off-by: Timo K --- src/input/StarRatingInput.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/input/StarRatingInput.tsx b/src/input/StarRatingInput.tsx index 34f01202..53620782 100644 --- a/src/input/StarRatingInput.tsx +++ b/src/input/StarRatingInput.tsx @@ -60,7 +60,7 @@ export function StarRatingInput({