diff --git a/README.md b/README.md index c80c7900..997af57c 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ Full mesh group calls powered by [Matrix](https://matrix.org), implementing [MatrixRTC](https://github.com/matrix-org/matrix-spec-proposals/blob/matthew/group-voip/proposals/3401-group-voip.md). +![A demo of Element Call with six people](demo.jpg) + To try it out, visit our hosted version at [call.element.io](https://call.element.io). You can also find the latest development version continuously deployed to [element-call.netlify.app](https://element-call.netlify.app). ## Host it yourself diff --git a/demo.jpg b/demo.jpg new file mode 100644 index 00000000..afce1ae3 Binary files /dev/null and b/demo.jpg differ diff --git a/package.json b/package.json index df74b20e..c896f3f7 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "i18next": "^21.10.0", "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#278dd6a3d35cf89c03f9172f9c81579577a267b3", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#ac97928a5d081b0502952295eea5b8fa7bdf8839", "matrix-widget-api": "^1.0.0", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/public/locales/es/app.json b/public/locales/es/app.json index 95eb0a51..c41d345c 100644 --- a/public/locales/es/app.json +++ b/public/locales/es/app.json @@ -136,5 +136,8 @@ "This will send anonymised data (such as the duration of a call and the number of participants) to the Element Call team to help us optimise the application based on how it is used.": "Esto enviará datos anónimos (como la duración de la llamada y el número de participantes) al equipo de Element Call para ayudarnos a optimizar la aplicación dependiendo de cómo se use.", "Whether to enable single-key keyboard shortcuts, e.g. 'm' to mute/unmute the mic.": "Habilita los atajos de teclado de una sola tecla, por ejemplo 'm' para silenciar/desilenciar el micrófono.", "Single-key keyboard shortcuts": "Atajos de teclado de una sola tecla", - "{{name}} (Waiting for video...)": "{{name}} (Esperando al video...)" + "{{name}} (Waiting for video...)": "{{name}} (Esperando al video...)", + "This feature is only supported on Firefox.": "Esta característica solo está disponible en Firefox.", + "<0>Submitting debug logs will help us track down the problem.": "<0>Subir los registros de depuración nos ayudará a encontrar el problema.", + "<0>Oops, something's gone wrong.": "<0>Ups, algo ha salido mal." } diff --git a/public/locales/ja/app.json b/public/locales/ja/app.json index 0967ef42..fd876f14 100644 --- a/public/locales/ja/app.json +++ b/public/locales/ja/app.json @@ -1 +1,95 @@ -{} +{ + "{{name}} (Connecting...)": "{{name}}(接続しています…)", + "{{count}} people connected|other": "{{count}}人が接続済", + "{{count}} people connected|one": "{{count}}人が接続済", + "{{name}} (Waiting for video...)": "{{name}}(ビデオを待機しています…)", + "<0>Already have an account?<1><0>Log in Or <2>Access as a guest": "<0>既にアカウントをお持ちですか?<1><0>ログインまたは<2>ゲストとしてアクセス", + "{{roomName}} - Walkie-talkie call": "{{roomName}} - トランシーバー通話", + "<0>Create an account Or <2>Access as a guest": "<0>アカウントを作成または<2>ゲストとしてアクセス", + "<0>Join call now<1>Or<2>Copy call link and join later": "<0>今すぐ通話に参加<1>または<2>通話リンクをコピーし、後で参加", + "Accept camera/microphone permissions to join the call.": "通話に参加するには、カメラ・マイクの許可が必要です。", + "<0>Oops, something's gone wrong.": "<0>何かがうまく行きませんでした。", + "Camera/microphone permissions needed to join the call.": "通話に参加する場合、カメラ・マイクの許可が必要です。", + "Allow analytics": "アナリティクスを許可", + "Camera": "カメラ", + "Call link copied": "通話リンクをコピーしました", + "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "「今すぐ通話に参加」をクリックすると、<2>利用規約に同意したとみなされます", + "By clicking \"Go\", you agree to our <2>Terms and conditions": "「続行」をクリックすると、 <2>利用規約に同意したとみなされます", + "Avatar": "アバター", + "Accept microphone permissions to join the call.": "通話に参加するには、マイクの許可が必要です。", + "Audio": "音声", + "Advanced": "高度", + "Connection lost": "接続が切断されました", + "Confirm password": "パスワードを確認", + "Close": "閉じる", + "Change layout": "レイアウトを変更", + "Copied!": "コピーしました!", + "Copy and share this call link": "通話リンクをコピーし共有", + "Copy": "コピー", + "Description (optional)": "概要(任意)", + "Debug log": "デバッグログ", + "Create account": "アカウントを作成", + "Having trouble? Help us fix it.": "問題が起きましたか?修正にご協力ください。", + "Go": "続行", + "Fetching group call timed out.": "グループ通話の取得がタイムアウトしました。", + "Element Call Home": "Element Call ホーム", + "Download debug logs": "デバッグログをダウンロード", + "Display name": "表示名", + "Developer": "開発者", + "Details": "詳細", + "Full screen": "全画面表示", + "Exit full screen": "全画面表示を終了", + "Include debug logs": "デバッグログを含める", + "Home": "ホーム", + "Incompatible versions!": "互換性のないバージョンです!", + "Incompatible versions": "互換性のないバージョン", + "Join existing call?": "既存の通話に参加しますか?", + "Join call now": "今すぐ通話に参加", + "Join call": "通話に参加", + "Invite": "招待", + "Invite people": "連絡先を招待", + "Not registered yet? <2>Create an account": "アカウントがありませんか? <2>アカウントを作成", + "Mute microphone": "マイクをミュート", + "Microphone permissions needed to join the call.": "通話の参加にはマイクの許可が必要です。", + "Microphone": "マイク", + "Login": "ログイン", + "Logging in…": "ログインしています…", + "Loading…": "読み込んでいます…", + "Loading room…": "ルームを読み込んでいます…", + "Leave": "退出", + "Version: {{version}}": "バージョン:{{version}}", + "Username": "ユーザー名", + "User menu": "ユーザーメニュー", + "User ID": "ユーザーID", + "Unmute microphone": "マイクのミュートを解除", + "Turn on camera": "カメラをつける", + "Turn off camera": "カメラを切る", + "Submitting feedback…": "フィードバックを送信しています…", + "Submit feedback": "フィードバックを送信", + "Stop sharing screen": "画面共有を停止", + "Spotlight": "スポットライト", + "Send debug logs": "デバッグログを送信", + "Sign out": "サインアウト", + "Sign in": "サインイン", + "Share screen": "画面共有", + "Settings": "設定", + "Sending…": "送信しています…", + "Sending debug logs…": "デバッグログを送信しています…", + "Saving…": "保存しています…", + "Save": "保存", + "Return to home screen": "ホーム画面に戻る", + "Registering…": "登録しています…", + "Register": "登録", + "Profile": "プロフィール", + "Press and hold spacebar to talk": "スペースを長押しで会話", + "Passwords must match": "パスワードが一致する必要があります", + "Password": "パスワード", + "Speaker": "スピーカー", + "Video call name": "ビデオ通話の名称", + "Video call": "ビデオ通話", + "Video": "ビデオ", + "Waiting for other participants…": "他の参加者を待機しています…", + "Waiting for network": "ネットワークを待機しています", + "Walkie-talkie call name": "トランシーバー通話の名前", + "Walkie-talkie call": "トランシーバー通話" +} diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 4ddc4b07..28f50d22 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -28,6 +28,7 @@ import { SignupTracker, MuteCameraTracker, MuteMicrophoneTracker, + UndecryptableToDeviceEventTracker, } from "./PosthogEvents"; import { Config } from "./config/Config"; import { getUrlParams } from "./UrlParams"; @@ -415,4 +416,5 @@ export class PosthogAnalytics { public eventLogin = new LoginTracker(); public eventMuteMicrophone = new MuteMicrophoneTracker(); public eventMuteCamera = new MuteCameraTracker(); + public eventUndecryptableToDevice = new UndecryptableToDeviceEventTracker(); } diff --git a/src/PosthogEvents.ts b/src/PosthogEvents.ts index f1b4baef..aa8aa329 100644 --- a/src/PosthogEvents.ts +++ b/src/PosthogEvents.ts @@ -149,3 +149,17 @@ export class MuteCameraTracker { }); } } + +interface UndecryptableToDeviceEvent { + eventName: "UndecryptableToDeviceEvent"; + callId: string; +} + +export class UndecryptableToDeviceEventTracker { + track(callId: string) { + PosthogAnalytics.instance.trackEvent({ + eventName: "UndecryptableToDeviceEvent", + callId, + }); + } +} diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index fb504fb7..09273aae 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -216,6 +216,12 @@ export function fullAliasFromRoomName( return `#${roomAliasLocalpartFromRoomName(roomName)}:${client.getDomain()}`; } +/** + * XXX: What is this trying to do? It looks like it's getting the localpart from + * a room alias, but why is it splitting on hyphens and then putting spaces in?? + * @param roomId + * @returns + */ export function roomNameFromRoomId(roomId: string): string { return roomId .match(/([^:]+):.*$/)[1] diff --git a/src/room/GroupCallInspector.tsx b/src/room/GroupCallInspector.tsx index 32579285..73b2a000 100644 --- a/src/room/GroupCallInspector.tsx +++ b/src/room/GroupCallInspector.tsx @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import * as Sentry from "@sentry/react"; import { Resizable } from "re-resizable"; import React, { useEffect, @@ -34,6 +35,7 @@ import { CallEvent } from "matrix-js-sdk/src/webrtc/call"; import styles from "./GroupCallInspector.module.css"; import { SelectInput } from "../input/SelectInput"; +import { PosthogAnalytics } from "../PosthogAnalytics"; interface InspectorContextState { eventsByUserId?: { [userId: string]: SequenceDiagramMatrixEvent[] }; @@ -108,6 +110,19 @@ function formatTimestamp(timestamp: number | Date) { return dateFormatter.format(timestamp); } +function formatType(event: SequenceDiagramMatrixEvent): string { + if (event.content.msgtype === "m.bad.encrypted") return "Undecryptable"; + return event.type; +} + +function lineForEvent(event: SequenceDiagramMatrixEvent): string { + return `${getUserName(event.from)} ${ + event.ignored ? "-x" : "->>" + } ${getUserName(event.to)}: ${formatTimestamp(event.timestamp)} ${formatType( + event + )} ${formatContent(event.type, event.content)}`; +} + export const InspectorContext = createContext< [ @@ -187,21 +202,7 @@ export function SequenceDiagramViewer({ participant ${getUserName(localUserId)} participant Room participant ${selectedUserId ? getUserName(selectedUserId) : "unknown"} - ${ - events - ? events - .map( - ({ to, from, timestamp, type, content, ignored }) => - `${getUserName(from)} ${ignored ? "-x" : "->>"} ${getUserName( - to - )}: ${formatTimestamp(timestamp)} ${type} ${formatContent( - type, - content - )}` - ) - .join("\n ") - : "" - } + ${events ? events.map(lineForEvent).join("\n ") : ""} `; mermaid.mermaidAPI.render("mermaid", graphDefinition, (svgCode: string) => { @@ -389,12 +390,23 @@ function useGroupCallState( function onSendVoipEvent(event: Record) { dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event }); } + + function onUndecryptableToDevice(event: MatrixEvent) { + dispatch({ type: ClientEvent.ReceivedVoipEvent, event }); + + Sentry.captureMessage("Undecryptable to-device Event"); + PosthogAnalytics.instance.eventUndecryptableToDevice.track( + groupCall.groupCallId + ); + } + client.on(RoomStateEvent.Events, onUpdateRoomState); //groupCall.on("calls_changed", onCallsChanged); groupCall.on(CallEvent.SendVoipEvent, onSendVoipEvent); //client.on("state", onCallsChanged); //client.on("hangup", onCallHangup); client.on(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent); + client.on(ClientEvent.UndecryptableToDeviceEvent, onUndecryptableToDevice); onUpdateRoomState(); @@ -405,6 +417,10 @@ function useGroupCallState( //client.removeListener("state", onCallsChanged); //client.removeListener("hangup", onCallHangup); client.removeListener(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent); + client.removeListener( + ClientEvent.UndecryptableToDeviceEvent, + onUndecryptableToDevice + ); }; }, [client, groupCall]); diff --git a/src/room/PTTButton.tsx b/src/room/PTTButton.tsx index abc02c68..1ff7b27b 100644 --- a/src/room/PTTButton.tsx +++ b/src/room/PTTButton.tsx @@ -17,6 +17,7 @@ limitations under the License. import React, { useCallback, useState, useRef } from "react"; import classNames from "classnames"; import { useSpring, animated } from "@react-spring/web"; +import { logger } from "@sentry/utils"; import styles from "./PTTButton.module.css"; import { ReactComponent as MicIcon } from "../icons/Mic.svg"; @@ -68,11 +69,23 @@ export const PTTButton: React.FC = ({ enqueueNetworkWaiting(true, 100); startTalking(); }, [enqueueNetworkWaiting, startTalking, buttonHeld]); + const unhold = useCallback(() => { + if (!buttonHeld) return; setButtonHeld(false); setNetworkWaiting(false); stopTalking(); - }, [setNetworkWaiting, stopTalking]); + }, [setNetworkWaiting, stopTalking, buttonHeld]); + + const onMouseUp = useCallback(() => { + logger.info("Mouse up event: unholding PTT button"); + unhold(); + }, [unhold]); + + const onBlur = useCallback(() => { + logger.info("Blur event: unholding PTT button"); + unhold(); + }, [unhold]); const onButtonMouseDown = useCallback( (e: React.MouseEvent) => { @@ -85,7 +98,7 @@ export const PTTButton: React.FC = ({ // These listeners go on the window so even if the user's cursor / finger // leaves the button while holding it, the button stays pushed until // they stop clicking / tapping. - useEventTarget(window, "mouseup", unhold); + useEventTarget(window, "mouseup", onMouseUp); useEventTarget( window, "touchend", @@ -103,6 +116,8 @@ export const PTTButton: React.FC = ({ } if (!touchFound) return; + logger.info("Touch event ended: unholding PTT button"); + e.preventDefault(); unhold(); setActiveTouchId(null); @@ -163,6 +178,8 @@ export const PTTButton: React.FC = ({ e.preventDefault(); + logger.info("Keyup event for spacebar: unholding PTT button"); + unhold(); } }, @@ -171,7 +188,7 @@ export const PTTButton: React.FC = ({ ); // TODO: We will need to disable this for a global PTT hotkey to work - useEventTarget(window, "blur", unhold); + useEventTarget(window, "blur", onBlur); const prefersReducedMotion = usePrefersReducedMotion(); const { shadow } = useSpring({ diff --git a/src/room/PTTCallView.tsx b/src/room/PTTCallView.tsx index 08836d09..a25f3371 100644 --- a/src/room/PTTCallView.tsx +++ b/src/room/PTTCallView.tsx @@ -210,36 +210,36 @@ export const PTTCallView: React.FC = ({ )}
- {showControls && ( - <> -
-

- {t("{{count}} people connected", { - count: participatingMembers.length, - })} -

- -
-
- - {!isEmbedded && } - inviteModalState.open()} /> -
- - )} + {/* Always render this because the window will become shorter when the on-screen + keyboard appears, so if we don't render it, the dialog will unmount. */} +
+
+

+ {t("{{count}} people connected", { + count: participatingMembers.length, + })} +

+ +
+
+ + {!isEmbedded && } + inviteModalState.open()} /> +
+
{showControls && diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx index e8b3fa9a..f4e460d7 100644 --- a/src/room/RoomPage.tsx +++ b/src/room/RoomPage.tsx @@ -50,9 +50,9 @@ export const RoomPage: FC = () => { const [isRegistering, setIsRegistering] = useState(false); useEffect(() => { - // If we're not already authed and we've been given a display name as + // If we've finished loading, are not already authed and we've been given a display name as // a URL param, automatically register a passwordless user - if (!isAuthenticated && displayName) { + if (!loading && !isAuthenticated && displayName) { setIsRegistering(true); registerPasswordlessUser(displayName).finally(() => { setIsRegistering(false); @@ -63,6 +63,7 @@ export const RoomPage: FC = () => { displayName, setIsRegistering, registerPasswordlessUser, + loading, ]); const groupCallView = useCallback( diff --git a/src/room/useLoadGroupCall.ts b/src/room/useLoadGroupCall.ts index 363739aa..ae4de802 100644 --- a/src/room/useLoadGroupCall.ts +++ b/src/room/useLoadGroupCall.ts @@ -52,12 +52,22 @@ export const useLoadGroupCall = ( const fetchOrCreateRoom = async (): Promise => { try { - const room = await client.joinRoom(roomIdOrAlias, { viaServers }); + // We lowercase the localpart when we create the room, so we must lowercase + // it here too (we just do the whole alias). We can't do the same to room IDs + // though. + const sanitisedIdOrAlias = + roomIdOrAlias[0] === "#" + ? roomIdOrAlias.toLowerCase() + : roomIdOrAlias; + + const room = await client.joinRoom(sanitisedIdOrAlias, { + viaServers, + }); logger.info( - `Joined ${roomIdOrAlias}, waiting room to be ready for group calls` + `Joined ${sanitisedIdOrAlias}, waiting room to be ready for group calls` ); await client.waitUntilRoomReadyForGroupCalls(room.roomId); - logger.info(`${roomIdOrAlias}, is ready for group calls`); + logger.info(`${sanitisedIdOrAlias}, is ready for group calls`); return room; } catch (error) { if ( diff --git a/src/room/usePTT.ts b/src/room/usePTT.ts index 71d7d4e6..a171f0b9 100644 --- a/src/room/usePTT.ts +++ b/src/room/usePTT.ts @@ -113,12 +113,14 @@ export const usePTT = ( }, setState, ] = useState(() => { + // slightly concerningly, this can end up null as we seem to sometimes get + // here before the room state contains our own member event const roomMember = groupCall.room.getMember(client.getUserId()); const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall); return { - isAdmin: roomMember.powerLevel >= 100, + isAdmin: roomMember ? roomMember.powerLevel >= 100 : false, talkOverEnabled: false, pttButtonHeld: false, activeSpeakerUserId: activeSpeakerFeed ? activeSpeakerFeed.userId : null, diff --git a/src/settings/rageshake.ts b/src/settings/rageshake.ts index 42441d1c..446f0365 100644 --- a/src/settings/rageshake.ts +++ b/src/settings/rageshake.ts @@ -1,21 +1,4 @@ /* -Copyright 2022 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/* eslint-disable @typescript-eslint/ban-ts-comment */ -/* Copyright 2017 OpenMarket Ltd Copyright 2018 New Vector Ltd Copyright 2019 The New Vector Ltd @@ -54,15 +37,23 @@ limitations under the License. // actually timestamps. We then purge the remaining logs. We also do this // purge on startup to prevent logs from accumulating. +import EventEmitter from "events"; +import { throttle } from "lodash"; import { logger } from "matrix-js-sdk/src/logger"; import { randomString } from "matrix-js-sdk/src/randomstring"; -// the frequency with which we flush to indexeddb -const FLUSH_RATE_MS = 30 * 1000; - // the length of log data we keep in indexeddb (and include in the reports) const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB +// Shortest amount of time between flushes. We are just appending to an +// IndexedDB table so we don't expect flushing to be that expensive, but +// we can batch the writes a little. +const MAX_FLUSH_INTERVAL_MS = 2 * 1000; + +enum ConsoleLoggerEvent { + Log = "log", +} + type LogFunction = ( ...args: (Error | DOMException | object | string)[] ) => void; @@ -76,7 +67,7 @@ interface LogEntry { index?: number; } -export class ConsoleLogger { +export class ConsoleLogger extends EventEmitter { private logs = ""; private originalFunctions: { [key in LogFunctionName]?: LogFunction } = {}; @@ -99,13 +90,6 @@ export class ConsoleLogger { }); } - public bypassRageshake( - fnName: LogFunctionName, - ...args: (Error | DOMException | object | string)[] - ): void { - this.originalFunctions[fnName](...args); - } - public log( level: string, ...args: (Error | DOMException | object | string)[] @@ -137,23 +121,27 @@ export class ConsoleLogger { // Using + really is the quickest way in JS // http://jsperf.com/concat-vs-plus-vs-join this.logs += line; + + this.emit(ConsoleLoggerEvent.Log); } /** - * Retrieve log lines to flush to disk. - * @param {boolean} keepLogs True to not delete logs after flushing. - * @return {string} \n delimited log lines to flush. + * Returns the log lines to flush to disk and empties the internal log buffer + * @return {string} \n delimited log lines */ - public flush(keepLogs?: boolean): string { - // The ConsoleLogger doesn't care how these end up on disk, it just - // flushes them to the caller. - if (keepLogs) { - return this.logs; - } + public popLogs(): string { const logsToFlush = this.logs; this.logs = ""; return logsToFlush; } + + /** + * Returns lines currently in the log buffer without removing them + * @return {string} \n delimited log lines + */ + public peekLogs(): string { + return this.logs; + } } // A class which stores log lines in an IndexedDB instance. @@ -164,8 +152,14 @@ export class IndexedDBLogStore { private flushAgainPromise: Promise = null; private id: string; - constructor(private indexedDB: IDBFactory, private logger: ConsoleLogger) { + constructor( + private indexedDB: IDBFactory, + private loggerInstance: ConsoleLogger + ) { this.id = "instance-" + randomString(16); + + loggerInstance.on(ConsoleLoggerEvent.Log, this.onLoggerLog); + window.addEventListener("beforeunload", this.flush); } /** @@ -174,30 +168,31 @@ export class IndexedDBLogStore { public connect(): Promise { const req = this.indexedDB.open("logs"); return new Promise((resolve, reject) => { - req.onsuccess = (event: Event) => { - // @ts-ignore - this.db = event.target.result; - // Periodically flush logs to local storage / indexeddb - setInterval(this.flush.bind(this), FLUSH_RATE_MS); + req.onsuccess = () => { + this.db = req.result; + resolve(); }; - req.onerror = (event) => { - const err = - // @ts-ignore - "Failed to open log database: " + event.target.error.name; + req.onerror = () => { + const err = "Failed to open log database: " + req.error.name; logger.error(err); reject(new Error(err)); }; // First time: Setup the object store - req.onupgradeneeded = (event) => { - // @ts-ignore - const db = event.target.result; + req.onupgradeneeded = () => { + const db = req.result; + // This is the log entries themselves. Each entry is a chunk of + // logs (ie multiple lines). 'id' is the instance ID (so logs with + // the same instance ID are all from the same session) and 'index' + // is a sequence number for the chunk. The log lines live in the + // 'lines' key, which is a chunk of many newline-separated log lines. const logObjStore = db.createObjectStore("logs", { keyPath: ["id", "index"], }); // Keys in the database look like: [ "instance-148938490", 0 ] + // (The instance ID plus the ID of each log chunk). // Later on we need to query everything based on an instance id. // In order to do this, we need to set up indexes "id". logObjStore.createIndex("id", "id", { unique: false }); @@ -206,6 +201,9 @@ export class IndexedDBLogStore { this.generateLogEntry(new Date() + " ::: Log database was created.") ); + // This records the last time each instance ID generated a log message, such + // that the logs from each session can be collated in the order they last logged + // something. const lastModifiedStore = db.createObjectStore("logslastmod", { keyPath: "id", }); @@ -214,6 +212,26 @@ export class IndexedDBLogStore { }); } + private onLoggerLog = () => { + if (!this.db) return; + + this.throttledFlush(); + }; + + // Throttled function to flush logs. We use throttle rather + // than debounce as we want logs to be written regularly, otherwise + // if there's a constant stream of logging, we'd never write anything. + private throttledFlush = throttle( + () => { + this.flush(); + }, + MAX_FLUSH_INTERVAL_MS, + { + leading: false, + trailing: true, + } + ); + /** * Flush logs to disk. * @@ -233,7 +251,7 @@ export class IndexedDBLogStore { * * @return {Promise} Resolved when the logs have been flushed. */ - public flush(): Promise { + public flush = (): Promise => { // check if a flush() operation is ongoing if (this.flushPromise) { if (this.flushAgainPromise) { @@ -258,20 +276,19 @@ export class IndexedDBLogStore { reject(new Error("No connected database")); return; } - const lines = this.logger.flush(); + const lines = this.loggerInstance.popLogs(); if (lines.length === 0) { resolve(); return; } const txn = this.db.transaction(["logs", "logslastmod"], "readwrite"); const objStore = txn.objectStore("logs"); - txn.oncomplete = (event) => { + txn.oncomplete = () => { resolve(); }; txn.onerror = (event) => { logger.error("Failed to flush logs : ", event); - // @ts-ignore - reject(new Error("Failed to write logs: " + event.target.errorCode)); + reject(new Error("Failed to write logs: " + txn.error.message)); }; objStore.add(this.generateLogEntry(lines)); const lastModStore = txn.objectStore("logslastmod"); @@ -280,7 +297,7 @@ export class IndexedDBLogStore { this.flushPromise = null; }); return this.flushPromise; - } + }; /** * Consume the most recent logs and return them. Older logs which are not @@ -307,13 +324,11 @@ export class IndexedDBLogStore { .index("id") .openCursor(IDBKeyRange.only(id), "prev"); let lines = ""; - query.onerror = (event) => { - // @ts-ignore - reject(new Error("Query failed: " + event.target.errorCode)); + query.onerror = () => { + reject(new Error("Query failed: " + query.error.message)); }; - query.onsuccess = (event) => { - // @ts-ignore - const cursor = event.target.result; + query.onsuccess = () => { + const cursor = query.result; if (!cursor) { resolve(lines); return; // end of results @@ -355,9 +370,8 @@ export class IndexedDBLogStore { const o = txn.objectStore("logs"); // only load the key path, not the data which may be huge const query = o.index("id").openKeyCursor(IDBKeyRange.only(id)); - query.onsuccess = (event) => { - // @ts-ignore - const cursor = event.target.result; + query.onsuccess = () => { + const cursor = query.result; if (!cursor) { return; } @@ -367,12 +381,10 @@ export class IndexedDBLogStore { txn.oncomplete = () => { resolve(); }; - txn.onerror = (event) => { + txn.onerror = () => { reject( new Error( - "Failed to delete logs for " + - // @ts-ignore - `'${id}' : ${event.target.errorCode}` + "Failed to delete logs for " + `'${id}' : ${txn.error.message}` ) ); }; @@ -456,14 +468,12 @@ function selectQuery( const query = store.openCursor(keyRange); return new Promise((resolve, reject) => { const results = []; - query.onerror = (event) => { - // @ts-ignore - reject(new Error("Query failed: " + event.target.errorCode)); + query.onerror = () => { + reject(new Error("Query failed: " + query.error.message)); }; // collect results - query.onsuccess = (event) => { - // @ts-ignore - const cursor = event.target.result; + query.onsuccess = () => { + const cursor = query.result; if (!cursor) { resolve(results); return; // end of results @@ -479,8 +489,6 @@ declare global { // eslint-disable-next-line no-var, camelcase var mx_rage_logger: ConsoleLogger; // eslint-disable-next-line no-var, camelcase - var mx_rage_initPromise: Promise; - // eslint-disable-next-line no-var, camelcase var mx_rage_initStoragePromise: Promise; } @@ -491,19 +499,11 @@ declare global { * be set up immediately for the logs. * @return {Promise} Resolves when set up. */ -export function init(setUpPersistence = true): Promise { - if (global.mx_rage_initPromise) { - return global.mx_rage_initPromise; - } +export function init(): Promise { global.mx_rage_logger = new ConsoleLogger(); global.mx_rage_logger.monkeyPatch(window.console); - if (setUpPersistence) { - return tryInitStorage(); - } - - global.mx_rage_initPromise = Promise.resolve(); - return global.mx_rage_initPromise; + return tryInitStorage(); } /** @@ -573,7 +573,7 @@ export async function getLogsForReport(): Promise { } else { return [ { - lines: global.mx_rage_logger.flush(true), + lines: global.mx_rage_logger.peekLogs(), id: "-", }, ]; diff --git a/yarn.lock b/yarn.lock index 423861a2..ad62950e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10362,9 +10362,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#278dd6a3d35cf89c03f9172f9c81579577a267b3": - version "23.0.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/278dd6a3d35cf89c03f9172f9c81579577a267b3" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#ac97928a5d081b0502952295eea5b8fa7bdf8839": + version "23.1.1" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/ac97928a5d081b0502952295eea5b8fa7bdf8839" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.2"