From 1c7110e4c9d5511dd130342a1c35237d5a427060 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 6 Jul 2023 00:37:16 -0400 Subject: [PATCH 01/76] Improve the double click detection So that it doesn't cause unnecessary renders, and interprets a series of three clicks as a double-click followed by a single click, rather than two overlapping double-clicks. (That behavior felt odd to me during testing of NewVideoGrid, which is why I picked up this small change.) --- src/video-grid/NewVideoGrid.tsx | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index 0361e971..57be72fa 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -60,6 +60,11 @@ interface DragState { cursorY: number; } +interface TapData { + tileId: string; + ts: number; +} + interface SlotProps { style?: CSSProperties; } @@ -257,10 +262,7 @@ export function NewVideoGrid({ ); }; - const [lastTappedTileId, setLastTappedTileId] = useState( - undefined - ); - const [lastTapTime, setLastTapTime] = useState(0); + const lastTap = useRef(null); // Callback for useDrag. We could call useDrag here, but the default // pattern of spreading {...bind()} across the children to bind the gesture @@ -279,12 +281,15 @@ export function NewVideoGrid({ if (tap) { const now = Date.now(); - if (tileId === lastTappedTileId && now - lastTapTime < 500) { + if ( + tileId === lastTap.current?.tileId && + now - lastTap.current.ts < 500 + ) { toggleFocus?.(items.find((i) => i.id === tileId)!); + lastTap.current = null; + } else { + lastTap.current = { tileId, ts: now }; } - - setLastTappedTileId(tileId); - setLastTapTime(now); } else { const tileController = springRef.current.find( (c) => (c.item as Tile).item.id === tileId From 3ac98c886520953eb4c6188b7efaab5f16aba0ee Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 6 Jul 2023 00:43:17 -0400 Subject: [PATCH 02/76] Make the expand and collapse interactions inverses of one another For the most part, at least. If the edge cases where they differ still feel weird, I can iterate on this further. The diff is unfortunately a bit impenetrable, because I had to change both the fillGaps and cycleTileSize core algorithms used by the big grid layout. But: the main change of significance is the addition of a function vacateArea, which clears out an area within the grid in a specific way that mirrors the motion performed by fillGaps. --- src/array-utils.ts | 14 +- src/video-grid/BigGrid.tsx | 741 ++++++++++++++++++++------------ test/video-grid/BigGrid-test.ts | 109 +++-- 3 files changed, 543 insertions(+), 321 deletions(-) diff --git a/src/array-utils.ts b/src/array-utils.ts index cf37e4cf..6aeb1818 100644 --- a/src/array-utils.ts +++ b/src/array-utils.ts @@ -22,10 +22,10 @@ limitations under the License. // Array.prototype.findLastIndex export function findLastIndex( array: T[], - predicate: (item: T) => boolean + predicate: (item: T, index: number) => boolean ): number | null { for (let i = array.length - 1; i >= 0; i--) { - if (predicate(array[i])) return i; + if (predicate(array[i], i)) return i; } return null; @@ -34,5 +34,11 @@ export function findLastIndex( /** * Counts the number of elements in an array that satsify the given predicate. */ -export const count = (array: T[], predicate: (item: T) => boolean): number => - array.reduce((acc, item) => (predicate(item) ? acc + 1 : acc), 0); +export const count = ( + array: T[], + predicate: (item: T, index: number) => boolean +): number => + array.reduce( + (acc, item, index) => (predicate(item, index) ? acc + 1 : acc), + 0 + ); diff --git a/src/video-grid/BigGrid.tsx b/src/video-grid/BigGrid.tsx index c2cba0a7..7eaaed0b 100644 --- a/src/video-grid/BigGrid.tsx +++ b/src/video-grid/BigGrid.tsx @@ -17,6 +17,7 @@ limitations under the License. import TinyQueue from "tinyqueue"; import { RectReadOnly } from "react-use-measure"; import { FC, memo, ReactNode } from "react"; +import { zip } from "lodash"; import { TileDescriptor } from "./VideoGrid"; import { Slot } from "./NewVideoGrid"; @@ -46,20 +47,19 @@ interface Cell { readonly rows: number; } -export interface BigGridState { - readonly columns: number; - /** - * The cells of the grid, in left-to-right top-to-bottom order. - * undefined = empty. - */ - readonly cells: (Cell | undefined)[]; -} - -interface MutableBigGridState { +export interface Grid { columns: number; /** * The cells of the grid, in left-to-right top-to-bottom order. - * undefined = empty. + */ + cells: Cell[]; +} + +interface SparseGrid { + columns: number; + /** + * The cells of the grid, in left-to-right top-to-bottom order. + * undefined = a gap in the grid. */ cells: (Cell | undefined)[]; } @@ -67,12 +67,19 @@ interface MutableBigGridState { /** * 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. + * @param dest The destination index. + * @param avoid A predicate defining the cells that paths should avoid going + * through. * @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. + * to to reach the destination, or null if it is the destination or otherwise + * immovable. */ -export function getPaths(dest: number, g: BigGridState): (number | null)[] { +export function getPaths( + g: SparseGrid, + dest: number, + avoid: (cell: number) => boolean = () => false +): (number | null)[] { const destRow = row(dest, g); const destColumn = column(dest, g); @@ -80,18 +87,11 @@ export function getPaths(dest: number, g: BigGridState): (number | null)[] { const distances = new Array(dest + 1).fill(Infinity); distances[dest] = 0; - const edges = new Array(dest).fill(undefined); + const edges = new Array(dest).fill(null); 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); - // 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); - + const visit = (curr: number, via: number, distanceVia: number) => { if (distanceVia < distances[curr]) { distances[curr] = distanceVia; edges[curr] = via; @@ -101,33 +101,43 @@ export function getPaths(dest: number, g: BigGridState): (number | null)[] { while (heap.length > 0) { const via = heap.pop()!; - 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 === destRow ? destColumn : g.columns - 1)) - visit(via + 1, via); - if ( - viaRow < destRow - 1 || - (viaRow === destRow - 1 && viaColumn <= destColumn) - ) - visit(via + g.columns, via); + if (!avoid(via)) { + const viaRow = row(via, g); + const viaColumn = column(via, g); + const viaCell = g.cells[via]; + const viaLargeTile = viaCell !== undefined && !is1By1(viaCell); + // 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); + + // Visit each neighbor + if (viaRow > 0) visit(via - g.columns, via, distanceVia); + if (viaColumn > 0) visit(via - 1, via, distanceVia); + if (viaColumn < (viaRow === destRow ? destColumn : g.columns - 1)) + visit(via + 1, via, distanceVia); + if ( + viaRow < destRow - 1 || + (viaRow === destRow - 1 && viaColumn <= destColumn) + ) + visit(via + g.columns, via, distanceVia); + } } // The heap is empty, so we've generated all paths - return edges as (number | null)[]; + return edges; } -const findLast1By1Index = (g: BigGridState): number | null => - findLastIndex(g.cells, (c) => c?.rows === 1 && c?.columns === 1); +const is1By1 = (c: Cell) => c.columns === 1 && c.rows === 1; -export function row(index: number, g: BigGridState): number { +const findLast1By1Index = (g: SparseGrid): number | null => + findLastIndex(g.cells, (c) => c !== undefined && is1By1(c)); + +export function row(index: number, g: SparseGrid): number { return Math.floor(index / g.columns); } -export function column(index: number, g: BigGridState): number { +export function column(index: number, g: SparseGrid): number { return ((index % g.columns) + g.columns) % g.columns; } @@ -135,7 +145,7 @@ function inArea( index: number, start: number, end: number, - g: BigGridState + g: SparseGrid ): boolean { const indexColumn = column(index, g); const indexRow = row(index, g); @@ -150,7 +160,7 @@ function inArea( function* cellsInArea( start: number, end: number, - g: BigGridState + g: SparseGrid ): Generator { const startColumn = column(start, g); const endColumn = column(end, g); @@ -165,20 +175,20 @@ function* cellsInArea( yield i; } -export function forEachCellInArea( +export function forEachCellInArea( start: number, end: number, - g: BigGridState, - fn: (c: Cell | undefined, i: number) => void + g: G, + fn: (c: G["cells"][0], i: number) => void ): void { for (const i of cellsInArea(start, end, g)) fn(g.cells[i], i); } -function allCellsInArea( +function allCellsInArea( start: number, end: number, - g: BigGridState, - fn: (c: Cell | undefined, i: number) => boolean + g: G, + fn: (c: G["cells"][0], i: number) => boolean ): boolean { for (const i of cellsInArea(start, end, g)) { if (!fn(g.cells[i], i)) return false; @@ -187,14 +197,30 @@ function allCellsInArea( return true; } +/** + * Counts the number of cells in the area that satsify the given predicate. + */ +function countCellsInArea( + start: number, + end: number, + g: G, + predicate: (c: G["cells"][0], i: number) => boolean +): number { + let count = 0; + for (const i of cellsInArea(start, end, g)) { + if (predicate(g.cells[i], i)) count++; + } + return count; +} + const areaEnd = ( start: number, columns: number, rows: number, - g: BigGridState + g: SparseGrid ): number => start + columns - 1 + g.columns * (rows - 1); -const cloneGrid = (g: BigGridState): BigGridState => ({ +const cloneGrid = (g: G): G => ({ ...g, cells: [...g.cells], }); @@ -203,62 +229,43 @@ const cloneGrid = (g: BigGridState): BigGridState => ({ * Gets the index of the next gap in the grid that should be backfilled by 1×1 * tiles. */ -function getNextGap(g: BigGridState): number | null { +function getNextGap( + g: SparseGrid, + ignoreGap: (cell: number) => boolean +): 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 = + const j = i; /* (row(i, g) === row(last1By1Index, g) ? last1By1Index : (row(i, g) + 1) * g.columns) - 1 - - column(i, g); + column(i, g);*/ - if (g.cells[j] === undefined) return j; + if (!ignoreGap(j) && g.cells[j] === undefined) return j; } return null; } -/** - * Gets the index of the origin of the tile to which the given cell belongs. - */ -function getOrigin(g: BigGridState, index: number): number { - const initialColumn = column(index, g); - - for ( - let i = index; - i >= 0; - i = column(i, g) === 0 ? i - g.columns + initialColumn : i - 1 - ) { - const cell = g.cells[i]; - if ( - cell !== undefined && - cell.origin && - inArea(index, i, areaEnd(i, cell.columns, cell.rows, g), g) - ) - return i; - } - - throw new Error("Tile is broken"); -} - /** * Moves the tile at index "from" over to index "to", displacing other tiles * along the way. * Precondition: the destination area must consist of only 1×1 tiles. */ -function moveTileUnchecked(g: BigGridState, from: number, to: number) { +function moveTileUnchecked(g: SparseGrid, from: number, to: number) { const tile = g.cells[from]!; const fromEnd = areaEnd(from, tile.columns, tile.rows, g); const toEnd = areaEnd(to, tile.columns, tile.rows, g); const displacedTiles: Cell[] = []; forEachCellInArea(to, toEnd, g, (c, i) => { - if (c !== undefined && !inArea(i, from, fromEnd, g)) displacedTiles.push(c); + if (c !== undefined && !inArea(i, from, fromEnd, g)) + displacedTiles.push(c!); }); const movingCells: Cell[] = []; @@ -284,11 +291,11 @@ function moveTileUnchecked(g: BigGridState, from: number, to: number) { /** * Moves the tile at index "from" over to index "to", if there is space. */ -export function moveTile( - g: BigGridState, +export function moveTile( + g: G, from: number, to: number -): BigGridState { +): G { const tile = g.cells[from]!; if ( @@ -303,9 +310,7 @@ export function moveTile( // 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); + c === undefined || is1By1(c) || inArea(i, from, fromEnd, g); if (allCellsInArea(to, toEnd, g, displaceable)) { // The target space is free; move @@ -320,62 +325,236 @@ export function moveTile( } /** - * Attempts to push a tile upwards by one row, displacing 1×1 tiles and shifting - * enlarged tiles around when necessary. - * @returns Whether the tile was actually pushed + * Attempts to push a tile upwards by a number of rows, displacing 1×1 tiles. + * @returns The number of rows the tile was successfully pushed (may be less + * than requested if there are obstacles blocking movement). */ -function pushTileUp(g: BigGridState, from: number): boolean { +function pushTileUp( + g: SparseGrid, + from: number, + rows: number, + avoid: (cell: number) => boolean = () => false +): number { const tile = g.cells[from]!; - // TODO: pushing large tiles sideways might be more successful in some - // situations - const cellsAboveAreDisplacable = - from - g.columns >= 0 && - allCellsInArea( - from - g.columns, - from - g.columns + tile.columns - 1, - g, - (c, i) => - c === undefined || - (c.columns === 1 && c.rows === 1) || - pushTileUp(g, getOrigin(g, i)) - ); + for (let tryRows = rows; tryRows > 0; tryRows--) { + const to = from - tryRows * g.columns; + const toEnd = areaEnd(to, tile.columns, tile.rows, g); - if (cellsAboveAreDisplacable) { - moveTileUnchecked(g, from, from - g.columns); - return true; - } else { - return false; + const cellsAboveAreDisplacable = + from - g.columns >= 0 && + allCellsInArea( + to, + Math.min(from - g.columns + tile.columns - 1, toEnd), + g, + (c, i) => (c === undefined || is1By1(c)) && !avoid(i) + ); + + if (cellsAboveAreDisplacable) { + moveTileUnchecked(g, from, to); + return tryRows; + } } + + return 0; +} + +function trimTrailingGaps(g: SparseGrid) { + // Shrink the array to remove trailing gaps + const newLength = (findLastIndex(g.cells, (c) => c !== undefined) ?? -1) + 1; + if (newLength !== g.cells.length) g.cells = g.cells.slice(0, newLength); +} + +/** + * Determines whether the given area is sufficiently clear of obstacles for + * vacateArea to work. + */ +function canVacateArea(g: SparseGrid, start: number, end: number): boolean { + const newCellCount = countCellsInArea(start, end, g, (c) => c !== undefined); + const newFullRows = Math.floor(newCellCount / g.columns); + return allCellsInArea( + start, + end - newFullRows * g.columns, + g, + (c) => c === undefined || is1By1(c) + ); +} + +/** + * Clears away all the tiles in a given area by pushing them elsewhere. + * Precondition: the area must first be checked with canVacateArea, and the only + * gaps in the given grid must lie either within the area being cleared, or + * after the last 1×1 tile. + */ +function vacateArea(g: SparseGrid, start: number, end: number): SparseGrid { + const newCellCount = countCellsInArea( + start, + end, + g, + (c, i) => c !== undefined || i >= g.cells.length + ); + const newFullRows = Math.floor(newCellCount / g.columns); + const endRow = row(end, g); + + // To avoid subverting users' expectations, this operation should be the exact + // inverse of fillGaps. We do this by reverse-engineering a grid G with the + // area cleared out and structured such that fillGaps(G) = g. + + // A grid that will have the same structure as the final result, but be filled + // with fake data + const outputStructure: SparseGrid = { + columns: g.columns, + cells: new Array(g.cells.length + newCellCount), + }; + + // The first step in populating outputStructure is to copy over all the large + // tiles, pushing those tiles downwards that fillGaps would push upwards + g.cells.forEach((cell, fromStart) => { + if (cell?.origin && !is1By1(cell)) { + const fromEnd = areaEnd(fromStart, cell.columns, cell.rows, g); + const offset = + row(fromStart, g) + newFullRows > endRow ? newFullRows * g.columns : 0; + forEachCellInArea(fromStart, fromEnd, g, (c, i) => { + outputStructure.cells[i + offset] = c; + }); + } + }); + + // Then, we need to fill it in with the same number of 1×1 tiles as appear in + // the input + const oneByOneTileCount = count(g.cells, (c) => c !== undefined && is1By1(c)); + let oneByOneTilesDistributed = 0; + + for (let i = 0; i < outputStructure.cells.length; i++) { + if (outputStructure.cells[i] === undefined) { + if (inArea(i, start, end, g)) { + // Leave the requested area clear + outputStructure.cells[i] = undefined; + } else if (oneByOneTilesDistributed < oneByOneTileCount) { + outputStructure.cells[i] = { + // Fake data because we only care about the grid's structure + item: {} as unknown as TileDescriptor, + origin: true, + columns: 1, + rows: 1, + }; + oneByOneTilesDistributed++; + } + } + } + + // Lastly, handle the edge case where there were gaps in the input after the + // last 1×1 tile by resizing the cells array to delete these gaps + trimTrailingGaps(outputStructure); + + // outputStructure is now fully populated, and so running fillGaps on it + // should produce a grid with the same structure as the input + const inputStructure = fillGaps( + outputStructure, + false, + (i) => inArea(i, start, end, g) && g.cells[i] === undefined + ); + + // We exploit the fact that g and inputStructure have the same structure to + // create a mapping between cells in the structure grids and cells in g + const structureMapping = new Map(zip(inputStructure.cells, g.cells)); + + // And finally, we can use that mapping to swap the fake data in + // outputStructure with the real thing + return { + columns: g.columns, + cells: outputStructure.cells.map((placeholder) => + structureMapping.get(placeholder) + ), + }; } /** * Backfill any gaps in the grid. */ -export function fillGaps(g: BigGridState): BigGridState { - const result = cloneGrid(g) as MutableBigGridState; +export function fillGaps( + g: SparseGrid, + packLargeTiles?: true, + ignoreGap?: () => false +): Grid; +export function fillGaps( + g: SparseGrid, + packLargeTiles?: boolean, + ignoreGap?: (cell: number) => boolean +): SparseGrid; +export function fillGaps( + g: SparseGrid, + packLargeTiles = true, + ignoreGap: (cell: number) => boolean = () => false +): SparseGrid { + const lastGap = findLastIndex( + g.cells, + (c, i) => c === undefined && !ignoreGap(i) + ); + if (lastGap === null) return g; // There are no gaps to fill + const lastGapRow = row(lastGap, g); - // This will hopefully be the size of the grid after we're done here, assuming - // that we can pack the large tiles tightly enough - const idealLength = count(result.cells, (c) => c !== undefined); + const result = cloneGrid(g); - // Step 1: Take any large tiles hanging off the bottom of the grid, and push - // them upwards - for (let i = result.cells.length - 1; i >= idealLength; i--) { - const cell = result.cells[i]; - if (cell !== undefined && (cell.columns > 1 || cell.rows > 1)) { - const originIndex = - i - (cell.columns - 1) - result.columns * (cell.rows - 1); - // If it's not possible to pack the large tiles any tighter, give up - if (!pushTileUp(result, originIndex)) break; + // This will be the size of the grid after we're done here (assuming we're + // allowed to pack the large tiles into the rest of the grid as necessary) + let idealLength = count( + result.cells, + (c, i) => c !== undefined || ignoreGap(i) + ); + const fullRowsRemoved = Math.floor( + (g.cells.length - idealLength) / g.columns + ); + + // Step 1: Push all large tiles below the last gap upwards, so that they move + // roughly the same distance that we're expecting 1×1 tiles to move + if (fullRowsRemoved > 0) { + for ( + let i = (lastGapRow + 1) * result.columns; + i < result.cells.length; + i++ + ) { + const cell = result.cells[i]; + if (cell?.origin && !is1By1(cell)) + pushTileUp(result, i, fullRowsRemoved, ignoreGap); } } - // Step 2: Fill all 1×1 gaps - let gap = getNextGap(result); + // Step 2: Deal with any large tiles that are still hanging off the bottom + if (packLargeTiles) { + for (let i = result.cells.length - 1; i >= idealLength; i--) { + const cell = result.cells[i]; + if (cell !== undefined && !is1By1(cell)) { + // First, try to just push it upwards a bit more + const originIndex = + i - (cell.columns - 1) - result.columns * (cell.rows - 1); + const pushed = pushTileUp(result, originIndex, 1, ignoreGap) === 1; + + // If that failed, collapse the tile to 1×1 so it can be dealt with in + // step 3 + if (!pushed) { + const collapsedTile: Cell = { + item: cell.item, + origin: true, + columns: 1, + rows: 1, + }; + forEachCellInArea(originIndex, i, result, (_c, j) => { + result.cells[j] = undefined; + }); + result.cells[i] = collapsedTile; + // Collapsing the tile makes the final grid size smaller + idealLength -= cell.columns * cell.rows - 1; + } + } + } + } + + // Step 3: Fill all remaining gaps with 1×1 tiles + let gap = getNextGap(result, ignoreGap); if (gap !== null) { - const pathsToEnd = getPaths(findLast1By1Index(result)!, result); + const pathsToEnd = getPaths(result, findLast1By1Index(result)!, ignoreGap); do { let filled = false; @@ -385,8 +564,8 @@ export function fillGaps(g: BigGridState): BigGridState { // First, attempt to fill the gap by moving 1×1 tiles backwards from the // end of the grid along a set path while (from !== null) { - const toCell = result.cells[to]; - const fromCell = result.cells[from]; + const toCell = result.cells[to] as Cell | undefined; + const fromCell = result.cells[from] as Cell | undefined; // Skip over slots that are already full if (toCell !== undefined) { @@ -394,11 +573,7 @@ export function fillGaps(g: BigGridState): BigGridState { // 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 - ) { + } else if (fromCell === undefined || !is1By1(fromCell)) { from = pathsToEnd[from]; } else { result.cells[to] = result.cells[from]; @@ -417,24 +592,17 @@ export function fillGaps(g: BigGridState): BigGridState { result.cells[last1By1Index] = undefined; } - gap = getNextGap(result); + gap = getNextGap(result, ignoreGap); } while (gap !== null); } - // 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); - + trimTrailingGaps(result); return result; } -function createRows( - g: BigGridState, - count: number, - atRow: number -): BigGridState { +// TODO: replace all usages of this function with vacateArea, as this results in +// somewhat unpredictable movement +function createRows(g: SparseGrid, count: number, atRow: number): SparseGrid { const result = { columns: g.columns, cells: new Array(g.cells.length + g.columns * count), @@ -461,13 +629,13 @@ function createRows( } /** - * Adds a set of new items into the grid. (May leave gaps.) + * Adds a set of new items into the grid. */ export function addItems( items: TileDescriptor[], - g: BigGridState -): BigGridState { - let result = cloneGrid(g); + g: SparseGrid +): SparseGrid { + let result: SparseGrid = cloneGrid(g); for (const item of items) { const cell = { @@ -521,30 +689,23 @@ export function addItems( return result; } -const largeTileDimensions = (g: BigGridState): [number, number] => [ +const largeTileDimensions = (g: SparseGrid): [number, number] => [ Math.min(3, Math.max(2, g.columns - 1)), 2, ]; -const extraLargeTileDimensions = (g: BigGridState): [number, number] => +const extraLargeTileDimensions = (g: SparseGrid): [number, number] => g.columns > 3 ? [4, 3] : [g.columns, 2]; -/** - * 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( - g: BigGridState, +export function cycleTileSize( + g: G, tile: TileDescriptor -): BigGridState { +): G { const from = g.cells.findIndex((c) => c?.item === tile); if (from === -1) return g; // Tile removed, no change const fromCell = g.cells[from]!; const fromWidth = fromCell.columns; const fromHeight = fromCell.rows; - const fromEnd = areaEnd(from, fromWidth, fromHeight, g); const [baseDimensions, enlargedDimensions] = fromCell.item.largeBaseSize ? [largeTileDimensions(g), extraLargeTileDimensions(g)] @@ -555,113 +716,151 @@ export function cycleTileSize( ? enlargedDimensions : baseDimensions; - // 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) - ); + return setTileSize(g, from, toWidth, toHeight); +} - // 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; +/** + * Finds the cell nearest to 'nearestTo' that satsifies the given predicate. + * @param shouldScan A predicate constraining the bounds of the search. + */ +function findNearestCell( + g: G, + nearestTo: number, + shouldScan: (index: number) => boolean, + predicate: (cell: G["cells"][0], index: number) => boolean +): number | null { + const scanLocations = new Set([nearestTo]); - // 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 - fromWidth) / 2); - const scanRowOffset = Math.floor((toHeight - fromHeight) / 2); + for (const scanLocation of scanLocations) { + if (shouldScan(scanLocation)) { + if (predicate(g.cells[scanLocation], scanLocation)) return scanLocation; - 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 && - endColumn - startColumn + 1 === candidateWidth && - allCellsInArea(start, end, g, displaceable) - ) { - // This location works! - to = start; - break; + // Scan outwards in all directions + const scanColumn = column(scanLocation, g); + const scanRow = row(scanLocation, g); + if (scanColumn > 0) scanLocations.add(scanLocation - 1); + if (scanColumn < g.columns - 1) scanLocations.add(scanLocation + 1); + if (scanRow > 0) scanLocations.add(scanLocation - g.columns); + scanLocations.add(scanLocation + g.columns); } - - // 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 (endRow < rows - 1) nextScanLocations.add(scanLocation + g.columns); } - // If there is no space in the grid, give up - if (to === null) return g; + return null; +} - const toRow = row(to, g); +/** + * 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 setTileSize( + g: G, + from: number, + toWidth: number, + toHeight: number +): G { + const fromCell = g.cells[from]!; + const fromWidth = fromCell.columns; + const fromHeight = fromCell.rows; + const fromEnd = areaEnd(from, fromWidth, fromHeight, g); + const newGridSize = + g.cells.length + toWidth * toHeight - fromWidth * fromHeight; - // This is the grid with the new rows added - const gappyGrid = createRows(g, newRows, toRow + candidateHeight); - - // Remove the original tile - const fromInGappyGrid = - from + (row(from, g) >= toRow + candidateHeight ? g.columns * newRows : 0); - const fromEndInGappyGrid = fromInGappyGrid - from + fromEnd; - forEachCellInArea( - fromInGappyGrid, - fromEndInGappyGrid, - gappyGrid, - (_c, i) => (gappyGrid.cells[i] = undefined) + const toColumn = Math.max( + 0, + Math.min( + g.columns - toWidth, + column(from, g) + Math.trunc((fromWidth - toWidth) / 2) + ) ); + const toRow = Math.max( + 0, + row(from, g) + Math.trunc((fromHeight - toHeight) / 2) + ); + const targetDest = toColumn + toRow * g.columns; - // 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) => { - if (c !== undefined) displacedTiles.push(c); - gappyGrid.cells[i] = { - item: g.cells[from]!.item, - origin: i === to, - columns: toWidth, - rows: toHeight, - }; + const gridWithoutTile = cloneGrid(g); + forEachCellInArea(from, fromEnd, gridWithoutTile, (_c, i) => { + gridWithoutTile.cells[i] = undefined; }); - // 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(); + const placeTile = (to: number, toEnd: number, grid: Grid | SparseGrid) => { + forEachCellInArea(to, toEnd, grid, (_c, i) => { + grid.cells[i] = { + item: fromCell.item, + origin: i === to, + columns: toWidth, + rows: toHeight, + }; + }); + }; + + if (toWidth <= fromWidth && toHeight <= fromHeight) { + // The tile is shrinking, which can always happen in-place + const to = targetDest; + const toEnd = areaEnd(to, toWidth, toHeight, g); + + const result: SparseGrid = gridWithoutTile; + placeTile(to, toEnd, result); + return fillGaps(result, true, (i) => inArea(i, to, toEnd, g)) as G; + } else if (toWidth >= fromWidth && toHeight >= fromHeight) { + // The tile is growing, which might be able to happen in-place + const to = findNearestCell( + gridWithoutTile, + targetDest, + (i) => { + const end = areaEnd(i, toWidth, toHeight, g); + return ( + column(i, g) + toWidth - 1 < g.columns && + inArea(from, i, end, g) && + inArea(fromEnd, i, end, g) + ); + }, + (_c, i) => { + const end = areaEnd(i, toWidth, toHeight, g); + return end < newGridSize && canVacateArea(gridWithoutTile, i, end); + } + ); + + if (to !== null) { + const toEnd = areaEnd(to, toWidth, toHeight, g); + const result = vacateArea(gridWithoutTile, to, toEnd); + + placeTile(to, toEnd, result); + return result as G; + } } - // Fill any gaps that remain - return fillGaps(gappyGrid); + // Catch-all path for when the tile is neither strictly shrinking nor + // growing, or when there's not enough space for it to grow in-place + + const packedGridWithoutTile = fillGaps(gridWithoutTile, false); + + const to = findNearestCell( + packedGridWithoutTile, + targetDest, + (i) => i < newGridSize && column(i, g) + toWidth - 1 < g.columns, + (_c, i) => { + const end = areaEnd(i, toWidth, toHeight, g); + return end < newGridSize && canVacateArea(packedGridWithoutTile, i, end); + } + ); + + if (to === null) return g; // There's no space anywhere; give up + + const toEnd = areaEnd(to, toWidth, toHeight, g); + const result = vacateArea(packedGridWithoutTile, to, toEnd); + + placeTile(to, toEnd, result); + return result as G; } /** * Resizes the grid to a new column width. */ -export function resize(g: BigGridState, columns: number): BigGridState { - const result: BigGridState = { columns, cells: [] }; +export function resize(g: Grid, columns: number): Grid { + const result: SparseGrid = { columns, cells: [] }; const [largeColumns, largeRows] = largeTileDimensions(result); // Copy each tile from the old grid to the resized one in the same order @@ -670,10 +869,11 @@ export function resize(g: BigGridState, columns: number): BigGridState { let next = 0; for (const cell of g.cells) { - if (cell?.origin) { + if (cell.origin) { // TODO make aware of extra large tiles - const [nextColumns, nextRows] = - cell.columns > 1 || cell.rows > 1 ? [largeColumns, largeRows] : [1, 1]; + const [nextColumns, nextRows] = is1By1(cell) + ? [1, 1] + : [largeColumns, largeRows]; // If there isn't enough space left on this row, jump to the next row if (columns - column(next, result) < nextColumns) @@ -704,7 +904,7 @@ export function resize(g: BigGridState, columns: number): BigGridState { /** * Promotes speakers to the first page of the grid. */ -export function promoteSpeakers(g: BigGridState) { +export function promoteSpeakers(g: SparseGrid) { // This is all a bit of a hack right now, because we don't know if the designs // will stick with this approach in the long run // We assume that 4 rows are probably about 1 page @@ -713,19 +913,12 @@ export function promoteSpeakers(g: BigGridState) { for (let from = firstPageEnd; from < g.cells.length; from++) { const fromCell = g.cells[from]; // Don't bother trying to promote enlarged tiles - if ( - fromCell?.item.isSpeaker && - fromCell.columns === 1 && - fromCell.rows === 1 - ) { + if (fromCell?.item.isSpeaker && is1By1(fromCell)) { // Promote this tile by making 10 attempts to place it on the first page for (let j = 0; j < 10; j++) { const to = Math.floor(Math.random() * firstPageEnd); const toCell = g.cells[to]; - if ( - toCell === undefined || - (toCell.columns === 1 && toCell.rows === 1) - ) { + if (toCell === undefined || is1By1(toCell)) { moveTileUnchecked(g, from, to); break; } @@ -737,14 +930,11 @@ export function promoteSpeakers(g: BigGridState) { /** * The algorithm for updating a grid with a new set of tiles. */ -function updateTiles( - g: BigGridState, - tiles: TileDescriptor[] -): BigGridState { +function updateTiles(g: Grid, tiles: TileDescriptor[]): Grid { // Step 1: Update tiles that still exist, and remove tiles that have left // the grid const itemsById = new Map(tiles.map((i) => [i.id, i])); - const grid1: BigGridState = { + const grid1: SparseGrid = { ...g, cells: g.cells.map((c) => { if (c === undefined) return undefined; @@ -766,12 +956,12 @@ function updateTiles( return fillGaps(grid2); } -function updateBounds(g: BigGridState, bounds: RectReadOnly): BigGridState { +function updateBounds(g: Grid, bounds: RectReadOnly): Grid { const columns = Math.max(2, Math.floor(bounds.width * 0.0045)); return columns === g.columns ? g : resize(g, columns); } -const Slots: FC<{ s: BigGridState }> = memo(({ s: g }) => { +const Slots: FC<{ s: Grid }> = memo(({ s: g }) => { const areas = new Array<(number | null)[]>( Math.ceil(g.cells.length / g.columns) ); @@ -781,7 +971,7 @@ const Slots: FC<{ s: BigGridState }> = memo(({ s: g }) => { let slotCount = 0; for (let i = 0; i < g.cells.length; i++) { const cell = g.cells[i]; - if (cell?.origin) { + if (cell.origin) { const slotEnd = i + cell.columns - 1 + g.columns * (cell.rows - 1); forEachCellInArea( i, @@ -822,7 +1012,7 @@ const Slots: FC<{ s: BigGridState }> = memo(({ s: g }) => { * lies. */ function positionOnTileToCell( - g: BigGridState, + g: SparseGrid, tileOriginIndex: number, xPositionOnTile: number, yPositionOnTile: number @@ -834,16 +1024,16 @@ function positionOnTileToCell( } function dragTile( - g: BigGridState, + g: Grid, from: TileDescriptor, to: TileDescriptor, xPositionOnFrom: number, yPositionOnFrom: number, xPositionOnTo: number, yPositionOnTo: number -): BigGridState { - const fromOrigin = g.cells.findIndex((c) => c?.item === from); - const toOrigin = g.cells.findIndex((c) => c?.item === to); +): Grid { + const fromOrigin = g.cells.findIndex((c) => c.item === from); + const toOrigin = g.cells.findIndex((c) => c.item === to); const fromCell = positionOnTileToCell( g, fromOrigin, @@ -860,12 +1050,11 @@ function dragTile( return moveTile(g, fromOrigin, fromOrigin + toCell - fromCell); } -export const BigGrid: Layout = { +export const BigGrid: Layout = { emptyState: { columns: 4, cells: [] }, updateTiles, updateBounds, - getTiles: (g) => - g.cells.filter((c) => c?.origin).map((c) => c!.item as T), + getTiles: (g) => g.cells.filter((c) => c.origin).map((c) => c!.item as T), canDragTile: () => true, dragTile, toggleFocus: cycleTileSize, diff --git a/test/video-grid/BigGrid-test.ts b/test/video-grid/BigGrid-test.ts index b035bd22..0c74b612 100644 --- a/test/video-grid/BigGrid-test.ts +++ b/test/video-grid/BigGrid-test.ts @@ -20,7 +20,7 @@ import { cycleTileSize, fillGaps, forEachCellInArea, - BigGridState, + Grid, resize, row, moveTile, @@ -30,13 +30,13 @@ import { TileDescriptor } from "../../src/video-grid/VideoGrid"; /** * Builds a grid from a string specifying the contents of each cell as a letter. */ -function mkGrid(spec: string): BigGridState { +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) ?? ([] as string[]); const areas = new Set(cells); areas.delete(" "); // Space represents an empty cell, not an area - const grid: BigGridState = { columns, cells: new Array(cells.length) }; + const grid: Grid = { columns, cells: new Array(cells.length) }; for (const area of areas) { const start = cells.indexOf(area); @@ -60,7 +60,7 @@ function mkGrid(spec: string): BigGridState { /** * Turns a grid into a string showing the contents of each cell as a letter. */ -function showGrid(g: BigGridState): string { +function showGrid(g: Grid): string { let result = "\n"; for (let i = 0; i < g.cells.length; i++) { if (i > 0 && i % g.columns == 0) result += "\n"; @@ -116,11 +116,11 @@ mno`, ` aebch difgl -monjk` +mjnok` ); testFillGaps( - "fills a big gap", + "fills a big gap with 1×1 tiles", ` abcd e f @@ -128,19 +128,19 @@ g h ijkl`, ` abcd -elhf -gkji` +ehkf +glji` ); testFillGaps( - "only moves 1×1 tiles", + "fills a big gap with a large tile", ` aa bc`, ` -bc -aa` +aa +cb` ); testFillGaps( @@ -186,7 +186,7 @@ iief` ); testFillGaps( - "pushes a chain of large tiles upwards", + "collapses large tiles trapped at the bottom", ` abcd e fg @@ -195,24 +195,24 @@ hh ii ii`, ` -hhcd +abcd hhfg -aiib -eii` +hhie` ); testFillGaps( "gives up on pushing large tiles upwards when not possible", ` -aabb -aabb -cc -cc`, +aa +aa +bccd +eccf +ghij`, ` -aabb -aabb -cc -cc` +aadf +aaji +bcch +eccg` ); function testCycleTileSize( @@ -237,9 +237,9 @@ def ghi`, ` acc -bcc -def -ghi` +dcc +gbe +ifh` ); testCycleTileSize( @@ -249,10 +249,10 @@ testCycleTileSize( abcd efgh`, ` -abcd -eggg +acdh +bggg fggg -h` +e` ); testCycleTileSize( @@ -264,9 +264,9 @@ dbbe fghi jk`, ` -akbc -djhe -fig` +abhc +djge +fik` ); testCycleTileSize( @@ -284,9 +284,9 @@ abb gbb dde ddf -cci +ccm cch -klm` +lik` ); testCycleTileSize( @@ -304,6 +304,34 @@ dde ddf` ); +test("cycleTileSize is its own inverse", () => { + const input = ` +abc +def +ghi +jk`; + + const grid = mkGrid(input); + let gridAfter = grid; + + const toggle = (tileId: string) => { + const tile = grid.cells.find((c) => c?.item.id === tileId)!.item; + gridAfter = cycleTileSize(gridAfter, tile); + }; + + // Toggle a series of tiles + toggle("j"); + toggle("h"); + toggle("a"); + // Now do the same thing in reverse + toggle("a"); + toggle("h"); + toggle("j"); + + // The grid should be back to its original state + expect(showGrid(gridAfter)).toBe(input); +}); + function testAddItems( title: string, items: TileDescriptor[], @@ -437,9 +465,9 @@ gh`, af bb bb +dd +dd ch -dd -dd eg` ); @@ -455,9 +483,8 @@ dd dd eg`, ` -bbbc -bbbf -addd -hddd -ge` +afcd +bbbg +bbbe +h` ); From a63dc637abf9c8db440542fb97334ec1469dcee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 6 Jul 2023 08:35:53 +0200 Subject: [PATCH 03/76] Add subtle primary color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .env.example | 1 + src/index.css | 1 + src/initializer.tsx | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/.env.example b/.env.example index f03e8821..4180a830 100644 --- a/.env.example +++ b/.env.example @@ -25,3 +25,4 @@ LIVEKIT_SECRET="secret" # VITE_THEME_SYSTEM=#21262c # VITE_THEME_BACKGROUND=#15191e # VITE_THEME_BACKGROUND_85=#15191ed9 +# VITE_THEME_SUBTLE_PRIMARY=#26282D diff --git a/src/index.css b/src/index.css index f9e00ef3..1f372a1e 100644 --- a/src/index.css +++ b/src/index.css @@ -52,6 +52,7 @@ limitations under the License. --background: #15191e; --background-85: rgba(23, 25, 28, 0.85); --bgColor3: #444; /* This isn't found anywhere in the designs or Compound */ + --subtle-primary: #26282d; } @font-face { diff --git a/src/initializer.tsx b/src/initializer.tsx index 37e659e7..df5c8af6 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -133,6 +133,10 @@ export class Initializer { "--background-85", import.meta.env.VITE_THEME_BACKGROUND_85 as string ); + style.setProperty( + "--subtle-primary", + import.meta.env.VITE_THEME_SUBTLE_PRIMARY as string + ); } // Custom fonts From 0d72e3ae9e643f8c4e0a43b57425807828881906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 6 Jul 2023 08:36:18 +0200 Subject: [PATCH 04/76] Add `LockOff` icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/icons/LockOff.svg | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/icons/LockOff.svg diff --git a/src/icons/LockOff.svg b/src/icons/LockOff.svg new file mode 100644 index 00000000..d0a73909 --- /dev/null +++ b/src/icons/LockOff.svg @@ -0,0 +1,4 @@ + + + + From 3cef00b6b695133eae551bee608a795afd4dad2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 6 Jul 2023 11:10:00 +0200 Subject: [PATCH 05/76] Add E2EE banner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/Banner.module.css | 22 +++++++++++++++++++++ src/Banner.tsx | 27 +++++++++++++++++++++++++ src/E2EEBanner.module.css | 22 +++++++++++++++++++++ src/E2EEBanner.tsx | 34 ++++++++++++++++++++++++++++++++ src/home/RegisteredView.tsx | 2 ++ src/home/UnauthenticatedView.tsx | 2 ++ 6 files changed, 109 insertions(+) create mode 100644 src/Banner.module.css create mode 100644 src/Banner.tsx create mode 100644 src/E2EEBanner.module.css create mode 100644 src/E2EEBanner.tsx diff --git a/src/Banner.module.css b/src/Banner.module.css new file mode 100644 index 00000000..6acc9418 --- /dev/null +++ b/src/Banner.module.css @@ -0,0 +1,22 @@ +/* +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. +*/ + +.banner { + flex: 1; + border-radius: 8px; + padding: 16px; + background-color: var(--subtle-primary); +} diff --git a/src/Banner.tsx b/src/Banner.tsx new file mode 100644 index 00000000..fcc68a3b --- /dev/null +++ b/src/Banner.tsx @@ -0,0 +1,27 @@ +/* +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 { ReactNode } from "react"; + +import styles from "./Banner.module.css"; + +interface Props { + children: ReactNode; +} + +export const Banner = ({ children }: Props) => { + return
{children}
; +}; diff --git a/src/E2EEBanner.module.css b/src/E2EEBanner.module.css new file mode 100644 index 00000000..80b06bba --- /dev/null +++ b/src/E2EEBanner.module.css @@ -0,0 +1,22 @@ +/* +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. +*/ + +.e2eeBanner { + display: flex; + flex-direction: row; + align-items: center; + gap: 16px; +} diff --git a/src/E2EEBanner.tsx b/src/E2EEBanner.tsx new file mode 100644 index 00000000..78e68155 --- /dev/null +++ b/src/E2EEBanner.tsx @@ -0,0 +1,34 @@ +/* +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 { Trans } from "react-i18next"; + +import { Banner } from "./Banner"; +import styles from "./E2EEBanner.module.css"; +import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg"; + +export const E2EEBanner = () => { + return ( + +
+ + + Element Call is temporarily not encrypted while we test scalability. + +
+
+ ); +}; diff --git a/src/home/RegisteredView.tsx b/src/home/RegisteredView.tsx index ee696d20..f46376a8 100644 --- a/src/home/RegisteredView.tsx +++ b/src/home/RegisteredView.tsx @@ -39,6 +39,7 @@ import { Form } from "../form/Form"; import { CallType, CallTypeDropdown } from "./CallTypeDropdown"; import { useOptInAnalytics } from "../settings/useSetting"; import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; +import { E2EEBanner } from "../E2EEBanner"; interface Props { client: MatrixClient; @@ -146,6 +147,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) { )} + {error && ( diff --git a/src/home/UnauthenticatedView.tsx b/src/home/UnauthenticatedView.tsx index 1fb1a84b..e8f96722 100644 --- a/src/home/UnauthenticatedView.tsx +++ b/src/home/UnauthenticatedView.tsx @@ -41,6 +41,7 @@ import commonStyles from "./common.module.css"; import { generateRandomName } from "../auth/generateRandomName"; import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; import { useOptInAnalytics } from "../settings/useSetting"; +import { E2EEBanner } from "../E2EEBanner"; export const UnauthenticatedView: FC = () => { const { setClient } = useClient(); @@ -168,6 +169,7 @@ export const UnauthenticatedView: FC = () => { Terms and conditions + {error && ( From 4a90a6d64c16ee6cf1ddf74ad20ca5a771785822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 6 Jul 2023 11:10:43 +0200 Subject: [PATCH 06/76] Add E2EE lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/E2EELock.module.css | 28 +++++++++++++++++++++ src/E2EELock.tsx | 56 +++++++++++++++++++++++++++++++++++++++++ src/room/InCallView.tsx | 2 ++ 3 files changed, 86 insertions(+) create mode 100644 src/E2EELock.module.css create mode 100644 src/E2EELock.tsx diff --git a/src/E2EELock.module.css b/src/E2EELock.module.css new file mode 100644 index 00000000..8d6e95fa --- /dev/null +++ b/src/E2EELock.module.css @@ -0,0 +1,28 @@ +/* +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. +*/ + +.e2eeLock { + width: 32px; + height: 32px; + + display: flex; + align-items: center; + justify-content: center; + margin: 8px; + + border-radius: 100%; + background-color: var(--subtle-primary); +} diff --git a/src/E2EELock.tsx b/src/E2EELock.tsx new file mode 100644 index 00000000..3832e7f0 --- /dev/null +++ b/src/E2EELock.tsx @@ -0,0 +1,56 @@ +/* +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 { useTranslation } from "react-i18next"; +import { useCallback } from "react"; +import { useObjectRef } from "@react-aria/utils"; +import { useButton } from "@react-aria/button"; + +import styles from "./E2EELock.module.css"; +import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg"; +import { TooltipTrigger } from "./Tooltip"; + +export const E2EELock = () => { + const { t } = useTranslation(); + const tooltip = useCallback( + () => + t("Element Call is temporarily not encrypted while we test scalability."), + [t] + ); + + return ( + + + + ); +}; + +/** + * This component is a bit of hack - for some reason for the TooltipTrigger to + * work, it needs to contain a component which uses the useButton hook; please + * note that for some reason this also needs to be a separate component and we + * cannot just use the useButton hook inside the E2EELock. + */ +const Icon = () => { + const buttonRef = useObjectRef(); + const { buttonProps } = useButton({}, buttonRef); + + return ( +
+ +
+ ); +}; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 98a3a63b..b82b1513 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -84,6 +84,7 @@ import { useMediaDevices } from "../livekit/useMediaDevices"; import { useFullscreen } from "./useFullscreen"; import { useLayoutStates } from "../video-grid/Layout"; import { useSFUConfig } from "../livekit/OpenIDLoader"; +import { E2EELock } from "../E2EELock"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); // There is currently a bug in Safari our our code with cloning and sending MediaStreams @@ -395,6 +396,7 @@ export function InCallView({ users={unencryptedEventsFromUsers} room={groupCall.room} /> + From 3b49fa079b3ff5c9ef73ab1739678a12132ffa84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 6 Jul 2023 11:36:34 +0200 Subject: [PATCH 07/76] 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 | 1 + 1 file changed, 1 insertion(+) diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index f14991d9..c96a54e5 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -37,6 +37,7 @@ "Display name": "Display name", "Download debug logs": "Download debug logs", "Element Call Home": "Element Call Home", + "Element Call is temporarily not encrypted while we test scalability.": "Element Call is temporarily not encrypted while we test scalability.", "Exit full screen": "Exit full screen", "Expose developer settings in the settings window.": "Expose developer settings in the settings window.", "Feedback": "Feedback", From 3d57d63f7ff1f7034f8795cd649152950df7cb2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 6 Jul 2023 12:12:28 +0200 Subject: [PATCH 08/76] Don't unnecessarily use `useEffect` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/room/GroupCallView.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 1b23fcf9..64c3e965 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -219,14 +219,7 @@ export function GroupCallView({ undefined ); - const [livekitServiceURL, setLivekitServiceURL] = useState< - string | undefined - >(groupCall.foci[0]?.livekitServiceUrl); - - useEffect(() => { - setLivekitServiceURL(groupCall.foci[0]?.livekitServiceUrl); - }, [setLivekitServiceURL, groupCall]); - + const livekitServiceURL = groupCall.foci[0]?.livekitServiceUrl; if (!livekitServiceURL) { return ; } From 2aae25d3b5913a268a519c24debd86a0e9c435c5 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 5 Jul 2023 16:58:23 +0000 Subject: [PATCH 09/76] Translated using Weblate (Greek) Currently translated at 100.0% (117 of 117 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/el/ --- public/locales/el/app.json | 44 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/public/locales/el/app.json b/public/locales/el/app.json index d61ba142..02191b05 100644 --- a/public/locales/el/app.json +++ b/public/locales/el/app.json @@ -11,7 +11,7 @@ "Remove": "Αφαίρεση", "Registering…": "Εγγραφή…", "Not registered yet? <2>Create an account": "Δεν έχετε εγγραφεί ακόμα; <2>Δημιουργήστε λογαριασμό", - "Login to your account": "Συνδεθείτε στο λογαριασμό σας", + "Login to your account": "Συνδεθείτε στον λογαριασμό σας", "Logging in…": "Σύνδεση…", "Invite people": "Προσκαλέστε άτομα", "Invite": "Πρόσκληση", @@ -71,5 +71,45 @@ "Close": "Κλείσιμο", "Change layout": "Αλλαγή διάταξης", "Camera": "Κάμερα", - "Audio": "Ήχος" + "Audio": "Ήχος", + "Send debug logs": "Αποστολή αρχείων καταγραφής", + "Recaptcha dismissed": "Το recaptcha απορρίφθηκε", + "<0>Thanks for your feedback!": "<0>Ευχαριστώ για τα σχόλιά σας!", + "Call type menu": "Μενού είδους κλήσης", + "Local volume": "Τοπική ένταση", + "Home": "Αρχική", + "Show connection stats": "Εμφάνιση στατιστικών σύνδεσης", + "Unmute microphone": "Κατάργηση σίγασης μικροφώνου", + "Take me Home": "Μετάβαση στην Αρχική", + "{{displayName}} is presenting": "{{displayName}} παρουσιάζει", + "<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>Join call now<1>Or<2>Copy call link and join later": "<0>Συμμετοχή στην κλήση τώρα<1>Or<2>Αντιγραφή συνδέσμου κλήσης και συμμετοχή αργότερα", + "<0>We'd love to hear your feedback so we can improve your experience.": "<0>Θα θέλαμε να ακούσουμε τα σχόλιά σας ώστε να βελτιώσουμε την εμπειρία σας.", + "<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>Θα μπορείτε να διατηρήσετε το όνομά σας και να ορίσετε ένα avatar για χρήση σε μελλοντικές κλήσεις.", + "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ένας άλλος χρήστης σε αυτή την κλήση έχει ένα πρόβλημα. Για την καλύτερη διάγνωση αυτών των προβλημάτων θα θέλαμε να συλλέξουμε ένα αρχείο καταγραφής σφαλμάτων.", + "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>Πολιτική cookies.", + "By clicking \"Go\", you agree to our <2>Terms and conditions": "Κάνοντας κλικ στο \"Μετάβαση\", συμφωνείτε με τους <2>Όρους και προϋποθέσεις μας", + "Grid layout menu": "Μενού διάταξης πλέγματος", + "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Εάν αντιμετωπίζετε προβλήματα ή απλά θέλετε να μας δώσετε κάποια σχόλια, παρακαλούμε στείλτε μας μια σύντομη περιγραφή παρακάτω.", + "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}": "Κάποιοι άλλοι χρήστες προσπαθούν να συμμετάσχουν σε αυτή την κλήση από ασύμβατες εκδόσεις. Αυτοί οι χρήστες θα πρέπει να βεβαιωθούν ότι έχουν κάνει ανανέωση (refresh) την καρτέλα του περιηγητή τους:<1>{userLis}", + "Thanks! We'll get right on it.": "Ευχαριστούμε! Θα το ερευνήσουμε αμέσως.", + "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 και ισχύουν η <2>Πολιτική απορρήτου και <6>Όροι παροχής υπηρεσιών της Google.<9>Κάνοντας κλικ στο \"Εγγραφή\", συμφωνείτε με τους <12>Όρους και προϋποθέσεις μας", + "Expose developer settings in the settings window.": "Εμφάνιση ρυθμίσεων προγραμματιστή στο παράθυρο ρυθμίσεων.", + "Feedback": "Ανατροφοδότηση", + "Submitting…": "Υποβολή…", + "Thanks, we received your feedback!": "Ευχαριστούμε, λάβαμε τα σχόλιά σας!", + "{{count}} stars|other": "{{count}} αστέρια", + "{{count}} stars|one": "{{count}} αστέρι", + "{{displayName}}, your call has ended.": "{{displayName}}, η κλήση σας τερματίστηκε.", + "<0>Submitting debug logs will help us track down the problem.": "<0>Η υποβολή αρχείων καταγραφής σφαλμάτων θα μας βοηθήσει να εντοπίσουμε το πρόβλημα.", + "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Κάνοντας κλικ στο \"Συμμετοχή στην κλήση τώρα\", συμφωνείτε με τους <2>Όρους και προϋποθέσεις μας", + "How did it go?": "Πώς σας φάνηκε;", + "Include debug logs": "Να συμπεριληφθούν αρχεία καταγραφής", + "Recaptcha not loaded": "Το Recaptcha δεν φορτώθηκε", + "Debug log": "Αρχείο καταγραφής", + "Developer": "Προγραμματιστής", + "Download debug logs": "Λήψη αρχείων καταγραφής", + "Sending debug logs…": "Αποστολή αρχείων καταγραφής…", + "Submit": "Υποβολή", + "Your feedback": "Τα σχόλιά σας" } From faeb2ae395ea0760895d22ee6903383a2363fd64 Mon Sep 17 00:00:00 2001 From: Dimitris Vagiakakos Date: Wed, 5 Jul 2023 22:21:22 +0000 Subject: [PATCH 10/76] Translated using Weblate (Greek) Currently translated at 100.0% (117 of 117 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/el/ --- public/locales/el/app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/locales/el/app.json b/public/locales/el/app.json index 02191b05..667065c9 100644 --- a/public/locales/el/app.json +++ b/public/locales/el/app.json @@ -111,5 +111,9 @@ "Download debug logs": "Λήψη αρχείων καταγραφής", "Sending debug logs…": "Αποστολή αρχείων καταγραφής…", "Submit": "Υποβολή", - "Your feedback": "Τα σχόλιά σας" + "Your feedback": "Τα σχόλιά σας", + "Fetching group call timed out.": "Η ομαδική κλήση έληξε από τέλος χρόνου.", + "Freedom": "Ελευθερία", + "Spotlight": "Spotlight", + "Element Call Home": "Element Κεντρική Οθόνη Κλήσεων" } From d70374119f8bed9607aefa5b1dcd0a4767177a08 Mon Sep 17 00:00:00 2001 From: Weblate Date: Thu, 6 Jul 2023 10:17:06 +0000 Subject: [PATCH 11/76] 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 | 2 -- public/locales/cs/app.json | 2 -- public/locales/de/app.json | 2 -- public/locales/el/app.json | 3 --- 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 | 1 - public/locales/pl/app.json | 3 --- public/locales/ru/app.json | 2 -- public/locales/sk/app.json | 2 -- public/locales/tr/app.json | 1 - public/locales/uk/app.json | 2 -- public/locales/vi/app.json | 1 - public/locales/zh-Hans/app.json | 3 --- public/locales/zh-Hant/app.json | 2 -- 18 files changed, 36 deletions(-) diff --git a/public/locales/bg/app.json b/public/locales/bg/app.json index 4cf46655..4555edb9 100644 --- a/public/locales/bg/app.json +++ b/public/locales/bg/app.json @@ -5,7 +5,6 @@ "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Друг потребител в този разговор има проблем. За да диагностицираме този проблем по-добре ни се иска да съберем debug логове.", "Audio": "Звук", "Avatar": "Аватар", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "Натискайки \"Напред\" се съгласявате с нашите <2>Правила и условия", "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Натискайки \"Влез в разговора сега\", се съгласявате с нашите <2>Правила и условия", "Call link copied": "Връзка към разговора бе копирана", "Call type menu": "Меню \"тип на разговора\"", @@ -75,7 +74,6 @@ "Take me Home": "Отиди в Начало", "Thanks! We'll get right on it.": "Благодарим! Веднага ще се заемем.", "This call already exists, would you like to join?": "Този разговор вече съществува, искате ли да се присъедините?", - "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 и важат <2>Политиката за поверителност и <6>Условията за ползване на услугата на Google.<9>Натискайки \"Регистрация\", се съгласявате с нашите <12>Правила и условия", "Turn off camera": "Изключи камерата", "Turn on camera": "Включи камерата", "Unmute microphone": "Включи микрофона", diff --git a/public/locales/cs/app.json b/public/locales/cs/app.json index 40bcd5ab..86ad6326 100644 --- a/public/locales/cs/app.json +++ b/public/locales/cs/app.json @@ -62,7 +62,6 @@ "Inspector": "Insepktor", "Incompatible versions!": "Nekompatibilní verze!", "Incompatible versions": "Nekompatibilní verze", - "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": "Tato stárnka je chráněna pomocí ReCAPTCHA a Google <2>zásad ochrany osobních údajů a <6>podmínky služby platí.<9>Kliknutím na \"Registrovat\", souhlasíte s <12>Pravidly a podmínkami", "Walkie-talkie call name": "Jméno vysílačkového hovoru", "Walkie-talkie call": "Vysílačkový hovor", "{{names}}, {{name}}": "{{names}}, {{name}}", @@ -90,7 +89,6 @@ "Create account": "Vytvořit účet", "Copy": "Kopírovat", "Call type menu": "Menu typu hovoru", - "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í.", "<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 ", diff --git a/public/locales/de/app.json b/public/locales/de/app.json index 516ad796..6d5f71a9 100644 --- a/public/locales/de/app.json +++ b/public/locales/de/app.json @@ -5,7 +5,6 @@ "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ein anderer Benutzer dieses Anrufs hat ein Problem. Um es besser diagnostizieren zu können, würden wir gerne ein Debug-Protokoll erstellen.", "Audio": "Audio", "Avatar": "Avatar", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "Wenn du auf „Los geht’s“ klickst, akzeptierst du unsere <2>Geschäftsbedingungen", "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Wenn du auf „Anruf beitreten“ klickst, akzeptierst du unsere <2>Geschäftsbedingungen", "Call link copied": "Anruflink kopiert", "Call type menu": "Anruftyp Menü", @@ -74,7 +73,6 @@ "Take me Home": "Zurück zur Startseite", "Thanks! We'll get right on it.": "Vielen Dank! Wir werden uns sofort darum kümmern.", "This call already exists, would you like to join?": "Dieser Aufruf existiert bereits, möchtest Du teilnehmen?", - "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": "Diese Website wird durch ReCAPTCHA geschützt und es gelten die <2>Datenschutzerklärung und <6>Nutzungsbedingungen von Google.<9>Indem Du auf „Registrieren“ klickst, stimmst du unseren <12>Geschäftsbedingungen zu", "Turn off camera": "Kamera ausschalten", "Turn on camera": "Kamera einschalten", "Unmute microphone": "Mikrofon aktivieren", diff --git a/public/locales/el/app.json b/public/locales/el/app.json index 667065c9..91f5554e 100644 --- a/public/locales/el/app.json +++ b/public/locales/el/app.json @@ -88,12 +88,10 @@ "<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>Θα μπορείτε να διατηρήσετε το όνομά σας και να ορίσετε ένα avatar για χρήση σε μελλοντικές κλήσεις.", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ένας άλλος χρήστης σε αυτή την κλήση έχει ένα πρόβλημα. Για την καλύτερη διάγνωση αυτών των προβλημάτων θα θέλαμε να συλλέξουμε ένα αρχείο καταγραφής σφαλμάτων.", "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>Πολιτική cookies.", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "Κάνοντας κλικ στο \"Μετάβαση\", συμφωνείτε με τους <2>Όρους και προϋποθέσεις μας", "Grid layout menu": "Μενού διάταξης πλέγματος", "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Εάν αντιμετωπίζετε προβλήματα ή απλά θέλετε να μας δώσετε κάποια σχόλια, παρακαλούμε στείλτε μας μια σύντομη περιγραφή παρακάτω.", "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}": "Κάποιοι άλλοι χρήστες προσπαθούν να συμμετάσχουν σε αυτή την κλήση από ασύμβατες εκδόσεις. Αυτοί οι χρήστες θα πρέπει να βεβαιωθούν ότι έχουν κάνει ανανέωση (refresh) την καρτέλα του περιηγητή τους:<1>{userLis}", "Thanks! We'll get right on it.": "Ευχαριστούμε! Θα το ερευνήσουμε αμέσως.", - "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 και ισχύουν η <2>Πολιτική απορρήτου και <6>Όροι παροχής υπηρεσιών της Google.<9>Κάνοντας κλικ στο \"Εγγραφή\", συμφωνείτε με τους <12>Όρους και προϋποθέσεις μας", "Expose developer settings in the settings window.": "Εμφάνιση ρυθμίσεων προγραμματιστή στο παράθυρο ρυθμίσεων.", "Feedback": "Ανατροφοδότηση", "Submitting…": "Υποβολή…", @@ -102,7 +100,6 @@ "{{count}} stars|one": "{{count}} αστέρι", "{{displayName}}, your call has ended.": "{{displayName}}, η κλήση σας τερματίστηκε.", "<0>Submitting debug logs will help us track down the problem.": "<0>Η υποβολή αρχείων καταγραφής σφαλμάτων θα μας βοηθήσει να εντοπίσουμε το πρόβλημα.", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Κάνοντας κλικ στο \"Συμμετοχή στην κλήση τώρα\", συμφωνείτε με τους <2>Όρους και προϋποθέσεις μας", "How did it go?": "Πώς σας φάνηκε;", "Include debug logs": "Να συμπεριληφθούν αρχεία καταγραφής", "Recaptcha not loaded": "Το Recaptcha δεν φορτώθηκε", diff --git a/public/locales/es/app.json b/public/locales/es/app.json index facea468..91504693 100644 --- a/public/locales/es/app.json +++ b/public/locales/es/app.json @@ -6,7 +6,6 @@ "Register": "Registrarse", "Not registered yet? <2>Create an account": "¿No estás registrado todavía? <2>Crear una cuenta", "Login to your account": "Iniciar sesión en tu cuenta", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Al hacer clic en \"Unirse a la llamada ahora\", aceptarás nuestros <2>Términos y condiciones", "By clicking \"Go\", you agree to our <2>Terms and conditions": "Al hacer clic en \"Comenzar\" aceptarás nuestros <2>Términos y condiciones", "Yes, join call": "Si, unirse a la llamada", "Walkie-talkie call name": "Nombre de la llamada Walkie-talkie", @@ -21,7 +20,6 @@ "Unmute microphone": "Desilenciar el micrófono", "Turn on camera": "Encender la cámara", "Turn off camera": "Apagar la cámara", - "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": "Este sitio está protegido por ReCAPTCHA y se aplica <2>la Política de Privacidad y <6>los Términos de Servicio de Google.<9>Al hacer clic en \"Registrar\" aceptarás nuestros <12>Términos y condiciones", "Thanks! We'll get right on it.": "¡Gracias! Nos encargaremos de ello.", "Take me Home": "Volver al inicio", "Submit feedback": "Enviar comentarios", diff --git a/public/locales/et/app.json b/public/locales/et/app.json index 8c6a4afc..1c0ac653 100644 --- a/public/locales/et/app.json +++ b/public/locales/et/app.json @@ -32,7 +32,6 @@ "Camera": "Kaamera", "Call type menu": "Kõnetüübi valik", "Call link copied": "Kõne link on kopeeritud", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Klõpsides „Liitu kõnega“nõustud sa meie <2>kasutustingimustega", "By clicking \"Go\", you agree to our <2>Terms and conditions": "Klõpsides „Jätka“nõustud sa meie <2>kasutustingimustega", "Avatar": "Tunnuspilt", "Audio": "Heli", @@ -93,7 +92,6 @@ "Walkie-talkie call": "Walkie-talkie stiilis kõne", "Walkie-talkie call name": "Walkie-talkie stiilis kõne nimi", "WebRTC is not supported or is being blocked in this browser.": "WebRTC pole kas selles brauseris toetatud või on keelatud.", - "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", "Element Call Home": "Element Call Home", "Copy": "Kopeeri", "<0>Submitting debug logs will help us track down the problem.": "<0>Kui saadad meile vealogid, siis on lihtsam vea põhjust otsida.", diff --git a/public/locales/fa/app.json b/public/locales/fa/app.json index 0877ab78..782e1f31 100644 --- a/public/locales/fa/app.json +++ b/public/locales/fa/app.json @@ -45,7 +45,6 @@ "Camera": "دوربین", "Call type menu": "منوی نوع تماس", "Call link copied": "لینک تماس کپی شد", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "با کلیک بر روی پیوستن به تماس، شما با <2>شرایط و قوانین استفاده موافقت می‌کنید", "By clicking \"Go\", you agree to our <2>Terms and conditions": "با کلیک بر روی برو، شما با <2>شرایط و قوانین استفاده موافقت می‌کنید", "Avatar": "آواتار", "Audio": "صدا", @@ -88,7 +87,6 @@ "Version: {{version}}": "نسخه: {{نسخه}}", "User menu": "فهرست کاربر", "Unmute microphone": "ناخموشی میکروفون", - "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 محافظت می شود و <2>خط مشی رازداری و <6>شرایط خدمات Google اعمال می شود.<9>با کلیک کردن بر روی \"ثبت نام\"، شما با <12 >شرایط و ضوابط ما موافقت می کنید", "This call already exists, would you like to join?": "این تماس از قبل وجود دارد، می‌خواهید بپیوندید؟", "Thanks! We'll get right on it.": "با تشکر! ما به درستی آن را انجام خواهیم داد.", "Submit feedback": "بازخورد ارائه دهید", diff --git a/public/locales/fr/app.json b/public/locales/fr/app.json index 837c677f..3ffcfc2a 100644 --- a/public/locales/fr/app.json +++ b/public/locales/fr/app.json @@ -4,7 +4,6 @@ "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Un autre utilisateur dans cet appel a un problème. Pour nous permettre de résoudre le problème, nous aimerions récupérer un journal de débogage.", "Audio": "Audio", "Avatar": "Avatar", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "En cliquant sur « Commencer » vous acceptez nos <2>conditions d’utilisation", "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "En cliquant sur « Rejoindre l’appel » vous acceptez nos <2>conditions d’utilisation", "Call link copied": "Lien de l’appel copié", "Call type menu": "Menu de type d’appel", @@ -88,7 +87,6 @@ "Unmute microphone": "Allumer le micro", "Turn on camera": "Allumer la caméra", "Turn off camera": "Couper la caméra", - "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": "Ce site est protégé par ReCAPTCHA, la <2>politique de confidentialité et les <6>conditions d’utilisation de Google s’appliquent.<9>En cliquant sur « S’enregistrer » vous acceptez également nos <12>conditions d’utilisation", "Speaker": "Intervenant", "Invite": "Inviter", "<0>Already have an account?<1><0>Log in Or <2>Access as a guest": "<0>Vous avez déjà un compte ?<1><0>Se connecter Ou <2>Accès invité", diff --git a/public/locales/id/app.json b/public/locales/id/app.json index 1388daeb..6db74b22 100644 --- a/public/locales/id/app.json +++ b/public/locales/id/app.json @@ -5,7 +5,6 @@ "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Pengguna yang lain di panggilan ini sedang mengalami masalah. Supaya dapat mendiagnosa masalah ini, kami ingin mengumpulkan sebuah catatan pengawakutuan.", "Audio": "Audio", "Avatar": "Avatar", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "Dengan mengeklik \"Bergabung\", Anda terima <2>syarat dan ketentuan kami", "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Dengan mengeklik \"Bergabung ke panggilan sekarang\", Anda terima <2>syarat dan ketentuan kami", "Call link copied": "Tautan panggilan disalin", "Call type menu": "Menu jenis panggilan", @@ -75,7 +74,6 @@ "Take me Home": "Bawa saya ke Beranda", "Thanks! We'll get right on it.": "Terima kasih! Kami akan melihatnya.", "This call already exists, would you like to join?": "Panggilan ini sudah ada, apakah Anda ingin bergabung?", - "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": "Situs ini dilindungi oleh ReCAPTCHA dan <2>Kebijakan Privasi dan <6>Ketentuan Layanan Google berlaku.<9>Dengan mengeklik \"Daftar\", Anda terima <12>syarat dan ketentuan kami", "Turn off camera": "Matikan kamera", "Turn on camera": "Nyalakan kamera", "Unmute microphone": "Suarakan mikrofon", diff --git a/public/locales/ja/app.json b/public/locales/ja/app.json index 9aac3347..1686ac3a 100644 --- a/public/locales/ja/app.json +++ b/public/locales/ja/app.json @@ -5,7 +5,6 @@ "<0>Oops, something's gone wrong.": "<0>何かがうまく行きませんでした。", "Camera": "カメラ", "Call link copied": "通話リンクをコピーしました", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "「今すぐ通話に参加」をクリックすると、<2>利用規約に同意したとみなされます", "By clicking \"Go\", you agree to our <2>Terms and conditions": "「続行」をクリックすると、 <2>利用規約に同意したとみなされます", "Avatar": "アバター", "Audio": "音声", diff --git a/public/locales/pl/app.json b/public/locales/pl/app.json index b702178b..df3c9a71 100644 --- a/public/locales/pl/app.json +++ b/public/locales/pl/app.json @@ -1,7 +1,6 @@ { "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", "Your recent calls": "Twoje ostatnie połączenia", "Yes, join call": "Tak, dołącz do połączenia", "WebRTC is not supported or is being blocked in this browser.": "WebRTC jest niewspierane lub zablokowane w tej przeglądarce.", @@ -84,7 +83,6 @@ "Camera": "Kamera", "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>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.", @@ -99,7 +97,6 @@ "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", - "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": "Ta strona jest chroniona przez ReCAPTCHA, więc obowiązują na niej <2>Polityka prywatności i <6>Warunki świadczenia usług Google.<9>Klikając \"Zarejestruj się\", zgadzasz się na nasze <12>Warunki świadczenia usług", "<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>Możesz wycofać swoją zgodę poprzez odznaczenie tego pola. Jeśli już jesteś w trakcie rozmowy, opcja zostanie zastosowana po jej zakończeniu.", "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.": "Uczestnicząc w tej becie, upoważniasz nas do zbierania anonimowych danych, które wykorzystamy do ulepszenia produktu. Dowiedz się więcej na temat danych, które zbieramy w naszej <2>Polityce prywatności i <5>Polityce ciasteczek.", "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Jeśli posiadasz problemy lub chciałbyś zgłosić swoją opinię, wyślij nam krótki opis.", diff --git a/public/locales/ru/app.json b/public/locales/ru/app.json index ad2b9340..c6bd0236 100644 --- a/public/locales/ru/app.json +++ b/public/locales/ru/app.json @@ -4,7 +4,6 @@ "Logging in…": "Вход…", "{{names}}, {{name}}": "{{names}}, {{name}}", "Waiting for other participants…": "Ожидание других участников…", - "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?": "Этот звонок уже существует, хотите присоединиться?", "Thanks! We'll get right on it.": "Спасибо! Мы учтём ваш отзыв.", "Submit feedback": "Отправить отзыв", @@ -12,7 +11,6 @@ "Select an option": "Выберите вариант", "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}", "Grid layout menu": "Меню \"Расположение сеткой\"", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Нажимая \"Присоединиться сейчас\", вы соглашаетесь с нашими <2>положениями и условиями", "By clicking \"Go\", you agree to our <2>Terms and conditions": "Нажимая \"Далее\", вы соглашаетесь с нашими <2>положениями и условиями", "<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>Так вы можете оставить своё имя и задать аватар для будущих звонков.", "<0>Create an account Or <2>Access as a guest": "<0>Создать аккаунт или <2>Зайти как гость", diff --git a/public/locales/sk/app.json b/public/locales/sk/app.json index 57b0e261..f87e380a 100644 --- a/public/locales/sk/app.json +++ b/public/locales/sk/app.json @@ -65,7 +65,6 @@ "Unmute microphone": "Zrušiť stlmenie mikrofónu", "Turn on camera": "Zapnúť kameru", "Turn off camera": "Vypnúť kameru", - "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": "Táto stránka je chránená systémom ReCAPTCHA a platia na ňu <2>Pravidlá ochrany osobných údajov a <6>Podmienky poskytovania služieb spoločnosti Google.<9>Kliknutím na tlačidlo \"Registrovať sa\" vyjadrujete súhlas s našimi <12>Podmienkami poskytovania služieb", "This call already exists, would you like to join?": "Tento hovor už existuje, chceli by ste sa k nemu pripojiť?", "Speaker": "Reproduktor", "Sign out": "Odhlásiť sa", @@ -86,7 +85,6 @@ "Camera": "Kamera", "Call type menu": "Ponuka typu hovoru", "Call link copied": "Odkaz na hovor skopírovaný", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Kliknutím na \"Pripojiť sa k hovoru\" súhlasíte s našimi <2>Podmienkami", "By clicking \"Go\", you agree to our <2>Terms and conditions": "Kliknutím na tlačidlo \"Prejsť\" súhlasíte s našimi <2>Podmienkami", "Avatar": "Obrázok", "Audio": "Audio", diff --git a/public/locales/tr/app.json b/public/locales/tr/app.json index dad45eb2..975251a1 100644 --- a/public/locales/tr/app.json +++ b/public/locales/tr/app.json @@ -3,7 +3,6 @@ "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Bu aramadaki başka bir kullanıcı sorun yaşıyor. Sorunu daha iyi çözebilmemiz için hata ayıklama kütüğünü almak isteriz.", "Audio": "Ses", "Avatar": "Avatar", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "\"Git\"e tıklayarak,<2>hükümler ve koşulları kabul etmiş sayılırsınız", "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "\"Şimdi katıl\"a tıklayarak, <2>hükümler ve koşulları kabul etmiş sayılırsınız", "Call link copied": "Arama bağlantısı kopyalandı", "Call type menu": "Arama tipi menüsü", diff --git a/public/locales/uk/app.json b/public/locales/uk/app.json index 6e5ab0c8..00635a7c 100644 --- a/public/locales/uk/app.json +++ b/public/locales/uk/app.json @@ -15,7 +15,6 @@ "Unmute microphone": "Увімкнути мікрофон", "Turn on camera": "Увімкнути камеру", "Turn off camera": "Вимкнути камеру", - "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 і до нього застосовується <2>Політика приватності і <6>Умови надання послуг Google.<9>Натискаючи кнопку «Зареєструватися», ви погоджуєтеся з нашими <12>Умовами та положеннями", "This call already exists, would you like to join?": "Цей виклик уже існує, бажаєте приєднатися?", "Thanks! We'll get right on it.": "Дякуємо! Ми зараз же візьмемося за це.", "Take me Home": "Перейти до Домівки", @@ -84,7 +83,6 @@ "Camera": "Камера", "Call type menu": "Меню типу виклику", "Call link copied": "Посилання на виклик скопійовано", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Натиснувши «Приєднатися до виклику зараз», ви погодитеся з нашими <2>Умовами та положеннями", "By clicking \"Go\", you agree to our <2>Terms and conditions": "Натиснувши «Далі», ви погодитеся з нашими <2>Умовами та положеннями", "Avatar": "Аватар", "Audio": "Звук", diff --git a/public/locales/vi/app.json b/public/locales/vi/app.json index d67f05ae..6757c05e 100644 --- a/public/locales/vi/app.json +++ b/public/locales/vi/app.json @@ -76,7 +76,6 @@ "This call already exists, would you like to join?": "Cuộc gọi đã tồn tại, bạn có muốn tham gia không?", "Recaptcha not loaded": "Chưa tải được Recaptcha", "Debug log request": "Yêu cầu nhật ký gỡ lỗi", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Khi nhấn vào \"Tham gia cuộc gọi\", bạn đồng ý với <2>Điều khoản và điều kiện của chúng tôi", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Một người dùng khác trong cuộc gọi đang gặp vấn đề. Để có thể chẩn đoán tốt hơn chúng tôi muốn thu thập nhật ký gỡ lỗi.", "<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>Tại sao lại không hoàn thiện bằng cách đặt mật khẩu để giữ tài khoản của bạn?<1>Bạn sẽ có thể giữ tên và đặt ảnh đại diện cho những cuộc gọi tiếp theo.", "<0>Oops, something's gone wrong.": "<0>Ối, có cái gì đó sai.", diff --git a/public/locales/zh-Hans/app.json b/public/locales/zh-Hans/app.json index a7126f8d..2673c2ee 100644 --- a/public/locales/zh-Hans/app.json +++ b/public/locales/zh-Hans/app.json @@ -14,7 +14,6 @@ "Unmute microphone": "取消麦克风静音", "Turn on camera": "开启摄像头", "Turn off camera": "关闭摄像头", - "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?": "该通话已存在,你想加入吗?", "Thanks! We'll get right on it.": "谢谢!我们会马上去做的。", "Take me Home": "返回主页", @@ -89,12 +88,10 @@ "Copied!": "已复制!", "Confirm password": "确认密码", "Close": "关闭", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "点击开始则代表同意我们的<2>条款和条件<2>", "Change layout": "更改布局", "Camera": "摄像头", "Call type menu": "通话类型菜单", "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>哎哟,出问题了。", "<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.": "" diff --git a/public/locales/zh-Hant/app.json b/public/locales/zh-Hant/app.json index b507a755..3e17d33e 100644 --- a/public/locales/zh-Hant/app.json +++ b/public/locales/zh-Hant/app.json @@ -22,7 +22,6 @@ "Unmute microphone": "取消麥克風靜音", "Turn on camera": "開啟相機", "Turn off camera": "關閉相機", - "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 call already exists, would you like to join?": "通話已經開始,請問您要加入嗎?", "Thanks! We'll get right on it.": "謝謝您!我們會盡快處理。", "Take me Home": "帶我回主畫面", @@ -94,7 +93,6 @@ "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": "語音", From 31fcf0634f02e1982a8b72f823dba1e8a5791694 Mon Sep 17 00:00:00 2001 From: Linerly Date: Thu, 6 Jul 2023 12:02:12 +0000 Subject: [PATCH 12/76] Translated using Weblate (Indonesian) Currently translated at 98.2% (115 of 117 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/id/ --- public/locales/id/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/locales/id/app.json b/public/locales/id/app.json index 6db74b22..66b856ee 100644 --- a/public/locales/id/app.json +++ b/public/locales/id/app.json @@ -5,7 +5,6 @@ "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Pengguna yang lain di panggilan ini sedang mengalami masalah. Supaya dapat mendiagnosa masalah ini, kami ingin mengumpulkan sebuah catatan pengawakutuan.", "Audio": "Audio", "Avatar": "Avatar", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Dengan mengeklik \"Bergabung ke panggilan sekarang\", Anda terima <2>syarat dan ketentuan kami", "Call link copied": "Tautan panggilan disalin", "Call type menu": "Menu jenis panggilan", "Camera": "Kamera", @@ -113,5 +112,6 @@ "<0>We'd love to hear your feedback so we can improve your experience.": "<0>Kami ingin mendengar masukan Anda supaya kami bisa meningkatkan pengalaman Anda.", "Show connection stats": "Tampilkan statistik koneksi", "{{displayName}} is presenting": "{{displayName}} sedang menampilkan", - "{{count}} stars|other": "{{count}} bintang" + "{{count}} stars|other": "{{count}} bintang", + "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)": "Dengan mengeklik \"Bergabung\", Anda menyetujui <2>" } From e52b3e6d5302c6143ed78a1499f5e5ade046a5e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 6 Jul 2023 08:53:57 +0200 Subject: [PATCH 13/76] Add EULA config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- config/config.sample.json | 3 ++- src/auth/RegisterPage.tsx | 5 ++--- src/auth/useInteractiveRegistration.ts | 14 +++++++------- src/auth/useRegisterPasswordlessUser.ts | 2 +- src/config/ConfigOptions.ts | 6 ++++++ src/home/UnauthenticatedView.tsx | 6 +++--- src/room/RoomAuthView.tsx | 5 +++-- 7 files changed, 24 insertions(+), 17 deletions(-) diff --git a/config/config.sample.json b/config/config.sample.json index 7afb8f69..35ad1846 100644 --- a/config/config.sample.json +++ b/config/config.sample.json @@ -4,5 +4,6 @@ "base_url": "https://call.ems.host", "server_name": "call.ems.host" } - } + }, + "eula": "https://static.element.io/legal/online-EULA.pdf" } diff --git a/src/auth/RegisterPage.tsx b/src/auth/RegisterPage.tsx index 1fd3911c..c96dea29 100644 --- a/src/auth/RegisterPage.tsx +++ b/src/auth/RegisterPage.tsx @@ -54,8 +54,7 @@ export const RegisterPage: FC = () => { const [error, setError] = useState(); const [password, setPassword] = useState(""); const [passwordConfirmation, setPasswordConfirmation] = useState(""); - const [privacyPolicyUrl, recaptchaKey, register] = - useInteractiveRegistration(); + const { recaptchaKey, register } = useInteractiveRegistration(); const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey); const onSubmitRegisterForm = useCallback( @@ -211,7 +210,7 @@ export const RegisterPage: FC = () => { apply.
By clicking "Register", you agree to our{" "} - + End User Licensing Agreement (EULA) diff --git a/src/auth/useInteractiveRegistration.ts b/src/auth/useInteractiveRegistration.ts index 156842b0..2db33773 100644 --- a/src/auth/useInteractiveRegistration.ts +++ b/src/auth/useInteractiveRegistration.ts @@ -22,17 +22,17 @@ import { initClient } from "../matrix-utils"; import { Session } from "../ClientContext"; import { Config } from "../config/Config"; -export const useInteractiveRegistration = (): [ - string, - string, - ( +export const useInteractiveRegistration = (): { + privacyPolicyUrl: string; + recaptchaKey: string; + register: ( username: string, password: string, displayName: string, recaptchaResponse: string, passwordlessUser?: boolean - ) => Promise<[MatrixClient, Session]> -] => { + ) => Promise<[MatrixClient, Session]>; +} => { const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState(); const [recaptchaKey, setRecaptchaKey] = useState(); @@ -126,5 +126,5 @@ export const useInteractiveRegistration = (): [ [] ); - return [privacyPolicyUrl, recaptchaKey, register]; + return { privacyPolicyUrl, recaptchaKey, register }; }; diff --git a/src/auth/useRegisterPasswordlessUser.ts b/src/auth/useRegisterPasswordlessUser.ts index d780e99b..94d40cd2 100644 --- a/src/auth/useRegisterPasswordlessUser.ts +++ b/src/auth/useRegisterPasswordlessUser.ts @@ -30,7 +30,7 @@ interface UseRegisterPasswordlessUserType { export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType { const { setClient } = useClient(); - const [privacyPolicyUrl, recaptchaKey, register] = + const { privacyPolicyUrl, recaptchaKey, register } = useInteractiveRegistration(); const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey); diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index f878ec5e..cbbc107c 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -66,6 +66,11 @@ export interface ConfigOptions { features?: { feature_group_calls_without_video_and_audio: boolean; }; + + /** + * A link to the end-user license agreement (EULA) + */ + eula: string; } // Overrides members from ConfigOptions that are always provided by the @@ -86,4 +91,5 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = { server_name: "localhost", }, }, + eula: "https://static.element.io/legal/online-EULA.pdf", }; diff --git a/src/home/UnauthenticatedView.tsx b/src/home/UnauthenticatedView.tsx index 32877e4f..25da76fe 100644 --- a/src/home/UnauthenticatedView.tsx +++ b/src/home/UnauthenticatedView.tsx @@ -41,6 +41,7 @@ import commonStyles from "./common.module.css"; import { generateRandomName } from "../auth/generateRandomName"; import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; import { useOptInAnalytics } from "../settings/useSetting"; +import { Config } from "../config/Config"; export const UnauthenticatedView: FC = () => { const { setClient } = useClient(); @@ -48,8 +49,7 @@ export const UnauthenticatedView: FC = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(); const [optInAnalytics] = useOptInAnalytics(); - const [privacyPolicyUrl, recaptchaKey, register] = - useInteractiveRegistration(); + const { recaptchaKey, register } = useInteractiveRegistration(); const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey); const { modalState, modalProps } = useModalTriggerState(); @@ -165,7 +165,7 @@ export const UnauthenticatedView: FC = () => { By clicking "Go", you agree to our{" "} - + End User Licensing Agreement (EULA) diff --git a/src/room/RoomAuthView.tsx b/src/room/RoomAuthView.tsx index e12013eb..8a50d310 100644 --- a/src/room/RoomAuthView.tsx +++ b/src/room/RoomAuthView.tsx @@ -26,12 +26,13 @@ import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { Form } from "../form/Form"; import { UserMenuContainer } from "../UserMenuContainer"; import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; +import { Config } from "../config/Config"; export function RoomAuthView() { const [loading, setLoading] = useState(false); const [error, setError] = useState(); - const { registerPasswordlessUser, recaptchaId, privacyPolicyUrl } = + const { registerPasswordlessUser, recaptchaId } = useRegisterPasswordlessUser(); const onSubmit = useCallback( @@ -83,7 +84,7 @@ export function RoomAuthView() { By clicking "Join call now", you agree to our{" "} - + End User Licensing Agreement (EULA) From b9e15ab9929968456d2e42dd90a2e2ec1e2d1d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 7 Jul 2023 10:11:27 +0200 Subject: [PATCH 14/76] Fix sizing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/E2EEBanner.module.css | 3 ++- src/E2EEBanner.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/E2EEBanner.module.css b/src/E2EEBanner.module.css index 80b06bba..dd777493 100644 --- a/src/E2EEBanner.module.css +++ b/src/E2EEBanner.module.css @@ -18,5 +18,6 @@ limitations under the License. display: flex; flex-direction: row; align-items: center; - gap: 16px; + gap: 12px; + font-size: var(--font-size-caption); } diff --git a/src/E2EEBanner.tsx b/src/E2EEBanner.tsx index 78e68155..08d1ffbb 100644 --- a/src/E2EEBanner.tsx +++ b/src/E2EEBanner.tsx @@ -24,7 +24,7 @@ export const E2EEBanner = () => { return (
- + Element Call is temporarily not encrypted while we test scalability. From 7a47d0504dabf2a4b7f089e50e9b5f155248d270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 7 Jul 2023 10:14:04 +0200 Subject: [PATCH 15/76] Size improvement numero dos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/E2EELock.module.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/E2EELock.module.css b/src/E2EELock.module.css index 8d6e95fa..504ace7b 100644 --- a/src/E2EELock.module.css +++ b/src/E2EELock.module.css @@ -15,8 +15,8 @@ limitations under the License. */ .e2eeLock { - width: 32px; - height: 32px; + width: 24px; + height: 24px; display: flex; align-items: center; From 23b1a28790f47fb6e2bc14f6794d03ff251c6ae5 Mon Sep 17 00:00:00 2001 From: Linerly Date: Thu, 6 Jul 2023 12:04:55 +0000 Subject: [PATCH 16/76] Translated using Weblate (Indonesian) Currently translated at 100.0% (117 of 117 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/id/ --- public/locales/id/app.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/locales/id/app.json b/public/locales/id/app.json index 66b856ee..bc1d49eb 100644 --- a/public/locales/id/app.json +++ b/public/locales/id/app.json @@ -90,7 +90,7 @@ "Your recent calls": "Panggilan Anda terkini", "{{names}}, {{name}}": "{{names}}, {{name}}", "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", + "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Bergabung ke panggilan sekarang<1>Atau<2>Salin tautan dan bergabung nanti", "Element Call Home": "Beranda Element Call", "Copy": "Salin", "<0>Submitting debug logs will help us track down the problem.": "<0>Mengirim catatan pengawakutuan akan membantu kami melacak masalahnya.", @@ -113,5 +113,7 @@ "Show connection stats": "Tampilkan statistik koneksi", "{{displayName}} is presenting": "{{displayName}} sedang menampilkan", "{{count}} stars|other": "{{count}} bintang", - "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)": "Dengan mengeklik \"Bergabung\", Anda menyetujui <2>" + "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)": "Dengan mengeklik \"Bergabung\", Anda menyetujui <2>Perjanjian Lisensi Pengguna Akhir (EULA)", + "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>End User Licensing Agreement (EULA)": "Situs ini dilindungi oleh reCAPTCHA dan <2>Kebijakan Privasi dan <6>Ketentuan Layanan Google berlaku.<9>Dengan mengeklik \"Daftar\", Anda menyetujui <12>Perjanjian Lisensi Pengguna Akhir (EULA) kami", + "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)": "Dengan mengeklik \"Bergabung ke panggilan sekarang\", Anda menyetujui <2>Perjanjian Lisensi Pengguna Akhir (EULA) kami" } From f48314bd9be8434a52c97e3348a3d76b1368e24f Mon Sep 17 00:00:00 2001 From: Glandos Date: Fri, 7 Jul 2023 07:10:11 +0000 Subject: [PATCH 17/76] Translated using Weblate (French) Currently translated at 100.0% (117 of 117 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/fr/ --- public/locales/fr/app.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/locales/fr/app.json b/public/locales/fr/app.json index 3ffcfc2a..5a942f85 100644 --- a/public/locales/fr/app.json +++ b/public/locales/fr/app.json @@ -4,7 +4,6 @@ "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Un autre utilisateur dans cet appel a un problème. Pour nous permettre de résoudre le problème, nous aimerions récupérer un journal de débogage.", "Audio": "Audio", "Avatar": "Avatar", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "En cliquant sur « Rejoindre l’appel » vous acceptez nos <2>conditions d’utilisation", "Call link copied": "Lien de l’appel copié", "Call type menu": "Menu de type d’appel", "Camera": "Caméra", @@ -113,5 +112,8 @@ "<0>Thanks for your feedback!": "<0>Merci pour votre commentaire !", "How did it go?": "Comment cela s’est-il passé ?", "{{displayName}} is presenting": "{{displayName}} est à l’écran", - "Show connection stats": "Afficher les statistiques de la connexion" + "Show connection stats": "Afficher les statistiques de la connexion", + "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)": "En cliquant sur « Rejoindre l’appel maintenant », vous acceptez notre <2>Contrat de Licence Utilisateur Final (CLUF)", + "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)": "En cliquant sur « Commencer », vous acceptez notre <2>Contrat de Licence Utilisateur Final (CLUF)", + "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>End User Licensing Agreement (EULA)": "Ce site est protégé par ReCAPTCHA, la <2>politique de confidentialité et les <6>conditions d’utilisation de Google s’appliquent.<9>En cliquant sur « S’enregistrer » vous acceptez également notre <12>Contrat de Licence Utilisateur Final (CLUF)" } From e466270cb19b2611e516e578b1e41a26e57731f8 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Thu, 6 Jul 2023 18:25:08 +0000 Subject: [PATCH 18/76] Translated using Weblate (Ukrainian) Currently translated at 100.0% (117 of 117 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/uk/ --- public/locales/uk/app.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/locales/uk/app.json b/public/locales/uk/app.json index 00635a7c..be0da18a 100644 --- a/public/locales/uk/app.json +++ b/public/locales/uk/app.json @@ -83,7 +83,6 @@ "Camera": "Камера", "Call type menu": "Меню типу виклику", "Call link copied": "Посилання на виклик скопійовано", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "Натиснувши «Далі», ви погодитеся з нашими <2>Умовами та положеннями", "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.": "Інший користувач у цьому виклику має проблему. Щоб краще визначити ці проблеми, ми хотіли б зібрати журнал налагодження.", @@ -113,5 +112,8 @@ "<0>We'd love to hear your feedback so we can improve your experience.": "<0>Ми будемо раді почути ваші відгуки, щоб поліпшити роботу застосунку.", "How did it go?": "Вам усе сподобалось?", "{{displayName}} is presenting": "{{displayName}} представляє", - "Show connection stats": "Показати стан з'єднання" + "Show connection stats": "Показати стан з'єднання", + "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)": "Натискаючи \"Далі\", ви погоджуєтеся з нашою <2>Ліцензійною угодою з кінцевим користувачем (EULA)", + "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)": "Натискаючи \"Приєднатися до виклику зараз\", ви погоджуєтеся з нашою <2>Ліцензійною угодою з кінцевим користувачем (EULA)", + "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>End User Licensing Agreement (EULA)": "Цей сайт захищений ReCAPTCHA і до нього застосовується <2>Політика приватності і <6>Умови надання послуг Google.<9>Натискаючи \"Зареєструватися\", ви погоджуєтеся з нашою <12>Ліцензійною угодою з кінцевим користувачем (EULA)" } From 8ddb1c42016bdd296af444894efd9324c8fd5049 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Fri, 7 Jul 2023 02:03:36 +0000 Subject: [PATCH 19/76] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (117 of 117 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, 4 insertions(+), 2 deletions(-) diff --git a/public/locales/zh-Hant/app.json b/public/locales/zh-Hant/app.json index 3e17d33e..fccd94ab 100644 --- a/public/locales/zh-Hant/app.json +++ b/public/locales/zh-Hant/app.json @@ -93,7 +93,6 @@ "Camera": "相機", "Call type menu": "通話類型選單", "Call link copied": "已複製通話連結", - "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.": "這通對話中的另一位使用者遇到了某些問題。為了診斷問題,我們將會建立除錯紀錄。", @@ -113,5 +112,8 @@ "{{displayName}}, your call has ended.": "{{displayName}},您的通話已結束。", "How did it go?": "進展如何?", "{{displayName}} is presenting": "{{displayName}} 正在展示", - "Show connection stats": "顯示連線統計資料" + "Show connection stats": "顯示連線統計資料", + "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)": "點擊「前往」即表示您同意我們的<2>終端使用者授權協議 (EULA)", + "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)": "點擊「立刻加入通話」即表示您同意我們的<2>終端使用者授權協議 (EULA)", + "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>End User Licensing Agreement (EULA)": "此網站被 ReCAPTCHA 保護,並適用 Google 的<2>隱私權政策與<6>服務條款。<9>點擊「註冊」即表示您同意我們的<12>終端使用者授權協議 (EULA)" } From f3c12f98cc8c352c3143e4d95c4e53e742727f74 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Thu, 6 Jul 2023 22:29:04 +0000 Subject: [PATCH 20/76] Translated using Weblate (Slovak) Currently translated at 100.0% (117 of 117 strings) Translation: Element Call/element-call Translate-URL: https://translate.element.io/projects/element-call/element-call/sk/ --- public/locales/sk/app.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/locales/sk/app.json b/public/locales/sk/app.json index f87e380a..12737076 100644 --- a/public/locales/sk/app.json +++ b/public/locales/sk/app.json @@ -85,7 +85,6 @@ "Camera": "Kamera", "Call type menu": "Ponuka typu hovoru", "Call link copied": "Odkaz na hovor skopírovaný", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "Kliknutím na tlačidlo \"Prejsť\" súhlasíte s našimi <2>Podmienkami", "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í.", @@ -113,5 +112,8 @@ "<0>Thanks for your feedback!": "<0> Ďakujeme za vašu spätnú väzbu!", "<0>We'd love to hear your feedback so we can improve your experience.": "<0> Radi si vypočujeme vašu spätnú väzbu, aby sme mohli zlepšiť vaše skúsenosti.", "{{displayName}} is presenting": "{{displayName}} prezentuje", - "Show connection stats": "Zobraziť štatistiky pripojenia" + "Show connection stats": "Zobraziť štatistiky pripojenia", + "By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)": "Kliknutím na \"Pripojiť sa k hovoru teraz\" súhlasíte s našou <2>Licenčnou zmluvou s koncovým používateľom (EULA)", + "By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)": "Kliknutím na tlačidlo \"Prejsť\" vyjadrujete súhlas s našou <2>Licenčnou zmluvou s koncovým používateľom (EULA)", + "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>End User Licensing Agreement (EULA)": "Táto stránka je chránená systémom ReCAPTCHA a platia na ňu <2>Pravidlá ochrany osobných údajov spoločnosti Google a <6>Podmienky poskytovania služieb.<9>Kliknutím na tlačidlo \"Registrovať sa\" súhlasíte s našou <12>Licenčnou zmluvou s koncovým používateľom (EULA)" } From 70c4edc76175bbbf7b24d6d3084aca1f6af6d0b3 Mon Sep 17 00:00:00 2001 From: Weblate Date: Fri, 7 Jul 2023 10:37:04 +0000 Subject: [PATCH 21/76] 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/es/app.json | 1 - public/locales/et/app.json | 1 - public/locales/fa/app.json | 1 - public/locales/ja/app.json | 1 - public/locales/ru/app.json | 1 - public/locales/tr/app.json | 1 - 9 files changed, 9 deletions(-) diff --git a/public/locales/bg/app.json b/public/locales/bg/app.json index 4555edb9..34561abd 100644 --- a/public/locales/bg/app.json +++ b/public/locales/bg/app.json @@ -5,7 +5,6 @@ "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Друг потребител в този разговор има проблем. За да диагностицираме този проблем по-добре ни се иска да съберем debug логове.", "Audio": "Звук", "Avatar": "Аватар", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Натискайки \"Влез в разговора сега\", се съгласявате с нашите <2>Правила и условия", "Call link copied": "Връзка към разговора бе копирана", "Call type menu": "Меню \"тип на разговора\"", "Camera": "Камера", diff --git a/public/locales/cs/app.json b/public/locales/cs/app.json index 86ad6326..3857c05f 100644 --- a/public/locales/cs/app.json +++ b/public/locales/cs/app.json @@ -89,7 +89,6 @@ "Create account": "Vytvořit účet", "Copy": "Kopírovat", "Call type menu": "Menu typu hovoru", - "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í.", "<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 6d5f71a9..9318c128 100644 --- a/public/locales/de/app.json +++ b/public/locales/de/app.json @@ -5,7 +5,6 @@ "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ein anderer Benutzer dieses Anrufs hat ein Problem. Um es besser diagnostizieren zu können, würden wir gerne ein Debug-Protokoll erstellen.", "Audio": "Audio", "Avatar": "Avatar", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "Wenn du auf „Anruf beitreten“ klickst, akzeptierst du unsere <2>Geschäftsbedingungen", "Call link copied": "Anruflink kopiert", "Call type menu": "Anruftyp Menü", "Camera": "Kamera", diff --git a/public/locales/es/app.json b/public/locales/es/app.json index 91504693..d4027923 100644 --- a/public/locales/es/app.json +++ b/public/locales/es/app.json @@ -6,7 +6,6 @@ "Register": "Registrarse", "Not registered yet? <2>Create an account": "¿No estás registrado todavía? <2>Crear una cuenta", "Login to your account": "Iniciar sesión en tu cuenta", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "Al hacer clic en \"Comenzar\" aceptarás nuestros <2>Términos y condiciones", "Yes, join call": "Si, unirse a la llamada", "Walkie-talkie call name": "Nombre de la llamada Walkie-talkie", "Walkie-talkie call": "Llamada Walkie-talkie", diff --git a/public/locales/et/app.json b/public/locales/et/app.json index 1c0ac653..778f6859 100644 --- a/public/locales/et/app.json +++ b/public/locales/et/app.json @@ -32,7 +32,6 @@ "Camera": "Kaamera", "Call type menu": "Kõnetüübi valik", "Call link copied": "Kõne link on kopeeritud", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "Klõpsides „Jätka“nõustud sa meie <2>kasutustingimustega", "Avatar": "Tunnuspilt", "Audio": "Heli", "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ühel teisel selles kõnes osalejal on lahenduse kasutamisel tekkinud probleem ning selle põhjuse leidmiseks me sooviksime koguda silumislogisid.", diff --git a/public/locales/fa/app.json b/public/locales/fa/app.json index 782e1f31..4014d69c 100644 --- a/public/locales/fa/app.json +++ b/public/locales/fa/app.json @@ -45,7 +45,6 @@ "Camera": "دوربین", "Call type menu": "منوی نوع تماس", "Call link copied": "لینک تماس کپی شد", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "با کلیک بر روی برو، شما با <2>شرایط و قوانین استفاده موافقت می‌کنید", "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.": "کاربر دیگری در این تماس مشکلی دارد. برای تشخیص بهتر مشکل، بهتر است ما لاگ عیب‌یابی را جمع‌آوری کنیم.", diff --git a/public/locales/ja/app.json b/public/locales/ja/app.json index 1686ac3a..e1d01ea8 100644 --- a/public/locales/ja/app.json +++ b/public/locales/ja/app.json @@ -5,7 +5,6 @@ "<0>Oops, something's gone wrong.": "<0>何かがうまく行きませんでした。", "Camera": "カメラ", "Call link copied": "通話リンクをコピーしました", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "「続行」をクリックすると、 <2>利用規約に同意したとみなされます", "Avatar": "アバター", "Audio": "音声", "Confirm password": "パスワードを確認", diff --git a/public/locales/ru/app.json b/public/locales/ru/app.json index c6bd0236..e4dda885 100644 --- a/public/locales/ru/app.json +++ b/public/locales/ru/app.json @@ -11,7 +11,6 @@ "Select an option": "Выберите вариант", "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}", "Grid layout menu": "Меню \"Расположение сеткой\"", - "By clicking \"Go\", you agree to our <2>Terms and conditions": "Нажимая \"Далее\", вы соглашаетесь с нашими <2>положениями и условиями", "<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>Так вы можете оставить своё имя и задать аватар для будущих звонков.", "<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>Зайти как гость", diff --git a/public/locales/tr/app.json b/public/locales/tr/app.json index 975251a1..b2494fe5 100644 --- a/public/locales/tr/app.json +++ b/public/locales/tr/app.json @@ -3,7 +3,6 @@ "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Bu aramadaki başka bir kullanıcı sorun yaşıyor. Sorunu daha iyi çözebilmemiz için hata ayıklama kütüğünü almak isteriz.", "Audio": "Ses", "Avatar": "Avatar", - "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "\"Şimdi katıl\"a tıklayarak, <2>hükümler ve koşulları kabul etmiş sayılırsınız", "Call link copied": "Arama bağlantısı kopyalandı", "Call type menu": "Arama tipi menüsü", "Camera": "Kamera", From 008d22a2a0868804cdba9b177092a77474a5a4a6 Mon Sep 17 00:00:00 2001 From: Daniel Abramov Date: Fri, 7 Jul 2023 12:36:29 +0100 Subject: [PATCH 22/76] Use unique and stable room names for LiveKit Fixes #1165. --- src/room/GroupCallView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 64c3e965..7df0a980 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -231,7 +231,7 @@ export function GroupCallView({ Date: Fri, 7 Jul 2023 14:17:09 +0200 Subject: [PATCH 23/76] Fix js-sdk version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 2fed945c..e236b18a 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "i18next-http-backend": "^1.4.4", "livekit-client": "^1.9.7", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#426d29d6b9a9d71a3c0d7fe6f7bac3473cd10832", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#4b3406daa95c8f969f386341b8b632ba4a60501a", "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/yarn.lock b/yarn.lock index 0051059e..a23c8853 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10968,9 +10968,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#af10b0c44b4a427c8d2224bfd6feb03a12cfd27e": +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#4b3406daa95c8f969f386341b8b632ba4a60501a": version "26.0.1" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/af10b0c44b4a427c8d2224bfd6feb03a12cfd27e" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4b3406daa95c8f969f386341b8b632ba4a60501a" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.10" From 9be925012440a17c1a6e5ab1025d637181906986 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Fri, 7 Jul 2023 14:41:29 +0200 Subject: [PATCH 24/76] Combined permission request with newer livekit sdk version (#1200) --------- Signed-off-by: Timo K --- package.json | 4 +- ...aDevices.ts => useMediaDevicesSwitcher.ts} | 17 +- src/room/InCallView.tsx | 6 +- src/room/VideoPreview.tsx | 149 ++++++++++-------- src/settings/SettingsModal.tsx | 9 +- yarn.lock | 59 ++++--- 6 files changed, 137 insertions(+), 107 deletions(-) rename src/livekit/{useMediaDevices.ts => useMediaDevicesSwitcher.ts} (84%) diff --git a/package.json b/package.json index e236b18a..09713cc6 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@juggle/resize-observer": "^3.3.1", - "@livekit/components-react": "^1.0.3", + "@livekit/components-react": "^1.0.7", "@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", @@ -55,7 +55,7 @@ "i18next": "^21.10.0", "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", - "livekit-client": "^1.9.7", + "livekit-client": "^1.11.4", "lodash": "^4.17.21", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#4b3406daa95c8f969f386341b8b632ba4a60501a", "matrix-widget-api": "^1.3.1", diff --git a/src/livekit/useMediaDevices.ts b/src/livekit/useMediaDevicesSwitcher.ts similarity index 84% rename from src/livekit/useMediaDevices.ts rename to src/livekit/useMediaDevicesSwitcher.ts index e5b0a151..303d73cd 100644 --- a/src/livekit/useMediaDevices.ts +++ b/src/livekit/useMediaDevicesSwitcher.ts @@ -1,5 +1,5 @@ import { useMediaDeviceSelect } from "@livekit/components-react"; -import { Room } from "livekit-client"; +import { LocalAudioTrack, LocalVideoTrack, Room } from "livekit-client"; import { useEffect } from "react"; import { useDefaultDevices } from "../settings/useSetting"; @@ -17,12 +17,21 @@ export type MediaDevicesState = { }; // if a room is passed this only affects the device selection inside a call. Without room it changes what we see in the lobby -export function useMediaDevices(room?: Room): MediaDevicesState { +export function useMediaDevicesSwitcher( + room?: Room, + tracks?: { videoTrack?: LocalVideoTrack; audioTrack?: LocalAudioTrack }, + requestPermissions = true +): MediaDevicesState { const { devices: videoDevices, activeDeviceId: activeVideoDevice, setActiveMediaDevice: setActiveVideoDevice, - } = useMediaDeviceSelect({ kind: "videoinput", room }); + } = useMediaDeviceSelect({ + kind: "videoinput", + room, + track: tracks?.videoTrack, + requestPermissions, + }); const { devices: audioDevices, @@ -31,6 +40,8 @@ export function useMediaDevices(room?: Room): MediaDevicesState { } = useMediaDeviceSelect({ kind: "audioinput", room, + track: tracks?.audioTrack, + requestPermissions, }); const { diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index b82b1513..8262f34d 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -80,7 +80,7 @@ import { useRageshakeRequestModal } from "../settings/submit-rageshake"; import { RageshakeRequestModal } from "./RageshakeRequestModal"; import { VideoTile } from "../video-grid/VideoTile"; import { UserChoices, useLiveKit } from "../livekit/useLiveKit"; -import { useMediaDevices } from "../livekit/useMediaDevices"; +import { useMediaDevicesSwitcher } from "../livekit/useMediaDevicesSwitcher"; import { useFullscreen } from "./useFullscreen"; import { useLayoutStates } from "../video-grid/Layout"; import { useSFUConfig } from "../livekit/OpenIDLoader"; @@ -148,7 +148,7 @@ export function InCallView({ ); // Managed media devices state coupled with an active room. - const roomMediaDevices = useMediaDevices(livekitRoom); + const roomMediaSwitcher = useMediaDevicesSwitcher(livekitRoom); const screenSharingTracks = useTracks( [{ source: Track.Source.ScreenShare, withPlaceholder: false }], @@ -427,7 +427,7 @@ export function InCallView({ )} diff --git a/src/room/VideoPreview.tsx b/src/room/VideoPreview.tsx index 9f6c6276..5f401c9b 100644 --- a/src/room/VideoPreview.tsx +++ b/src/room/VideoPreview.tsx @@ -14,11 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useState, useEffect, useRef, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useRef } from "react"; import useMeasure from "react-use-measure"; import { ResizeObserver } from "@juggle/resize-observer"; import { OverlayTriggerState } from "@react-stately/overlays"; -import { usePreviewDevice } from "@livekit/components-react"; +import { usePreviewTracks } from "@livekit/components-react"; +import { LocalAudioTrack, LocalVideoTrack, Track } from "livekit-client"; import { MicButton, SettingsButton, VideoButton } from "../button"; import { Avatar } from "../Avatar"; @@ -26,7 +27,7 @@ import styles from "./VideoPreview.module.css"; import { useModalTriggerState } from "../Modal"; import { SettingsModal } from "../settings/SettingsModal"; import { useClient } from "../ClientContext"; -import { useMediaDevices } from "../livekit/useMediaDevices"; +import { useMediaDevicesSwitcher } from "../livekit/useMediaDevicesSwitcher"; import { DeviceChoices, UserChoices } from "../livekit/useLiveKit"; import { useDefaultDevices } from "../settings/useSetting"; @@ -61,85 +62,107 @@ export function VideoPreview({ matrixInfo, onUserChoicesChanged }: Props) { settingsModalState.open(); }, [settingsModalState]); - // Fetch user media devices. - const mediaDevices = useMediaDevices(); - // Create local media tracks. const [videoEnabled, setVideoEnabled] = useState(true); const [audioEnabled, setAudioEnabled] = useState(true); - const [videoId, audioId] = [ - mediaDevices.videoIn.selectedId, - mediaDevices.audioIn.selectedId, - ]; - const [defaultDevices] = useDefaultDevices(); - const video = usePreviewDevice( - videoEnabled, - videoId != "" ? videoId : defaultDevices.videoinput, - "videoinput" - ); - const audio = usePreviewDevice( - audioEnabled, - audioId != "" ? audioId : defaultDevices.audioinput, - "audioinput" - ); - const activeVideoId = video?.selectedDevice?.deviceId; - const activeAudioId = audio?.selectedDevice?.deviceId; + // The settings are updated as soon as the device changes. We wrap the settings value in a ref to store their initial value. + // Not changing the device options prohibits the usePreviewTracks hook to recreate the tracks. + const initialDefaultDevices = useRef(useDefaultDevices()[0]); + + const tracks = usePreviewTracks( + { + audio: { deviceId: initialDefaultDevices.current.audioinput }, + video: { deviceId: initialDefaultDevices.current.videoinput }, + }, + (error) => { + console.error("Error while creating preview Tracks:", error); + } + ); + const videoTrack = React.useMemo( + () => + tracks?.filter((t) => t.kind === Track.Kind.Video)[0] as LocalVideoTrack, + [tracks] + ); + const audioTrack = React.useMemo( + () => + tracks?.filter((t) => t.kind === Track.Kind.Audio)[0] as LocalAudioTrack, + [tracks] + ); + // Only let the MediaDeviceSwitcher request permissions if a video track is already available. + // Otherwise we would end up asking for permissions in usePreviewTracks and in useMediaDevicesSwitcher. + const requestPermissions = !!videoTrack; + const mediaSwitcher = useMediaDevicesSwitcher( + undefined, + { + videoTrack, + audioTrack, + }, + requestPermissions + ); + const { videoIn, audioIn } = mediaSwitcher; + + const videoEl = React.useRef(null); + useEffect(() => { + // Effect to update the settings const createChoices = ( enabled: boolean, deviceId?: string ): DeviceChoices | undefined => { - if (deviceId === undefined) { - return undefined; - } - - return { - selectedId: deviceId, - enabled, - }; + return deviceId + ? { + selectedId: deviceId, + enabled, + } + : undefined; }; - onUserChoicesChanged({ - video: createChoices(videoEnabled, activeVideoId), - audio: createChoices(audioEnabled, activeAudioId), + video: createChoices(videoEnabled, videoIn.selectedId), + audio: createChoices(audioEnabled, audioIn.selectedId), }); }, [ onUserChoicesChanged, - activeVideoId, + videoIn.selectedId, videoEnabled, - activeAudioId, + audioIn.selectedId, audioEnabled, ]); - const [selectVideo, selectAudio] = [ - mediaDevices.videoIn.setSelected, - mediaDevices.audioIn.setSelected, - ]; useEffect(() => { - if (activeVideoId && activeVideoId !== "") { - selectVideo(activeVideoId); + // Effect to update the initial device selection for the ui elements based on the current preview track. + if (!videoIn.selectedId || videoIn.selectedId == "") { + videoTrack?.getDeviceId().then((videoId) => { + if (videoId) { + videoIn.setSelected(videoId); + } + }); } - if (activeAudioId && activeAudioId !== "") { - selectAudio(activeAudioId); + if (!audioIn.selectedId || audioIn.selectedId == "") { + audioTrack?.getDeviceId().then((audioId) => { + if (audioId) { + audioIn.setSelected(audioId); + } + }); } - }, [selectVideo, selectAudio, activeVideoId, activeAudioId]); + }, [videoIn, audioIn, videoTrack, audioTrack]); - const mediaElement = useRef(null); useEffect(() => { - if (mediaElement.current) { - video?.localTrack?.attach(mediaElement.current); + // Effect to connect the videoTrack with the video element. + if (videoEl.current) { + videoTrack?.unmute(); + videoTrack?.attach(videoEl.current); } return () => { - video?.localTrack?.detach(); + videoTrack?.detach(); }; - }, [video?.localTrack, mediaElement]); + }, [videoTrack]); return (
-