From c6a058fb6fa0af190c117c2f3b5bc01a6eab17e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 19 Dec 2020 19:32:58 +0100 Subject: [PATCH 001/445] Added surround with MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/rooms/BasicMessageComposer.tsx | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 2ececdeaed..cd34e25926 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -418,6 +418,10 @@ export default class BasicMessageEditor extends React.Component }; private onKeyDown = (event: React.KeyboardEvent) => { + const selectionRange = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection()); + // trim the range as we want it to exclude leading/trailing spaces + selectionRange.trim(); + const model = this.props.model; const modKey = IS_MAC ? event.metaKey : event.ctrlKey; let handled = false; @@ -471,6 +475,43 @@ export default class BasicMessageEditor extends React.Component }); handled = true; // autocomplete or enter to send below shouldn't have any modifier keys pressed. + } else if (document.getSelection().type != "Caret") { + if (event.key === '(') { + this.historyManager.ensureLastChangesPushed(this.props.model); + this.modifiedFlag = true; + toggleInlineFormat(selectionRange, "(", ")"); + handled = true; + } else if (event.key === '[') { + this.historyManager.ensureLastChangesPushed(this.props.model); + this.modifiedFlag = true; + toggleInlineFormat(selectionRange, "[", "]"); + handled = true; + } else if (event.key === '{') { + this.historyManager.ensureLastChangesPushed(this.props.model); + this.modifiedFlag = true; + toggleInlineFormat(selectionRange, "{", "}"); + handled = true; + } else if (event.key === '<') { + this.historyManager.ensureLastChangesPushed(this.props.model); + this.modifiedFlag = true; + toggleInlineFormat(selectionRange, "<", ">"); + handled = true; + } else if (event.key === '"') { + this.historyManager.ensureLastChangesPushed(this.props.model); + this.modifiedFlag = true; + toggleInlineFormat(selectionRange, "\""); + handled = true; + } else if (event.key === '`') { + this.historyManager.ensureLastChangesPushed(this.props.model); + this.modifiedFlag = true; + toggleInlineFormat(selectionRange, "`"); + handled = true; + } else if (event.key === '\'') { + this.historyManager.ensureLastChangesPushed(this.props.model); + this.modifiedFlag = true; + toggleInlineFormat(selectionRange, "'"); + handled = true; + } } else { const metaOrAltPressed = event.metaKey || event.altKey; const modifierPressed = metaOrAltPressed || event.shiftKey; From e90f5ddf5b6ac14648d7fc36ecf414a04d668c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 19 Dec 2020 19:36:56 +0100 Subject: [PATCH 002/445] Added a comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/BasicMessageComposer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index cd34e25926..587f13e8c2 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -512,6 +512,7 @@ export default class BasicMessageEditor extends React.Component toggleInlineFormat(selectionRange, "'"); handled = true; } + // Surround selected text with a character } else { const metaOrAltPressed = event.metaKey || event.altKey; const modifierPressed = metaOrAltPressed || event.shiftKey; From b330dd55a0cd61fe9b014d47c6dcfb085e835b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 12 Feb 2021 07:53:09 +0100 Subject: [PATCH 003/445] Hide surround with behind a setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/BasicMessageComposer.tsx | 3 ++- .../views/settings/tabs/user/PreferencesUserSettingsTab.js | 1 + src/settings/Settings.ts | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 587f13e8c2..a91e92123b 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -418,6 +418,7 @@ export default class BasicMessageEditor extends React.Component }; private onKeyDown = (event: React.KeyboardEvent) => { + const surroundWith = SettingsStore.getValue("MessageComposerInput.surroundWith"); const selectionRange = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection()); // trim the range as we want it to exclude leading/trailing spaces selectionRange.trim(); @@ -475,7 +476,7 @@ export default class BasicMessageEditor extends React.Component }); handled = true; // autocomplete or enter to send below shouldn't have any modifier keys pressed. - } else if (document.getSelection().type != "Caret") { + } else if (surroundWith && document.getSelection().type != "Caret") { if (event.key === '(') { this.historyManager.ensureLastChangesPushed(this.props.model); this.modifiedFlag = true; diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js index 4d8493401e..2544c03a22 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js @@ -34,6 +34,7 @@ export default class PreferencesUserSettingsTab extends React.Component { 'MessageComposerInput.suggestEmoji', 'sendTypingNotifications', 'MessageComposerInput.ctrlEnterToSend', + 'MessageComposerInput.surroundWith', ]; static TIMELINE_SETTINGS = [ diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index b239b809fe..ed9b37d632 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -336,6 +336,11 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: isMac ? _td("Use Command + Enter to send a message") : _td("Use Ctrl + Enter to send a message"), default: false, }, + "MessageComposerInput.surroundWith": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td("Use surround with"), + default: false, + }, "MessageComposerInput.autoReplaceEmoji": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Automatically replace plain text Emoji'), From 3f0d7673725f12b99e48b0f2e94c9c0f78f9c5d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 12 Feb 2021 07:57:15 +0100 Subject: [PATCH 004/445] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a9d31bb9f2..3af2a62c94 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -816,6 +816,7 @@ "Use Ctrl + F to search": "Use Ctrl + F to search", "Use Command + Enter to send a message": "Use Command + Enter to send a message", "Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message", + "Use surround with": "Use surround with", "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Mirror local video feed": "Mirror local video feed", "Enable Community Filter Panel": "Enable Community Filter Panel", From cf25e15eb6d3071dccb92c07e1f96bd726d56755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20V=C3=A1gner?= Date: Sat, 12 Jun 2021 12:49:15 +0200 Subject: [PATCH 005/445] Make call control buttons accessible to screen reader users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Peter Vágner --- src/components/views/voip/CallView.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index c084dacaa8..178df246d1 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -441,6 +441,7 @@ export default class CallView extends React.Component { const vidMuteButton = this.props.call.type === CallType.Video ? : null; // The dial pad & 'more' button actions are only relevant in a connected call @@ -450,6 +451,7 @@ export default class CallView extends React.Component { inputRef={this.dialpadButton} onClick={this.onDialpadClick} isExpanded={this.state.showDialpad} + aria-label={_t("Dialpad")} /> :
; const contextMenuButton = this.state.callState === CallState.Connected ? { onClick={this.onMoreClick} inputRef={this.contextMenuButton} isExpanded={this.state.showMoreMenu} + aria-label={_t("More")} /> :
; // in the near future, the dial pad button will go on the left. For now, it's the nothing button @@ -466,6 +469,7 @@ export default class CallView extends React.Component { Date: Sat, 12 Jun 2021 13:53:44 +0200 Subject: [PATCH 006/445] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Peter Vágner --- src/i18n/strings/en_EN.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 874dc11bd2..7df22432de 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -889,6 +889,12 @@ "sends snowfall": "sends snowfall", "Sends the given message with a space themed effect": "Sends the given message with a space themed effect", "sends space invaders": "sends space invaders", + "Start the camera": "Start the camera", + "Stop the camera": "Stop the camera", + "Dialpad": "Dialpad", + "More": "More", + "Unmute the microphone": "Unmute the microphone", + "Mute the microphone": "Mute the microphone", "unknown person": "unknown person", "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Consulting with %(transferTarget)s. Transfer to %(transferee)s", "You held the call Switch": "You held the call Switch", From a94d11235ed5352b8da0ae87964abf2a00eddb33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20V=C3=A1gner?= Date: Sat, 12 Jun 2021 14:17:10 +0200 Subject: [PATCH 007/445] Changed the buttons to TooltipButtons and added the tooltip for the hangup button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Peter Vágner --- src/components/views/voip/CallView.tsx | 21 ++++++++++++--------- src/i18n/strings/en_EN.json | 1 + 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 178df246d1..66b3f6b2d4 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -25,6 +25,8 @@ import RoomAvatar from "../avatars/RoomAvatar"; import { CallState, CallType, MatrixCall, CallEvent } from 'matrix-js-sdk/src/webrtc/call'; import classNames from 'classnames'; import AccessibleButton from '../elements/AccessibleButton'; +import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; +import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard'; import {alwaysAboveLeftOf, alwaysAboveRightOf, ChevronFace, ContextMenuButton} from '../../structures/ContextMenu'; import CallContextMenu from '../context_menus/CallContextMenu'; @@ -438,40 +440,40 @@ export default class CallView extends React.Component { mx_CallView_callControls_hidden: !this.state.controlsVisible, }); - const vidMuteButton = this.props.call.type === CallType.Video ? : null; // The dial pad & 'more' button actions are only relevant in a connected call // When not connected, we have to put something there to make the flexbox alignment correct - const dialpadButton = this.state.callState === CallState.Connected ? :
; - const contextMenuButton = this.state.callState === CallState.Connected ? :
; // in the near future, the dial pad button will go on the left. For now, it's the nothing button // because something needs to have margin-right: auto to make the alignment correct. const callControls =
{dialpadButton} - - { dis.dispatch({ @@ -479,6 +481,7 @@ export default class CallView extends React.Component { room_id: callRoomId, }); }} + title={_t("Hangup")} /> {vidMuteButton}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7df22432de..d50348954a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -895,6 +895,7 @@ "More": "More", "Unmute the microphone": "Unmute the microphone", "Mute the microphone": "Mute the microphone", + "Hangup": "Hangup", "unknown person": "unknown person", "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Consulting with %(transferTarget)s. Transfer to %(transferee)s", "You held the call Switch": "You held the call Switch", From fd7eaddb2d4c170084fbfc6ca3ca4c26f40a57b9 Mon Sep 17 00:00:00 2001 From: pvagner Date: Wed, 16 Jun 2021 10:18:35 +0200 Subject: [PATCH 008/445] Update src/components/views/voip/CallView.tsx Co-authored-by: Michael Telatynski <7t3chguy@googlemail.com> --- src/components/views/voip/CallView.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 66b3f6b2d4..f7f82d4300 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -26,9 +26,13 @@ import { CallState, CallType, MatrixCall, CallEvent } from 'matrix-js-sdk/src/we import classNames from 'classnames'; import AccessibleButton from '../elements/AccessibleButton'; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; -import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard'; -import {alwaysAboveLeftOf, alwaysAboveRightOf, ChevronFace, ContextMenuButton} from '../../structures/ContextMenu'; +import { + alwaysAboveLeftOf, + alwaysAboveRightOf, + ChevronFace, + ContextMenuTooltipButton, +} from '../../structures/ContextMenu'; import CallContextMenu from '../context_menus/CallContextMenu'; import { avatarUrlForMember } from '../../../Avatar'; import DialpadContextMenu from '../context_menus/DialpadContextMenu'; From 38c0cd27163effd85b5c12cd499f6291e1a06c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 20 Jun 2021 08:21:33 +0200 Subject: [PATCH 009/445] Cache surroundWith setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/rooms/BasicMessageComposer.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index f5fddff45b..de72f8a348 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -107,6 +107,7 @@ interface IState { showVisualBell?: boolean; autoComplete?: AutocompleteWrapperModel; completionIndex?: number; + surroundWith: boolean, } @replaceableComponent("views.rooms.BasicMessageEditor") @@ -125,12 +126,14 @@ export default class BasicMessageEditor extends React.Component private readonly emoticonSettingHandle: string; private readonly shouldShowPillAvatarSettingHandle: string; + private readonly surroundWithHandle: string; private readonly historyManager = new HistoryManager(); constructor(props) { super(props); this.state = { showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"), + surroundWith: SettingsStore.getValue("MessageComposerInput.surroundWith"), }; this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null, @@ -138,6 +141,8 @@ export default class BasicMessageEditor extends React.Component this.configureEmoticonAutoReplace(); this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null, this.configureShouldShowPillAvatar); + this.surroundWithHandle = SettingsStore.watchSetting("MessageComposerInput.surroundWith", null, + this.surroundWithSettingChanged); } public componentDidUpdate(prevProps: IProps) { @@ -428,7 +433,6 @@ export default class BasicMessageEditor extends React.Component }; private onKeyDown = (event: React.KeyboardEvent) => { - const surroundWith = SettingsStore.getValue("MessageComposerInput.surroundWith"); const selectionRange = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection()); // trim the range as we want it to exclude leading/trailing spaces selectionRange.trim(); @@ -436,7 +440,7 @@ export default class BasicMessageEditor extends React.Component const model = this.props.model; let handled = false; - if (surroundWith && document.getSelection().type != "Caret") { + if (this.state.surroundWith && document.getSelection().type != "Caret") { // Surround selected text with a character if (event.key === '(') { this.historyManager.ensureLastChangesPushed(this.props.model); @@ -628,6 +632,11 @@ export default class BasicMessageEditor extends React.Component this.setState({ showPillAvatar }); }; + private surroundWithSettingChanged = () => { + const surroundWith = SettingsStore.getValue("MessageComposerInput.surroundWith"); + this.setState({ surroundWith }); + }; + componentWillUnmount() { document.removeEventListener("selectionchange", this.onSelectionChange); this.editorRef.current.removeEventListener("input", this.onInput, true); @@ -635,6 +644,7 @@ export default class BasicMessageEditor extends React.Component this.editorRef.current.removeEventListener("compositionend", this.onCompositionEnd, true); SettingsStore.unwatchSetting(this.emoticonSettingHandle); SettingsStore.unwatchSetting(this.shouldShowPillAvatarSettingHandle); + SettingsStore.unwatchSetting(this.surroundWithHandle); } componentDidMount() { From a772460f63681b85fbc1ad9d972d329d871cbd7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 20 Jun 2021 08:38:01 +0200 Subject: [PATCH 010/445] Simplifie surround with and make it more extensible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/rooms/BasicMessageComposer.tsx | 45 +++++++------------ 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index de72f8a348..239624f5d8 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -55,6 +55,14 @@ const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.sourc const IS_MAC = navigator.platform.indexOf("Mac") !== -1; +const SURROUND_WITH_CHARACTERS = ["\"", "_", "`", "'", "*", "~", "$"]; +const SURROUND_WITH_DOUBLE_CHARACTERS = new Map([ + ["(", ")"], + ["[", "]"], + ["{", "}"], + ["<", ">"], +]); + function ctrlShortcutLabel(key) { return (IS_MAC ? "⌘" : "Ctrl") + "+" + key; } @@ -441,41 +449,18 @@ export default class BasicMessageEditor extends React.Component let handled = false; if (this.state.surroundWith && document.getSelection().type != "Caret") { - // Surround selected text with a character - if (event.key === '(') { + // This surrounds the selected text with a character. This is + // intentionally left out of the keybinding manager as the keybinds + // here shouldn't be changeable + if (SURROUND_WITH_CHARACTERS.includes(event.key)) { this.historyManager.ensureLastChangesPushed(this.props.model); this.modifiedFlag = true; - toggleInlineFormat(selectionRange, "(", ")"); + toggleInlineFormat(selectionRange, event.key); handled = true; - } else if (event.key === '[') { + } else if ([...SURROUND_WITH_DOUBLE_CHARACTERS.keys()].includes(event.key)) { this.historyManager.ensureLastChangesPushed(this.props.model); this.modifiedFlag = true; - toggleInlineFormat(selectionRange, "[", "]"); - handled = true; - } else if (event.key === '{') { - this.historyManager.ensureLastChangesPushed(this.props.model); - this.modifiedFlag = true; - toggleInlineFormat(selectionRange, "{", "}"); - handled = true; - } else if (event.key === '<') { - this.historyManager.ensureLastChangesPushed(this.props.model); - this.modifiedFlag = true; - toggleInlineFormat(selectionRange, "<", ">"); - handled = true; - } else if (event.key === '"') { - this.historyManager.ensureLastChangesPushed(this.props.model); - this.modifiedFlag = true; - toggleInlineFormat(selectionRange, "\""); - handled = true; - } else if (event.key === '`') { - this.historyManager.ensureLastChangesPushed(this.props.model); - this.modifiedFlag = true; - toggleInlineFormat(selectionRange, "`"); - handled = true; - } else if (event.key === '\'') { - this.historyManager.ensureLastChangesPushed(this.props.model); - this.modifiedFlag = true; - toggleInlineFormat(selectionRange, "'"); + toggleInlineFormat(selectionRange, event.key, SURROUND_WITH_DOUBLE_CHARACTERS.get(event.key)); handled = true; } } From 3e97847e7de3830e4a63d2d884a374a9091b03eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 20 Jun 2021 08:47:21 +0200 Subject: [PATCH 011/445] Get selection range only if necessary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/BasicMessageComposer.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 239624f5d8..d8a872f1c6 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -441,10 +441,6 @@ export default class BasicMessageEditor extends React.Component }; private onKeyDown = (event: React.KeyboardEvent) => { - const selectionRange = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection()); - // trim the range as we want it to exclude leading/trailing spaces - selectionRange.trim(); - const model = this.props.model; let handled = false; @@ -452,6 +448,15 @@ export default class BasicMessageEditor extends React.Component // This surrounds the selected text with a character. This is // intentionally left out of the keybinding manager as the keybinds // here shouldn't be changeable + + const selectionRange = getRangeForSelection( + this.editorRef.current, + this.props.model, + document.getSelection(), + ); + // trim the range as we want it to exclude leading/trailing spaces + selectionRange.trim(); + if (SURROUND_WITH_CHARACTERS.includes(event.key)) { this.historyManager.ensureLastChangesPushed(this.props.model); this.modifiedFlag = true; From 314ab7a94d7868c6375d3efca8d46d6c4f97ffa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 21 Jun 2021 11:20:20 +0200 Subject: [PATCH 012/445] If there already is a Jitsi widget pin it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/CallHandler.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 448b1cb780..5138d526dd 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -61,7 +61,6 @@ import Modal from './Modal'; import { _t } from './languageHandler'; import dis from './dispatcher/dispatcher'; import WidgetUtils from './utils/WidgetUtils'; -import WidgetEchoStore from './stores/WidgetEchoStore'; import SettingsStore from './settings/SettingsStore'; import {Jitsi} from "./widgets/Jitsi"; import {WidgetType} from "./widgets/WidgetType"; @@ -88,6 +87,7 @@ import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/ import EventEmitter from 'events'; import SdkConfig from './SdkConfig'; import { ensureDMExists, findDMForUser } from './createRoom'; +import { WidgetLayoutStore, Container } from './stores/widgets/WidgetLayoutStore'; export const PROTOCOL_PSTN = 'm.protocol.pstn'; export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn'; @@ -940,14 +940,10 @@ export default class CallHandler extends EventEmitter { // prevent double clicking the call button const room = MatrixClientPeg.get().getRoom(roomId); - const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI); - const hasJitsi = currentJitsiWidgets.length > 0 - || WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI); - if (hasJitsi) { - Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, { - title: _t('Call in Progress'), - description: _t('A call is currently being placed!'), - }); + const jitsiWidget = WidgetStore.instance.getApps(roomId).find((app) => WidgetType.JITSI.matches(app.type)); + if (jitsiWidget) { + // If there already is a Jitsi widget pin it + WidgetLayoutStore.instance.moveToContainer(room, jitsiWidget, Container.Top); return; } From ce47662b55e4296ad73cd395109934c4fe689ec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 21 Jun 2021 11:23:49 +0200 Subject: [PATCH 013/445] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b88dc79da5..1401fca4ba 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -65,8 +65,6 @@ "You cannot place a call with yourself.": "You cannot place a call with yourself.", "Unable to look up phone number": "Unable to look up phone number", "There was an error looking up the phone number": "There was an error looking up the phone number", - "Call in Progress": "Call in Progress", - "A call is currently being placed!": "A call is currently being placed!", "Permission Required": "Permission Required", "You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room", "End conference": "End conference", From b3ac0c71e107571ea5312d58ad53b8614d7eec2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 21 Jun 2021 12:17:51 +0200 Subject: [PATCH 014/445] Fix access token copy button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../tabs/user/_HelpUserSettingsTab.scss | 34 +++++++++++-------- .../tabs/user/HelpUserSettingsTab.tsx | 4 +-- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss index 0f879d209e..1498f6fbf0 100644 --- a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss @@ -28,7 +28,7 @@ limitations under the License. user-select: all; } -.mx_HelpUserSettingsTab_accessToken { +.mx_HelpUserSettingsTab_copy { display: flex; justify-content: space-between; border-radius: 5px; @@ -36,20 +36,24 @@ limitations under the License. margin-bottom: 10px; margin-top: 10px; padding: 10px; -} -.mx_HelpUserSettingsTab_accessToken_copy { - flex-shrink: 0; - cursor: pointer; - margin-left: 20px; - display: inherit; -} + .mx_HelpUserSettingsTab_copyButton { + flex-shrink: 0; + width: 20px; + height: 20px; + cursor: pointer; + margin-left: 20px; + display: block; -.mx_HelpUserSettingsTab_accessToken_copy > div { - mask-image: url($copy-button-url); - background-color: $message-action-bar-fg-color; - margin-left: 5px; - width: 20px; - height: 20px; - background-repeat: no-repeat; + &::before { + content: ""; + + mask-image: url($copy-button-url); + background-color: $message-action-bar-fg-color; + width: 20px; + height: 20px; + display: block; + background-repeat: no-repeat; + } + } } diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index beff033001..5288dc8977 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -300,12 +300,12 @@ export default class HelpUserSettingsTab extends React.Component {_t("Access Token")}
{_t("Your access token gives full access to your account." + " Do not share it with anyone." )} -
+
{MatrixClientPeg.get().getAccessToken()}

From d7b10e2ff4704afc17ed22983f086fa44b22d270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 21 Jun 2021 17:26:06 +0200 Subject: [PATCH 015/445] Simplifie code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/BasicMessageComposer.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index d8a872f1c6..06759d0bf5 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -457,12 +457,7 @@ export default class BasicMessageEditor extends React.Component // trim the range as we want it to exclude leading/trailing spaces selectionRange.trim(); - if (SURROUND_WITH_CHARACTERS.includes(event.key)) { - this.historyManager.ensureLastChangesPushed(this.props.model); - this.modifiedFlag = true; - toggleInlineFormat(selectionRange, event.key); - handled = true; - } else if ([...SURROUND_WITH_DOUBLE_CHARACTERS.keys()].includes(event.key)) { + if ([...SURROUND_WITH_DOUBLE_CHARACTERS.keys(), ...SURROUND_WITH_CHARACTERS].includes(event.key)) { this.historyManager.ensureLastChangesPushed(this.props.model); this.modifiedFlag = true; toggleInlineFormat(selectionRange, event.key, SURROUND_WITH_DOUBLE_CHARACTERS.get(event.key)); From 0b1fbf7e530cb9d1753f54175d0fbefdbd76c5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 21 Jun 2021 12:31:58 +0200 Subject: [PATCH 016/445] Make version copiable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../tabs/user/HelpUserSettingsTab.tsx | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 5288dc8977..6147265a51 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -68,6 +68,18 @@ export default class HelpUserSettingsTab extends React.Component if (this.closeCopiedTooltip) this.closeCopiedTooltip(); } + private getVersionInfo(): { appVersion: string, olmVersion: string } { + const brand = SdkConfig.get().brand; + const appVersion = this.state.appVersion || 'unknown'; + let olmVersion = MatrixClientPeg.get().olmVersion; + olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : ''; + + return { + appVersion: `${_t("%(brand)s version:", { brand })} ${appVersion}`, + olmVersion: `${_t("Olm version:")} ${olmVersion}`, + }; + } + private onClearCacheAndReload = (e) => { if (!PlatformPeg.get()) return; @@ -179,6 +191,21 @@ export default class HelpUserSettingsTab extends React.Component this.closeCopiedTooltip = target.onmouseleave = close; } + private onCopyVersionClicked = async (e) => { + e.preventDefault(); + const target = e.target; // copy target before we go async and React throws it away + + const { appVersion, olmVersion } = this.getVersionInfo(); + const successful = await copyPlaintext(`${appVersion}\n${olmVersion}`); + const buttonRect = target.getBoundingClientRect(); + const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu'); + const { close } = ContextMenu.createMenu(GenericTextContextMenu, { + ...toRightOf(buttonRect, 2), + message: successful ? _t('Copied!') : _t('Failed to copy'), + }); + this.closeCopiedTooltip = target.onmouseleave = close; + }; + render() { const brand = SdkConfig.get().brand; @@ -225,11 +252,6 @@ export default class HelpUserSettingsTab extends React.Component ); } - const appVersion = this.state.appVersion || 'unknown'; - - let olmVersion = MatrixClientPeg.get().olmVersion; - olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : ''; - let updateButton = null; if (this.state.canUpdate) { updateButton = ; @@ -267,6 +289,8 @@ export default class HelpUserSettingsTab extends React.Component ); } + const { appVersion, olmVersion } = this.getVersionInfo(); + return (
{_t("Help & About")}
@@ -283,8 +307,15 @@ export default class HelpUserSettingsTab extends React.Component
{_t("Versions")}
- {_t("%(brand)s version:", { brand })} {appVersion}
- {_t("olm version:")} {olmVersion}
+
+ { appVersion }
+ { olmVersion }
+ +
{updateButton}
From d74d3aaf73866f2f1864ce2ccf613b903e6ca39e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 21 Jun 2021 12:40:31 +0200 Subject: [PATCH 017/445] Move copying into a separate method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../tabs/user/HelpUserSettingsTab.tsx | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 6147265a51..7a286617a1 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import {_t, getCurrentLanguage} from "../../../../../languageHandler"; import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; -import AccessibleButton from "../../../elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton"; import AccessibleTooltipButton from '../../../elements/AccessibleTooltipButton'; import SdkConfig from "../../../../../SdkConfig"; import createRoom from "../../../../../createRoom"; @@ -177,11 +177,11 @@ export default class HelpUserSettingsTab extends React.Component ); } - onAccessTokenCopyClick = async (e) => { + private async copy(text: string, e: ButtonEvent) { e.preventDefault(); - const target = e.target; // copy target before we go async and React throws it away + const target = e.target as HTMLDivElement; // copy target before we go async and React throws it away - const successful = await copyPlaintext(MatrixClientPeg.get().getAccessToken()); + const successful = await copyPlaintext(text); const buttonRect = target.getBoundingClientRect(); const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu'); const {close} = ContextMenu.createMenu(GenericTextContextMenu, { @@ -191,19 +191,13 @@ export default class HelpUserSettingsTab extends React.Component this.closeCopiedTooltip = target.onmouseleave = close; } - private onCopyVersionClicked = async (e) => { - e.preventDefault(); - const target = e.target; // copy target before we go async and React throws it away + private onAccessTokenCopyClick = (e: ButtonEvent) => { + this.copy(MatrixClientPeg.get().getAccessToken(), e); + }; + private onCopyVersionClicked = (e: ButtonEvent) => { const { appVersion, olmVersion } = this.getVersionInfo(); - const successful = await copyPlaintext(`${appVersion}\n${olmVersion}`); - const buttonRect = target.getBoundingClientRect(); - const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu'); - const { close } = ContextMenu.createMenu(GenericTextContextMenu, { - ...toRightOf(buttonRect, 2), - message: successful ? _t('Copied!') : _t('Failed to copy'), - }); - this.closeCopiedTooltip = target.onmouseleave = close; + this.copy(`${appVersion}\n${olmVersion}`, e); }; render() { From 1bf9e592e951b0959a6f978fbe9f63773356689d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 21 Jun 2021 12:41:07 +0200 Subject: [PATCH 018/445] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b88dc79da5..d9cfd2fea4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1247,6 +1247,8 @@ "Deactivate account": "Deactivate account", "Discovery": "Discovery", "General": "General", + "%(brand)s version:": "%(brand)s version:", + "Olm version:": "Olm version:", "Legal": "Legal", "Credits": "Credits", "For help with using %(brand)s, click here.": "For help with using %(brand)s, click here.", @@ -1260,13 +1262,11 @@ "FAQ": "FAQ", "Keyboard Shortcuts": "Keyboard Shortcuts", "Versions": "Versions", - "%(brand)s version:": "%(brand)s version:", - "olm version:": "olm version:", + "Copy": "Copy", "Homeserver is": "Homeserver is", "Identity Server is": "Identity Server is", "Access Token": "Access Token", "Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.", - "Copy": "Copy", "Clear cache and reload": "Clear cache and reload", "Labs": "Labs", "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.", From b2292268bc9a551a1b9ecfe535eafd3b823789d4 Mon Sep 17 00:00:00 2001 From: pvagner Date: Wed, 30 Jun 2021 07:06:10 +0200 Subject: [PATCH 019/445] Update src/components/views/voip/CallView.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Šimon Brandner --- src/components/views/voip/CallView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 6cb245f7b1..3f959622e7 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -26,7 +26,7 @@ import { CallState, CallType, MatrixCall, CallEvent } from 'matrix-js-sdk/src/we import classNames from 'classnames'; import AccessibleButton from '../elements/AccessibleButton'; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; -import { isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard'; +import { isOnlyCtrlOrCmdKeyEvent, Key } from '../../../Keyboard'; import { alwaysAboveLeftOf, alwaysAboveRightOf, From 7a329b7a018d9343c7514c3c5ef83edce97c345e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 10:31:24 +0200 Subject: [PATCH 020/445] Add ReactUtils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/utils/ReactUtils.tsx | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/utils/ReactUtils.tsx diff --git a/src/utils/ReactUtils.tsx b/src/utils/ReactUtils.tsx new file mode 100644 index 0000000000..ce92dd8a51 --- /dev/null +++ b/src/utils/ReactUtils.tsx @@ -0,0 +1,33 @@ +/* +Copyright 2021 Šimon Brandner + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; + +/** + * Joins an array into one value with a joiner. E.g. join(["hello", "world"], " ") -> hello world + * @param array the array of element to join + * @param joiner the string/JSX.Element to join with + * @returns the joined array + */ +export function join(array: Array, joiner?: string | JSX.Element): JSX.Element { + const newArray = []; + array.forEach((element, index) => { + newArray.push(element, (index === array.length - 1) ? null : joiner); + }); + return ( + { newArray } + ); +} From 3e95cd1854017de2cf7cd3e6ecda9c4c09992bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 10:34:15 +0200 Subject: [PATCH 021/445] Handle JSX in MELS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/EventListSummary.tsx | 2 +- .../views/elements/MemberEventListSummary.tsx | 14 +++++++++++--- src/utils/FormattingUtils.ts | 7 ++++--- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index 681817ca86..4f6df2aa0e 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -33,7 +33,7 @@ interface IProps { // The list of room members for which to show avatars next to the summary summaryMembers?: RoomMember[]; // The text to show as the summary of this event list - summaryText?: string; + summaryText?: string | JSX.Element; // An array of EventTiles to render when expanded children: ReactNode[]; // Called when the event list expansion is toggled diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx index d52462f629..cef6195067 100644 --- a/src/components/views/elements/MemberEventListSummary.tsx +++ b/src/components/views/elements/MemberEventListSummary.tsx @@ -25,6 +25,7 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import { isValid3pidInvite } from "../../../RoomInvite"; import EventListSummary from "./EventListSummary"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { join } from '../../../utils/ReactUtils'; interface IProps extends Omit, "summaryText" | "summaryMembers"> { // The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left" @@ -89,7 +90,10 @@ export default class MemberEventListSummary extends React.Component { * `Object.keys(eventAggregates)`. * @returns {string} the textual summary of the aggregated events that occurred. */ - private generateSummary(eventAggregates: Record, orderedTransitionSequences: string[]) { + private generateSummary( + eventAggregates: Record, + orderedTransitionSequences: string[], + ): string | JSX.Element { const summaries = orderedTransitionSequences.map((transitions) => { const userNames = eventAggregates[transitions]; const nameList = this.renderNameList(userNames); @@ -118,7 +122,7 @@ export default class MemberEventListSummary extends React.Component { return null; } - return summaries.join(", "); + return join(summaries, ", "); } /** @@ -212,7 +216,11 @@ export default class MemberEventListSummary extends React.Component { * @param {number} repeats the number of times the transition was repeated in a row. * @returns {string} the written Human Readable equivalent of the transition. */ - private static getDescriptionForTransition(t: TransitionType, userCount: number, repeats: number) { + private static getDescriptionForTransition( + t: TransitionType, + userCount: number, + repeats: number, + ): string | JSX.Element { // The empty interpolations 'severalUsers' and 'oneUser' // are there only to show translators to non-English languages // that the verb is conjugated to plural or singular Subject. diff --git a/src/utils/FormattingUtils.ts b/src/utils/FormattingUtils.ts index 1fe3669f26..53a4adb238 100644 --- a/src/utils/FormattingUtils.ts +++ b/src/utils/FormattingUtils.ts @@ -16,6 +16,7 @@ limitations under the License. */ import { _t } from '../languageHandler'; +import { join } from './ReactUtils'; /** * formats numbers to fit into ~3 characters, suitable for badge counts @@ -103,7 +104,7 @@ export function getUserNameColorClass(userId: string): string { * @returns {string} a string constructed by joining `items` with a comma * between each item, but with the last item appended as " and [lastItem]". */ -export function formatCommaSeparatedList(items: string[], itemLimit?: number): string { +export function formatCommaSeparatedList(items: Array, itemLimit?: number): string | JSX.Element { const remaining = itemLimit === undefined ? 0 : Math.max( items.length - itemLimit, 0, ); @@ -113,9 +114,9 @@ export function formatCommaSeparatedList(items: string[], itemLimit?: number): s return items[0]; } else if (remaining > 0) { items = items.slice(0, itemLimit); - return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } ); + return _t("%(items)s and %(count)s others", { items: join(items, ', '), count: remaining } ); } else { const lastItem = items.pop(); - return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem }); + return _t("%(items)s and %(lastItem)s", { items: join(items, ', '), lastItem: lastItem }); } } From f80f4620dfec779c6b47c3bc059e6a4d86f124d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 10:35:20 +0200 Subject: [PATCH 022/445] Add pinned messages to MELS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/MessagePanel.tsx | 7 +++- .../views/elements/MemberEventListSummary.tsx | 39 +++++++++++++++---- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index a0a1ac9b10..16b1c0064b 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -50,7 +50,12 @@ import EditorStateTransfer from "../../utils/EditorStateTransfer"; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = [EventType.Sticker, EventType.RoomMessage]; -const membershipTypes = [EventType.RoomMember, EventType.RoomThirdPartyInvite, EventType.RoomServerAcl]; +const membershipTypes = [ + EventType.RoomMember, + EventType.RoomThirdPartyInvite, + EventType.RoomServerAcl, + EventType.RoomPinnedEvents, +]; // check if there is a previous event and it has the same sender as this event // and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx index cef6195067..80efb2bea8 100644 --- a/src/components/views/elements/MemberEventListSummary.tsx +++ b/src/components/views/elements/MemberEventListSummary.tsx @@ -25,7 +25,22 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import { isValid3pidInvite } from "../../../RoomInvite"; import EventListSummary from "./EventListSummary"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import defaultDispatcher from '../../../dispatcher/dispatcher'; +import { RightPanelPhases } from '../../../stores/RightPanelStorePhases'; +import { Action } from '../../../dispatcher/actions'; +import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload'; import { join } from '../../../utils/ReactUtils'; +import { EventType } from '../../../../../matrix-js-sdk/src/@types/event'; + +const onPinnedMessagesClick = (): void => { + defaultDispatcher.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.PinnedMessages, + allowClose: false, + }); +}; + +const SENDER_AS_DISPLAY_NAME_EVENTS = [EventType.RoomServerAcl, EventType.RoomPinnedEvents]; interface IProps extends Omit, "summaryText" | "summaryMembers"> { // The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left" @@ -58,6 +73,7 @@ enum TransitionType { ChangedAvatar = "changed_avatar", NoChange = "no_change", ServerAcl = "server_acl", + PinnedMessages = "pinned_messages" } const SEP = ","; @@ -303,6 +319,15 @@ export default class MemberEventListSummary extends React.Component { { severalUsers: "", count: repeats }) : _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count: repeats }); break; + case "pinned_messages": + res = (userCount > 1) + ? _t("%(severalUsers)schanged the pinned messages for the room %(count)s times.", + { severalUsers: "", count: repeats }, + { "a": (sub) => { sub } }) + : _t("%(oneUser)schanged the pinned messages for the room %(count)s times.", + { oneUser: "", count: repeats }, + { "a": (sub) => { sub } }); + break; } return res; @@ -321,16 +346,16 @@ export default class MemberEventListSummary extends React.Component { * if a transition is not recognised. */ private static getTransition(e: IUserEvents): TransitionType { - if (e.mxEvent.getType() === 'm.room.third_party_invite') { + if (e.mxEvent.getType() === EventType.RoomThirdPartyInvite) { // Handle 3pid invites the same as invites so they get bundled together if (!isValid3pidInvite(e.mxEvent)) { return TransitionType.InviteWithdrawal; } return TransitionType.Invited; - } - - if (e.mxEvent.getType() === 'm.room.server_acl') { + } else if (e.mxEvent.getType() === EventType.RoomServerAcl) { return TransitionType.ServerAcl; + } else if (e.mxEvent.getType() === EventType.RoomPinnedEvents) { + return TransitionType.PinnedMessages; } switch (e.mxEvent.getContent().membership) { @@ -425,16 +450,16 @@ export default class MemberEventListSummary extends React.Component { userEvents[userId] = []; } - if (e.getType() === 'm.room.server_acl') { + if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(e.getType() as EventType)) { latestUserAvatarMember.set(userId, e.sender); } else if (e.target) { latestUserAvatarMember.set(userId, e.target); } let displayName = userId; - if (e.getType() === 'm.room.third_party_invite') { + if (e.getType() === EventType.RoomThirdPartyInvite) { displayName = e.getContent().display_name; - } else if (e.getType() === 'm.room.server_acl') { + } else if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(e.getType() as EventType)) { displayName = e.sender.name; } else if (e.target) { displayName = e.target.name; From 1b993ef1bde87f797ce4af6dbb9b5708ca56c430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 10:36:56 +0200 Subject: [PATCH 023/445] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7795bb2610..9c22efdb3e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2058,6 +2058,8 @@ "%(severalUsers)schanged the server ACLs %(count)s times|one": "%(severalUsers)schanged the server ACLs", "%(oneUser)schanged the server ACLs %(count)s times|other": "%(oneUser)schanged the server ACLs %(count)s times", "%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)schanged the server ACLs", + "%(severalUsers)schanged the pinned messages for the room %(count)s times.|other": "%(severalUsers)schanged the pinned messages for the room %(count)s times.", + "%(oneUser)schanged the pinned messages for the room %(count)s times.|other": "%(oneUser)schanged the pinned messages for the room %(count)s times.", "Power level": "Power level", "Custom level": "Custom level", "QR Code": "QR Code", From db8ebd6df009f268ced03b2f1bacedec5b3c300f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 10:45:16 +0200 Subject: [PATCH 024/445] Fix import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/MemberEventListSummary.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx index 80efb2bea8..e3dbd0a906 100644 --- a/src/components/views/elements/MemberEventListSummary.tsx +++ b/src/components/views/elements/MemberEventListSummary.tsx @@ -30,7 +30,7 @@ import { RightPanelPhases } from '../../../stores/RightPanelStorePhases'; import { Action } from '../../../dispatcher/actions'; import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload'; import { join } from '../../../utils/ReactUtils'; -import { EventType } from '../../../../../matrix-js-sdk/src/@types/event'; +import { EventType } from 'matrix-js-sdk/src/@types/event'; const onPinnedMessagesClick = (): void => { defaultDispatcher.dispatch({ From 29ef5905d60b14f3751698afb96cffe3c663b6b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 10:45:30 +0200 Subject: [PATCH 025/445] Export type into a var MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/elements/MemberEventListSummary.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx index e3dbd0a906..4ae64b65a5 100644 --- a/src/components/views/elements/MemberEventListSummary.tsx +++ b/src/components/views/elements/MemberEventListSummary.tsx @@ -346,15 +346,17 @@ export default class MemberEventListSummary extends React.Component { * if a transition is not recognised. */ private static getTransition(e: IUserEvents): TransitionType { - if (e.mxEvent.getType() === EventType.RoomThirdPartyInvite) { + const type = e.mxEvent.getType(); + + if (type === EventType.RoomThirdPartyInvite) { // Handle 3pid invites the same as invites so they get bundled together if (!isValid3pidInvite(e.mxEvent)) { return TransitionType.InviteWithdrawal; } return TransitionType.Invited; - } else if (e.mxEvent.getType() === EventType.RoomServerAcl) { + } else if (type === EventType.RoomServerAcl) { return TransitionType.ServerAcl; - } else if (e.mxEvent.getType() === EventType.RoomPinnedEvents) { + } else if (type === EventType.RoomPinnedEvents) { return TransitionType.PinnedMessages; } @@ -444,22 +446,23 @@ export default class MemberEventListSummary extends React.Component { // Object mapping user IDs to an array of IUserEvents const userEvents: Record = {}; eventsToRender.forEach((e, index) => { - const userId = e.getType() === 'm.room.server_acl' ? e.getSender() : e.getStateKey(); + const type = e.getType(); + const userId = type === EventType.RoomServerAcl ? e.getSender() : e.getStateKey(); // Initialise a user's events if (!userEvents[userId]) { userEvents[userId] = []; } - if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(e.getType() as EventType)) { + if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) { latestUserAvatarMember.set(userId, e.sender); } else if (e.target) { latestUserAvatarMember.set(userId, e.target); } let displayName = userId; - if (e.getType() === EventType.RoomThirdPartyInvite) { + if (type === EventType.RoomThirdPartyInvite) { displayName = e.getContent().display_name; - } else if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(e.getType() as EventType)) { + } else if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) { displayName = e.sender.name; } else if (e.target) { displayName = e.target.name; From 19f14e4b2e6495cbbe9b7c70f9753b1005ed2e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 10:58:04 +0200 Subject: [PATCH 026/445] Fix tests? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/utils/ReactUtils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/ReactUtils.tsx b/src/utils/ReactUtils.tsx index ce92dd8a51..25669d2d9b 100644 --- a/src/utils/ReactUtils.tsx +++ b/src/utils/ReactUtils.tsx @@ -28,6 +28,6 @@ export function join(array: Array, joiner?: string | JSX.E newArray.push(element, (index === array.length - 1) ? null : joiner); }); return ( - { newArray } + { newArray } ); } From 98808aabcab35f38ceb0c8d8f7ae6e3fc59cb3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 18:53:29 +0200 Subject: [PATCH 027/445] Set contentEditable for PillParts to false MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/editor/parts.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 351df5062f..39e92ded1c 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -249,6 +249,7 @@ abstract class PillPart extends BasePart implements IPillPart { toDOMNode() { const container = document.createElement("span"); container.setAttribute("spellcheck", "false"); + container.setAttribute("contentEditable", "false"); container.className = this.className; container.appendChild(document.createTextNode(this.text)); this.setAvatar(container); From 5423421240cf1abbe749eb0dde82c8255753784a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 19:09:39 +0200 Subject: [PATCH 028/445] Give singletonRoomViewStore a type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/RoomViewStore.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 10f42f3166..1a85ff59b1 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -429,7 +429,7 @@ class RoomViewStore extends Store { } } -let singletonRoomViewStore = null; +let singletonRoomViewStore: RoomViewStore = null; if (!singletonRoomViewStore) { singletonRoomViewStore = new RoomViewStore(); } From 667abca31f42bfba9c055e521afb1540429dd840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 20:02:32 +0200 Subject: [PATCH 029/445] Handle pill onclick MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_BasicMessageComposer.scss | 1 + src/editor/parts.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index e1ba468204..d87444441a 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -47,6 +47,7 @@ limitations under the License. &.mx_BasicMessageComposer_input_shouldShowPillAvatar { span.mx_UserPill, span.mx_RoomPill { position: relative; + cursor: pointer; // avatar psuedo element &::before { diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 39e92ded1c..8f662f9367 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -25,6 +25,10 @@ import AutocompleteWrapperModel, { UpdateQuery, } from "./autocomplete"; import * as Avatar from "../Avatar"; +import defaultDispatcher from "../dispatcher/dispatcher"; +import { Action } from "../dispatcher/actions"; +import singletonRoomViewStore from "../stores/RoomViewStore"; +import { MatrixClientPeg } from "../MatrixClientPeg"; interface ISerializedPart { type: Type.Plain | Type.Newline | Type.Command | Type.PillCandidate; @@ -74,6 +78,7 @@ interface IPillCandidatePart extends Omit { type: Type.AtRoomPill | Type.RoomPill | Type.UserPill; resourceId: string; + onClick?(): void; } export type Part = IBasePart | IPillCandidatePart | IPillPart; @@ -250,6 +255,7 @@ abstract class PillPart extends BasePart implements IPillPart { const container = document.createElement("span"); container.setAttribute("spellcheck", "false"); container.setAttribute("contentEditable", "false"); + container.onclick = this.onClick; container.className = this.className; container.appendChild(document.createTextNode(this.text)); this.setAvatar(container); @@ -304,6 +310,8 @@ abstract class PillPart extends BasePart implements IPillPart { abstract get className(): string; + abstract onClick?(): void; + abstract setAvatar(node: HTMLElement): void; } @@ -365,6 +373,9 @@ class RoomPillPart extends PillPart { get className() { return "mx_RoomPill mx_Pill"; } + + // FIXME: We do this to shut up the linter, is there a way to do this properly + onClick = undefined; } class AtRoomPillPart extends RoomPillPart { @@ -403,6 +414,13 @@ class UserPillPart extends PillPart { this._setAvatarVars(node, avatarUrl, initialLetter); } + onClick = () => { + defaultDispatcher.dispatch({ + action: Action.ViewUser, + member: MatrixClientPeg.get().getRoom(singletonRoomViewStore.getRoomId()).getMember(this.resourceId), + }); + }; + get type(): IPillPart["type"] { return Type.UserPill; } From 780f9b6add39c4ca9cb9df80c1c22509fbbffc30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 09:29:41 +0200 Subject: [PATCH 030/445] Handle pill deletion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/rooms/BasicMessageComposer.tsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 3258674cf6..d707a25e44 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -507,6 +507,7 @@ export default class BasicMessageEditor extends React.Component handled = true; } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) { this.formatBarRef.current.hide(); + handled = this.fakeDeletion(event.key === Key.BACKSPACE ? "deleteContentBackward" : "deleteContentForward"); } if (handled) { @@ -515,6 +516,29 @@ export default class BasicMessageEditor extends React.Component } }; + /** + * Because pills have contentEditable="false" there is no event emitted when + * the user tries to delete them. Therefore we need to fake what would + * normally happen + * @param direction in which to delete + * @returns handled + */ + private fakeDeletion(direction: "deleteContentForward" | "deleteContentBackward" ): boolean { + const selection = document.getSelection(); + // Use the default handling for ranges + if (selection.type === "Range") return false; + + this.modifiedFlag = true; + const { caret, text } = getCaretOffsetAndText(this.editorRef.current, selection); + + // Do the deletion itself + if (direction === "deleteContentBackward") caret.offset--; + const newText = text.slice(0, caret.offset) + text.slice(caret.offset + 1); + + this.props.model.update(newText, direction, caret); + return true; + } + private async tabCompleteName(): Promise { try { await new Promise(resolve => this.setState({ showVisualBell: false }, resolve)); From 113b6319b129ab550669ed506b5f4b10e8b7ed60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 09:52:01 +0200 Subject: [PATCH 031/445] This looks a bit nicer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/editor/parts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 8f662f9367..3d50c39cd0 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -27,7 +27,7 @@ import AutocompleteWrapperModel, { import * as Avatar from "../Avatar"; import defaultDispatcher from "../dispatcher/dispatcher"; import { Action } from "../dispatcher/actions"; -import singletonRoomViewStore from "../stores/RoomViewStore"; +import RoomViewStore from "../stores/RoomViewStore"; import { MatrixClientPeg } from "../MatrixClientPeg"; interface ISerializedPart { @@ -417,7 +417,7 @@ class UserPillPart extends PillPart { onClick = () => { defaultDispatcher.dispatch({ action: Action.ViewUser, - member: MatrixClientPeg.get().getRoom(singletonRoomViewStore.getRoomId()).getMember(this.resourceId), + member: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()).getMember(this.resourceId), }); }; From 4ef8c9fd297a616658152bba6e0406a301cbe948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 09:52:32 +0200 Subject: [PATCH 032/445] Delint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index ddcb9057ec..f3b580cdca 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -240,7 +240,7 @@ export default class CallPreview extends React.Component { this.scheduledUpdate.mark(); }; - private onRoomViewStoreUpdate = (payload) => { + private onRoomViewStoreUpdate = () => { if (RoomViewStore.getRoomId() === this.state.roomId) return; const roomId = RoomViewStore.getRoomId(); From d7811d9db7f1ac6f2d5a75efb49fd3bd38fbdfb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 09:59:31 +0200 Subject: [PATCH 033/445] Maybe this shuts it up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/ActiveRoomObserver.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ActiveRoomObserver.ts b/src/ActiveRoomObserver.ts index 1126dc9496..c7423fab8f 100644 --- a/src/ActiveRoomObserver.ts +++ b/src/ActiveRoomObserver.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { EventSubscription } from 'fbemitter'; import RoomViewStore from './stores/RoomViewStore'; type Listener = (isActive: boolean) => void; @@ -30,7 +31,7 @@ type Listener = (isActive: boolean) => void; export class ActiveRoomObserver { private listeners: {[key: string]: Listener[]} = {}; private _activeRoomId = RoomViewStore.getRoomId(); - private readonly roomStoreToken: string; + private readonly roomStoreToken: EventSubscription; constructor() { // TODO: We could self-destruct when the last listener goes away, or at least stop listening. From b79f2d06991711a2233e1832e7a37a4d614c7b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 12:21:59 +0200 Subject: [PATCH 034/445] Fix the ugly solution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/editor/parts.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 3d50c39cd0..af741c4502 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -78,7 +78,6 @@ interface IPillCandidatePart extends Omit { type: Type.AtRoomPill | Type.RoomPill | Type.UserPill; resourceId: string; - onClick?(): void; } export type Part = IBasePart | IPillCandidatePart | IPillPart; @@ -310,7 +309,7 @@ abstract class PillPart extends BasePart implements IPillPart { abstract get className(): string; - abstract onClick?(): void; + protected onClick?: () => void; abstract setAvatar(node: HTMLElement): void; } @@ -373,9 +372,6 @@ class RoomPillPart extends PillPart { get className() { return "mx_RoomPill mx_Pill"; } - - // FIXME: We do this to shut up the linter, is there a way to do this properly - onClick = undefined; } class AtRoomPillPart extends RoomPillPart { @@ -414,7 +410,7 @@ class UserPillPart extends PillPart { this._setAvatarVars(node, avatarUrl, initialLetter); } - onClick = () => { + protected onClick = () => { defaultDispatcher.dispatch({ action: Action.ViewUser, member: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()).getMember(this.resourceId), From 48a6a83745a7f799a0647509b4a796ca4cbfb8ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 12:41:58 +0200 Subject: [PATCH 035/445] Set cursor for each pill type separately MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_BasicMessageComposer.scss | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index d87444441a..544a96daba 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -47,7 +47,6 @@ limitations under the License. &.mx_BasicMessageComposer_input_shouldShowPillAvatar { span.mx_UserPill, span.mx_RoomPill { position: relative; - cursor: pointer; // avatar psuedo element &::before { @@ -66,6 +65,14 @@ limitations under the License. font-size: $font-10-4px; } } + + span.mx_UserPill { + cursor: pointer; + } + + span.mx_RoomPill { + cursor: default; + } } &.mx_BasicMessageComposer_input_disabled { From 069c1f466520ddfe424b5b1b58e9054e01b1f7e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 12:52:05 +0200 Subject: [PATCH 036/445] Make code a bit cleaner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/BasicMessageComposer.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index d707a25e44..81211c57b7 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -507,7 +507,7 @@ export default class BasicMessageEditor extends React.Component handled = true; } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) { this.formatBarRef.current.hide(); - handled = this.fakeDeletion(event.key === Key.BACKSPACE ? "deleteContentBackward" : "deleteContentForward"); + handled = this.fakeDeletion(event.key === Key.BACKSPACE); } if (handled) { @@ -523,7 +523,7 @@ export default class BasicMessageEditor extends React.Component * @param direction in which to delete * @returns handled */ - private fakeDeletion(direction: "deleteContentForward" | "deleteContentBackward" ): boolean { + private fakeDeletion(backward: boolean): boolean { const selection = document.getSelection(); // Use the default handling for ranges if (selection.type === "Range") return false; @@ -532,10 +532,10 @@ export default class BasicMessageEditor extends React.Component const { caret, text } = getCaretOffsetAndText(this.editorRef.current, selection); // Do the deletion itself - if (direction === "deleteContentBackward") caret.offset--; + if (backward) caret.offset--; const newText = text.slice(0, caret.offset) + text.slice(caret.offset + 1); - this.props.model.update(newText, direction, caret); + this.props.model.update(newText, backward ? "deleteContentBackward" : "deleteContentForward", caret); return true; } From 3515b2ca05e25d43f1d1ff3bb803e319f5b93c63 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 12 Jul 2021 12:51:27 +0100 Subject: [PATCH 037/445] Fix edge case behaviour caused by our weird reuse of DOM nodes between owners --- src/editor/parts.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/editor/parts.ts b/src/editor/parts.ts index af741c4502..c1724c09bb 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -269,6 +269,9 @@ abstract class PillPart extends BasePart implements IPillPart { if (node.className !== this.className) { node.className = this.className; } + if (node.onclick !== this.onClick) { + node.onclick = this.onClick; + } this.setAvatar(node); } From 8139aeb073a6a3b018e10614a4a39fd0a1841623 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 12 Jul 2021 12:51:49 +0100 Subject: [PATCH 038/445] skip loading room & finding member, use existing member field --- src/editor/parts.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/editor/parts.ts b/src/editor/parts.ts index c1724c09bb..688116ab90 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -27,8 +27,6 @@ import AutocompleteWrapperModel, { import * as Avatar from "../Avatar"; import defaultDispatcher from "../dispatcher/dispatcher"; import { Action } from "../dispatcher/actions"; -import RoomViewStore from "../stores/RoomViewStore"; -import { MatrixClientPeg } from "../MatrixClientPeg"; interface ISerializedPart { type: Type.Plain | Type.Newline | Type.Command | Type.PillCandidate; @@ -416,7 +414,7 @@ class UserPillPart extends PillPart { protected onClick = () => { defaultDispatcher.dispatch({ action: Action.ViewUser, - member: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()).getMember(this.resourceId), + member: this.member, }); }; From 51f0f5718a8882897d99174a0879e610cc158223 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 12 Jul 2021 13:26:34 +0100 Subject: [PATCH 039/445] improve types --- .../views/rooms/BasicMessageComposer.tsx | 10 +- .../views/rooms/EditMessageComposer.tsx | 8 +- .../views/rooms/SendMessageComposer.tsx | 6 +- src/editor/autocomplete.ts | 24 +-- src/editor/caret.ts | 6 +- src/editor/deserialize.ts | 4 +- src/editor/diff.ts | 2 +- src/editor/history.ts | 14 +- src/editor/offset.ts | 5 +- src/editor/operations.ts | 26 +-- src/editor/parts.ts | 162 +++++++++--------- src/editor/position.ts | 14 +- src/editor/range.ts | 20 +-- src/editor/render.ts | 32 ++-- src/editor/serialize.ts | 49 +++--- 15 files changed, 196 insertions(+), 186 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 81211c57b7..bf6a6a27d2 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -32,7 +32,7 @@ import { } from '../../../editor/operations'; import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom'; import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete'; -import { getAutoCompleteCreator } from '../../../editor/parts'; +import { getAutoCompleteCreator, Type } from '../../../editor/parts'; import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize'; import { renderModel } from '../../../editor/render'; import TypingStore from "../../../stores/TypingStore"; @@ -157,7 +157,7 @@ export default class BasicMessageEditor extends React.Component range.expandBackwardsWhile((index, offset) => { const part = model.parts[index]; n -= 1; - return n >= 0 && (part.type === "plain" || part.type === "pill-candidate"); + return n >= 0 && (part.type === Type.Plain || part.type === Type.PillCandidate); }); const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text); if (emoticonMatch) { @@ -548,9 +548,9 @@ export default class BasicMessageEditor extends React.Component const range = model.startRange(position); range.expandBackwardsWhile((index, offset, part) => { return part.text[offset] !== " " && part.text[offset] !== "+" && ( - part.type === "plain" || - part.type === "pill-candidate" || - part.type === "command" + part.type === Type.Plain || + part.type === Type.PillCandidate || + part.type === Type.Command ); }); const { partCreator } = model; diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index e4b13e2155..b7e067ee93 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -25,7 +25,7 @@ import { getCaretOffsetAndText } from '../../../editor/dom'; import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from '../../../editor/serialize'; import { findEditableEvent } from '../../../utils/EventUtils'; import { parseEvent } from '../../../editor/deserialize'; -import { CommandPartCreator, Part, PartCreator } from '../../../editor/parts'; +import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts'; import EditorStateTransfer from '../../../utils/EditorStateTransfer'; import BasicMessageComposer from "./BasicMessageComposer"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; @@ -242,12 +242,12 @@ export default class EditMessageComposer extends React.Component const parts = this.model.parts; const firstPart = parts[0]; if (firstPart) { - if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) { + if (firstPart.type === Type.Command && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) { return true; } if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//") - && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) { + && (firstPart.type === Type.Plain || firstPart.type === Type.PillCandidate)) { return true; } } @@ -268,7 +268,7 @@ export default class EditMessageComposer extends React.Component private getSlashCommand(): [Command, string, string] { const commandText = this.model.parts.reduce((text, part) => { // use mxid to textify user pills in a command - if (part.type === "user-pill") { + if (part.type === Type.UserPill) { return text + part.resourceId; } return text + part.text; diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 0639c20fef..76e33ce4b7 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -31,7 +31,7 @@ import { textSerialize, unescapeMessage, } from '../../../editor/serialize'; -import { CommandPartCreator, Part, PartCreator, SerializedPart } from '../../../editor/parts'; +import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from '../../../editor/parts'; import BasicMessageComposer from "./BasicMessageComposer"; import ReplyThread from "../elements/ReplyThread"; import { findEditableEvent } from '../../../utils/EventUtils'; @@ -240,14 +240,14 @@ export default class SendMessageComposer extends React.Component { const parts = this.model.parts; const firstPart = parts[0]; if (firstPart) { - if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) { + if (firstPart.type === Type.Command && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) { return true; } // be extra resilient when somehow the AutocompleteWrapperModel or // CommandPartCreator fails to insert a command part, so we don't send // a command as a message if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//") - && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) { + && (firstPart.type === Type.Plain || firstPart.type === Type.PillCandidate)) { return true; } } diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts index 518c77fa6c..bf8f457d0c 100644 --- a/src/editor/autocomplete.ts +++ b/src/editor/autocomplete.ts @@ -43,7 +43,7 @@ export default class AutocompleteWrapperModel { ) { } - public onEscape(e: KeyboardEvent) { + public onEscape(e: KeyboardEvent): void { this.getAutocompleterComponent().onEscape(e); this.updateCallback({ replaceParts: [this.partCreator.plain(this.queryPart.text)], @@ -51,27 +51,27 @@ export default class AutocompleteWrapperModel { }); } - public close() { + public close(): void { this.updateCallback({ close: true }); } - public hasSelection() { + public hasSelection(): boolean { return this.getAutocompleterComponent().hasSelection(); } - public hasCompletions() { + public hasCompletions(): boolean { const ac = this.getAutocompleterComponent(); return ac && ac.countCompletions() > 0; } - public onEnter() { + public onEnter(): void { this.updateCallback({ close: true }); } /** * If there is no current autocompletion, start one and move to the first selection. */ - public async startSelection() { + public async startSelection(): Promise { const acComponent = this.getAutocompleterComponent(); if (acComponent.countCompletions() === 0) { // Force completions to show for the text currently entered @@ -81,15 +81,15 @@ export default class AutocompleteWrapperModel { } } - public selectPreviousSelection() { + public selectPreviousSelection(): void { this.getAutocompleterComponent().moveSelection(-1); } - public selectNextSelection() { + public selectNextSelection(): void { this.getAutocompleterComponent().moveSelection(+1); } - public onPartUpdate(part: Part, pos: DocumentPosition) { + public onPartUpdate(part: Part, pos: DocumentPosition): Promise { // cache the typed value and caret here // so we can restore it in onComponentSelectionChange when the value is undefined (meaning it should be the typed text) this.queryPart = part; @@ -97,7 +97,7 @@ export default class AutocompleteWrapperModel { return this.updateQuery(part.text); } - public onComponentSelectionChange(completion: ICompletion) { + public onComponentSelectionChange(completion: ICompletion): void { if (!completion) { this.updateCallback({ replaceParts: [this.queryPart], @@ -109,14 +109,14 @@ export default class AutocompleteWrapperModel { } } - public onComponentConfirm(completion: ICompletion) { + public onComponentConfirm(completion: ICompletion): void { this.updateCallback({ replaceParts: this.partForCompletion(completion), close: true, }); } - private partForCompletion(completion: ICompletion) { + private partForCompletion(completion: ICompletion): Part[] { const { completionId } = completion; const text = completion.completion; switch (completion.type) { diff --git a/src/editor/caret.ts b/src/editor/caret.ts index 67d10ddbb5..2b5035b567 100644 --- a/src/editor/caret.ts +++ b/src/editor/caret.ts @@ -19,7 +19,7 @@ import { needsCaretNodeBefore, needsCaretNodeAfter } from "./render"; import Range from "./range"; import EditorModel from "./model"; import DocumentPosition, { IPosition } from "./position"; -import { Part } from "./parts"; +import { Part, Type } from "./parts"; export type Caret = Range | DocumentPosition; @@ -113,7 +113,7 @@ function findNodeInLineForPart(parts: Part[], partIndex: number) { // to find newline parts for (let i = 0; i <= partIndex; ++i) { const part = parts[i]; - if (part.type === "newline") { + if (part.type === Type.Newline) { lineIndex += 1; nodeIndex = -1; prevPart = null; @@ -128,7 +128,7 @@ function findNodeInLineForPart(parts: Part[], partIndex: number) { // and not an adjacent caret node if (i < partIndex) { const nextPart = parts[i + 1]; - const isLastOfLine = !nextPart || nextPart.type === "newline"; + const isLastOfLine = !nextPart || nextPart.type === Type.Newline; if (needsCaretNodeAfter(part, isLastOfLine)) { nodeIndex += 1; } diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts index eb8adfda9d..beef3be5cf 100644 --- a/src/editor/deserialize.ts +++ b/src/editor/deserialize.ts @@ -20,7 +20,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { walkDOMDepthFirst } from "./dom"; import { checkBlockNode } from "../HtmlUtils"; import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks"; -import { PartCreator } from "./parts"; +import { PartCreator, Type } from "./parts"; import SdkConfig from "../SdkConfig"; function parseAtRoomMentions(text: string, partCreator: PartCreator) { @@ -200,7 +200,7 @@ function prefixQuoteLines(isFirstNode, parts, partCreator) { parts.splice(0, 0, partCreator.plain(QUOTE_LINE_PREFIX)); } for (let i = 0; i < parts.length; i += 1) { - if (parts[i].type === "newline") { + if (parts[i].type === Type.Newline) { parts.splice(i + 1, 0, partCreator.plain(QUOTE_LINE_PREFIX)); i += 1; } diff --git a/src/editor/diff.ts b/src/editor/diff.ts index de8efc9c21..5cf94560ce 100644 --- a/src/editor/diff.ts +++ b/src/editor/diff.ts @@ -21,7 +21,7 @@ export interface IDiff { at?: number; } -function firstDiff(a: string, b: string) { +function firstDiff(a: string, b: string): number { const compareLen = Math.min(a.length, b.length); for (let i = 0; i < compareLen; ++i) { if (a[i] !== b[i]) { diff --git a/src/editor/history.ts b/src/editor/history.ts index 350ba6c99a..7764dbf682 100644 --- a/src/editor/history.ts +++ b/src/editor/history.ts @@ -36,7 +36,7 @@ export default class HistoryManager { private addedSinceLastPush = false; private removedSinceLastPush = false; - clear() { + public clear(): void { this.stack = []; this.newlyTypedCharCount = 0; this.currentIndex = -1; @@ -103,7 +103,7 @@ export default class HistoryManager { } // needs to persist parts and caret position - tryPush(model: EditorModel, caret: Caret, inputType: string, diff: IDiff) { + public tryPush(model: EditorModel, caret: Caret, inputType: string, diff: IDiff): boolean { // ignore state restoration echos. // these respect the inputType values of the input event, // but are actually passed in from MessageEditor calling model.reset() @@ -121,22 +121,22 @@ export default class HistoryManager { return shouldPush; } - ensureLastChangesPushed(model: EditorModel) { + public ensureLastChangesPushed(model: EditorModel): void { if (this.changedSinceLastPush) { this.pushState(model, this.lastCaret); } } - canUndo() { + public canUndo(): boolean { return this.currentIndex >= 1 || this.changedSinceLastPush; } - canRedo() { + public canRedo(): boolean { return this.currentIndex < (this.stack.length - 1); } // returns state that should be applied to model - undo(model: EditorModel) { + public undo(model: EditorModel): IHistory { if (this.canUndo()) { this.ensureLastChangesPushed(model); this.currentIndex -= 1; @@ -145,7 +145,7 @@ export default class HistoryManager { } // returns state that should be applied to model - redo() { + public redo(): IHistory { if (this.canRedo()) { this.changedSinceLastPush = false; this.currentIndex += 1; diff --git a/src/editor/offset.ts b/src/editor/offset.ts index 413a22c71b..2e6e0ffe21 100644 --- a/src/editor/offset.ts +++ b/src/editor/offset.ts @@ -15,16 +15,17 @@ limitations under the License. */ import EditorModel from "./model"; +import DocumentPosition from "./position"; export default class DocumentOffset { constructor(public offset: number, public readonly atNodeEnd: boolean) { } - asPosition(model: EditorModel) { + public asPosition(model: EditorModel): DocumentPosition { return model.positionForOffset(this.offset, this.atNodeEnd); } - add(delta: number, atNodeEnd = false) { + public add(delta: number, atNodeEnd = false): DocumentOffset { return new DocumentOffset(this.offset + delta, atNodeEnd); } } diff --git a/src/editor/operations.ts b/src/editor/operations.ts index a738f2d111..2ff09ccce6 100644 --- a/src/editor/operations.ts +++ b/src/editor/operations.ts @@ -15,13 +15,13 @@ limitations under the License. */ import Range from "./range"; -import { Part } from "./parts"; +import { Part, Type } from "./parts"; /** * Some common queries and transformations on the editor model */ -export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]) { +export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]): void { const { model } = range; model.transform(() => { const oldLen = range.length; @@ -32,7 +32,7 @@ export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]) { }); } -export function replaceRangeAndMoveCaret(range: Range, newParts: Part[]) { +export function replaceRangeAndMoveCaret(range: Range, newParts: Part[]): void { const { model } = range; model.transform(() => { const oldLen = range.length; @@ -43,29 +43,29 @@ export function replaceRangeAndMoveCaret(range: Range, newParts: Part[]) { }); } -export function rangeStartsAtBeginningOfLine(range: Range) { +export function rangeStartsAtBeginningOfLine(range: Range): boolean { const { model } = range; const startsWithPartial = range.start.offset !== 0; const isFirstPart = range.start.index === 0; - const previousIsNewline = !isFirstPart && model.parts[range.start.index - 1].type === "newline"; + const previousIsNewline = !isFirstPart && model.parts[range.start.index - 1].type === Type.Newline; return !startsWithPartial && (isFirstPart || previousIsNewline); } -export function rangeEndsAtEndOfLine(range: Range) { +export function rangeEndsAtEndOfLine(range: Range): boolean { const { model } = range; const lastPart = model.parts[range.end.index]; const endsWithPartial = range.end.offset !== lastPart.text.length; const isLastPart = range.end.index === model.parts.length - 1; - const nextIsNewline = !isLastPart && model.parts[range.end.index + 1].type === "newline"; + const nextIsNewline = !isLastPart && model.parts[range.end.index + 1].type === Type.Newline; return !endsWithPartial && (isLastPart || nextIsNewline); } -export function formatRangeAsQuote(range: Range) { +export function formatRangeAsQuote(range: Range): void { const { model, parts } = range; const { partCreator } = model; for (let i = 0; i < parts.length; ++i) { const part = parts[i]; - if (part.type === "newline") { + if (part.type === Type.Newline) { parts.splice(i + 1, 0, partCreator.plain("> ")); } } @@ -81,10 +81,10 @@ export function formatRangeAsQuote(range: Range) { replaceRangeAndExpandSelection(range, parts); } -export function formatRangeAsCode(range: Range) { +export function formatRangeAsCode(range: Range): void { const { model, parts } = range; const { partCreator } = model; - const needsBlock = parts.some(p => p.type === "newline"); + const needsBlock = parts.some(p => p.type === Type.Newline); if (needsBlock) { parts.unshift(partCreator.plain("```"), partCreator.newline()); if (!rangeStartsAtBeginningOfLine(range)) { @@ -105,9 +105,9 @@ export function formatRangeAsCode(range: Range) { // parts helper methods const isBlank = part => !part.text || !/\S/.test(part.text); -const isNL = part => part.type === "newline"; +const isNL = part => part.type === Type.Newline; -export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix) { +export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix): void { const { model, parts } = range; const { partCreator } = model; diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 688116ab90..4e0235bdf7 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -41,7 +41,7 @@ interface ISerializedPillPart { export type SerializedPart = ISerializedPart | ISerializedPillPart; -enum Type { +export enum Type { Plain = "plain", Newline = "newline", Command = "command", @@ -59,12 +59,12 @@ interface IBasePart { createAutoComplete(updateCallback: UpdateCallback): void; serialize(): SerializedPart; - remove(offset: number, len: number): string; + remove(offset: number, len: number): string | undefined; split(offset: number): IBasePart; validateAndInsert(offset: number, str: string, inputType: string): boolean; - appendUntilRejected(str: string, inputType: string): string; - updateDOMNode(node: Node); - canUpdateDOMNode(node: Node); + appendUntilRejected(str: string, inputType: string): string | undefined; + updateDOMNode(node: Node): void; + canUpdateDOMNode(node: Node): boolean; toDOMNode(): Node; } @@ -87,19 +87,19 @@ abstract class BasePart { this._text = text; } - acceptsInsertion(chr: string, offset: number, inputType: string) { + protected acceptsInsertion(chr: string, offset: number, inputType: string): boolean { return true; } - acceptsRemoval(position: number, chr: string) { + protected acceptsRemoval(position: number, chr: string): boolean { return true; } - merge(part: Part) { + public merge(part: Part): boolean { return false; } - split(offset: number) { + public split(offset: number): IBasePart { const splitText = this.text.substr(offset); this._text = this.text.substr(0, offset); return new PlainPart(splitText); @@ -107,7 +107,7 @@ abstract class BasePart { // removes len chars, or returns the plain text this part should be replaced with // if the part would become invalid if it removed everything. - remove(offset: number, len: number) { + public remove(offset: number, len: number): string | undefined { // validate const strWithRemoval = this.text.substr(0, offset) + this.text.substr(offset + len); for (let i = offset; i < (len + offset); ++i) { @@ -120,7 +120,7 @@ abstract class BasePart { } // append str, returns the remaining string if a character was rejected. - appendUntilRejected(str: string, inputType: string) { + public appendUntilRejected(str: string, inputType: string): string | undefined { const offset = this.text.length; for (let i = 0; i < str.length; ++i) { const chr = str.charAt(i); @@ -134,7 +134,7 @@ abstract class BasePart { // inserts str at offset if all the characters in str were accepted, otherwise don't do anything // return whether the str was accepted or not. - validateAndInsert(offset: number, str: string, inputType: string) { + public validateAndInsert(offset: number, str: string, inputType: string): boolean { for (let i = 0; i < str.length; ++i) { const chr = str.charAt(i); if (!this.acceptsInsertion(chr, offset + i, inputType)) { @@ -147,42 +147,42 @@ abstract class BasePart { return true; } - createAutoComplete(updateCallback: UpdateCallback): void {} + public createAutoComplete(updateCallback: UpdateCallback): void {} - trim(len: number) { + protected trim(len: number): string { const remaining = this._text.substr(len); this._text = this._text.substr(0, len); return remaining; } - get text() { + public get text(): string { return this._text; } - abstract get type(): Type; + public abstract get type(): Type; - get canEdit() { + public get canEdit(): boolean { return true; } - toString() { + public toString(): string { return `${this.type}(${this.text})`; } - serialize(): SerializedPart { + public serialize(): SerializedPart { return { type: this.type as ISerializedPart["type"], text: this.text, }; } - abstract updateDOMNode(node: Node); - abstract canUpdateDOMNode(node: Node); - abstract toDOMNode(): Node; + public abstract updateDOMNode(node: Node): void; + public abstract canUpdateDOMNode(node: Node): boolean; + public abstract toDOMNode(): Node; } abstract class PlainBasePart extends BasePart { - acceptsInsertion(chr: string, offset: number, inputType: string) { + protected acceptsInsertion(chr: string, offset: number, inputType: string): boolean { if (chr === "\n") { return false; } @@ -205,11 +205,11 @@ abstract class PlainBasePart extends BasePart { return true; } - toDOMNode() { + public toDOMNode(): Node { return document.createTextNode(this.text); } - merge(part) { + public merge(part): boolean { if (part.type === this.type) { this._text = this.text + part.text; return true; @@ -217,38 +217,38 @@ abstract class PlainBasePart extends BasePart { return false; } - updateDOMNode(node: Node) { + public updateDOMNode(node: Node): void { if (node.textContent !== this.text) { node.textContent = this.text; } } - canUpdateDOMNode(node: Node) { + public canUpdateDOMNode(node: Node): boolean { return node.nodeType === Node.TEXT_NODE; } } // exported for unit tests, should otherwise only be used through PartCreator export class PlainPart extends PlainBasePart implements IBasePart { - get type(): IBasePart["type"] { + public get type(): IBasePart["type"] { return Type.Plain; } } -abstract class PillPart extends BasePart implements IPillPart { +export abstract class PillPart extends BasePart implements IPillPart { constructor(public resourceId: string, label) { super(label); } - acceptsInsertion(chr: string) { + protected acceptsInsertion(chr: string): boolean { return chr !== " "; } - acceptsRemoval(position: number, chr: string) { + protected acceptsRemoval(position: number, chr: string): boolean { return position !== 0; //if you remove initial # or @, pill should become plain } - toDOMNode() { + public toDOMNode(): Node { const container = document.createElement("span"); container.setAttribute("spellcheck", "false"); container.setAttribute("contentEditable", "false"); @@ -259,7 +259,7 @@ abstract class PillPart extends BasePart implements IPillPart { return container; } - updateDOMNode(node: HTMLElement) { + public updateDOMNode(node: HTMLElement): void { const textNode = node.childNodes[0]; if (textNode.textContent !== this.text) { textNode.textContent = this.text; @@ -273,7 +273,7 @@ abstract class PillPart extends BasePart implements IPillPart { this.setAvatar(node); } - canUpdateDOMNode(node: HTMLElement) { + public canUpdateDOMNode(node: HTMLElement): boolean { return node.nodeType === Node.ELEMENT_NODE && node.nodeName === "SPAN" && node.childNodes.length === 1 && @@ -281,7 +281,7 @@ abstract class PillPart extends BasePart implements IPillPart { } // helper method for subclasses - _setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string) { + protected setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string): void { const avatarBackground = `url('${avatarUrl}')`; const avatarLetter = `'${initialLetter}'`; // check if the value is changing, @@ -294,7 +294,7 @@ abstract class PillPart extends BasePart implements IPillPart { } } - serialize(): ISerializedPillPart { + public serialize(): ISerializedPillPart { return { type: this.type, text: this.text, @@ -302,43 +302,43 @@ abstract class PillPart extends BasePart implements IPillPart { }; } - get canEdit() { + public get canEdit(): boolean { return false; } - abstract get type(): IPillPart["type"]; + public abstract get type(): IPillPart["type"]; - abstract get className(): string; + protected abstract get className(): string; protected onClick?: () => void; - abstract setAvatar(node: HTMLElement): void; + protected abstract setAvatar(node: HTMLElement): void; } class NewlinePart extends BasePart implements IBasePart { - acceptsInsertion(chr: string, offset: number) { + protected acceptsInsertion(chr: string, offset: number): boolean { return offset === 0 && chr === "\n"; } - acceptsRemoval(position: number, chr: string) { + protected acceptsRemoval(position: number, chr: string): boolean { return true; } - toDOMNode() { + public toDOMNode(): Node { return document.createElement("br"); } - merge() { + public merge(): boolean { return false; } - updateDOMNode() {} + public updateDOMNode(): void {} - canUpdateDOMNode(node: HTMLElement) { + public canUpdateDOMNode(node: HTMLElement): boolean { return node.tagName === "BR"; } - get type(): IBasePart["type"] { + public get type(): IBasePart["type"] { return Type.Newline; } @@ -346,7 +346,7 @@ class NewlinePart extends BasePart implements IBasePart { // rather than trying to append to it, which is what we want. // As a newline can also be only one character, it makes sense // as it can only be one character long. This caused #9741. - get canEdit() { + public get canEdit(): boolean { return false; } } @@ -356,21 +356,21 @@ class RoomPillPart extends PillPart { super(resourceId, label); } - setAvatar(node: HTMLElement) { + protected setAvatar(node: HTMLElement): void { let initialLetter = ""; let avatarUrl = Avatar.avatarUrlForRoom(this.room, 16, 16, "crop"); if (!avatarUrl) { initialLetter = Avatar.getInitialLetter(this.room ? this.room.name : this.resourceId); avatarUrl = Avatar.defaultAvatarUrlForString(this.room ? this.room.roomId : this.resourceId); } - this._setAvatarVars(node, avatarUrl, initialLetter); + this.setAvatarVars(node, avatarUrl, initialLetter); } - get type(): IPillPart["type"] { + public get type(): IPillPart["type"] { return Type.RoomPill; } - get className() { + protected get className() { return "mx_RoomPill mx_Pill"; } } @@ -380,11 +380,11 @@ class AtRoomPillPart extends RoomPillPart { super(text, text, room); } - get type(): IPillPart["type"] { + public get type(): IPillPart["type"] { return Type.AtRoomPill; } - serialize(): ISerializedPillPart { + public serialize(): ISerializedPillPart { return { type: this.type, text: this.text, @@ -397,7 +397,7 @@ class UserPillPart extends PillPart { super(userId, displayName); } - setAvatar(node: HTMLElement) { + protected setAvatar(node: HTMLElement): void { if (!this.member) { return; } @@ -408,21 +408,21 @@ class UserPillPart extends PillPart { if (avatarUrl === defaultAvatarUrl) { initialLetter = Avatar.getInitialLetter(name); } - this._setAvatarVars(node, avatarUrl, initialLetter); + this.setAvatarVars(node, avatarUrl, initialLetter); } - protected onClick = () => { + protected onClick = (): void => { defaultDispatcher.dispatch({ action: Action.ViewUser, member: this.member, }); }; - get type(): IPillPart["type"] { + public get type(): IPillPart["type"] { return Type.UserPill; } - get className() { + protected get className() { return "mx_UserPill mx_Pill"; } } @@ -432,11 +432,11 @@ class PillCandidatePart extends PlainBasePart implements IPillCandidatePart { super(text); } - createAutoComplete(updateCallback: UpdateCallback): AutocompleteWrapperModel { + public createAutoComplete(updateCallback: UpdateCallback): AutocompleteWrapperModel { return this.autoCompleteCreator.create(updateCallback); } - acceptsInsertion(chr: string, offset: number, inputType: string) { + protected acceptsInsertion(chr: string, offset: number, inputType: string): boolean { if (offset === 0) { return true; } else { @@ -444,11 +444,11 @@ class PillCandidatePart extends PlainBasePart implements IPillCandidatePart { } } - merge() { + public merge(): boolean { return false; } - acceptsRemoval(position: number, chr: string) { + protected acceptsRemoval(position: number, chr: string): boolean { return true; } @@ -479,17 +479,21 @@ interface IAutocompleteCreator { export class PartCreator { protected readonly autoCompleteCreator: IAutocompleteCreator; - constructor(private room: Room, private client: MatrixClient, autoCompleteCreator: AutoCompleteCreator = null) { + constructor( + private readonly room: Room, + private readonly client: MatrixClient, + autoCompleteCreator: AutoCompleteCreator = null, + ) { // pre-create the creator as an object even without callback so it can already be passed // to PillCandidatePart (e.g. while deserializing) and set later on - this.autoCompleteCreator = { create: autoCompleteCreator && autoCompleteCreator(this) }; + this.autoCompleteCreator = { create: autoCompleteCreator?.(this) }; } - setAutoCompleteCreator(autoCompleteCreator: AutoCompleteCreator) { + public setAutoCompleteCreator(autoCompleteCreator: AutoCompleteCreator): void { this.autoCompleteCreator.create = autoCompleteCreator(this); } - createPartForInput(input: string, partIndex: number, inputType?: string): Part { + public createPartForInput(input: string, partIndex: number, inputType?: string): Part { switch (input[0]) { case "#": case "@": @@ -503,11 +507,11 @@ export class PartCreator { } } - createDefaultPart(text: string) { + public createDefaultPart(text: string): Part { return this.plain(text); } - deserializePart(part: SerializedPart): Part { + public deserializePart(part: SerializedPart): Part { switch (part.type) { case Type.Plain: return this.plain(part.text); @@ -524,19 +528,19 @@ export class PartCreator { } } - plain(text: string) { + public plain(text: string): PlainPart { return new PlainPart(text); } - newline() { + public newline(): NewlinePart { return new NewlinePart("\n"); } - pillCandidate(text: string) { + public pillCandidate(text: string): PillCandidatePart { return new PillCandidatePart(text, this.autoCompleteCreator); } - roomPill(alias: string, roomId?: string) { + public roomPill(alias: string, roomId?: string): RoomPillPart { let room; if (roomId || alias[0] !== "#") { room = this.client.getRoom(roomId || alias); @@ -549,16 +553,20 @@ export class PartCreator { return new RoomPillPart(alias, room ? room.name : alias, room); } - atRoomPill(text: string) { + public atRoomPill(text: string): AtRoomPillPart { return new AtRoomPillPart(text, this.room); } - userPill(displayName: string, userId: string) { + public userPill(displayName: string, userId: string): UserPillPart { const member = this.room.getMember(userId); return new UserPillPart(userId, displayName, member); } - createMentionParts(insertTrailingCharacter: boolean, displayName: string, userId: string) { + public createMentionParts( + insertTrailingCharacter: boolean, + displayName: string, + userId: string, + ): [UserPillPart, PlainPart] { const pill = this.userPill(displayName, userId); const postfix = this.plain(insertTrailingCharacter ? ": " : " "); return [pill, postfix]; @@ -583,7 +591,7 @@ export class CommandPartCreator extends PartCreator { } public deserializePart(part: SerializedPart): Part { - if (part.type === "command") { + if (part.type === Type.Command) { return this.command(part.text); } else { return super.deserializePart(part); diff --git a/src/editor/position.ts b/src/editor/position.ts index 37d2a07b43..50dc283eb3 100644 --- a/src/editor/position.ts +++ b/src/editor/position.ts @@ -30,7 +30,7 @@ export default class DocumentPosition implements IPosition { constructor(public readonly index: number, public readonly offset: number) { } - compare(otherPos: DocumentPosition) { + public compare(otherPos: DocumentPosition): number { if (this.index === otherPos.index) { return this.offset - otherPos.offset; } else { @@ -38,7 +38,7 @@ export default class DocumentPosition implements IPosition { } } - iteratePartsBetween(other: DocumentPosition, model: EditorModel, callback: Callback) { + public iteratePartsBetween(other: DocumentPosition, model: EditorModel, callback: Callback): void { if (this.index === -1 || other.index === -1) { return; } @@ -57,7 +57,7 @@ export default class DocumentPosition implements IPosition { } } - forwardsWhile(model: EditorModel, predicate: Predicate) { + public forwardsWhile(model: EditorModel, predicate: Predicate): DocumentPosition { if (this.index === -1) { return this; } @@ -82,7 +82,7 @@ export default class DocumentPosition implements IPosition { } } - backwardsWhile(model: EditorModel, predicate: Predicate) { + public backwardsWhile(model: EditorModel, predicate: Predicate): DocumentPosition { if (this.index === -1) { return this; } @@ -107,7 +107,7 @@ export default class DocumentPosition implements IPosition { } } - asOffset(model: EditorModel) { + public asOffset(model: EditorModel): DocumentOffset { if (this.index === -1) { return new DocumentOffset(0, true); } @@ -121,7 +121,7 @@ export default class DocumentPosition implements IPosition { return new DocumentOffset(offset, atEnd); } - isAtEnd(model: EditorModel) { + public isAtEnd(model: EditorModel): boolean { if (model.parts.length === 0) { return true; } @@ -130,7 +130,7 @@ export default class DocumentPosition implements IPosition { return this.index === lastPartIdx && this.offset === lastPart.text.length; } - isAtStart() { + public isAtStart(): boolean { return this.index === 0 && this.offset === 0; } } diff --git a/src/editor/range.ts b/src/editor/range.ts index 634805702f..13776177a7 100644 --- a/src/editor/range.ts +++ b/src/editor/range.ts @@ -32,23 +32,23 @@ export default class Range { this._end = bIsLarger ? positionB : positionA; } - moveStart(delta: number) { + public moveStart(delta: number): void { this._start = this._start.forwardsWhile(this.model, () => { delta -= 1; return delta >= 0; }); } - trim() { + public trim(): void { this._start = this._start.forwardsWhile(this.model, whitespacePredicate); this._end = this._end.backwardsWhile(this.model, whitespacePredicate); } - expandBackwardsWhile(predicate: Predicate) { + public expandBackwardsWhile(predicate: Predicate): void { this._start = this._start.backwardsWhile(this.model, predicate); } - get text() { + public get text(): string { let text = ""; this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => { const t = part.text.substring(startIdx, endIdx); @@ -63,7 +63,7 @@ export default class Range { * @param {Part[]} parts the parts to replace the range with * @return {Number} the net amount of characters added, can be negative. */ - replace(parts: Part[]) { + public replace(parts: Part[]): number { const newLength = parts.reduce((sum, part) => sum + part.text.length, 0); let oldLength = 0; this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => { @@ -77,8 +77,8 @@ export default class Range { * Returns a copy of the (partial) parts within the range. * For partial parts, only the text is adjusted to the part that intersects with the range. */ - get parts() { - const parts = []; + public get parts(): Part[] { + const parts: Part[] = []; this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => { const serializedPart = part.serialize(); serializedPart.text = part.text.substring(startIdx, endIdx); @@ -88,7 +88,7 @@ export default class Range { return parts; } - get length() { + public get length(): number { let len = 0; this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => { len += endIdx - startIdx; @@ -96,11 +96,11 @@ export default class Range { return len; } - get start() { + public get start(): DocumentPosition { return this._start; } - get end() { + public get end(): DocumentPosition { return this._end; } } diff --git a/src/editor/render.ts b/src/editor/render.ts index 0e0b7d2145..d9997de855 100644 --- a/src/editor/render.ts +++ b/src/editor/render.ts @@ -15,19 +15,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Part } from "./parts"; +import { Part, Type } from "./parts"; import EditorModel from "./model"; -export function needsCaretNodeBefore(part: Part, prevPart: Part) { - const isFirst = !prevPart || prevPart.type === "newline"; +export function needsCaretNodeBefore(part: Part, prevPart: Part): boolean { + const isFirst = !prevPart || prevPart.type === Type.Newline; return !part.canEdit && (isFirst || !prevPart.canEdit); } -export function needsCaretNodeAfter(part: Part, isLastOfLine: boolean) { +export function needsCaretNodeAfter(part: Part, isLastOfLine: boolean): boolean { return !part.canEdit && isLastOfLine; } -function insertAfter(node: HTMLElement, nodeToInsert: HTMLElement) { +function insertAfter(node: HTMLElement, nodeToInsert: HTMLElement): void { const next = node.nextSibling; if (next) { node.parentElement.insertBefore(nodeToInsert, next); @@ -44,25 +44,25 @@ export const CARET_NODE_CHAR = "\ufeff"; // a caret node is a node that allows the caret to be placed // where otherwise it wouldn't be possible // (e.g. next to a pill span without adjacent text node) -function createCaretNode() { +function createCaretNode(): HTMLElement { const span = document.createElement("span"); span.className = "caretNode"; span.appendChild(document.createTextNode(CARET_NODE_CHAR)); return span; } -function updateCaretNode(node: HTMLElement) { +function updateCaretNode(node: HTMLElement): void { // ensure the caret node contains only a zero-width space if (node.textContent !== CARET_NODE_CHAR) { node.textContent = CARET_NODE_CHAR; } } -export function isCaretNode(node: HTMLElement) { +export function isCaretNode(node: HTMLElement): boolean { return node && node.tagName === "SPAN" && node.className === "caretNode"; } -function removeNextSiblings(node: ChildNode) { +function removeNextSiblings(node: ChildNode): void { if (!node) { return; } @@ -74,7 +74,7 @@ function removeNextSiblings(node: ChildNode) { } } -function removeChildren(parent: HTMLElement) { +function removeChildren(parent: HTMLElement): void { const firstChild = parent.firstChild; if (firstChild) { removeNextSiblings(firstChild); @@ -82,7 +82,7 @@ function removeChildren(parent: HTMLElement) { } } -function reconcileLine(lineContainer: ChildNode, parts: Part[]) { +function reconcileLine(lineContainer: ChildNode, parts: Part[]): void { let currentNode; let prevPart; const lastPart = parts[parts.length - 1]; @@ -131,13 +131,13 @@ function reconcileLine(lineContainer: ChildNode, parts: Part[]) { removeNextSiblings(currentNode); } -function reconcileEmptyLine(lineContainer) { +function reconcileEmptyLine(lineContainer: HTMLElement): void { // empty div needs to have a BR in it to give it height let foundBR = false; let partNode = lineContainer.firstChild; while (partNode) { const nextNode = partNode.nextSibling; - if (!foundBR && partNode.tagName === "BR") { + if (!foundBR && (partNode as HTMLElement).tagName === "BR") { foundBR = true; } else { partNode.remove(); @@ -149,9 +149,9 @@ function reconcileEmptyLine(lineContainer) { } } -export function renderModel(editor: HTMLDivElement, model: EditorModel) { +export function renderModel(editor: HTMLDivElement, model: EditorModel): void { const lines = model.parts.reduce((linesArr, part) => { - if (part.type === "newline") { + if (part.type === Type.Newline) { linesArr.push([]); } else { const lastLine = linesArr[linesArr.length - 1]; @@ -175,7 +175,7 @@ export function renderModel(editor: HTMLDivElement, model: EditorModel) { if (parts.length) { reconcileLine(lineContainer, parts); } else { - reconcileEmptyLine(lineContainer); + reconcileEmptyLine(lineContainer as HTMLElement); } }); if (lines.length) { diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index f68173ae29..38a73cc945 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -22,30 +22,31 @@ import { AllHtmlEntities } from 'html-entities'; import SettingsStore from '../settings/SettingsStore'; import SdkConfig from '../SdkConfig'; import cheerio from 'cheerio'; +import { Type } from './parts'; -export function mdSerialize(model: EditorModel) { +export function mdSerialize(model: EditorModel): string { return model.parts.reduce((html, part) => { switch (part.type) { - case "newline": + case Type.Newline: return html + "\n"; - case "plain": - case "command": - case "pill-candidate": - case "at-room-pill": + case Type.Plain: + case Type.Command: + case Type.PillCandidate: + case Type.AtRoomPill: return html + part.text; - case "room-pill": + case Type.RoomPill: // Here we use the resourceId for compatibility with non-rich text clients // See https://github.com/vector-im/element-web/issues/16660 return html + `[${part.resourceId.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`; - case "user-pill": + case Type.UserPill: return html + `[${part.text.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`; } }, ""); } -export function htmlSerializeIfNeeded(model: EditorModel, { forceHTML = false } = {}) { +export function htmlSerializeIfNeeded(model: EditorModel, { forceHTML = false } = {}): string { let md = mdSerialize(model); // copy of raw input to remove unwanted math later const orig = md; @@ -156,31 +157,31 @@ export function htmlSerializeIfNeeded(model: EditorModel, { forceHTML = false } } } -export function textSerialize(model: EditorModel) { +export function textSerialize(model: EditorModel): string { return model.parts.reduce((text, part) => { switch (part.type) { - case "newline": + case Type.Newline: return text + "\n"; - case "plain": - case "command": - case "pill-candidate": - case "at-room-pill": + case Type.Plain: + case Type.Command: + case Type.PillCandidate: + case Type.AtRoomPill: return text + part.text; - case "room-pill": + case Type.RoomPill: // Here we use the resourceId for compatibility with non-rich text clients // See https://github.com/vector-im/element-web/issues/16660 return text + `${part.resourceId}`; - case "user-pill": + case Type.UserPill: return text + `${part.text}`; } }, ""); } -export function containsEmote(model: EditorModel) { +export function containsEmote(model: EditorModel): boolean { return startsWith(model, "/me ", false); } -export function startsWith(model: EditorModel, prefix: string, caseSensitive = true) { +export function startsWith(model: EditorModel, prefix: string, caseSensitive = true): boolean { const firstPart = model.parts[0]; // part type will be "plain" while editing, // and "command" while composing a message. @@ -190,26 +191,26 @@ export function startsWith(model: EditorModel, prefix: string, caseSensitive = t text = text.toLowerCase(); } - return firstPart && (firstPart.type === "plain" || firstPart.type === "command") && text.startsWith(prefix); + return firstPart && (firstPart.type === Type.Plain || firstPart.type === Type.Command) && text.startsWith(prefix); } -export function stripEmoteCommand(model: EditorModel) { +export function stripEmoteCommand(model: EditorModel): EditorModel { // trim "/me " return stripPrefix(model, "/me "); } -export function stripPrefix(model: EditorModel, prefix: string) { +export function stripPrefix(model: EditorModel, prefix: string): EditorModel { model = model.clone(); model.removeText({ index: 0, offset: 0 }, prefix.length); return model; } -export function unescapeMessage(model: EditorModel) { +export function unescapeMessage(model: EditorModel): EditorModel { const { parts } = model; if (parts.length) { const firstPart = parts[0]; // only unescape \/ to / at start of editor - if (firstPart.type === "plain" && firstPart.text.startsWith("\\/")) { + if (firstPart.type === Type.Plain && firstPart.text.startsWith("\\/")) { model = model.clone(); model.removeText({ index: 0, offset: 0 }, 1); } From 226224b0394e9100fb56f1e7f80e9212c37b47fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 14 Jul 2021 15:12:35 +0200 Subject: [PATCH 040/445] Allow sending hidden RRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/TimelinePanel.tsx | 8 ++++++-- .../views/settings/tabs/user/LabsUserSettingsTab.js | 1 + src/settings/Settings.tsx | 7 +++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 85a048e9b8..bd90b637d6 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -758,16 +758,20 @@ class TimelinePanel extends React.Component { } this.lastRMSentEventId = this.state.readMarkerEventId; + const roomId = this.props.timelineSet.room.roomId; + const hiddenRR = !SettingsStore.getValue("sendReadReceipts", roomId); + debuglog('TimelinePanel: Sending Read Markers for ', this.props.timelineSet.room.roomId, 'rm', this.state.readMarkerEventId, lastReadEvent ? 'rr ' + lastReadEvent.getId() : '', + ' hidden:' + hiddenRR, ); MatrixClientPeg.get().setRoomReadMarkers( - this.props.timelineSet.room.roomId, + roomId, this.state.readMarkerEventId, lastReadEvent, // Could be null, in which case no RR is sent - {}, + { hidden: hiddenRR }, ).catch((e) => { // /read_markers API is not implemented on this HS, fallback to just RR if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) { diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js index abf9709f50..e57ad80bbc 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js @@ -74,6 +74,7 @@ export default class LabsUserSettingsTab extends React.Component { +
; } diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 1751eddb2c..163bfad2b3 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -325,6 +325,13 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_ACCOUNT_SETTINGS, default: null, }, + "sendReadReceipts": { + supportedLevels: LEVELS_ROOM_SETTINGS, + displayName: _td( + "Send read receipts for messages (requires compatible homeserver to disable)", + ), + default: true, + }, "baseFontSize": { displayName: _td("Font size"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, From 18343d839c9756785ecffdf5d294b0504c597525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 15 Jul 2021 09:23:15 +0200 Subject: [PATCH 041/445] Show MSC2285 only if supported by server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../settings/tabs/user/LabsUserSettingsTab.js | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js index e57ad80bbc..18e78ae7b0 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js @@ -19,11 +19,12 @@ import { _t } from "../../../../../languageHandler"; import PropTypes from "prop-types"; import SettingsStore from "../../../../../settings/SettingsStore"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; -import * as sdk from "../../../../../index"; import { SettingLevel } from "../../../../../settings/SettingLevel"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; import SdkConfig from "../../../../../SdkConfig"; import BetaCard from "../../../beta/BetaCard"; +import SettingsFlag from '../../../elements/SettingsFlag'; +import { MatrixClientPeg } from '../../../../../MatrixClientPeg'; export class LabsSettingToggle extends React.Component { static propTypes = { @@ -47,6 +48,14 @@ export class LabsSettingToggle extends React.Component { export default class LabsUserSettingsTab extends React.Component { constructor() { super(); + + MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285").then((showHiddenReadReceipts) => { + this.setState({ showHiddenReadReceipts }); + }); + + this.state = { + showHiddenReadReceipts: false, + }; } render() { @@ -65,16 +74,22 @@ export default class LabsUserSettingsTab extends React.Component { let labsSection; if (SdkConfig.get()['showLabsSettings']) { - const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); const flags = labs.map(f => ); + let hiddenReadReceipts; + if (this.state.showHiddenReadReceipts) { + hiddenReadReceipts = ( + + ); + } + labsSection =
- {flags} + { flags } - + { hiddenReadReceipts }
; } From d9b3c4d19ccf002968a1d242f7f88c462178b66f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 16 Jul 2021 10:22:02 +0200 Subject: [PATCH 042/445] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5cc900a21b..cad6e438d8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -823,6 +823,7 @@ "Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices", "Enable advanced debugging for the room list": "Enable advanced debugging for the room list", "Show info about bridges in room settings": "Show info about bridges in room settings", + "Send read receipts for messages (requires compatible homeserver to disable)": "Send read receipts for messages (requires compatible homeserver to disable)", "Font size": "Font size", "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", From 2a48d3c9bc83e9083c3a9d7366c008f3909c9f6d Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 07:40:39 +0100 Subject: [PATCH 043/445] First pass at a PosthogAnalytics class --- package.json | 1 + src/PosthogAnalytics.ts | 82 ++++++++++++++++++++++++++++++++++ test/PosthogAnalytics-test.ts | 84 +++++++++++++++++++++++++++++++++++ yarn.lock | 12 +++++ 4 files changed, 179 insertions(+) create mode 100644 src/PosthogAnalytics.ts create mode 100644 test/PosthogAnalytics-test.ts diff --git a/package.json b/package.json index e80ed8dd5a..805531abff 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "pako": "^2.0.3", "parse5": "^6.0.1", "png-chunks-extract": "^1.0.0", + "posthog-js": "^1.12.1", "prop-types": "^15.7.2", "qrcode": "^1.4.4", "re-resizable": "^6.9.0", diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts new file mode 100644 index 0000000000..1ca2d37de7 --- /dev/null +++ b/src/PosthogAnalytics.ts @@ -0,0 +1,82 @@ +import posthog from 'posthog-js'; +import SdkConfig from './SdkConfig'; + +export interface IEvent { + key: string; + properties: {} +} + +export interface IOnboardingLoginBegin extends IEvent { + key: "onboarding_login_begin", +} + +const hashHex = async (input: string): Promise => { + const buf = new TextEncoder().encode(input); + const digestBuf = await window.crypto.subtle.digest("sha-256", buf); + return [...new Uint8Array(digestBuf)].map((b: number) => b.toString(16).padStart(2, "0")).join(""); +}; + +export class PosthogAnalytics { + private onlyTrackAnonymousEvents = false; + private initialised = false; + private posthog = null; + + private static _instance = null; + + public static instance(): PosthogAnalytics { + if (!this.instance) { + this._instance = new PosthogAnalytics(posthog); + } + return this._instance; + } + + constructor(posthog) { + this.posthog = posthog; + } + + public init(onlyTrackAnonymousEvents: boolean) { + if (Boolean(navigator.doNotTrack === "1")) { + this.initialised = false; + return; + } + this.onlyTrackAnonymousEvents = onlyTrackAnonymousEvents; + const posthogConfig = SdkConfig.get()["posthog"]; + if (posthogConfig) { + console.log(`Initialising Posthog for ${posthogConfig.apiHost}`); + this.posthog.init(posthogConfig.projectApiKey, { api_host: posthogConfig.apiHost }); + this.initialised = true; + } + } + + public isInitialised(): boolean { + return this.initialised; + } + + public setOnlyTrackAnonymousEvents(enabled: boolean) { + this.onlyTrackAnonymousEvents = enabled; + } + + public track( + key: E["key"], + properties: E["properties"], + anonymous = false, + ) { + if (!this.initialised) return; + if (this.onlyTrackAnonymousEvents && !anonymous) return; + + this.posthog.capture(key, properties); + } + + public async trackRoomEvent( + key: E["key"], + roomId: string, + properties: E["properties"], + ...args + ) { + const updatedProperties = { + ...properties, + hashedRoomId: roomId ? await hashHex(roomId) : null, + }; + this.track(key, updatedProperties, ...args); + } +} diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts new file mode 100644 index 0000000000..a37d5cb2c8 --- /dev/null +++ b/test/PosthogAnalytics-test.ts @@ -0,0 +1,84 @@ +import { IEvent, PosthogAnalytics } from '../src/PosthogAnalytics'; +import SdkConfig from '../src/SdkConfig'; +const crypto = require('crypto'); + +class FakePosthog { + public capture; + public init; + + constructor() { + this.capture = jest.fn(); + this.init = jest.fn(); + } +} + +export interface ITestEvent extends IEvent { + key: "jest_test_event", + properties: { + foo: string + } +} + +describe("PosthogAnalytics", () => { + let analytics: PosthogAnalytics; + let fakePosthog: FakePosthog; + + beforeEach(() => { + fakePosthog = new FakePosthog(); + analytics = new PosthogAnalytics(fakePosthog); + window.crypto = { + subtle: crypto.webcrypto.subtle, + }; + }); + + afterEach(() => { + navigator.doNotTrack = null; + window.crypto = null; + }); + + it("Should not initialise if DNT is enabled", () => { + navigator.doNotTrack = "1"; + analytics.init(false); + expect(analytics.isInitialised()).toBe(false); + }); + + it("Should not initialise if config is not set", () => { + jest.spyOn(SdkConfig, "get").mockReturnValue({}); + analytics.init(false); + expect(analytics.isInitialised()).toBe(false); + }); + + it("Should initialise if config is set", () => { + jest.spyOn(SdkConfig, "get").mockReturnValue({ + posthog: { + projectApiKey: "foo", + apiHost: "bar", + }, + }); + analytics.init(false); + expect(analytics.isInitialised()).toBe(true); + }); + + it("Should pass track() to posthog", () => { + analytics.init(false); + analytics.track("jest_test_event", { + foo: "bar", + }); + expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); + expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ foo: "bar" }); + }); + + it("Should pass trackRoomEvent to posthog", () => { + analytics.init(false); + const roomId = "42"; + return analytics.trackRoomEvent("jest_test_event", roomId, { + foo: "bar", + }).then(() => { + expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); + expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ + foo: "bar", + hashedRoomId: "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049", + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 96c02681fd..9d41c37b12 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3601,6 +3601,11 @@ fbjs@^0.8.4: setimmediate "^1.0.5" ua-parser-js "^0.7.18" +fflate@^0.4.1: + version "0.4.8" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" + integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== + file-entry-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.0.tgz#7921a89c391c6d93efec2169ac6bf300c527ea0a" @@ -6287,6 +6292,13 @@ postcss@^8.0.2: nanoid "^3.1.20" source-map "^0.6.1" +posthog-js@^1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.12.1.tgz#97834ee2574f34ffb5db2f5b07452c847e3c4d27" + integrity sha512-Y3lzcWkS8xFY6Ryj3I4ees7qWP2WGkLw0Arcbk5xaT0+5YlA6UC2jlL/+fN9bz/Bl62EoN3BML901Cuot/QNjg== + dependencies: + fflate "^0.4.1" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" From d4550c1a28a61a8026590f83ebea5c6589406a8f Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 07:42:44 +0100 Subject: [PATCH 044/445] Remove console logging --- src/PosthogAnalytics.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 1ca2d37de7..5b2a601adc 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -42,7 +42,6 @@ export class PosthogAnalytics { this.onlyTrackAnonymousEvents = onlyTrackAnonymousEvents; const posthogConfig = SdkConfig.get()["posthog"]; if (posthogConfig) { - console.log(`Initialising Posthog for ${posthogConfig.apiHost}`); this.posthog.init(posthogConfig.projectApiKey, { api_host: posthogConfig.apiHost }); this.initialised = true; } From 3135e425865232bfd1e1b131a97ec067c150802d Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 07:42:55 +0100 Subject: [PATCH 045/445] Add test for silently ignoring messages when not initialised --- test/PosthogAnalytics-test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index a37d5cb2c8..56e6af8666 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -81,4 +81,12 @@ describe("PosthogAnalytics", () => { }); }); }); + + it("Should silently not send messages if not inititalised", () => { + analytics.track("jest_test_event", { + foo: "bar", + }); + + expect(fakePosthog.capture.mock.calls.length).toBe(0); + }); }); From 74b0e52f9a2ac243af50ab419da9c296b74a1540 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 08:23:42 +0100 Subject: [PATCH 046/445] Enforce anon/pseudo-anon via types --- src/PosthogAnalytics.ts | 48 +++++++++++++++++++++++++---------- test/PosthogAnalytics-test.ts | 25 +++++++++++++----- 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 5b2a601adc..133c9275d4 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -1,11 +1,28 @@ import posthog from 'posthog-js'; import SdkConfig from './SdkConfig'; -export interface IEvent { - key: string; +interface IEvent { + // The event name that will be used by PostHog. + // TODO: standard format (camel case? snake? UpperCase?) + eventName: string; + + // The properties of the event that will be stored in PostHog. properties: {} } +// If an event extends IPseudonymousEvent, the event contains pseudonymous data +// that won't be sent unless the user has explicitly consented to pseudonymous tracking. +// For example, hashed user IDs or room IDs. +export interface IPseudonymousEvent extends IEvent {} + +// If an event extends IAnonymousEvent, the event strictly contains *only* anonymous data which +// may be sent without explicit user consent. +export interface IAnonymousEvent extends IEvent {} + +export interface IRoomEvent extends IPseudonymousEvent { + hashedRoomId: string +} + export interface IOnboardingLoginBegin extends IEvent { key: "onboarding_login_begin", } @@ -55,27 +72,32 @@ export class PosthogAnalytics { this.onlyTrackAnonymousEvents = enabled; } - public track( - key: E["key"], + public trackPseudonymousEvent( + eventName: E["eventName"], properties: E["properties"], - anonymous = false, ) { if (!this.initialised) return; - if (this.onlyTrackAnonymousEvents && !anonymous) return; - - this.posthog.capture(key, properties); + if (this.onlyTrackAnonymousEvents) return; + this.posthog.capture(eventName, properties); } - public async trackRoomEvent( - key: E["key"], - roomId: string, + public trackAnonymousEvent( + eventName: E["eventName"], properties: E["properties"], - ...args + ) { + if (!this.initialised) return; + this.posthog.capture(eventName, properties); + } + + public async trackRoomEvent( + eventName: E["eventName"], + roomId: string, + properties: Omit, ) { const updatedProperties = { ...properties, hashedRoomId: roomId ? await hashHex(roomId) : null, }; - this.track(key, updatedProperties, ...args); + this.trackPseudonymousEvent(eventName, updatedProperties); } } diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index 56e6af8666..dfadac921d 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -1,4 +1,4 @@ -import { IEvent, PosthogAnalytics } from '../src/PosthogAnalytics'; +import { IAnonymousEvent, IRoomEvent, PosthogAnalytics } from '../src/PosthogAnalytics'; import SdkConfig from '../src/SdkConfig'; const crypto = require('crypto'); @@ -12,13 +12,20 @@ class FakePosthog { } } -export interface ITestEvent extends IEvent { +export interface ITestEvent extends IAnonymousEvent { key: "jest_test_event", properties: { foo: string } } +export interface ITestRoomEvent extends IRoomEvent { + key: "jest_test_room_event", + properties: { + foo: string + } +} + describe("PosthogAnalytics", () => { let analytics: PosthogAnalytics; let fakePosthog: FakePosthog; @@ -61,7 +68,7 @@ describe("PosthogAnalytics", () => { it("Should pass track() to posthog", () => { analytics.init(false); - analytics.track("jest_test_event", { + analytics.trackAnonymousEvent("jest_test_event", { foo: "bar", }); expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); @@ -71,7 +78,7 @@ describe("PosthogAnalytics", () => { it("Should pass trackRoomEvent to posthog", () => { analytics.init(false); const roomId = "42"; - return analytics.trackRoomEvent("jest_test_event", roomId, { + return analytics.trackRoomEvent("jest_test_event", roomId, { foo: "bar", }).then(() => { expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); @@ -82,11 +89,17 @@ describe("PosthogAnalytics", () => { }); }); - it("Should silently not send messages if not inititalised", () => { - analytics.track("jest_test_event", { + it("Should silently not track if not inititalised", () => { + analytics.trackAnonymousEvent("jest_test_event", { foo: "bar", }); expect(fakePosthog.capture.mock.calls.length).toBe(0); }); + + it("Should not track non-anonymous messages if onlyTrackAnonymousEvents is true", () => { + analytics.trackAnonymousEvent("jest_test_event", { + foo: "bar", + }); + }); }); From 4b0cb409a078e0baa701e91ff88eae2d49e41bb9 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 08:38:58 +0100 Subject: [PATCH 047/445] Add identifyUser --- src/PosthogAnalytics.ts | 5 +++++ test/PosthogAnalytics-test.ts | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 133c9275d4..404f0e5f20 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -64,6 +64,11 @@ export class PosthogAnalytics { } } + public async identifyUser(userId: string) { + if (this.onlyTrackAnonymousEvents) return; + this.posthog.identify(await hashHex(userId)); + } + public isInitialised(): boolean { return this.initialised; } diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index dfadac921d..fd49255fa1 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -5,10 +5,12 @@ const crypto = require('crypto'); class FakePosthog { public capture; public init; + public identify; constructor() { this.capture = jest.fn(); this.init = jest.fn(); + this.identify = jest.fn(); } } @@ -75,7 +77,7 @@ describe("PosthogAnalytics", () => { expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ foo: "bar" }); }); - it("Should pass trackRoomEvent to posthog", () => { + it("Should pass trackRoomEvent to posthog", async () => { analytics.init(false); const roomId = "42"; return analytics.trackRoomEvent("jest_test_event", roomId, { @@ -102,4 +104,18 @@ describe("PosthogAnalytics", () => { foo: "bar", }); }); + + it("Should identify the user to posthog if onlyTrackAnonymousEvents is false", async () => { + analytics.init(false); + await analytics.identifyUser("foo"); + expect(fakePosthog.identify.mock.calls[0][0]) + .toBe("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"); + }); + + it("Should not identify the user to posthog if onlyTrackAnonymousEvents is true", async () => { + analytics.init(true); + return analytics.identifyUser("foo").then(() => { + expect(fakePosthog.identify.mock.calls.length).toBe(0); + }); + }); }); From b5564a0de08088ad59179f7948c110b118f52e54 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 08:42:29 +0100 Subject: [PATCH 048/445] Add getAnalytics helper --- src/PosthogAnalytics.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 404f0e5f20..0195142d9b 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -106,3 +106,7 @@ export class PosthogAnalytics { this.trackPseudonymousEvent(eventName, updatedProperties); } } + +export default function getAnalytics() { + return PosthogAnalytics.instance(); +} From 678474c0e8abac4bb1353329ed85aebe835def03 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 11:23:18 +0100 Subject: [PATCH 049/445] Fix missing underscore --- src/PosthogAnalytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 0195142d9b..1b5988e0df 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -41,7 +41,7 @@ export class PosthogAnalytics { private static _instance = null; public static instance(): PosthogAnalytics { - if (!this.instance) { + if (!this._instance) { this._instance = new PosthogAnalytics(posthog); } return this._instance; From d9594c428a8defd13ca31dd4200619bbf39c6141 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 11:23:42 +0100 Subject: [PATCH 050/445] login event should be IAnonymousEvent --- src/PosthogAnalytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 1b5988e0df..d1149095e0 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -23,7 +23,7 @@ export interface IRoomEvent extends IPseudonymousEvent { hashedRoomId: string } -export interface IOnboardingLoginBegin extends IEvent { +export interface IOnboardingLoginBegin extends IAnonymousEvent { key: "onboarding_login_begin", } From 7e549f84e7da7a6286c1cfc1f806f9b0b1fb91fc Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 11:23:55 +0100 Subject: [PATCH 051/445] Don't make getAnalytics the default export, its weird --- src/PosthogAnalytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index d1149095e0..80b5861c17 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -107,6 +107,6 @@ export class PosthogAnalytics { } } -export default function getAnalytics() { +export function getAnalytics(): PosthogAnalytics { return PosthogAnalytics.instance(); } From 6da3cc8ca1baf268d768ed63e4b9cb16e40ce33d Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 13:48:10 +0100 Subject: [PATCH 052/445] Redact sensitive data --- src/PosthogAnalytics.ts | 103 +++++++++++++++++++++++++++++----- test/PosthogAnalytics-test.ts | 66 ++++++++++++++++------ 2 files changed, 138 insertions(+), 31 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 80b5861c17..d5f9b1d83c 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -1,4 +1,4 @@ -import posthog from 'posthog-js'; +import posthog, { PostHog } from 'posthog-js'; import SdkConfig from './SdkConfig'; interface IEvent { @@ -10,6 +10,11 @@ interface IEvent { properties: {} } +export enum Anonymity { + Anonymous, + Pseudonymous +} + // If an event extends IPseudonymousEvent, the event contains pseudonymous data // that won't be sent unless the user has explicitly consented to pseudonymous tracking. // For example, hashed user IDs or room IDs. @@ -33,10 +38,38 @@ const hashHex = async (input: string): Promise => { return [...new Uint8Array(digestBuf)].map((b: number) => b.toString(16).padStart(2, "0")).join(""); }; +const knownScreens = new Set([ + "register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory", + "start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group", +]); + +export async function getRedactedCurrentLocation(origin: string, hash: string, pathname: string, anonymity: Anonymity) { + // Redact PII from the current location. + // If anonymous is true, redact entirely, if false, substitute it with a hash. + // For known screens, assumes a URL structure of //might/be/pii + if (origin.startsWith('file://')) { + pathname = "//"; + } + + let [_, screen, ...parts] = hash.split("/"); + + if (!knownScreens.has(screen)) { + screen = ""; + } + + for (let i = 0; i < parts.length; i++) { + parts[i] = anonymity === Anonymity.Anonymous ? `` : await hashHex(parts[i]); + } + + const hashStr = `${_}/${screen}/${parts.join("/")}`; + return origin + pathname + hashStr; +} + export class PosthogAnalytics { private onlyTrackAnonymousEvents = false; private initialised = false; - private posthog = null; + private posthog?: PostHog = null; + private redactedCurrentLocation = null; private static _instance = null; @@ -47,23 +80,63 @@ export class PosthogAnalytics { return this._instance; } - constructor(posthog) { + constructor(posthog: PostHog) { this.posthog = posthog; } - public init(onlyTrackAnonymousEvents: boolean) { + public async init(onlyTrackAnonymousEvents: boolean) { if (Boolean(navigator.doNotTrack === "1")) { this.initialised = false; return; } this.onlyTrackAnonymousEvents = onlyTrackAnonymousEvents; + const posthogConfig = SdkConfig.get()["posthog"]; if (posthogConfig) { - this.posthog.init(posthogConfig.projectApiKey, { api_host: posthogConfig.apiHost }); + // Update the redacted current location before initialising posthog, as posthog.init triggers + // an immediate pageview event which calls the sanitize_properties callback + await this.updateRedactedCurrentLocation(); + + this.posthog.init(posthogConfig.projectApiKey, { + api_host: posthogConfig.apiHost, + autocapture: false, + mask_all_text: true, + mask_all_element_attributes: true, + sanitize_properties: this.sanitizeProperties.bind(this), + }); this.initialised = true; } } + private async updateRedactedCurrentLocation() { + // TODO only calculate this when the location changes as its expensive + const { origin, hash, pathname } = window.location; + this.redactedCurrentLocation = await getRedactedCurrentLocation( + origin, hash, pathname, this.onlyTrackAnonymousEvents ? Anonymity.Anonymous : Anonymity.Pseudonymous); + } + + private sanitizeProperties(properties: posthog.Properties, _: string): posthog.Properties { + // Sanitize posthog's built in properties which leak PII e.g. url reporting + // see utils.js _.info.properties in posthog-js + + // this.redactedCurrentLocation needs to have been updated prior to reaching this point as + // updating it involves async, which this callback is not + properties['$current_url'] = this.redactedCurrentLocation; + + if (this.onlyTrackAnonymousEvents) { + // drop referrer information for anonymous users + properties['$referrer'] = null; + properties['$referring_domain'] = null; + properties['$initial_referrer'] = null; + properties['$initial_referring_domain'] = null; + + // drop device ID, which is a UUID persisted in local storage + properties['$device_id'] = null; + } + + return properties; + } + public async identifyUser(userId: string) { if (this.onlyTrackAnonymousEvents) return; this.posthog.identify(await hashHex(userId)); @@ -77,21 +150,25 @@ export class PosthogAnalytics { this.onlyTrackAnonymousEvents = enabled; } - public trackPseudonymousEvent( - eventName: E["eventName"], - properties: E["properties"], - ) { + private async capture(eventName: string, properties: posthog.Properties, anonymity: Anonymity) { if (!this.initialised) return; - if (this.onlyTrackAnonymousEvents) return; + await this.updateRedactedCurrentLocation(anonymity); this.posthog.capture(eventName, properties); } - public trackAnonymousEvent( + public async trackPseudonymousEvent( eventName: E["eventName"], properties: E["properties"], ) { - if (!this.initialised) return; - this.posthog.capture(eventName, properties); + if (this.onlyTrackAnonymousEvents) return; + this.capture(eventName, properties, Anonymity.Pseudonyomous); + } + + public async trackAnonymousEvent( + eventName: E["eventName"], + properties: E["properties"], + ) { + this.capture(eventName, properties, Anonymity.Anonymous); } public async trackRoomEvent( diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index fd49255fa1..e9efeffa7d 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -1,4 +1,5 @@ -import { IAnonymousEvent, IRoomEvent, PosthogAnalytics } from '../src/PosthogAnalytics'; +import { Anonymity, getRedactedCurrentLocation, IAnonymousEvent, IRoomEvent, + PosthogAnalytics } from '../src/PosthogAnalytics'; import SdkConfig from '../src/SdkConfig'; const crypto = require('crypto'); @@ -68,9 +69,9 @@ describe("PosthogAnalytics", () => { expect(analytics.isInitialised()).toBe(true); }); - it("Should pass track() to posthog", () => { + it("Should pass track() to posthog", async () => { analytics.init(false); - analytics.trackAnonymousEvent("jest_test_event", { + await analytics.trackAnonymousEvent("jest_test_event", { foo: "bar", }); expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); @@ -80,29 +81,29 @@ describe("PosthogAnalytics", () => { it("Should pass trackRoomEvent to posthog", async () => { analytics.init(false); const roomId = "42"; - return analytics.trackRoomEvent("jest_test_event", roomId, { + await analytics.trackRoomEvent("jest_test_event", roomId, { foo: "bar", - }).then(() => { - expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); - expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ - foo: "bar", - hashedRoomId: "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049", - }); + }); + expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); + expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ + foo: "bar", + hashedRoomId: "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049", }); }); - it("Should silently not track if not inititalised", () => { - analytics.trackAnonymousEvent("jest_test_event", { + it("Should silently not track if not inititalised", async () => { + await analytics.trackAnonymousEvent("jest_test_event", { foo: "bar", }); - expect(fakePosthog.capture.mock.calls.length).toBe(0); }); - it("Should not track non-anonymous messages if onlyTrackAnonymousEvents is true", () => { - analytics.trackAnonymousEvent("jest_test_event", { + it("Should not track non-anonymous messages if onlyTrackAnonymousEvents is true", async () => { + analytics.init(true); + await analytics.trackPseudonymousEvent("jest_test_event", { foo: "bar", }); + expect(fakePosthog.capture.mock.calls.length).toBe(0); }); it("Should identify the user to posthog if onlyTrackAnonymousEvents is false", async () => { @@ -114,8 +115,37 @@ describe("PosthogAnalytics", () => { it("Should not identify the user to posthog if onlyTrackAnonymousEvents is true", async () => { analytics.init(true); - return analytics.identifyUser("foo").then(() => { - expect(fakePosthog.identify.mock.calls.length).toBe(0); - }); + await analytics.identifyUser("foo"); + expect(fakePosthog.identify.mock.calls.length).toBe(0); + }); + + it("Should pseudonymise a location of a known screen", async () => { + const location = await getRedactedCurrentLocation( + "https://foo.bar", "#/register/some/pii", "/", Anonymity.Pseudonymous); + expect(location).toBe( + `https://foo.bar/#/register/\ +a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b/\ +bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`); + }); + + it("Should anonymise a location of a known screen", async () => { + const location = await getRedactedCurrentLocation( + "https://foo.bar", "#/register/some/pii", "/", Anonymity.Anonymous); + expect(location).toBe("https://foo.bar/#/register//"); + }); + + it("Should pseudonymise a location of an unknown screen", async () => { + const location = await getRedactedCurrentLocation( + "https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Pseudonymous); + expect(location).toBe( + `https://foo.bar/#//\ +a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b/\ +bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`); + }); + + it("Should anonymise a location of an unknown screen", async () => { + const location = await getRedactedCurrentLocation( + "https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Anonymous); + expect(location).toBe("https://foo.bar/#///"); }); }); From 4c6b0d35add7ae8d58f71ea1711587e31081444b Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 14:57:24 +0100 Subject: [PATCH 053/445] Improve analytics interface * Make it an error to call it before its initialised, and separately track whether its been enabled * Use anonmity enum in the public interface * Properly await upstream calls * Fix accidental test fixture cross-reliance --- src/PosthogAnalytics.ts | 60 ++++++++---- test/PosthogAnalytics-test.ts | 177 ++++++++++++++++++---------------- 2 files changed, 136 insertions(+), 101 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index d5f9b1d83c..63cb3bc422 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -66,10 +66,11 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p } export class PosthogAnalytics { - private onlyTrackAnonymousEvents = false; + private anonymity = Anonymity.Anonymous; private initialised = false; private posthog?: PostHog = null; private redactedCurrentLocation = null; + private enabled = false; private static _instance = null; @@ -84,12 +85,8 @@ export class PosthogAnalytics { this.posthog = posthog; } - public async init(onlyTrackAnonymousEvents: boolean) { - if (Boolean(navigator.doNotTrack === "1")) { - this.initialised = false; - return; - } - this.onlyTrackAnonymousEvents = onlyTrackAnonymousEvents; + public async init(anonymity: Anonymity) { + this.anonymity = Boolean(navigator.doNotTrack === "1") ? Anonymity.Anonymous : anonymity; const posthogConfig = SdkConfig.get()["posthog"]; if (posthogConfig) { @@ -105,6 +102,9 @@ export class PosthogAnalytics { sanitize_properties: this.sanitizeProperties.bind(this), }); this.initialised = true; + this.enabled = true; + } else { + this.enabled = false; } } @@ -112,7 +112,7 @@ export class PosthogAnalytics { // TODO only calculate this when the location changes as its expensive const { origin, hash, pathname } = window.location; this.redactedCurrentLocation = await getRedactedCurrentLocation( - origin, hash, pathname, this.onlyTrackAnonymousEvents ? Anonymity.Anonymous : Anonymity.Pseudonymous); + origin, hash, pathname, this.anonymity); } private sanitizeProperties(properties: posthog.Properties, _: string): posthog.Properties { @@ -123,7 +123,7 @@ export class PosthogAnalytics { // updating it involves async, which this callback is not properties['$current_url'] = this.redactedCurrentLocation; - if (this.onlyTrackAnonymousEvents) { + if (this.anonymity == Anonymity.Anonymous) { // drop referrer information for anonymous users properties['$referrer'] = null; properties['$referring_domain'] = null; @@ -138,21 +138,41 @@ export class PosthogAnalytics { } public async identifyUser(userId: string) { - if (this.onlyTrackAnonymousEvents) return; + if (this.anonymity == Anonymity.Anonymous) return; this.posthog.identify(await hashHex(userId)); } - public isInitialised(): boolean { + public isInitialised() { return this.initialised; } - public setOnlyTrackAnonymousEvents(enabled: boolean) { - this.onlyTrackAnonymousEvents = enabled; + public isEnabled() { + return this.enabled; } - private async capture(eventName: string, properties: posthog.Properties, anonymity: Anonymity) { - if (!this.initialised) return; - await this.updateRedactedCurrentLocation(anonymity); + public setAnonymity(anonymity: Anonymity) { + this.anonymity = anonymity; + } + + public getAnonymity() { + return this.anonymity; + } + + public logout() { + if (this.enabled) { + this.posthog.reset(); + } + this.setAnonymity(Anonymity.Anonymous); + } + + private async capture(eventName: string, properties: posthog.Properties) { + if (!this.enabled) { + return; + } + if (!this.initialised) { + throw Error("Tried to track event before PoshogAnalytics.init has completed"); + } + await this.updateRedactedCurrentLocation(); this.posthog.capture(eventName, properties); } @@ -160,15 +180,15 @@ export class PosthogAnalytics { eventName: E["eventName"], properties: E["properties"], ) { - if (this.onlyTrackAnonymousEvents) return; - this.capture(eventName, properties, Anonymity.Pseudonyomous); + if (this.anonymity == Anonymity.Anonymous) return; + await this.capture(eventName, properties); } public async trackAnonymousEvent( eventName: E["eventName"], properties: E["properties"], ) { - this.capture(eventName, properties, Anonymity.Anonymous); + await this.capture(eventName, properties); } public async trackRoomEvent( @@ -180,7 +200,7 @@ export class PosthogAnalytics { ...properties, hashedRoomId: roomId ? await hashHex(roomId) : null, }; - this.trackPseudonymousEvent(eventName, updatedProperties); + await this.trackPseudonymousEvent(eventName, updatedProperties); } } diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index e9efeffa7d..515d51b8e4 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -46,106 +46,121 @@ describe("PosthogAnalytics", () => { window.crypto = null; }); - it("Should not initialise if DNT is enabled", () => { - navigator.doNotTrack = "1"; - analytics.init(false); - expect(analytics.isInitialised()).toBe(false); - }); - - it("Should not initialise if config is not set", () => { - jest.spyOn(SdkConfig, "get").mockReturnValue({}); - analytics.init(false); - expect(analytics.isInitialised()).toBe(false); - }); - - it("Should initialise if config is set", () => { - jest.spyOn(SdkConfig, "get").mockReturnValue({ - posthog: { - projectApiKey: "foo", - apiHost: "bar", - }, + describe("Initialisation", () => { + it("Should not initialise if config is not set", async () => { + jest.spyOn(SdkConfig, "get").mockReturnValue({}); + await analytics.init(Anonymity.Pseudonymous); + expect(analytics.isEnabled()).toBe(false); }); - analytics.init(false); - expect(analytics.isInitialised()).toBe(true); - }); - it("Should pass track() to posthog", async () => { - analytics.init(false); - await analytics.trackAnonymousEvent("jest_test_event", { - foo: "bar", + it("Should initialise if config is set", async () => { + jest.spyOn(SdkConfig, "get").mockReturnValue({ + posthog: { + projectApiKey: "foo", + apiHost: "bar", + }, + }); + await analytics.init(Anonymity.Pseudonymous); + expect(analytics.isInitialised()).toBe(true); + expect(analytics.isEnabled()).toBe(true); }); - expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); - expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ foo: "bar" }); - }); - it("Should pass trackRoomEvent to posthog", async () => { - analytics.init(false); - const roomId = "42"; - await analytics.trackRoomEvent("jest_test_event", roomId, { - foo: "bar", - }); - expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); - expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ - foo: "bar", - hashedRoomId: "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049", + it("Should force anonymous if DNT is enabled", async () => { + navigator.doNotTrack = "1"; + await analytics.init(Anonymity.Pseudonymous); + expect(analytics.getAnonymity()).toBe(Anonymity.Anonymous); }); }); - it("Should silently not track if not inititalised", async () => { - await analytics.trackAnonymousEvent("jest_test_event", { - foo: "bar", + describe("Tracking", () => { + beforeEach(() => { + navigator.doNotTrack = null; + jest.spyOn(SdkConfig, "get").mockReturnValue({ + posthog: { + projectApiKey: "foo", + apiHost: "bar", + }, + }); }); - expect(fakePosthog.capture.mock.calls.length).toBe(0); - }); - it("Should not track non-anonymous messages if onlyTrackAnonymousEvents is true", async () => { - analytics.init(true); - await analytics.trackPseudonymousEvent("jest_test_event", { - foo: "bar", + it("Should pass track() to posthog", async () => { + await analytics.init(Anonymity.Pseudonymous); + await analytics.trackAnonymousEvent("jest_test_event", { + foo: "bar", + }); + expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); + expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ foo: "bar" }); }); - expect(fakePosthog.capture.mock.calls.length).toBe(0); - }); - it("Should identify the user to posthog if onlyTrackAnonymousEvents is false", async () => { - analytics.init(false); - await analytics.identifyUser("foo"); - expect(fakePosthog.identify.mock.calls[0][0]) - .toBe("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"); - }); + it("Should pass trackRoomEvent to posthog", async () => { + await analytics.init(Anonymity.Pseudonymous); + const roomId = "42"; + await analytics.trackRoomEvent("jest_test_event", roomId, { + foo: "bar", + }); + expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); + expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ + foo: "bar", + hashedRoomId: "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049", + }); + }); - it("Should not identify the user to posthog if onlyTrackAnonymousEvents is true", async () => { - analytics.init(true); - await analytics.identifyUser("foo"); - expect(fakePosthog.identify.mock.calls.length).toBe(0); - }); + it("Should silently not track if not inititalised", async () => { + await analytics.trackAnonymousEvent("jest_test_event", { + foo: "bar", + }); + expect(fakePosthog.capture.mock.calls.length).toBe(0); + }); - it("Should pseudonymise a location of a known screen", async () => { - const location = await getRedactedCurrentLocation( - "https://foo.bar", "#/register/some/pii", "/", Anonymity.Pseudonymous); - expect(location).toBe( - `https://foo.bar/#/register/\ + it("Should not track non-anonymous messages if anonymous", async () => { + await analytics.init(Anonymity.Anonymous); + await analytics.trackPseudonymousEvent("jest_test_event", { + foo: "bar", + }); + expect(fakePosthog.capture.mock.calls.length).toBe(0); + }); + + it("Should pseudonymise a location of a known screen", async () => { + const location = await getRedactedCurrentLocation( + "https://foo.bar", "#/register/some/pii", "/", Anonymity.Pseudonymous); + expect(location).toBe( + `https://foo.bar/#/register/\ a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b/\ bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`); - }); + }); - it("Should anonymise a location of a known screen", async () => { - const location = await getRedactedCurrentLocation( - "https://foo.bar", "#/register/some/pii", "/", Anonymity.Anonymous); - expect(location).toBe("https://foo.bar/#/register//"); - }); + it("Should anonymise a location of a known screen", async () => { + const location = await getRedactedCurrentLocation( + "https://foo.bar", "#/register/some/pii", "/", Anonymity.Anonymous); + expect(location).toBe("https://foo.bar/#/register//"); + }); - it("Should pseudonymise a location of an unknown screen", async () => { - const location = await getRedactedCurrentLocation( - "https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Pseudonymous); - expect(location).toBe( - `https://foo.bar/#//\ + it("Should pseudonymise a location of an unknown screen", async () => { + const location = await getRedactedCurrentLocation( + "https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Pseudonymous); + expect(location).toBe( + `https://foo.bar/#//\ a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b/\ bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`); - }); + }); - it("Should anonymise a location of an unknown screen", async () => { - const location = await getRedactedCurrentLocation( - "https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Anonymous); - expect(location).toBe("https://foo.bar/#///"); + it("Should anonymise a location of an unknown screen", async () => { + const location = await getRedactedCurrentLocation( + "https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Anonymous); + expect(location).toBe("https://foo.bar/#///"); + }); + + it("Should identify the user to posthog if pseudonymous", async () => { + await analytics.init(Anonymity.Pseudonymous); + await analytics.identifyUser("foo"); + expect(fakePosthog.identify.mock.calls[0][0]) + .toBe("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"); + }); + + it("Should not identify the user to posthog if anonymous", async () => { + await analytics.init(Anonymity.Anonymous); + await analytics.identifyUser("foo"); + expect(fakePosthog.identify.mock.calls.length).toBe(0); + }); }); }); From 726b4497b2a54fcf6fe743d988325879d8e77890 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 16:06:09 +0100 Subject: [PATCH 054/445] Remove redactedCurrentLocation and rely on posthog for DNT * Redact and pass the redacted url as a property. redactedCurrentLocation might have issues with concurrent events * Remove DNT code and rely on posthog --- src/PosthogAnalytics.ts | 42 +++++++++++++++++------------------ test/PosthogAnalytics-test.ts | 30 +++++++++---------------- 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 63cb3bc422..fd8bb44e0b 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -69,7 +69,6 @@ export class PosthogAnalytics { private anonymity = Anonymity.Anonymous; private initialised = false; private posthog?: PostHog = null; - private redactedCurrentLocation = null; private enabled = false; private static _instance = null; @@ -85,21 +84,20 @@ export class PosthogAnalytics { this.posthog = posthog; } - public async init(anonymity: Anonymity) { - this.anonymity = Boolean(navigator.doNotTrack === "1") ? Anonymity.Anonymous : anonymity; - + public init(anonymity: Anonymity) { const posthogConfig = SdkConfig.get()["posthog"]; if (posthogConfig) { - // Update the redacted current location before initialising posthog, as posthog.init triggers - // an immediate pageview event which calls the sanitize_properties callback - await this.updateRedactedCurrentLocation(); - this.posthog.init(posthogConfig.projectApiKey, { api_host: posthogConfig.apiHost, autocapture: false, mask_all_text: true, mask_all_element_attributes: true, + // this is disabled for now as its tricky to sanitize properties of the pageview + // event because sanitization requires async crypto calls and the sanitize_properties + // callback is synchronous. + capture_pageview: false, sanitize_properties: this.sanitizeProperties.bind(this), + respect_dnt: true, }); this.initialised = true; this.enabled = true; @@ -108,20 +106,20 @@ export class PosthogAnalytics { } } - private async updateRedactedCurrentLocation() { - // TODO only calculate this when the location changes as its expensive - const { origin, hash, pathname } = window.location; - this.redactedCurrentLocation = await getRedactedCurrentLocation( - origin, hash, pathname, this.anonymity); - } - private sanitizeProperties(properties: posthog.Properties, _: string): posthog.Properties { - // Sanitize posthog's built in properties which leak PII e.g. url reporting - // see utils.js _.info.properties in posthog-js + // Callback from posthog to sanitize properties before sending them to the server. + // + // Here we sanitize posthog's built in properties which leak PII e.g. url reporting. + // See utils.js _.info.properties in posthog-js. - // this.redactedCurrentLocation needs to have been updated prior to reaching this point as - // updating it involves async, which this callback is not - properties['$current_url'] = this.redactedCurrentLocation; + // Replace the $current_url with a redacted version. + // $redacted_current_url is injected by this class earlier in capture(), as its generation + // is async and can't be done in this non-async callback. + if (!properties['$redacted_current_url']) { + console.log("$redacted_current_url not set in sanitizeProperties, will drop $current_url entirely"); + } + properties['$current_url'] = properties['$redacted_current_url']; + delete properties['$redacted_current_url']; if (this.anonymity == Anonymity.Anonymous) { // drop referrer information for anonymous users @@ -172,7 +170,9 @@ export class PosthogAnalytics { if (!this.initialised) { throw Error("Tried to track event before PoshogAnalytics.init has completed"); } - await this.updateRedactedCurrentLocation(); + const { origin, hash, pathname } = window.location; + properties['$redacted_current_url'] = await getRedactedCurrentLocation( + origin, hash, pathname, this.anonymity); this.posthog.capture(eventName, properties); } diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index 515d51b8e4..a1e54dc05d 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -49,7 +49,7 @@ describe("PosthogAnalytics", () => { describe("Initialisation", () => { it("Should not initialise if config is not set", async () => { jest.spyOn(SdkConfig, "get").mockReturnValue({}); - await analytics.init(Anonymity.Pseudonymous); + analytics.init(Anonymity.Pseudonymous); expect(analytics.isEnabled()).toBe(false); }); @@ -60,21 +60,14 @@ describe("PosthogAnalytics", () => { apiHost: "bar", }, }); - await analytics.init(Anonymity.Pseudonymous); + analytics.init(Anonymity.Pseudonymous); expect(analytics.isInitialised()).toBe(true); expect(analytics.isEnabled()).toBe(true); }); - - it("Should force anonymous if DNT is enabled", async () => { - navigator.doNotTrack = "1"; - await analytics.init(Anonymity.Pseudonymous); - expect(analytics.getAnonymity()).toBe(Anonymity.Anonymous); - }); }); describe("Tracking", () => { beforeEach(() => { - navigator.doNotTrack = null; jest.spyOn(SdkConfig, "get").mockReturnValue({ posthog: { projectApiKey: "foo", @@ -84,25 +77,24 @@ describe("PosthogAnalytics", () => { }); it("Should pass track() to posthog", async () => { - await analytics.init(Anonymity.Pseudonymous); + analytics.init(Anonymity.Pseudonymous); await analytics.trackAnonymousEvent("jest_test_event", { foo: "bar", }); expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); - expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ foo: "bar" }); + expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar"); }); it("Should pass trackRoomEvent to posthog", async () => { - await analytics.init(Anonymity.Pseudonymous); + analytics.init(Anonymity.Pseudonymous); const roomId = "42"; await analytics.trackRoomEvent("jest_test_event", roomId, { foo: "bar", }); expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); - expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ - foo: "bar", - hashedRoomId: "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049", - }); + expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar"); + expect(fakePosthog.capture.mock.calls[0][1]["hashedRoomId"]) + .toEqual("73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049"); }); it("Should silently not track if not inititalised", async () => { @@ -113,7 +105,7 @@ describe("PosthogAnalytics", () => { }); it("Should not track non-anonymous messages if anonymous", async () => { - await analytics.init(Anonymity.Anonymous); + analytics.init(Anonymity.Anonymous); await analytics.trackPseudonymousEvent("jest_test_event", { foo: "bar", }); @@ -151,14 +143,14 @@ bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`); }); it("Should identify the user to posthog if pseudonymous", async () => { - await analytics.init(Anonymity.Pseudonymous); + analytics.init(Anonymity.Pseudonymous); await analytics.identifyUser("foo"); expect(fakePosthog.identify.mock.calls[0][0]) .toBe("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"); }); it("Should not identify the user to posthog if anonymous", async () => { - await analytics.init(Anonymity.Anonymous); + analytics.init(Anonymity.Anonymous); await analytics.identifyUser("foo"); expect(fakePosthog.identify.mock.calls.length).toBe(0); }); From 5697eeaab8aab98d5a8294775ef60a4bc0ebf577 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 16:08:00 +0100 Subject: [PATCH 055/445] Put back accidentally removed anonymity update --- src/PosthogAnalytics.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index fd8bb44e0b..2e5b9446de 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -85,6 +85,7 @@ export class PosthogAnalytics { } public init(anonymity: Anonymity) { + this.anonymity = anonymity; const posthogConfig = SdkConfig.get()["posthog"]; if (posthogConfig) { this.posthog.init(posthogConfig.projectApiKey, { From 53b6749f73d5b8f545024f64597d267dc7aa18ec Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 16:09:22 +0100 Subject: [PATCH 056/445] Change onboarding_login_begin to welcome_screen_load --- src/PosthogAnalytics.ts | 4 ++-- src/components/views/auth/Welcome.js | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 2e5b9446de..faf9af6ba5 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -28,8 +28,8 @@ export interface IRoomEvent extends IPseudonymousEvent { hashedRoomId: string } -export interface IOnboardingLoginBegin extends IAnonymousEvent { - key: "onboarding_login_begin", +export interface IWelcomeScreenLoad extends IAnonymousEvent { + key: "welcome_screen_load", } const hashHex = async (input: string): Promise => { diff --git a/src/components/views/auth/Welcome.js b/src/components/views/auth/Welcome.js index e3f7a601f2..470ea4223c 100644 --- a/src/components/views/auth/Welcome.js +++ b/src/components/views/auth/Welcome.js @@ -25,6 +25,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; import CountlyAnalytics from "../../../CountlyAnalytics"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { getAnalytics } from "../../../PosthogAnalytics"; // translatable strings for Welcome pages _td("Sign in with SSO"); @@ -68,4 +69,8 @@ export default class Welcome extends React.PureComponent { ); } + + componentDidMount() { + getAnalytics().trackAnonymousEvent("welcome_screen_load", {}); + } } From 6737cfd2978e44c47d8eef73375457c0b91f4505 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 16:10:06 +0100 Subject: [PATCH 057/445] remove superflous dnt clear --- test/PosthogAnalytics-test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index a1e54dc05d..a33544e738 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -42,7 +42,6 @@ describe("PosthogAnalytics", () => { }); afterEach(() => { - navigator.doNotTrack = null; window.crypto = null; }); From 34f8c60b346a76ac3511999c7f6f256f4231d17e Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 16:15:18 +0100 Subject: [PATCH 058/445] Hook analytics into the SDK --- src/Lifecycle.ts | 3 +++ src/components/structures/MatrixChat.tsx | 5 +++++ .../views/settings/tabs/user/SecurityUserSettingsTab.js | 2 ++ 3 files changed, 10 insertions(+) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 410124a637..8536f808ff 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -48,6 +48,7 @@ import { Jitsi } from "./widgets/Jitsi"; import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY } from "./BasePlatform"; import ThreepidInviteStore from "./stores/ThreepidInviteStore"; import CountlyAnalytics from "./CountlyAnalytics"; +import { getAnalytics } from "./PosthogAnalytics"; import CallHandler from './CallHandler'; import LifecycleCustomisations from "./customisations/Lifecycle"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; @@ -700,6 +701,8 @@ export function logout(): void { CountlyAnalytics.instance.enable(/* anonymous = */ true); } + getAnalytics().logout(); + if (MatrixClientPeg.get().isGuest()) { // logout doesn't work for guest sessions // Also we sometimes want to re-log in a guest session if we abort the login. diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 785838ffca..3edc463a23 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -107,6 +107,7 @@ import UIStore, { UI_EVENTS } from "../../stores/UIStore"; import SoftLogout from './auth/SoftLogout'; import { makeRoomPermalink } from "../../utils/permalinks/Permalinks"; import { copyPlaintext } from "../../utils/strings"; +import { Anonymity, getAnalytics } from '../../PosthogAnalytics'; /** constants for MatrixChat.state.view */ export enum Views { @@ -387,6 +388,7 @@ export default class MatrixChat extends React.PureComponent { if (SettingsStore.getValue("analyticsOptIn")) { Analytics.enable(); } + getAnalytics().init(SettingsStore.getValue("analyticsOptIn") ? Anonymity.Pseudonymous : Anonymity.Anonymous); CountlyAnalytics.instance.enable(/* anonymous = */ true); } @@ -498,6 +500,8 @@ export default class MatrixChat extends React.PureComponent { } else if (SettingsStore.getValue("analyticsOptIn")) { CountlyAnalytics.instance.enable(/* anonymous = */ false); } + getAnalytics().setAnonymity(SettingsStore.getValue("analyticsOptIn") ? + Anonymity.Pseudonymous: Anonymity.Anonymous); }); // Note we don't catch errors from this: we catch everything within // loadSession as there's logic there to ask the user if they want @@ -822,6 +826,7 @@ export default class MatrixChat extends React.PureComponent { if (CountlyAnalytics.instance.canEnable()) { CountlyAnalytics.instance.enable(/* anonymous = */ false); } + getAnalytics().setAnonymity(Anonymity.Pseudonymous); break; case 'reject_cookies': SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false); diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index a03598b21f..15b4992cd8 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -36,6 +36,7 @@ import { UIFeature } from "../../../../../settings/UIFeature"; import { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel"; import CountlyAnalytics from "../../../../../CountlyAnalytics"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +import { Anonymity, getAnalytics } from "../../../../../PosthogAnalytics"; export class IgnoredUser extends React.Component { static propTypes = { @@ -106,6 +107,7 @@ export default class SecurityUserSettingsTab extends React.Component { _updateAnalytics = (checked) => { checked ? Analytics.enable() : Analytics.disable(); CountlyAnalytics.instance.enable(/* anonymous = */ !checked); + getAnalytics().setAnonymity(checked ? Anonymity.Pseudonymous : Anonymity.Anonymous); }; _onExportE2eKeysClicked = () => { From 93962c0acaa0887aef2a5bf425226d8004798fcd Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 16:29:11 +0100 Subject: [PATCH 059/445] Update reasoning around disabling capture_pageview --- src/PosthogAnalytics.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index faf9af6ba5..c18cb98f03 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -93,9 +93,11 @@ export class PosthogAnalytics { autocapture: false, mask_all_text: true, mask_all_element_attributes: true, - // this is disabled for now as its tricky to sanitize properties of the pageview - // event because sanitization requires async crypto calls and the sanitize_properties - // callback is synchronous. + // This only triggers on page load, which for our SPA isn't particularly useful. + // Plus, the .capture call originating from somewhere in posthog makes it hard + // to redact URLs, which requires async code. + // + // To raise this manually, just call .capture("$pageview") or posthog.capture_pageview. capture_pageview: false, sanitize_properties: this.sanitizeProperties.bind(this), respect_dnt: true, From f358deb6c4dc2281993d275f5c8fdcff7d328da1 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 16:52:57 +0100 Subject: [PATCH 060/445] Manually track page views --- src/PosthogAnalytics.ts | 13 +++++++++++++ src/components/structures/MatrixChat.tsx | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index c18cb98f03..026c8d9c5e 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -28,6 +28,13 @@ export interface IRoomEvent extends IPseudonymousEvent { hashedRoomId: string } +interface IPageView extends IAnonymousEvent { + eventName: "$pageview", + properties: { + durationMs?: number + } +} + export interface IWelcomeScreenLoad extends IAnonymousEvent { key: "welcome_screen_load", } @@ -205,6 +212,12 @@ export class PosthogAnalytics { }; await this.trackPseudonymousEvent(eventName, updatedProperties); } + + public async trackPageView(durationMs: number) { + await this.trackAnonymousEvent("$pageview", { + durationMs, + }); + } } export function getAnalytics(): PosthogAnalytics { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 3edc463a23..b29ede409b 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -107,7 +107,7 @@ import UIStore, { UI_EVENTS } from "../../stores/UIStore"; import SoftLogout from './auth/SoftLogout'; import { makeRoomPermalink } from "../../utils/permalinks/Permalinks"; import { copyPlaintext } from "../../utils/strings"; -import { Anonymity, getAnalytics } from '../../PosthogAnalytics'; +import { Anonymity, getAnalytics, IPageChange } from '../../PosthogAnalytics'; /** constants for MatrixChat.state.view */ export enum Views { @@ -445,6 +445,7 @@ export default class MatrixChat extends React.PureComponent { const durationMs = this.stopPageChangeTimer(); Analytics.trackPageChange(durationMs); CountlyAnalytics.instance.trackPageChange(durationMs); + getAnalytics().trackPageView(durationMs); } if (this.focusComposer) { dis.fire(Action.FocusSendMessageComposer); From b380a89ac6ab72526faba13dabfd00ab4ca3cffb Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 17:01:45 +0100 Subject: [PATCH 061/445] Fix wrong overriden attribute --- src/PosthogAnalytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 026c8d9c5e..d49fb567a0 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -36,7 +36,7 @@ interface IPageView extends IAnonymousEvent { } export interface IWelcomeScreenLoad extends IAnonymousEvent { - key: "welcome_screen_load", + eventName: "welcome_screen_load", } const hashHex = async (input: string): Promise => { From e9e0e4847f45abda0eef86e98c9e01d75ceabe81 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 17:02:14 +0100 Subject: [PATCH 062/445] Move Welcome.js to tsx --- src/components/views/auth/{Welcome.js => Welcome.tsx} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/components/views/auth/{Welcome.js => Welcome.tsx} (93%) diff --git a/src/components/views/auth/Welcome.js b/src/components/views/auth/Welcome.tsx similarity index 93% rename from src/components/views/auth/Welcome.js rename to src/components/views/auth/Welcome.tsx index 470ea4223c..92fe9df4d5 100644 --- a/src/components/views/auth/Welcome.js +++ b/src/components/views/auth/Welcome.tsx @@ -25,7 +25,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; import CountlyAnalytics from "../../../CountlyAnalytics"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { getAnalytics } from "../../../PosthogAnalytics"; +import { getAnalytics, IWelcomeScreenLoad } from "../../../PosthogAnalytics"; // translatable strings for Welcome pages _td("Sign in with SSO"); @@ -71,6 +71,6 @@ export default class Welcome extends React.PureComponent { } componentDidMount() { - getAnalytics().trackAnonymousEvent("welcome_screen_load", {}); + getAnalytics().trackAnonymousEvent("welcome_screen_load", { foo: "bar" }); } } From 0c89eb51d4df8ce683584ec93d04d17bd9b11974 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 18:24:14 +0100 Subject: [PATCH 063/445] add registerSuperProperties --- src/PosthogAnalytics.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index d49fb567a0..3f13762327 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -150,6 +150,10 @@ export class PosthogAnalytics { this.posthog.identify(await hashHex(userId)); } + public registerSuperProperties(properties) { + this.posthog.register(properties); + } + public isInitialised() { return this.initialised; } From 585b702652d1ea1e86de130177867151bc29c5aa Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 18:25:05 +0100 Subject: [PATCH 064/445] Add tip about shasum --- src/PosthogAnalytics.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 3f13762327..8ebeff3565 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -40,6 +40,8 @@ export interface IWelcomeScreenLoad extends IAnonymousEvent { } const hashHex = async (input: string): Promise => { + // on os x (e.g. if you want to know the sha-256 of your own matrix ID so you can look it up): + // echo -n | shasum -a 256 const buf = new TextEncoder().encode(input); const digestBuf = await window.crypto.subtle.digest("sha-256", buf); return [...new Uint8Array(digestBuf)].map((b: number) => b.toString(16).padStart(2, "0")).join(""); From c34afdb4bdd834ea833016d0da795532c41b21b7 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 18:35:25 +0100 Subject: [PATCH 065/445] Refactor platform properties loading --- src/Lifecycle.ts | 10 +++++++++- src/PosthogAnalytics.ts | 17 +++++++++++++++++ src/components/structures/MatrixChat.tsx | 14 ++++++++++---- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 8536f808ff..c27c774cd7 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -48,7 +48,7 @@ import { Jitsi } from "./widgets/Jitsi"; import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY } from "./BasePlatform"; import ThreepidInviteStore from "./stores/ThreepidInviteStore"; import CountlyAnalytics from "./CountlyAnalytics"; -import { getAnalytics } from "./PosthogAnalytics"; +import { Anonymity, getAnalytics, getPlatformProperties } from "./PosthogAnalytics"; import CallHandler from './CallHandler'; import LifecycleCustomisations from "./customisations/Lifecycle"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; @@ -574,6 +574,14 @@ async function doSetLoggedIn( await abortLogin(); } + if (SettingsStore.getValue("analyticsOptIn")) { + const analytics = getAnalytics(); + analytics.setAnonymity(Anonymity.Pseudonymous); + await analytics.identifyUser(credentials.userId); + } else { + getAnalytics().setAnonymity(Anonymity.Anonymous); + } + Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl); MatrixClientPeg.replaceUsingCreds(credentials); diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 8ebeff3565..9c167f5464 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -1,4 +1,5 @@ import posthog, { PostHog } from 'posthog-js'; +import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; interface IEvent { @@ -226,6 +227,22 @@ export class PosthogAnalytics { } } +export async function getPlatformProperties() { + const platform = PlatformPeg.get(); + let appVersion; + try { + appVersion = await platform.getAppVersion(); + } catch (e) { + // this happens if no version is set i.e. in dev + appVersion = "unknown"; + } + + return { + appVersion, + appPlatform: platform.getHumanReadableName(), + }; +} + export function getAnalytics(): PosthogAnalytics { return PosthogAnalytics.instance(); } diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index b29ede409b..513200520f 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -107,7 +107,7 @@ import UIStore, { UI_EVENTS } from "../../stores/UIStore"; import SoftLogout from './auth/SoftLogout'; import { makeRoomPermalink } from "../../utils/permalinks/Permalinks"; import { copyPlaintext } from "../../utils/strings"; -import { Anonymity, getAnalytics, IPageChange } from '../../PosthogAnalytics'; +import { Anonymity, getAnalytics, getPlatformProperties } from '../../PosthogAnalytics'; /** constants for MatrixChat.state.view */ export enum Views { @@ -388,7 +388,11 @@ export default class MatrixChat extends React.PureComponent { if (SettingsStore.getValue("analyticsOptIn")) { Analytics.enable(); } - getAnalytics().init(SettingsStore.getValue("analyticsOptIn") ? Anonymity.Pseudonymous : Anonymity.Anonymous); + + const analytics = getAnalytics(); + analytics.init(SettingsStore.getValue("analyticsOptIn") ? Anonymity.Pseudonymous : Anonymity.Anonymous); + getPlatformProperties().then((properties) => analytics.registerSuperProperties(properties)); + CountlyAnalytics.instance.enable(/* anonymous = */ true); } @@ -501,8 +505,6 @@ export default class MatrixChat extends React.PureComponent { } else if (SettingsStore.getValue("analyticsOptIn")) { CountlyAnalytics.instance.enable(/* anonymous = */ false); } - getAnalytics().setAnonymity(SettingsStore.getValue("analyticsOptIn") ? - Anonymity.Pseudonymous: Anonymity.Anonymous); }); // Note we don't catch errors from this: we catch everything within // loadSession as there's logic there to ask the user if they want @@ -828,6 +830,10 @@ export default class MatrixChat extends React.PureComponent { CountlyAnalytics.instance.enable(/* anonymous = */ false); } getAnalytics().setAnonymity(Anonymity.Pseudonymous); + // TODO: this is an async call and we're not waiting for it to complete - + // so potentially an event could be fired prior to it completing and would be + // missing the user identification. + getAnalytics().identifyUser(MatrixClientPeg.get().getUserId()); break; case 'reject_cookies': SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false); From 0e3cc6b8f45856f467485b8404815099c3b7866f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Jul 2021 18:09:35 -0600 Subject: [PATCH 066/445] Remove voice messages labs flag Fixes https://github.com/vector-im/element-web/issues/17151 --- src/components/views/messages/MVoiceOrAudioBody.tsx | 3 +-- src/components/views/rooms/MessageComposer.tsx | 10 ++++------ src/settings/Settings.tsx | 6 ------ 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/components/views/messages/MVoiceOrAudioBody.tsx b/src/components/views/messages/MVoiceOrAudioBody.tsx index adfd102e19..8a9f39400e 100644 --- a/src/components/views/messages/MVoiceOrAudioBody.tsx +++ b/src/components/views/messages/MVoiceOrAudioBody.tsx @@ -27,8 +27,7 @@ export default class MVoiceOrAudioBody extends React.PureComponent { // MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245 const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice'] || !!this.props.mxEvent.getContent()['org.matrix.msc3245.voice']; - const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages"); - if (isVoiceMessage && voiceMessagesEnabled) { + if (isVoiceMessage) { return ; } else { return ; diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index b16d22b416..863d401442 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -391,12 +391,10 @@ export default class MessageComposer extends React.Component { controls.push(); } - if (SettingsStore.getValue("feature_voice_messages")) { - controls.push( this.voiceRecordingButton = c} - room={this.props.room} />); - } + controls.push( this.voiceRecordingButton = c} + room={this.props.room} />); if (!this.state.isComposerEmpty || this.state.haveRecording) { controls.push( diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index f0bdb2e0e5..bff16403e6 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -212,12 +212,6 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_FEATURE, default: false, }, - "feature_voice_messages": { - isFeature: true, - displayName: _td("Send and receive voice messages"), - supportedLevels: LEVELS_FEATURE, - default: false, - }, "feature_latex_maths": { isFeature: true, displayName: _td("Render LaTeX maths in messages"), From 11773fb0f476ac224bfdc75f58494e48d19c30be Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Jul 2021 18:12:32 -0600 Subject: [PATCH 067/445] Clean up imports --- src/components/views/messages/MVoiceOrAudioBody.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/messages/MVoiceOrAudioBody.tsx b/src/components/views/messages/MVoiceOrAudioBody.tsx index 8a9f39400e..2d78ea192e 100644 --- a/src/components/views/messages/MVoiceOrAudioBody.tsx +++ b/src/components/views/messages/MVoiceOrAudioBody.tsx @@ -17,7 +17,6 @@ limitations under the License. import React from "react"; import MAudioBody from "./MAudioBody"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import SettingsStore from "../../../settings/SettingsStore"; import MVoiceMessageBody from "./MVoiceMessageBody"; import { IBodyProps } from "./IBodyProps"; From 363175e9a64f2a9f1276fb228d1e1297a9df0f6f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Jul 2021 18:13:01 -0600 Subject: [PATCH 068/445] Clean up i18n --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1116e4cdc1..85aabae46a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -795,7 +795,6 @@ "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.": "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.", "Show notification badges for People in Spaces": "Show notification badges for People in Spaces", "Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode", - "Send and receive voice messages": "Send and receive voice messages", "Render LaTeX maths in messages": "Render LaTeX maths in messages", "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.", "Message Pinning": "Message Pinning", From e7c5711bb76c93a9cd7e8215a847a247aa54d764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 22 Jul 2021 07:45:48 +0200 Subject: [PATCH 069/445] membershipTypes -> groupedEvents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/MessagePanel.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 16b1c0064b..1e113b0b7b 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -50,7 +50,7 @@ import EditorStateTransfer from "../../utils/EditorStateTransfer"; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = [EventType.Sticker, EventType.RoomMessage]; -const membershipTypes = [ +const groupedEvents = [ EventType.RoomMember, EventType.RoomThirdPartyInvite, EventType.RoomServerAcl, @@ -1185,7 +1185,7 @@ class RedactionGrouper extends BaseGrouper { // Wrap consecutive member events in a ListSummary, ignore if redacted class MemberGrouper extends BaseGrouper { static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean { - return panel.shouldShowEvent(ev) && membershipTypes.includes(ev.getType() as EventType); + return panel.shouldShowEvent(ev) && groupedEvents.includes(ev.getType() as EventType); }; constructor( @@ -1202,7 +1202,7 @@ class MemberGrouper extends BaseGrouper { if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) { return false; } - return membershipTypes.includes(ev.getType() as EventType); + return groupedEvents.includes(ev.getType() as EventType); } public add(ev: MatrixEvent): void { From 51c112fd821995c4435488a5bbf2974b39ea706d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 22 Jul 2021 07:46:41 +0200 Subject: [PATCH 070/445] PinnedMessages -> ChangedPins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/MemberEventListSummary.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx index 4ae64b65a5..3b1557b9ad 100644 --- a/src/components/views/elements/MemberEventListSummary.tsx +++ b/src/components/views/elements/MemberEventListSummary.tsx @@ -73,7 +73,7 @@ enum TransitionType { ChangedAvatar = "changed_avatar", NoChange = "no_change", ServerAcl = "server_acl", - PinnedMessages = "pinned_messages" + ChangedPins = "pinned_messages" } const SEP = ","; @@ -357,7 +357,7 @@ export default class MemberEventListSummary extends React.Component { } else if (type === EventType.RoomServerAcl) { return TransitionType.ServerAcl; } else if (type === EventType.RoomPinnedEvents) { - return TransitionType.PinnedMessages; + return TransitionType.ChangedPins; } switch (e.mxEvent.getContent().membership) { From 9f227893b1a093de8b1b41dea724dc3be3058f63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 22 Jul 2021 07:47:04 +0200 Subject: [PATCH 071/445] join -> jsxJoin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/MemberEventListSummary.tsx | 4 ++-- src/utils/FormattingUtils.ts | 6 +++--- src/utils/ReactUtils.tsx | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx index 3b1557b9ad..46a27415ca 100644 --- a/src/components/views/elements/MemberEventListSummary.tsx +++ b/src/components/views/elements/MemberEventListSummary.tsx @@ -29,7 +29,7 @@ import defaultDispatcher from '../../../dispatcher/dispatcher'; import { RightPanelPhases } from '../../../stores/RightPanelStorePhases'; import { Action } from '../../../dispatcher/actions'; import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload'; -import { join } from '../../../utils/ReactUtils'; +import { jsxJoin } from '../../../utils/ReactUtils'; import { EventType } from 'matrix-js-sdk/src/@types/event'; const onPinnedMessagesClick = (): void => { @@ -138,7 +138,7 @@ export default class MemberEventListSummary extends React.Component { return null; } - return join(summaries, ", "); + return jsxJoin(summaries, ", "); } /** diff --git a/src/utils/FormattingUtils.ts b/src/utils/FormattingUtils.ts index 53a4adb238..b527ee7ea2 100644 --- a/src/utils/FormattingUtils.ts +++ b/src/utils/FormattingUtils.ts @@ -16,7 +16,7 @@ limitations under the License. */ import { _t } from '../languageHandler'; -import { join } from './ReactUtils'; +import { jsxJoin } from './ReactUtils'; /** * formats numbers to fit into ~3 characters, suitable for badge counts @@ -114,9 +114,9 @@ export function formatCommaSeparatedList(items: Array, ite return items[0]; } else if (remaining > 0) { items = items.slice(0, itemLimit); - return _t("%(items)s and %(count)s others", { items: join(items, ', '), count: remaining } ); + return _t("%(items)s and %(count)s others", { items: jsxJoin(items, ', '), count: remaining } ); } else { const lastItem = items.pop(); - return _t("%(items)s and %(lastItem)s", { items: join(items, ', '), lastItem: lastItem }); + return _t("%(items)s and %(lastItem)s", { items: jsxJoin(items, ', '), lastItem: lastItem }); } } diff --git a/src/utils/ReactUtils.tsx b/src/utils/ReactUtils.tsx index 25669d2d9b..4cd2d750f3 100644 --- a/src/utils/ReactUtils.tsx +++ b/src/utils/ReactUtils.tsx @@ -22,7 +22,7 @@ import React from "react"; * @param joiner the string/JSX.Element to join with * @returns the joined array */ -export function join(array: Array, joiner?: string | JSX.Element): JSX.Element { +export function jsxJoin(array: Array, joiner?: string | JSX.Element): JSX.Element { const newArray = []; array.forEach((element, index) => { newArray.push(element, (index === array.length - 1) ? null : joiner); From f53de5de99d3099564022c79e917dfc80c1af622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 22 Jul 2021 18:04:24 +0200 Subject: [PATCH 072/445] Make bubble layout background color less agressive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/themes/dark/css/_dark.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 655492661c..7b83fe0cb2 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -230,7 +230,7 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28); // Bubble tiles $eventbubble-self-bg: #143A34; $eventbubble-others-bg: #394049; -$eventbubble-bg-hover: #433C23; +$eventbubble-bg-hover: $header-panel-bg-color; $eventbubble-avatar-outline: $bg-color; $eventbubble-reply-color: #C1C6CD; From 6c0e0dc64bbd2d219dff9fae016e5e52c05d2955 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Thu, 22 Jul 2021 21:45:20 -0500 Subject: [PATCH 073/445] Allow all of the URL schemes that Firefox allows Signed-off-by: Aaron Raimist --- src/HtmlUtils.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index af5d2b3019..27556a4012 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -57,7 +57,10 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i'); const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; -export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet', 'matrix']; +export const PERMITTED_URL_SCHEMES = ["bitcoin", "ftp", "geo", "http", "https", "im", "irc", + "ircs", "magnet", "mailto", "matrix", "mms", "news", + "nntp", "openpgp4fpr", "sip", "sftp", "sms", "smsto", + "ssh", "tel", "urn", "webcal", "wtai", "xmpp"]; const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/; From 95f4275807ca3207f179288ae7cbde6352f61167 Mon Sep 17 00:00:00 2001 From: James Salter Date: Fri, 23 Jul 2021 16:47:02 +0100 Subject: [PATCH 074/445] Add Disabled anonymity, improve tests --- src/PosthogAnalytics.ts | 10 ++++---- test/PosthogAnalytics-test.ts | 45 ++++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 9c167f5464..3e757060db 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -12,6 +12,7 @@ interface IEvent { } export enum Anonymity { + Disabled, Anonymous, Pseudonymous } @@ -181,12 +182,12 @@ export class PosthogAnalytics { } private async capture(eventName: string, properties: posthog.Properties) { - if (!this.enabled) { - return; - } if (!this.initialised) { throw Error("Tried to track event before PoshogAnalytics.init has completed"); } + if (!this.enabled) { + return; + } const { origin, hash, pathname } = window.location; properties['$redacted_current_url'] = await getRedactedCurrentLocation( origin, hash, pathname, this.anonymity); @@ -197,7 +198,7 @@ export class PosthogAnalytics { eventName: E["eventName"], properties: E["properties"], ) { - if (this.anonymity == Anonymity.Anonymous) return; + if (this.anonymity == Anonymity.Anonymous || this.anonymity == Anonymity.Disabled) return; await this.capture(eventName, properties); } @@ -205,6 +206,7 @@ export class PosthogAnalytics { eventName: E["eventName"], properties: E["properties"], ) { + if (this.anonymity == Anonymity.Disabled) return; await this.capture(eventName, properties); } diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index a33544e738..cefaafe78f 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -1,4 +1,4 @@ -import { Anonymity, getRedactedCurrentLocation, IAnonymousEvent, IRoomEvent, +import { Anonymity, getRedactedCurrentLocation, IAnonymousEvent, IPseudonymousEvent, IRoomEvent, PosthogAnalytics } from '../src/PosthogAnalytics'; import SdkConfig from '../src/SdkConfig'; const crypto = require('crypto'); @@ -22,6 +22,13 @@ export interface ITestEvent extends IAnonymousEvent { } } +export interface ITestPseudonymousEvent extends IPseudonymousEvent { + key: "jest_test_pseudo_event", + properties: { + foo: string + } +} + export interface ITestRoomEvent extends IRoomEvent { key: "jest_test_room_event", properties: { @@ -75,7 +82,7 @@ describe("PosthogAnalytics", () => { }); }); - it("Should pass track() to posthog", async () => { + it("Should pass trackAnonymousEvent() to posthog", async () => { analytics.init(Anonymity.Pseudonymous); await analytics.trackAnonymousEvent("jest_test_event", { foo: "bar", @@ -96,18 +103,44 @@ describe("PosthogAnalytics", () => { .toEqual("73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049"); }); - it("Should silently not track if not inititalised", async () => { - await analytics.trackAnonymousEvent("jest_test_event", { + it("Should pass trackPseudonymousEvent() to posthog", async () => { + analytics.init(Anonymity.Pseudonymous); + await analytics.trackPseudonymousEvent("jest_test_pseudo_event", { + foo: "bar", + }); + expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_pseudo_event"); + expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar"); + }); + + it("Should blow up if not inititalised prior to tracking", async () => { + const fn = () => { + return analytics.trackAnonymousEvent("jest_test_event", { + foo: "bar", + }); + }; + await expect(fn()).rejects.toThrow(); + }); + + it("Should not track pseudonymous messages if anonymous", async () => { + analytics.init(Anonymity.Anonymous); + await analytics.trackPseudonymousEvent("jest_test_event", { foo: "bar", }); expect(fakePosthog.capture.mock.calls.length).toBe(0); }); - it("Should not track non-anonymous messages if anonymous", async () => { - analytics.init(Anonymity.Anonymous); + it("Should not track any events if disabled", async () => { + analytics.init(Anonymity.Disabled); await analytics.trackPseudonymousEvent("jest_test_event", { foo: "bar", }); + await analytics.trackAnonymousEvent("jest_test_event", { + foo: "bar", + }); + await analytics.trackRoomEvent("room id", "jest_test_room_event", { + foo: "bar", + }); + await analytics.trackPageView(200); expect(fakePosthog.capture.mock.calls.length).toBe(0); }); From 5e0a3976316d267ebedb285c6dd61f7e471dde22 Mon Sep 17 00:00:00 2001 From: James Salter Date: Fri, 23 Jul 2021 17:58:31 +0100 Subject: [PATCH 075/445] Refactor anonymity derivation --- src/PosthogAnalytics.ts | 32 ++++++++++++++++++++++-- src/components/structures/MatrixChat.tsx | 6 +++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 3e757060db..535781cb08 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -1,6 +1,7 @@ import posthog, { PostHog } from 'posthog-js'; import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; +import SettingsStore from './settings/SettingsStore'; interface IEvent { // The event name that will be used by PostHog. @@ -78,10 +79,14 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p export class PosthogAnalytics { private anonymity = Anonymity.Anonymous; - private initialised = false; private posthog?: PostHog = null; + + // set true during init() if posthog config is present private enabled = false; + // set to true after init() has been called + private initialised = false; + private static _instance = null; public static instance(): PosthogAnalytics { @@ -155,7 +160,9 @@ export class PosthogAnalytics { } public registerSuperProperties(properties) { - this.posthog.register(properties); + if (this.enabled) { + this.posthog.register(properties); + } } public isInitialised() { @@ -248,3 +255,24 @@ export async function getPlatformProperties() { export function getAnalytics(): PosthogAnalytics { return PosthogAnalytics.instance(); } + +export function getAnonymityFromSettings(): Anonymity { + // determine the current anonymity level based on curernt user settings + + // "Send anonymous usage data which helps us improve Element. This will use a cookie." + const analyticsOptIn = SettingsStore.getValue("analyticsOptIn"); + + // "Send pseudonymous usage data which helps us improve Element. This will use a cookie." + const pseudonumousOptIn = SettingsStore.getValue("pseudonymousAnalyticsOptIn"); + + let anonymity; + if (pseudonumousOptIn) { + anonymity = Anonymity.Pseudonymous; + } else if (analyticsOptIn) { + anonymity = Anonymity.Anonymous; + } else { + anonymity = Anonymity.Disabled; + } + + return anonymity; +} diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 513200520f..bd54b0ebc9 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -107,7 +107,7 @@ import UIStore, { UI_EVENTS } from "../../stores/UIStore"; import SoftLogout from './auth/SoftLogout'; import { makeRoomPermalink } from "../../utils/permalinks/Permalinks"; import { copyPlaintext } from "../../utils/strings"; -import { Anonymity, getAnalytics, getPlatformProperties } from '../../PosthogAnalytics'; +import { Anonymity, getAnalytics, getAnonymityFromSettings, getPlatformProperties } from '../../PosthogAnalytics'; /** constants for MatrixChat.state.view */ export enum Views { @@ -390,7 +390,9 @@ export default class MatrixChat extends React.PureComponent { } const analytics = getAnalytics(); - analytics.init(SettingsStore.getValue("analyticsOptIn") ? Anonymity.Pseudonymous : Anonymity.Anonymous); + analytics.init(getAnonymityFromSettings()); + // note this requires a network request in the browser, so some events can potentially + // before before registerSuperProperties has been called getPlatformProperties().then((properties) => analytics.registerSuperProperties(properties)); CountlyAnalytics.instance.enable(/* anonymous = */ true); From ff37b8cc79205e57969ed44d867b215a492bbf63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 12:47:30 +0200 Subject: [PATCH 076/445] Remove IncomingCallBox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallContainer.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/voip/CallContainer.tsx b/src/components/views/voip/CallContainer.tsx index fa963e4e28..41046b9952 100644 --- a/src/components/views/voip/CallContainer.tsx +++ b/src/components/views/voip/CallContainer.tsx @@ -1,5 +1,6 @@ /* Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import IncomingCallBox from './IncomingCallBox'; import CallPreview from './CallPreview'; import { replaceableComponent } from "../../../utils/replaceableComponent"; @@ -31,7 +31,6 @@ interface IState { export default class CallContainer extends React.PureComponent { public render() { return
-
; } From 07be6dd78065e325f1f31ce9de51efaa2b5c6388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 13:04:06 +0200 Subject: [PATCH 077/445] Allow suppliing whole body to toasts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/ToastContainer.tsx | 22 +++++++++++++------- src/stores/ToastStore.ts | 4 +++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index b7b0b7c652..75cf4a51fc 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -58,7 +58,7 @@ export default class ToastContainer extends React.Component<{}, IState> { let containerClasses; if (totalCount !== 0) { const topToast = this.state.toasts[0]; - const { title, icon, key, component, className, props } = topToast; + const { title, icon, key, component, className, props, supplyWholeBody } = topToast; const toastClasses = classNames("mx_Toast_toast", { "mx_Toast_hasIcon": icon, [`mx_Toast_icon_${icon}`]: icon, @@ -73,16 +73,22 @@ export default class ToastContainer extends React.Component<{}, IState> { key, toastKey: key, }); - toast = (
-
-

{ title }

- { countIndicator } -
-
{ React.createElement(component, toastProps) }
-
); + + const content = React.createElement(component, toastProps); + + toast = supplyWholeBody + ? content + :
+
+

{ title }

+ { countIndicator } +
+
{ content }
+
; containerClasses = classNames("mx_ToastContainer", { "mx_ToastContainer_stacked": isStacked, + [className]: supplyWholeBody, }); } return toast diff --git a/src/stores/ToastStore.ts b/src/stores/ToastStore.ts index 850c3cb026..e831be7203 100644 --- a/src/stores/ToastStore.ts +++ b/src/stores/ToastStore.ts @@ -22,11 +22,13 @@ export interface IToast { key: string; // higher priority number will be shown on top of lower priority priority: number; - title: string; + title?: string; icon?: string; component: C; className?: string; props?: Omit, "toastKey">; // toastKey is injected by ToastContainer + supplyWholeBody?: boolean; + content?: JSX.Element; } /** From 410928745f41e86451f0f27c944577a82a800170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 13:05:14 +0200 Subject: [PATCH 078/445] IncomingCallBox -> IncomingCallToast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/_components.scss | 1 + res/css/views/toasts/_IncomingCallToast.scss | 100 ++++++++++ res/css/views/voip/_CallContainer.scss | 80 -------- src/CallHandler.tsx | 17 ++ src/components/views/voip/IncomingCallBox.tsx | 176 ------------------ src/toasts/IncomingCallToast.tsx | 139 ++++++++++++++ 6 files changed, 257 insertions(+), 256 deletions(-) create mode 100644 res/css/views/toasts/_IncomingCallToast.scss delete mode 100644 src/components/views/voip/IncomingCallBox.tsx create mode 100644 src/toasts/IncomingCallToast.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index f9e3ab1160..b87b45093c 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -263,6 +263,7 @@ @import "./views/spaces/_SpacePublicShare.scss"; @import "./views/terms/_InlineTermsAgreement.scss"; @import "./views/toasts/_AnalyticsToast.scss"; +@import "./views/toasts/_IncomingCallToast.scss"; @import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/verification/_VerificationShowSas.scss"; @import "./views/voip/_CallContainer.scss"; diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss new file mode 100644 index 0000000000..5ce99bd11e --- /dev/null +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -0,0 +1,100 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner + +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. +*/ + +.mx_IncomingCallToast { + // mx_Toast overrides + padding: 8px !important; + display: unset !important; + top: 8px !important; + border-radius: 8px; + + min-width: 250px; + box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); + background-color: $voipcall-plinth-color; // To match mx_Toast + + pointer-events: initial; // restore pointer events so the user can accept/decline + cursor: pointer; + + .mx_IncomingCallToast_CallerInfo { + display: flex; + direction: row; + + img, .mx_BaseAvatar_initial { + margin: 8px; + } + + > div { + display: flex; + flex-direction: column; + + justify-content: center; + } + + h1, p { + margin: 0px; + padding: 0px; + font-size: $font-14px; + line-height: $font-16px; + } + + h1 { + font-weight: bold; + } + } + + .mx_IncomingCallToast_buttons { + padding: 8px; + display: flex; + flex-direction: row; + + > .mx_IncomingCallToast_spacer { + width: 8px; + } + + > * { + flex-shrink: 0; + flex-grow: 1; + margin-right: 0; + font-size: $font-15px; + line-height: $font-24px; + } + } + + .mx_IncomingCallToast_iconButton { + position: absolute; + right: 8px; + + &::before { + content: ''; + + height: 20px; + width: 20px; + background-color: $icon-button-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } + } + + .mx_IncomingCallToast_silence::before { + mask-image: url('$(res)/img/voip/silence.svg'); + } + + .mx_IncomingCallToast_unSilence::before { + mask-image: url('$(res)/img/voip/un-silence.svg'); + } +} diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss index 0c09070334..181a5ee0a3 100644 --- a/res/css/views/voip/_CallContainer.scss +++ b/res/css/views/voip/_CallContainer.scss @@ -43,84 +43,4 @@ limitations under the License. .mx_AppTile_persistedWrapper div { min-width: 350px; } - - .mx_IncomingCallBox { - min-width: 250px; - background-color: $voipcall-plinth-color; - padding: 8px; - box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); - border-radius: 8px; - - pointer-events: initial; // restore pointer events so the user can accept/decline - cursor: pointer; - - .mx_IncomingCallBox_CallerInfo { - display: flex; - direction: row; - - img, .mx_BaseAvatar_initial { - margin: 8px; - } - - > div { - display: flex; - flex-direction: column; - - justify-content: center; - } - - h1, p { - margin: 0px; - padding: 0px; - font-size: $font-14px; - line-height: $font-16px; - } - - h1 { - font-weight: bold; - } - } - - .mx_IncomingCallBox_buttons { - padding: 8px; - display: flex; - flex-direction: row; - - > .mx_IncomingCallBox_spacer { - width: 8px; - } - - > * { - flex-shrink: 0; - flex-grow: 1; - margin-right: 0; - font-size: $font-15px; - line-height: $font-24px; - } - } - - .mx_IncomingCallBox_iconButton { - position: absolute; - right: 8px; - - &::before { - content: ''; - - height: 20px; - width: 20px; - background-color: $icon-button-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - } - } - - .mx_IncomingCallBox_silence::before { - mask-image: url('$(res)/img/voip/silence.svg'); - } - - .mx_IncomingCallBox_unSilence::before { - mask-image: url('$(res)/img/voip/un-silence.svg'); - } - } } diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index e7ba1aa9fb..e9831b5315 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -88,6 +88,9 @@ import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/ import EventEmitter from 'events'; import SdkConfig from './SdkConfig'; import { ensureDMExists, findDMForUser } from './createRoom'; +import { getIncomingCallToastKey } from './toasts/IncomingCallToast'; +import ToastStore from './stores/ToastStore'; +import IncomingCallToast from "./toasts/IncomingCallToast"; export const PROTOCOL_PSTN = 'm.protocol.pstn'; export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn'; @@ -641,6 +644,20 @@ export default class CallHandler extends EventEmitter { `Call state in ${mappedRoomId} changed to ${status}`, ); + const toastKey = getIncomingCallToastKey(call.callId); + if (status === CallState.Ringing) { + ToastStore.sharedInstance().addOrReplaceToast({ + key: toastKey, + supplyWholeBody: true, + priority: 100, + component: IncomingCallToast, + className: "mx_IncomingCallToast", + props: { call }, + }); + } else { + ToastStore.sharedInstance().dismissToast(toastKey); + } + dis.dispatch({ action: 'call_state', room_id: mappedRoomId, diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx deleted file mode 100644 index 95e97f1080..0000000000 --- a/src/components/views/voip/IncomingCallBox.tsx +++ /dev/null @@ -1,176 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import dis from '../../../dispatcher/dispatcher'; -import { _t } from '../../../languageHandler'; -import { ActionPayload } from '../../../dispatcher/payloads'; -import CallHandler, { CallHandlerEvent } from '../../../CallHandler'; -import RoomAvatar from '../avatars/RoomAvatar'; -import AccessibleButton from '../elements/AccessibleButton'; -import { CallState } from 'matrix-js-sdk/src/webrtc/call'; -import { replaceableComponent } from "../../../utils/replaceableComponent"; -import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; -import classNames from 'classnames'; - -interface IProps { -} - -interface IState { - incomingCall: any; - silenced: boolean; -} - -@replaceableComponent("views.voip.IncomingCallBox") -export default class IncomingCallBox extends React.Component { - private dispatcherRef: string; - - constructor(props: IProps) { - super(props); - - this.dispatcherRef = dis.register(this.onAction); - this.state = { - incomingCall: null, - silenced: false, - }; - } - - componentDidMount = () => { - CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); - }; - - public componentWillUnmount() { - dis.unregister(this.dispatcherRef); - CallHandler.sharedInstance().removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); - } - - private onAction = (payload: ActionPayload) => { - switch (payload.action) { - case 'call_state': { - const call = CallHandler.sharedInstance().getCallForRoom(payload.room_id); - if (call && call.state === CallState.Ringing) { - this.setState({ - incomingCall: call, - silenced: false, // Reset silenced state for new call - }); - } else { - this.setState({ - incomingCall: null, - }); - } - } - } - }; - - private onSilencedCallsChanged = () => { - const callId = this.state.incomingCall?.callId; - if (!callId) return; - this.setState({ silenced: CallHandler.sharedInstance().isCallSilenced(callId) }); - }; - - private onAnswerClick: React.MouseEventHandler = (e) => { - e.stopPropagation(); - dis.dispatch({ - action: 'answer', - room_id: CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall), - }); - }; - - private onRejectClick: React.MouseEventHandler = (e) => { - e.stopPropagation(); - dis.dispatch({ - action: 'reject', - room_id: CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall), - }); - }; - - private onSilenceClick: React.MouseEventHandler = (e) => { - e.stopPropagation(); - const callId = this.state.incomingCall.callId; - this.state.silenced ? - CallHandler.sharedInstance().unSilenceCall(callId): - CallHandler.sharedInstance().silenceCall(callId); - }; - - public render() { - if (!this.state.incomingCall) { - return null; - } - - let room = null; - if (this.state.incomingCall) { - room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall)); - } - - const caller = room ? room.name : _t("Unknown caller"); - - let incomingCallText = null; - if (this.state.incomingCall) { - if (this.state.incomingCall.type === "voice") { - incomingCallText = _t("Incoming voice call"); - } else if (this.state.incomingCall.type === "video") { - incomingCallText = _t("Incoming video call"); - } else { - incomingCallText = _t("Incoming call"); - } - } - - const silenceClass = classNames({ - "mx_IncomingCallBox_iconButton": true, - "mx_IncomingCallBox_unSilence": this.state.silenced, - "mx_IncomingCallBox_silence": !this.state.silenced, - }); - - return
-
- -
-

{ caller }

-

{ incomingCallText }

-
- -
-
- - { _t("Decline") } - -
- - { _t("Accept") } - -
-
; - } -} diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx new file mode 100644 index 0000000000..2a3e2bd805 --- /dev/null +++ b/src/toasts/IncomingCallToast.tsx @@ -0,0 +1,139 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; +import classNames from 'classnames'; +import { replaceableComponent } from '../utils/replaceableComponent'; +import CallHandler, { CallHandlerEvent } from '../CallHandler'; +import dis from '../dispatcher/dispatcher'; +import { MatrixClientPeg } from '../MatrixClientPeg'; +import { _t } from '../languageHandler'; +import RoomAvatar from '../components/views/avatars/RoomAvatar'; +import AccessibleTooltipButton from '../components/views/elements/AccessibleTooltipButton'; +import AccessibleButton from '../components/views/elements/AccessibleButton'; + +export const getIncomingCallToastKey = (callId: string) => `call_${callId}`; + +interface IProps { + call: MatrixCall; +} + +interface IState { + silenced: boolean; +} + +@replaceableComponent("views.voip.IncomingCallToast") +export default class IncomingCallToast extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + silenced: false, + }; + } + + componentDidMount = () => { + CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); + }; + + public componentWillUnmount() { + CallHandler.sharedInstance().removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); + } + + private onSilencedCallsChanged = () => { + this.setState({ silenced: CallHandler.sharedInstance().isCallSilenced(this.props.call.callId) }); + }; + + private onAnswerClick= (e: React.MouseEvent) => { + e.stopPropagation(); + dis.dispatch({ + action: 'answer', + room_id: CallHandler.sharedInstance().roomIdForCall(this.props.call), + }); + }; + + private onRejectClick= (e: React.MouseEvent) => { + e.stopPropagation(); + dis.dispatch({ + action: 'reject', + room_id: CallHandler.sharedInstance().roomIdForCall(this.props.call), + }); + }; + + private onSilenceClick = (e: React.MouseEvent) => { + e.stopPropagation(); + const callId = this.props.call.callId; + this.state.silenced ? + CallHandler.sharedInstance().unSilenceCall(callId) : + CallHandler.sharedInstance().silenceCall(callId); + }; + + public render() { + const call = this.props.call; + let room = null; + room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(call)); + + const caller = room ? room.name : _t("Unknown caller"); + + const incomingCallText = call.type === CallType.Voice ? _t("Incoming voice call") : _t("Incoming video call"); + + const silenceClass = classNames({ + "mx_IncomingCallToast_iconButton": true, + "mx_IncomingCallToast_unSilence": this.state.silenced, + "mx_IncomingCallToast_silence": !this.state.silenced, + }); + + return +
+ +
+

{ caller }

+

{ incomingCallText }

+
+ +
+
+ + { _t("Decline") } + +
+ + { _t("Accept") } + +
+ ; + } +} From af22588682a8b64da1e929bbccec8fe3c197aa26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 13:39:39 +0200 Subject: [PATCH 079/445] Don't use a spacer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 5 +---- src/toasts/IncomingCallToast.tsx | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index 5ce99bd11e..f7edf6a7bd 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -60,10 +60,7 @@ limitations under the License. padding: 8px; display: flex; flex-direction: row; - - > .mx_IncomingCallToast_spacer { - width: 8px; - } + gap: 12px; > * { flex-shrink: 0; diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index 2a3e2bd805..83cd7aba80 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -125,7 +125,6 @@ export default class IncomingCallToast extends React.Component { > { _t("Decline") } -
Date: Sat, 24 Jul 2021 13:46:06 +0200 Subject: [PATCH 080/445] Correct button sizes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index f7edf6a7bd..02b27a94ab 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -63,6 +63,8 @@ limitations under the License. gap: 12px; > * { + height: 24px; + padding: 0px 12px; flex-shrink: 0; flex-grow: 1; margin-right: 0; From 24e6fc96f6c84b1d1676ee609671abbb3663ddfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 14:04:22 +0200 Subject: [PATCH 081/445] Reorganize content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 22 +++---- src/toasts/IncomingCallToast.tsx | 61 ++++++++++---------- 2 files changed, 36 insertions(+), 47 deletions(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index 02b27a94ab..d2947395dc 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -18,10 +18,12 @@ limitations under the License. .mx_IncomingCallToast { // mx_Toast overrides padding: 8px !important; - display: unset !important; + display: flex !important; top: 8px !important; border-radius: 8px; + flex-direction: row; + gap: 8px; min-width: 250px; box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); background-color: $voipcall-plinth-color; // To match mx_Toast @@ -29,20 +31,9 @@ limitations under the License. pointer-events: initial; // restore pointer events so the user can accept/decline cursor: pointer; - .mx_IncomingCallToast_CallerInfo { + .mx_IncomingCallToast_content { display: flex; - direction: row; - - img, .mx_BaseAvatar_initial { - margin: 8px; - } - - > div { - display: flex; - flex-direction: column; - - justify-content: center; - } + flex-direction: column; h1, p { margin: 0px; @@ -57,7 +48,7 @@ limitations under the License. } .mx_IncomingCallToast_buttons { - padding: 8px; + margin-top: 8px; display: flex; flex-direction: row; gap: 12px; @@ -74,6 +65,7 @@ limitations under the License. } .mx_IncomingCallToast_iconButton { + display: flex; position: absolute; right: 8px; diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index 83cd7aba80..cff0c82782 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -87,12 +87,7 @@ export default class IncomingCallToast extends React.Component { public render() { const call = this.props.call; - let room = null; - room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(call)); - - const caller = room ? room.name : _t("Unknown caller"); - - const incomingCallText = call.type === CallType.Voice ? _t("Incoming voice call") : _t("Incoming video call"); + const room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(call)); const silenceClass = classNames({ "mx_IncomingCallToast_iconButton": true, @@ -101,37 +96,39 @@ export default class IncomingCallToast extends React.Component { }); return -
- -
-

{ caller }

-

{ incomingCallText }

-
+ +
+

+ { room ? room.name : _t("Unknown caller") } +

+

+ { call.type === CallType.Voice ? _t("Incoming voice call") : _t("Incoming video call") } +

-
-
- - { _t("Decline") } - - - { _t("Accept") } - +
+ + { _t("Decline") } + + + { _t("Accept") } + +
; } From dd800549d734231b68aef31e484f56cd25410146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 14:06:52 +0200 Subject: [PATCH 082/445] Fix the silence icon color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index d2947395dc..0a36084f7a 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -74,7 +74,7 @@ limitations under the License. height: 20px; width: 20px; - background-color: $icon-button-color; + background-color: $secondary-fg-color; mask-repeat: no-repeat; mask-size: contain; mask-position: center; From a6f10a4aaa25b91172c6133b484da0df7be7dbba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 18:59:54 +0200 Subject: [PATCH 083/445] Move around some CSS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 66 ++++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index 0a36084f7a..a2d775c969 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -45,47 +45,47 @@ limitations under the License. h1 { font-weight: bold; } - } - .mx_IncomingCallToast_buttons { - margin-top: 8px; - display: flex; - flex-direction: row; - gap: 12px; + .mx_IncomingCallToast_buttons { + margin-top: 8px; + display: flex; + flex-direction: row; + gap: 12px; - > * { - height: 24px; - padding: 0px 12px; - flex-shrink: 0; - flex-grow: 1; - margin-right: 0; - font-size: $font-15px; - line-height: $font-24px; + > * { + height: 24px; + padding: 0px 12px; + flex-shrink: 0; + flex-grow: 1; + margin-right: 0; + font-size: $font-15px; + line-height: $font-24px; + } } - } - .mx_IncomingCallToast_iconButton { - display: flex; - position: absolute; - right: 8px; + .mx_IncomingCallToast_iconButton { + display: flex; + position: absolute; + right: 8px; - &::before { - content: ''; + &::before { + content: ''; - height: 20px; - width: 20px; - background-color: $secondary-fg-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; + height: 20px; + width: 20px; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } } - } - .mx_IncomingCallToast_silence::before { - mask-image: url('$(res)/img/voip/silence.svg'); - } + .mx_IncomingCallToast_silence::before { + mask-image: url('$(res)/img/voip/silence.svg'); + } - .mx_IncomingCallToast_unSilence::before { - mask-image: url('$(res)/img/voip/un-silence.svg'); + .mx_IncomingCallToast_unSilence::before { + mask-image: url('$(res)/img/voip/un-silence.svg'); + } } } From 25d62983de958b16300e88bc6524fb247d053eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 19:12:12 +0200 Subject: [PATCH 084/445] Add button icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 36 +++++++++++++++++++- src/toasts/IncomingCallToast.tsx | 20 ++++++----- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index a2d775c969..665109bc0f 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -46,13 +46,25 @@ limitations under the License. font-weight: bold; } + &.mx_IncomingCallToast_content_voice { + .mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before { + mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); + } + } + + &.mx_IncomingCallToast_content_video { + .mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before { + mask-image: url('$(res)/img/element-icons/call/video-call.svg'); + } + } + .mx_IncomingCallToast_buttons { margin-top: 8px; display: flex; flex-direction: row; gap: 12px; - > * { + .mx_IncomingCallToast_button { height: 24px; padding: 0px 12px; flex-shrink: 0; @@ -60,6 +72,28 @@ limitations under the License. margin-right: 0; font-size: $font-15px; line-height: $font-24px; + + span { + padding: 8px 0; + display: flex; + align-items: center; + + &::before { + content: ''; + display: inline-block; + background-color: $button-fg-color; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 16px; + width: 16px; + height: 16px; + margin-right: 8px; + } + } + + &.mx_IncomingCallToast_button_decline span::before { + mask-image: url('$(res)/img/element-icons/call/hangup.svg'); + } } } diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index cff0c82782..9e5528d6a7 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -88,9 +88,13 @@ export default class IncomingCallToast extends React.Component { public render() { const call = this.props.call; const room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(call)); + const isVoice = call.type === CallType.Voice; - const silenceClass = classNames({ - "mx_IncomingCallToast_iconButton": true, + const contentClass = classNames("mx_IncomingCallToast_content", { + "mx_IncomingCallToast_content_voice": isVoice, + "mx_IncomingCallToast_content_video": !isVoice, + }); + const silenceClass = classNames("mx_IncomingCallToast_iconButton", { "mx_IncomingCallToast_unSilence": this.state.silenced, "mx_IncomingCallToast_silence": !this.state.silenced, }); @@ -101,12 +105,12 @@ export default class IncomingCallToast extends React.Component { height={32} width={32} /> -
+

{ room ? room.name : _t("Unknown caller") }

- { call.type === CallType.Voice ? _t("Incoming voice call") : _t("Incoming video call") } + { isVoice ? _t("Incoming voice call") : _t("Incoming video call") }

{ />
- { _t("Decline") } + { _t("Decline") } - { _t("Accept") } + { _t("Accept") }
From 064544369a033fe3a84734466cca4710db9ac16a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 19:30:37 +0200 Subject: [PATCH 085/445] Add call type icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 27 ++++++++++++++++++-- src/toasts/IncomingCallToast.tsx | 7 ++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index 665109bc0f..bcfb61ed21 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -35,7 +35,7 @@ limitations under the License. display: flex; flex-direction: column; - h1, p { + h1, .mx_CallEvent_type { margin: 0px; padding: 0px; font-size: $font-14px; @@ -46,13 +46,36 @@ limitations under the License. font-weight: bold; } + .mx_CallEvent_type { + display: flex; + flex-direction: row; + + .mx_CallEvent_type_icon { + height: 16px; + width: 16px; + margin-right: 6px; + + &::before { + content: ''; + position: absolute; + height: inherit; + width: inherit; + background-color: $tertiary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + } + } + } + &.mx_IncomingCallToast_content_voice { + .mx_CallEvent_type .mx_CallEvent_type_icon::before, .mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before { mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); } } &.mx_IncomingCallToast_content_video { + .mx_CallEvent_type .mx_CallEvent_type_icon::before, .mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before { mask-image: url('$(res)/img/element-icons/call/video-call.svg'); } @@ -107,7 +130,7 @@ limitations under the License. height: 20px; width: 20px; - background-color: $secondary-fg-color; + background-color: $tertiary-fg-color; mask-repeat: no-repeat; mask-size: contain; mask-position: center; diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index 9e5528d6a7..f8a7f0591f 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -109,9 +109,10 @@ export default class IncomingCallToast extends React.Component {

{ room ? room.name : _t("Unknown caller") }

-

- { isVoice ? _t("Incoming voice call") : _t("Incoming video call") } -

+
+
+ { isVoice ? _t("Voice call") : _t("Video call") } +
Date: Sat, 24 Jul 2021 20:39:44 +0200 Subject: [PATCH 086/445] Move silence button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 43 ++++++++++---------- src/toasts/IncomingCallToast.tsx | 10 ++--- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index bcfb61ed21..04f92bb095 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -29,7 +29,6 @@ limitations under the License. background-color: $voipcall-plinth-color; // To match mx_Toast pointer-events: initial; // restore pointer events so the user can accept/decline - cursor: pointer; .mx_IncomingCallToast_content { display: flex; @@ -119,30 +118,30 @@ limitations under the License. } } } + } - .mx_IncomingCallToast_iconButton { - display: flex; - position: absolute; - right: 8px; + .mx_IncomingCallToast_iconButton { + display: flex; + height: 20px; + width: 20px; - &::before { - content: ''; + &::before { + content: ''; - height: 20px; - width: 20px; - background-color: $tertiary-fg-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - } - } - - .mx_IncomingCallToast_silence::before { - mask-image: url('$(res)/img/voip/silence.svg'); - } - - .mx_IncomingCallToast_unSilence::before { - mask-image: url('$(res)/img/voip/un-silence.svg'); + height: inherit; + width: inherit; + background-color: $tertiary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; } } + + .mx_IncomingCallToast_silence::before { + mask-image: url('$(res)/img/voip/silence.svg'); + } + + .mx_IncomingCallToast_unSilence::before { + mask-image: url('$(res)/img/voip/un-silence.svg'); + } } diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index f8a7f0591f..7a7aacac12 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -113,11 +113,6 @@ export default class IncomingCallToast extends React.Component {
{ isVoice ? _t("Voice call") : _t("Video call") }
-
{
+ ; } } From 1f9cd79bcfe4d5b3aeb024e72d316fa93297dbac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 20:59:15 +0200 Subject: [PATCH 087/445] Fix some spacing and other tiny things MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 37 +++++++++++++------- src/toasts/IncomingCallToast.tsx | 4 +-- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index 04f92bb095..6bdbcdb8b0 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -23,7 +23,6 @@ limitations under the License. border-radius: 8px; flex-direction: row; - gap: 8px; min-width: 250px; box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); background-color: $voipcall-plinth-color; // To match mx_Toast @@ -33,21 +32,27 @@ limitations under the License. .mx_IncomingCallToast_content { display: flex; flex-direction: column; + margin-left: 8px; - h1, .mx_CallEvent_type { - margin: 0px; - padding: 0px; - font-size: $font-14px; - line-height: $font-16px; - } - - h1 { + .mx_CallEvent_caller { font-weight: bold; + font-size: $font-15px; + line-height: $font-18px; + + margin-top: 2px; } .mx_CallEvent_type { + font-size: $font-12px; + line-height: $font-15px; + color: $tertiary-fg-color; + + margin-top: 4px; + margin-bottom: 6px; + display: flex; flex-direction: row; + align-items: center; .mx_CallEvent_type_icon { height: 16px; @@ -88,7 +93,7 @@ limitations under the License. .mx_IncomingCallToast_button { height: 24px; - padding: 0px 12px; + padding: 0px 8px; flex-shrink: 0; flex-grow: 1; margin-right: 0; @@ -106,15 +111,21 @@ limitations under the License. background-color: $button-fg-color; mask-position: center; mask-repeat: no-repeat; - mask-size: 16px; - width: 16px; - height: 16px; margin-right: 8px; } } + &.mx_IncomingCallToast_button_accept span::before { + mask-size: 13px; + width: 13px; + height: 13px; + } + &.mx_IncomingCallToast_button_decline span::before { mask-image: url('$(res)/img/element-icons/call/hangup.svg'); + mask-size: 16px; + width: 16px; + height: 16px; } } } diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index 7a7aacac12..8d14fbd883 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -106,9 +106,9 @@ export default class IncomingCallToast extends React.Component { width={32} />
-

+ { room ? room.name : _t("Unknown caller") } -

+
{ isVoice ? _t("Voice call") : _t("Video call") } From 85095df4b9663fd1d078b57efe01a6fa47f4d2b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 21:03:26 +0200 Subject: [PATCH 088/445] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b36910b41b..46600c48f2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -734,6 +734,13 @@ "Notifications": "Notifications", "Enable desktop notifications": "Enable desktop notifications", "Enable": "Enable", + "Unknown caller": "Unknown caller", + "Voice call": "Voice call", + "Video call": "Video call", + "Decline": "Decline", + "Accept": "Accept", + "Sound on": "Sound on", + "Silence call": "Silence call", "Use app for a better experience": "Use app for a better experience", "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.", "Use app": "Use app", @@ -911,14 +918,6 @@ "Fill Screen": "Fill Screen", "Return to call": "Return to call", "%(name)s on hold": "%(name)s on hold", - "Unknown caller": "Unknown caller", - "Incoming voice call": "Incoming voice call", - "Incoming video call": "Incoming video call", - "Incoming call": "Incoming call", - "Sound on": "Sound on", - "Silence call": "Silence call", - "Decline": "Decline", - "Accept": "Accept", "The other party cancelled the verification.": "The other party cancelled the verification.", "Verified!": "Verified!", "You've successfully verified this user.": "You've successfully verified this user.", @@ -1580,8 +1579,6 @@ "Hide Widgets": "Hide Widgets", "Show Widgets": "Show Widgets", "Search": "Search", - "Voice call": "Voice call", - "Video call": "Video call", "Invites": "Invites", "Favourites": "Favourites", "People": "People", From 379101e3ff3e0b77bcf5c215292e649d9bd4c084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 21:09:00 +0200 Subject: [PATCH 089/445] Remove an unused member MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/ToastStore.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stores/ToastStore.ts b/src/stores/ToastStore.ts index e831be7203..093ea9fb6b 100644 --- a/src/stores/ToastStore.ts +++ b/src/stores/ToastStore.ts @@ -28,7 +28,6 @@ export interface IToast { className?: string; props?: Omit, "toastKey">; // toastKey is injected by ToastContainer supplyWholeBody?: boolean; - content?: JSX.Element; } /** From f2204aa1ffdb16de1592778409ab909fad82c52c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 21:12:29 +0200 Subject: [PATCH 090/445] Remove nonsense comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index 6bdbcdb8b0..d49014efdb 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -20,12 +20,12 @@ limitations under the License. padding: 8px !important; display: flex !important; top: 8px !important; - border-radius: 8px; + border-radius: 8px; flex-direction: row; min-width: 250px; box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); - background-color: $voipcall-plinth-color; // To match mx_Toast + background-color: $voipcall-plinth-color; pointer-events: initial; // restore pointer events so the user can accept/decline From 3287d51b179096a29b3f45f62458bad25c86d0c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 25 Jul 2021 08:06:11 +0200 Subject: [PATCH 091/445] Add some return types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/toasts/IncomingCallToast.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index 8d14fbd883..c842581f9a 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -53,15 +53,15 @@ export default class IncomingCallToast extends React.Component { CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); }; - public componentWillUnmount() { + public componentWillUnmount(): void { CallHandler.sharedInstance().removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); } - private onSilencedCallsChanged = () => { + private onSilencedCallsChanged = (): void => { this.setState({ silenced: CallHandler.sharedInstance().isCallSilenced(this.props.call.callId) }); }; - private onAnswerClick= (e: React.MouseEvent) => { + private onAnswerClick= (e: React.MouseEvent): void => { e.stopPropagation(); dis.dispatch({ action: 'answer', @@ -69,7 +69,7 @@ export default class IncomingCallToast extends React.Component { }); }; - private onRejectClick= (e: React.MouseEvent) => { + private onRejectClick= (e: React.MouseEvent): void => { e.stopPropagation(); dis.dispatch({ action: 'reject', @@ -77,7 +77,7 @@ export default class IncomingCallToast extends React.Component { }); }; - private onSilenceClick = (e: React.MouseEvent) => { + private onSilenceClick = (e: React.MouseEvent): void => { e.stopPropagation(); const callId = this.props.call.callId; this.state.silenced ? From 1d629f2557a7d618355436f99c31cc6b9ce856f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 10:34:59 +0200 Subject: [PATCH 092/445] More TS Co-authored-by: Germain --- src/toasts/IncomingCallToast.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index c842581f9a..a853e1652a 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -49,7 +49,7 @@ export default class IncomingCallToast extends React.Component { }; } - componentDidMount = () => { + public componentDidMount = (): void => { CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); }; From 40947a2a681f03b62013c9c002b1e4ddea7916db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 12:21:58 +0200 Subject: [PATCH 093/445] Simplifie toast handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 11 +----- src/CallHandler.tsx | 3 +- src/components/structures/ToastContainer.tsx | 37 +++++++++++--------- src/stores/ToastStore.ts | 2 +- 4 files changed, 24 insertions(+), 29 deletions(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index d49014efdb..975628f948 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -16,17 +16,8 @@ limitations under the License. */ .mx_IncomingCallToast { - // mx_Toast overrides - padding: 8px !important; - display: flex !important; - top: 8px !important; - - border-radius: 8px; + display: flex; flex-direction: row; - min-width: 250px; - box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); - background-color: $voipcall-plinth-color; - pointer-events: initial; // restore pointer events so the user can accept/decline .mx_IncomingCallToast_content { diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index e9831b5315..5018c44488 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -648,10 +648,9 @@ export default class CallHandler extends EventEmitter { if (status === CallState.Ringing) { ToastStore.sharedInstance().addOrReplaceToast({ key: toastKey, - supplyWholeBody: true, priority: 100, component: IncomingCallToast, - className: "mx_IncomingCallToast", + bodyClassName: "mx_IncomingCallToast", props: { call }, }); } else { diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index 75cf4a51fc..0b0e871975 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -58,37 +58,42 @@ export default class ToastContainer extends React.Component<{}, IState> { let containerClasses; if (totalCount !== 0) { const topToast = this.state.toasts[0]; - const { title, icon, key, component, className, props, supplyWholeBody } = topToast; - const toastClasses = classNames("mx_Toast_toast", { + const { title, icon, key, component, className, bodyClassName, props } = topToast; + const bodyClasses = classNames("mx_Toast_body", bodyClassName); + const toastClasses = classNames("mx_Toast_toast", className, { "mx_Toast_hasIcon": icon, [`mx_Toast_icon_${icon}`]: icon, - }, className); - - let countIndicator; - if (isStacked || this.state.countSeen > 0) { - countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`; - } - + }); const toastProps = Object.assign({}, props, { key, toastKey: key, }); - const content = React.createElement(component, toastProps); - toast = supplyWholeBody - ? content - :
+ let countIndicator; + if (title && isStacked || this.state.countSeen > 0) { + countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`; + } + + let titleElement; + if (title) { + titleElement = (

{ title }

{ countIndicator }
-
{ content }
-
; + ); + } + + toast = ( +
+ { titleElement } +
{ content }
+
+ ); containerClasses = classNames("mx_ToastContainer", { "mx_ToastContainer_stacked": isStacked, - [className]: supplyWholeBody, }); } return toast diff --git a/src/stores/ToastStore.ts b/src/stores/ToastStore.ts index 093ea9fb6b..5e51de3e26 100644 --- a/src/stores/ToastStore.ts +++ b/src/stores/ToastStore.ts @@ -26,8 +26,8 @@ export interface IToast { icon?: string; component: C; className?: string; + bodyClassName?: string; props?: Omit, "toastKey">; // toastKey is injected by ToastContainer - supplyWholeBody?: boolean; } /** From 277fdf1711186390ee3e6c119da1574d830888aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 12:42:18 +0200 Subject: [PATCH 094/445] voipcall-plinth-color -> quinary-content-color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/structures/_ToastContainer.scss | 4 ++-- res/css/views/voip/_CallView.scss | 2 +- res/themes/dark/css/_dark.scss | 2 +- res/themes/legacy-dark/css/_legacy-dark.scss | 2 +- res/themes/legacy-light/css/_legacy-light.scss | 8 ++++---- res/themes/light/css/_light.scss | 8 ++++---- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index d248568740..b6034be42d 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -28,7 +28,7 @@ limitations under the License. margin: 0 4px; grid-row: 2 / 4; grid-column: 1; - background-color: $dark-panel-bg-color; + background-color: $quinary-content-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); border-radius: 8px; } @@ -37,7 +37,7 @@ limitations under the License. grid-row: 1 / 3; grid-column: 1; color: $primary-fg-color; - background-color: $dark-panel-bg-color; + background-color: $quinary-content-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); border-radius: 8px; overflow: hidden; diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 205d431752..2be4a4b802 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -39,7 +39,7 @@ limitations under the License. .mx_CallView_pip { width: 320px; padding-bottom: 8px; - background-color: $voipcall-plinth-color; + background-color: $quinary-content-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20); border-radius: 8px; diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 655492661c..0907ccdd9a 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -113,7 +113,7 @@ $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #394049; +$quinary-content-color: #394049; // ******************** diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 0c0197cfb0..323fe0651e 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -112,7 +112,7 @@ $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #394049; +$quinary-content-color: #394049; // ******************** diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index b7d45452ff..a4e7af2bb9 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -8,9 +8,9 @@ /* Noto Color Emoji contains digits, in fixed-width, therefore causing digits in flowed text to stand out. TODO: Consider putting all emoji fonts to the end rather than the front. */ -$font-family: Nunito, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Arial, Helvetica, Sans-Serif, 'Noto Color Emoji'; +$font-family: nunito, twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', arial, helvetica, sans-serif, 'Noto Color Emoji'; -$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Courier, monospace, 'Noto Color Emoji'; +$monospace-font-family: inconsolata, twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', courier, monospace, 'Noto Color Emoji'; // unified palette // try to use these colors when possible @@ -179,7 +179,7 @@ $composer-e2e-icon-color: #91a1c0; $header-divider-color: #91a1c0; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #F4F6FA; +$quinary-content-color: #F4F6FA; // ******************** @@ -390,7 +390,7 @@ $eventbubble-reply-color: #C1C6CD; @define-mixin mx_DialogButton_secondary { // flip colours for the secondary ones font-weight: 600; - border: 1px solid $accent-color ! important; + border: 1px solid $accent-color !important; color: $accent-color; background-color: $button-secondary-bg-color; } diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 32722515d8..2f81cb3407 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -8,9 +8,9 @@ /* Noto Color Emoji contains digits, in fixed-width, therefore causing digits in flowed text to stand out. TODO: Consider putting all emoji fonts to the end rather than the front. */ -$font-family: Inter, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Arial, Helvetica, Sans-Serif, 'Noto Color Emoji'; +$font-family: inter, twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', arial, helvetica, sans-serif, 'Noto Color Emoji'; -$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Courier, monospace, 'Noto Color Emoji'; +$monospace-font-family: inconsolata, twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', courier, monospace, 'Noto Color Emoji'; // unified palette // try to use these colors when possible @@ -168,7 +168,7 @@ $composer-e2e-icon-color: #91A1C0; $header-divider-color: #91A1C0; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #F4F6FA; +$quinary-content-color: #F4F6FA; // ******************** @@ -392,7 +392,7 @@ $eventbubble-reply-color: #C1C6CD; @define-mixin mx_DialogButton_secondary { // flip colours for the secondary ones font-weight: 600; - border: 1px solid $accent-color ! important; + border: 1px solid $accent-color !important; color: $accent-color; background-color: $button-secondary-bg-color; } From 7ed5dee74bd57deb9de7e20a3b7813f1f94a07d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 13:17:06 +0200 Subject: [PATCH 095/445] Make colors a bit clearer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/structures/_ToastContainer.scss | 4 ++-- res/css/views/voip/_CallView.scss | 2 +- res/themes/dark/css/_dark.scss | 2 +- res/themes/legacy-dark/css/_legacy-dark.scss | 2 +- res/themes/legacy-light/css/_legacy-light.scss | 4 ++-- res/themes/light/css/_light.scss | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index b6034be42d..2c3f1c705c 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -28,7 +28,7 @@ limitations under the License. margin: 0 4px; grid-row: 2 / 4; grid-column: 1; - background-color: $quinary-content-color; + background-color: $toast-bg-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); border-radius: 8px; } @@ -37,7 +37,7 @@ limitations under the License. grid-row: 1 / 3; grid-column: 1; color: $primary-fg-color; - background-color: $quinary-content-color; + background-color: $toast-bg-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); border-radius: 8px; overflow: hidden; diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 2be4a4b802..8299ad8f9a 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -39,7 +39,7 @@ limitations under the License. .mx_CallView_pip { width: 320px; padding-bottom: 8px; - background-color: $quinary-content-color; + background-color: $toast-bg-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20); border-radius: 8px; diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 0907ccdd9a..6e8d64b807 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -112,8 +112,8 @@ $eventtile-meta-color: $roomtopic-color; $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; -// this probably shouldn't have it's own colour $quinary-content-color: #394049; +$toast-bg-color: $quinary-content-color; // ******************** diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 323fe0651e..064b532bb0 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -111,8 +111,8 @@ $eventtile-meta-color: $roomtopic-color; $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; -// this probably shouldn't have it's own colour $quinary-content-color: #394049; +$toast-bg-color: $quinary-content-color; // ******************** diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index a4e7af2bb9..cbf3d6d1b6 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -178,8 +178,8 @@ $eventtile-meta-color: $roomtopic-color; $composer-e2e-icon-color: #91a1c0; $header-divider-color: #91a1c0; -// this probably shouldn't have it's own colour -$quinary-content-color: #F4F6FA; +$system-light-color: #F4F6FA; +$toast-bg-color: $system-light-color; // ******************** diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 2f81cb3407..1d786383f3 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -167,8 +167,8 @@ $eventtile-meta-color: $roomtopic-color; $composer-e2e-icon-color: #91A1C0; $header-divider-color: #91A1C0; -// this probably shouldn't have it's own colour -$quinary-content-color: #F4F6FA; +$system-light-color: #F4F6FA; +$toast-bg-color: $system-light-color; // ******************** From 5dd34de5fe89504ea1354d3e81c3a23cefcc3805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 27 Jul 2021 14:31:42 +0200 Subject: [PATCH 096/445] Handle mute state changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/VideoFeed.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 95cc5ee3e3..3d873cef0a 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -85,10 +85,12 @@ export default class VideoFeed extends React.Component { if (oldFeed) { this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream); + this.props.feed.removeListener(CallFeedEvent.MuteStateChanged, this.onMuteStateChanged); this.stopMedia(); } if (newFeed) { this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream); + this.props.feed.addListener(CallFeedEvent.MuteStateChanged, this.onMuteStateChanged); this.playMedia(); } } @@ -137,6 +139,14 @@ export default class VideoFeed extends React.Component { this.playMedia(); }; + private onMuteStateChanged = () => { + this.setState({ + audioMuted: this.props.feed.isAudioMuted(), + videoMuted: this.props.feed.isVideoMuted(), + }); + this.playMedia(); + }; + private onResize = (e) => { if (this.props.onResize && !this.props.feed.isLocal()) { this.props.onResize(e); From 474561600e4e04cf112e367e1b3e1c1b8937a956 Mon Sep 17 00:00:00 2001 From: James Salter Date: Tue, 27 Jul 2021 13:29:56 +0100 Subject: [PATCH 097/445] Fix hash == "" --- src/PosthogAnalytics.ts | 23 ++++++++++++++--------- test/PosthogAnalytics-test.ts | 6 ++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 535781cb08..cdb23e582c 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -63,17 +63,22 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p pathname = "//"; } - let [_, screen, ...parts] = hash.split("/"); + let hashStr; + if (hash == "") { + hashStr = ""; + } else { + let [_, screen, ...parts] = hash.split("/"); - if (!knownScreens.has(screen)) { - screen = ""; + if (!knownScreens.has(screen)) { + screen = ""; + } + + for (let i = 0; i < parts.length; i++) { + parts[i] = anonymity === Anonymity.Anonymous ? `` : await hashHex(parts[i]); + } + + hashStr = `${_}/${screen}/${parts.join("/")}`; } - - for (let i = 0; i < parts.length; i++) { - parts[i] = anonymity === Anonymity.Anonymous ? `` : await hashHex(parts[i]); - } - - const hashStr = `${_}/${screen}/${parts.join("/")}`; return origin + pathname + hashStr; } diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index cefaafe78f..7d81b6e86d 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -174,6 +174,12 @@ bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`); expect(location).toBe("https://foo.bar/#///"); }); + it("Should currently handle an empty hash", async () => { + const location = await getRedactedCurrentLocation( + "https://foo.bar", "", "/", Anonymity.Anonymous); + expect(location).toBe("https://foo.bar/"); + }); + it("Should identify the user to posthog if pseudonymous", async () => { analytics.init(Anonymity.Pseudonymous); await analytics.identifyUser("foo"); From 78eb8ffc261b948be714634bfecca04d35a19359 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 27 Jul 2021 15:51:16 +0100 Subject: [PATCH 098/445] Upgrade matrix-js-sdk to 12.2.0-rc.1 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b73462d188..97367ae6d2 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "12.1.0", + "matrix-js-sdk": "12.2.0-rc.1", "matrix-widget-api": "^0.1.0-beta.15", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index ee531265b7..b339f69c1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5445,10 +5445,10 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@12.1.0: - version "12.1.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.1.0.tgz#7d159dd9bc03701e45a6b2777f1fa582a7e8b970" - integrity sha512-/fSqOjD+mTlMD+/B3s3Ja6BfI46FnTDl43ojzGDUOsHRRmSYUmoONb83qkH5Fjm8cI2q5ZBJMsBfjuZwLVeiZw== +matrix-js-sdk@12.2.0-rc.1: + version "12.2.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.2.0-rc.1.tgz#fbbb462dd98c64edb6f4bcd5403d802c98625f01" + integrity sha512-aHxL6wsLRrnJMLJ17V1IVOm2dCGOA8jHWZi43xNzkdsmQeU9UiUmUcT9RxsYcc7YhNv8ZaZ1plIwvBmoz3H4mA== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From 3003d489145d301db81d15cdce40e044ed336a43 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 27 Jul 2021 16:01:51 +0100 Subject: [PATCH 099/445] Prepare changelog for v3.27.0-rc.1 --- CHANGELOG.md | 211 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73b383d76d..cfecd838bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,214 @@ +Changes in [3.27.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.27.0-rc.1) (2021-07-27) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.26.0...v3.27.0-rc.1) + + * Fix timing of voice message recording UI appearing + [\#6479](https://github.com/matrix-org/matrix-react-sdk/pull/6479) + * Fix PiP resize issues + [\#6483](https://github.com/matrix-org/matrix-react-sdk/pull/6483) + * Translations update from Weblate + [\#6482](https://github.com/matrix-org/matrix-react-sdk/pull/6482) + * Make new reply UI clickable + [\#6474](https://github.com/matrix-org/matrix-react-sdk/pull/6474) + * Fix infinite pagination loop when offline + [\#6478](https://github.com/matrix-org/matrix-react-sdk/pull/6478) + * Fix deleted message left offset in the timeline + [\#6473](https://github.com/matrix-org/matrix-react-sdk/pull/6473) + * Fix broken layout of the space hierarchy view + [\#6481](https://github.com/matrix-org/matrix-react-sdk/pull/6481) + * Add data-layout to MELS for better CSS structure + [\#6480](https://github.com/matrix-org/matrix-react-sdk/pull/6480) + * Style markdown quotes + [\#6468](https://github.com/matrix-org/matrix-react-sdk/pull/6468) + * Update ESLint Config + [\#6476](https://github.com/matrix-org/matrix-react-sdk/pull/6476) + * Fix VoIP event tile issues + [\#6471](https://github.com/matrix-org/matrix-react-sdk/pull/6471) + * Fix editing of & & + [\#6469](https://github.com/matrix-org/matrix-react-sdk/pull/6469) + * Fix avatar overlapping with timestamp + [\#6461](https://github.com/matrix-org/matrix-react-sdk/pull/6461) + * Fix reactions row pushing content on IRC layout + [\#6464](https://github.com/matrix-org/matrix-react-sdk/pull/6464) + * Fix blurhash rounded corners missing regression + [\#6467](https://github.com/matrix-org/matrix-react-sdk/pull/6467) + * Adhere to better eslint rules + [\#6459](https://github.com/matrix-org/matrix-react-sdk/pull/6459) + * Clean up voice messages code + [\#6453](https://github.com/matrix-org/matrix-react-sdk/pull/6453) + * Fix position of the space hierarchy spinner + [\#6462](https://github.com/matrix-org/matrix-react-sdk/pull/6462) + * Fix clipped avatar in room list + [\#6463](https://github.com/matrix-org/matrix-react-sdk/pull/6463) + * Make inline events feel less claustrophobic in bubble layout + [\#6460](https://github.com/matrix-org/matrix-react-sdk/pull/6460) + * Initial MSC3083 + MSC3244 support + [\#6212](https://github.com/matrix-org/matrix-react-sdk/pull/6212) + * Add event selected state for message bubbles + [\#6449](https://github.com/matrix-org/matrix-react-sdk/pull/6449) + * Make images fit inside message bubble + [\#6448](https://github.com/matrix-org/matrix-react-sdk/pull/6448) + * Don't show scrollbar for URL previews + [\#6450](https://github.com/matrix-org/matrix-react-sdk/pull/6450) + * Fix display of image messages that lack thumbnails + [\#6456](https://github.com/matrix-org/matrix-react-sdk/pull/6456) + * Fix avatar obstructing membership and state changes + [\#6439](https://github.com/matrix-org/matrix-react-sdk/pull/6439) + * Zoom images in lightbox to where the cursor points + [\#6418](https://github.com/matrix-org/matrix-react-sdk/pull/6418) + * Always display the Sender in the reply tile + [\#6446](https://github.com/matrix-org/matrix-react-sdk/pull/6446) + * Use modern layout in file and notification panel + [\#6447](https://github.com/matrix-org/matrix-react-sdk/pull/6447) + * Add right padding for event replies + [\#6444](https://github.com/matrix-org/matrix-react-sdk/pull/6444) + * Fix event tile cut off in share preview + [\#6445](https://github.com/matrix-org/matrix-react-sdk/pull/6445) + * Remove excessive padding after url previews + [\#6443](https://github.com/matrix-org/matrix-react-sdk/pull/6443) + * Make quotes thinner + [\#6441](https://github.com/matrix-org/matrix-react-sdk/pull/6441) + * Prevent action bar to overlap the event content + [\#6438](https://github.com/matrix-org/matrix-react-sdk/pull/6438) + * Use a MediaElementSourceAudioNode to process large audio files + [\#6436](https://github.com/matrix-org/matrix-react-sdk/pull/6436) + * Revert 100% on codeblocks + [\#6440](https://github.com/matrix-org/matrix-react-sdk/pull/6440) + * Fix duration placeholder parsing for audio files + [\#6435](https://github.com/matrix-org/matrix-react-sdk/pull/6435) + * Handle upload errors in voice messages + [\#6434](https://github.com/matrix-org/matrix-react-sdk/pull/6434) + * Render error state for audio components + [\#6433](https://github.com/matrix-org/matrix-react-sdk/pull/6433) + * Clean up visual style of files and voice messages + [\#6432](https://github.com/matrix-org/matrix-react-sdk/pull/6432) + * Convert a few things to TS + [\#6413](https://github.com/matrix-org/matrix-react-sdk/pull/6413) + * Fix onPaste handler to work with copying files from Finder + [\#5389](https://github.com/matrix-org/matrix-react-sdk/pull/5389) + * Increase hit area for timestamp in message bubbles + [\#6428](https://github.com/matrix-org/matrix-react-sdk/pull/6428) + * Navigate to the first room with notifications when clicked on space + notification dot + [\#5974](https://github.com/matrix-org/matrix-react-sdk/pull/5974) + * Add matrix: to the list of permitted URL schemes + [\#6388](https://github.com/matrix-org/matrix-react-sdk/pull/6388) + * Make diff colors in codeblocks more pleasant + [\#6355](https://github.com/matrix-org/matrix-react-sdk/pull/6355) + * Add alwaysShowTimestamps and others to RoomView setting watchers + [\#6261](https://github.com/matrix-org/matrix-react-sdk/pull/6261) + * Restore 'use default' naming on room notifications + [\#6431](https://github.com/matrix-org/matrix-react-sdk/pull/6431) + * Use cached value to read member count + [\#6429](https://github.com/matrix-org/matrix-react-sdk/pull/6429) + * yarn upgrade + [\#6430](https://github.com/matrix-org/matrix-react-sdk/pull/6430) + * Improve new layout switcher UI + [\#6427](https://github.com/matrix-org/matrix-react-sdk/pull/6427) + * Play only one audio file at a time + [\#6417](https://github.com/matrix-org/matrix-react-sdk/pull/6417) + * Improve file labeling in replies + [\#6404](https://github.com/matrix-org/matrix-react-sdk/pull/6404) + * Fix replies line clamping + [\#6425](https://github.com/matrix-org/matrix-react-sdk/pull/6425) + * Add null guard for room prop in EventTile + [\#6426](https://github.com/matrix-org/matrix-react-sdk/pull/6426) + * Fix font slider preview for message bubbles + [\#6421](https://github.com/matrix-org/matrix-react-sdk/pull/6421) + * Add spoiler support for message bubbles + [\#6419](https://github.com/matrix-org/matrix-react-sdk/pull/6419) + * Fix error when hovering over non-emoji reactions + [\#6416](https://github.com/matrix-org/matrix-react-sdk/pull/6416) + * Fix sticker display for message bubbles + [\#6423](https://github.com/matrix-org/matrix-react-sdk/pull/6423) + * Reintroduce grouped events padding on modern layout + [\#6420](https://github.com/matrix-org/matrix-react-sdk/pull/6420) + * TypeScript migration for auth components + [\#6412](https://github.com/matrix-org/matrix-react-sdk/pull/6412) + * Fix grecaptcha throwing useless error sometimes + [\#6401](https://github.com/matrix-org/matrix-react-sdk/pull/6401) + * Move download button for media to the action bar + [\#6386](https://github.com/matrix-org/matrix-react-sdk/pull/6386) + * Remove hover effect from files in the files panel + [\#6405](https://github.com/matrix-org/matrix-react-sdk/pull/6405) + * Revert accidental renaming of dispatcherRef + [\#6415](https://github.com/matrix-org/matrix-react-sdk/pull/6415) + * Add VoIP event tiles + [\#6121](https://github.com/matrix-org/matrix-react-sdk/pull/6121) + * Update Emojibase and Twemoji and switch to IamCal (Slack-style) shortcodes + [\#6347](https://github.com/matrix-org/matrix-react-sdk/pull/6347) + * Conform to new react and typescript eslint rules + [\#6408](https://github.com/matrix-org/matrix-react-sdk/pull/6408) + * Remove unwanted comma in EventTile + [\#6414](https://github.com/matrix-org/matrix-react-sdk/pull/6414) + * 💭 Message bubble layout + [\#6291](https://github.com/matrix-org/matrix-react-sdk/pull/6291) + * Fix sticker placeholder centering + [\#6411](https://github.com/matrix-org/matrix-react-sdk/pull/6411) + * Fix avatar placeholders not getting capitalized + [\#6407](https://github.com/matrix-org/matrix-react-sdk/pull/6407) + * Revert order of notification setting radios + [\#6406](https://github.com/matrix-org/matrix-react-sdk/pull/6406) + * Respect compound emojis in default avatar initial generation + [\#6397](https://github.com/matrix-org/matrix-react-sdk/pull/6397) + * Update eslint-plugin-matrix-org + [\#6403](https://github.com/matrix-org/matrix-react-sdk/pull/6403) + * Rename Copy Link to Copy Room Link + [\#6402](https://github.com/matrix-org/matrix-react-sdk/pull/6402) + * Don't throw exception from setStickyRoom as it split-brains the + RoomListStore + [\#6399](https://github.com/matrix-org/matrix-react-sdk/pull/6399) + * Fix bug where 'other homeserver' would unfocus + [\#6394](https://github.com/matrix-org/matrix-react-sdk/pull/6394) + * Cleanup reply code + [\#6392](https://github.com/matrix-org/matrix-react-sdk/pull/6392) + * Match colors of room and user avatars in DMs + [\#6393](https://github.com/matrix-org/matrix-react-sdk/pull/6393) + * Remove title from Image View + [\#6395](https://github.com/matrix-org/matrix-react-sdk/pull/6395) + * Notification settings UI refresh + [\#6352](https://github.com/matrix-org/matrix-react-sdk/pull/6352) + * Avoid hitting the settings store from TextForEvent + [\#6205](https://github.com/matrix-org/matrix-react-sdk/pull/6205) + * Fix issues with room list duplication + [\#6391](https://github.com/matrix-org/matrix-react-sdk/pull/6391) + * Use URLSearchParams instead of transitive dependency `querystring` + [\#4399](https://github.com/matrix-org/matrix-react-sdk/pull/4399) + * Add "Copy Link" to room context menu + [\#6374](https://github.com/matrix-org/matrix-react-sdk/pull/6374) + * Fix EventIndex double handling events and erroring + [\#6385](https://github.com/matrix-org/matrix-react-sdk/pull/6385) + * Fix IRC layout replies + [\#6387](https://github.com/matrix-org/matrix-react-sdk/pull/6387) + * Improve reply rendering + [\#3553](https://github.com/matrix-org/matrix-react-sdk/pull/3553) + * Update PR template for new changelog generation + [\#6380](https://github.com/matrix-org/matrix-react-sdk/pull/6380) + * Silence / Fix some console warnings/errors + [\#6382](https://github.com/matrix-org/matrix-react-sdk/pull/6382) + * Cache value of feature_spaces* flags as they cause page refresh so are + immutable + [\#6381](https://github.com/matrix-org/matrix-react-sdk/pull/6381) + * Standardise spelling and casing of homeserver, identity server, and + integration manager + [\#6365](https://github.com/matrix-org/matrix-react-sdk/pull/6365) + * Convert CONTRIBUTING to markdown + [\#6379](https://github.com/matrix-org/matrix-react-sdk/pull/6379) + * Move blurhashing into a Worker and use OffscreenCanvas for thumbnailing + [\#6366](https://github.com/matrix-org/matrix-react-sdk/pull/6366) + * Exclude state events from widgets reading room events + [\#6378](https://github.com/matrix-org/matrix-react-sdk/pull/6378) + * Use webpack worker-loader instead of homegrown hack + [\#6356](https://github.com/matrix-org/matrix-react-sdk/pull/6356) + * Send clear events to widgets when permitted + [\#6371](https://github.com/matrix-org/matrix-react-sdk/pull/6371) + * Comment why end to end tests are only on the develop branch + [\#6377](https://github.com/matrix-org/matrix-react-sdk/pull/6377) + * Improve and consolidate typing + [\#6345](https://github.com/matrix-org/matrix-react-sdk/pull/6345) + * Fix 'User' type import + [\#6375](https://github.com/matrix-org/matrix-react-sdk/pull/6375) + Changes in [3.26.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.26.0) (2021-07-19) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.26.0-rc.1...v3.26.0) From cc0ff41360e7378ae920a17939d6410999948e6d Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 27 Jul 2021 16:01:52 +0100 Subject: [PATCH 100/445] v3.27.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 97367ae6d2..83f260eae3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.26.0", + "version": "3.27.0-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -25,7 +25,7 @@ "bin": { "reskindex": "scripts/reskindex.js" }, - "main": "./src/index.js", + "main": "./lib/index.js", "matrix_src_main": "./src/index.js", "matrix_lib_main": "./lib/index.js", "matrix_lib_typings": "./lib/index.d.ts", @@ -200,5 +200,6 @@ "coverageReporters": [ "text" ] - } + }, + "typings": "./lib/index.d.ts" } From 12461a79e1ddaa45355b231bc18accd867cfa7a2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 27 Jul 2021 17:19:45 +0100 Subject: [PATCH 101/445] Move SettingsStore `setting_updated` dispatch to action enum --- src/dispatcher/actions.ts | 7 +++++ .../payloads/SettingUpdatedPayload.ts | 29 +++++++++++++++++++ src/settings/SettingsStore.ts | 10 ++++--- src/stores/BreadcrumbsStore.ts | 9 ++++-- src/stores/room-list/RoomListStore.ts | 9 ++++-- 5 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 src/dispatcher/payloads/SettingUpdatedPayload.ts diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 5732428201..043c69df36 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -193,4 +193,11 @@ export enum Action { * Switches space. Should be used with SwitchSpacePayload. */ SwitchSpace = "switch_space", + + /** + * Fires when a monitored setting is updated, + * see SettingsStore::monitorSetting for more details. + * Should be used with SettingUpdatedPayload. + */ + SettingUpdated = "setting_updated", } diff --git a/src/dispatcher/payloads/SettingUpdatedPayload.ts b/src/dispatcher/payloads/SettingUpdatedPayload.ts new file mode 100644 index 0000000000..8d457facfb --- /dev/null +++ b/src/dispatcher/payloads/SettingUpdatedPayload.ts @@ -0,0 +1,29 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +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 { ActionPayload } from "../payloads"; +import { Action } from "../actions"; +import { SettingLevel } from "../../settings/SettingLevel"; + +export interface SettingUpdatedPayload extends ActionPayload { + action: Action.SettingUpdated; + + settingName: string; + roomId: string; + level: SettingLevel; + newValueAtLevel: SettingLevel; + newValue: any; +} diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index 44f3d5d838..c5b83cbcd0 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -29,6 +29,8 @@ import LocalEchoWrapper from "./handlers/LocalEchoWrapper"; import { WatchManager, CallbackFn as WatchCallbackFn } from "./WatchManager"; import { SettingLevel } from "./SettingLevel"; import SettingsHandler from "./handlers/SettingsHandler"; +import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload"; +import { Action } from "../dispatcher/actions"; const defaultWatchManager = new WatchManager(); @@ -147,7 +149,7 @@ export default class SettingsStore { * if the change in value is worthwhile enough to react upon. * @returns {string} A reference to the watcher that was employed. */ - public static watchSetting(settingName: string, roomId: string, callbackFn: CallbackFn): string { + public static watchSetting(settingName: string, roomId: string | null, callbackFn: CallbackFn): string { const setting = SETTINGS[settingName]; const originalSettingName = settingName; if (!setting) throw new Error(`${settingName} is not a setting`); @@ -193,7 +195,7 @@ export default class SettingsStore { * @param {string} settingName The setting name to monitor. * @param {String} roomId The room ID to monitor for changes in. Use null for all rooms. */ - public static monitorSetting(settingName: string, roomId: string) { + public static monitorSetting(settingName: string, roomId: string | null) { roomId = roomId || null; // the thing wants null specifically to work, so appease it. if (!this.monitors.has(settingName)) this.monitors.set(settingName, new Map()); @@ -201,8 +203,8 @@ export default class SettingsStore { const registerWatcher = () => { this.monitors.get(settingName).set(roomId, SettingsStore.watchSetting( settingName, roomId, (settingName, inRoomId, level, newValueAtLevel, newValue) => { - dis.dispatch({ - action: 'setting_updated', + dis.dispatch({ + action: Action.SettingUpdated, settingName, roomId: inRoomId, level, diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index aceaf8b898..8a85ca354f 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -23,6 +23,8 @@ import { arrayHasDiff } from "../utils/arrays"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { SettingLevel } from "../settings/SettingLevel"; import SpaceStore from "./SpaceStore"; +import { Action } from "../dispatcher/actions"; +import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload"; const MAX_ROOMS = 20; // arbitrary const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up @@ -63,10 +65,11 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { protected async onAction(payload: ActionPayload) { if (!this.matrixClient) return; - if (payload.action === 'setting_updated') { - if (payload.settingName === 'breadcrumb_rooms') { + if (payload.action === Action.SettingUpdated) { + const settingUpdatedPayload = payload as SettingUpdatedPayload; + if (settingUpdatedPayload.settingName === 'breadcrumb_rooms') { await this.updateRooms(); - } else if (payload.settingName === 'breadcrumbs') { + } else if (settingUpdatedPayload.settingName === 'breadcrumbs') { await this.updateState({ enabled: SettingsStore.getValue("breadcrumbs", null) }); } } else if (payload.action === 'view_room') { diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 3913a2220f..b7af70ad99 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -36,6 +36,8 @@ import { RoomNotificationStateStore } from "../notifications/RoomNotificationSta import { VisibilityProvider } from "./filters/VisibilityProvider"; import { SpaceWatcher } from "./SpaceWatcher"; import SpaceStore from "../SpaceStore"; +import { Action } from "../../dispatcher/actions"; +import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload"; interface IState { tagsEnabled?: boolean; @@ -213,10 +215,11 @@ export class RoomListStoreClass extends AsyncStoreWithClient { const logicallyReady = this.matrixClient && this.initialListsGenerated; if (!logicallyReady) return; - if (payload.action === 'setting_updated') { - if (this.watchedSettings.includes(payload.settingName)) { + if (payload.action === Action.SettingUpdated) { + const settingUpdatedPayload = payload as SettingUpdatedPayload; + if (this.watchedSettings.includes(settingUpdatedPayload.settingName)) { // TODO: Remove with https://github.com/vector-im/element-web/issues/14602 - if (payload.settingName === "advancedRoomListLogging") { + if (settingUpdatedPayload.settingName === "advancedRoomListLogging") { // Log when the setting changes so we know when it was turned on in the rageshake const enabled = SettingsStore.getValue("advancedRoomListLogging"); console.warn("Advanced room list logging is enabled? " + enabled); From 5f6a1e336e1be8e45144aaa4739d8bd518eaa208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 27 Jul 2021 18:46:19 +0200 Subject: [PATCH 102/445] Remove unnecessary curly braces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/settings/tabs/user/LabsUserSettingsTab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js index 20444c1ce7..5274d5191d 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js @@ -79,7 +79,7 @@ export default class LabsUserSettingsTab extends React.Component { let hiddenReadReceipts; if (this.state.showHiddenReadReceipts) { hiddenReadReceipts = ( - + ); } From 8c073a643904bc70d71a7b864aa2a149cceefff7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 27 Jul 2021 17:53:03 +0100 Subject: [PATCH 103/445] RoomListStore removeFilter skip triggering update if nothing changed --- src/stores/room-list/RoomListStore.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index b7af70ad99..1a5ef0484e 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -711,6 +711,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { } let promise = Promise.resolve(); let idx = this.filterConditions.indexOf(filter); + let removed = false; if (idx >= 0) { this.filterConditions.splice(idx, 1); @@ -721,14 +722,20 @@ export class RoomListStoreClass extends AsyncStoreWithClient { if (SpaceStore.spacesEnabled) { promise = this.recalculatePrefiltering(); } + removed = true; } + idx = this.prefilterConditions.indexOf(filter); if (idx >= 0) { filter.off(FILTER_CHANGED, this.onPrefilterUpdated); this.prefilterConditions.splice(idx, 1); promise = this.recalculatePrefiltering(); + removed = true; + } + + if (removed) { + promise.then(() => this.updateFn.trigger()); } - promise.then(() => this.updateFn.trigger()); } /** From 507dcd91d9acd1527ad030f66daf5a948f9e7831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 27 Jul 2021 18:53:57 +0200 Subject: [PATCH 104/445] Fix i18n? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 65 +++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8e05630f05..89252c5f07 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -529,7 +529,9 @@ "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s set the main address for this room to %(address)s.", "%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.", "%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s added the alternative addresses %(addresses)s for this room.", + "%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s added alternative address %(addresses)s for this room.", "%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s removed the alternative addresses %(addresses)s for this room.", + "%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s removed alternative address %(addresses)s for this room.", "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s changed the alternative addresses for this room.", "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s changed the main and alternative addresses for this room.", "%(senderName)s changed the addresses for this room.": "%(senderName)s changed the addresses for this room.", @@ -570,6 +572,7 @@ "Dark": "Dark", "%(displayName)s is typing …": "%(displayName)s is typing …", "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", + "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", "Remain on your screen when viewing another room, when running": "Remain on your screen when viewing another room, when running", "Remain on your screen while running": "Remain on your screen while running", @@ -650,6 +653,7 @@ "Unable to connect to Homeserver. Retrying...": "Unable to connect to Homeserver. Retrying...", "Attachment": "Attachment", "%(items)s and %(count)s others|other": "%(items)s and %(count)s others", + "%(items)s and %(count)s others|one": "%(items)s and one other", "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", "a few seconds ago": "a few seconds ago", "about a minute ago": "about a minute ago", @@ -1102,11 +1106,15 @@ "Your homeserver does not support session management.": "Your homeserver does not support session management.", "Unable to load session list": "Unable to load session list", "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Confirm deleting these sessions by using Single Sign On to prove your identity.", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Confirm deleting this session by using Single Sign On to prove your identity.", "Confirm deleting these sessions": "Confirm deleting these sessions", "Click the button below to confirm deleting these sessions.|other": "Click the button below to confirm deleting these sessions.", + "Click the button below to confirm deleting these sessions.|one": "Click the button below to confirm deleting this session.", "Delete sessions|other": "Delete sessions", + "Delete sessions|one": "Delete session", "Authentication": "Authentication", "Delete %(count)s sessions|other": "Delete %(count)s sessions", + "Delete %(count)s sessions|one": "Delete %(count)s session", "ID": "ID", "Public Name": "Public Name", "Last seen": "Last seen", @@ -1114,6 +1122,7 @@ "Encryption": "Encryption", "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.", + "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s room.", "Manage": "Manage", "Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.", "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.", @@ -1503,8 +1512,10 @@ "Failed to send": "Failed to send", "Scroll to most recent messages": "Scroll to most recent messages", "Show %(count)s other previews|other": "Show %(count)s other previews", + "Show %(count)s other previews|one": "Show %(count)s other preview", "Close preview": "Close preview", "and %(count)s others...|other": "and %(count)s others...", + "and %(count)s others...|one": "and one other...", "Invite to this room": "Invite to this room", "Invite to this community": "Invite to this community", "Invite to this space": "Invite to this space", @@ -1566,6 +1577,7 @@ "Screen sharing is here!": "Screen sharing is here!", "You can now share your screen by pressing the \"screen share\" button during a call. You can even do this in audio calls if both sides support it!": "You can now share your screen by pressing the \"screen share\" button during a call. You can even do this in audio calls if both sides support it!", "(~%(count)s results)|other": "(~%(count)s results)", + "(~%(count)s results)|one": "(~%(count)s result)", "Join Room": "Join Room", "Forget room": "Forget room", "Hide Widgets": "Hide Widgets", @@ -1595,7 +1607,9 @@ "Quick actions": "Quick actions", "Use the + to make a new room or explore existing ones below": "Use the + to make a new room or explore existing ones below", "%(count)s results in all spaces|other": "%(count)s results in all spaces", + "%(count)s results in all spaces|one": "%(count)s result in all spaces", "%(count)s results|other": "%(count)s results", + "%(count)s results|one": "%(count)s result", "This room": "This room", "Joining room …": "Joining room …", "Loading …": "Loading …", @@ -1644,6 +1658,7 @@ "Jump to first unread room.": "Jump to first unread room.", "Jump to first invite.": "Jump to first invite.", "Show %(count)s more|other": "Show %(count)s more", + "Show %(count)s more|one": "Show %(count)s more", "Show less": "Show less", "Use default": "Use default", "All messages": "All messages", @@ -1658,7 +1673,9 @@ "Leave Room": "Leave Room", "Room options": "Room options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", + "%(count)s unread messages including mentions.|one": "1 unread mention.", "%(count)s unread messages.|other": "%(count)s unread messages.", + "%(count)s unread messages.|one": "1 unread message.", "Unread messages.": "Unread messages.", "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.", "This room has already been upgraded.": "This room has already been upgraded.", @@ -1760,14 +1777,17 @@ "Not encrypted": "Not encrypted", "About": "About", "%(count)s people|other": "%(count)s people", + "%(count)s people|one": "%(count)s person", "Show files": "Show files", "Share room": "Share room", "Room settings": "Room settings", "Trusted": "Trusted", "Not trusted": "Not trusted", "%(count)s verified sessions|other": "%(count)s verified sessions", + "%(count)s verified sessions|one": "1 verified session", "Hide verified sessions": "Hide verified sessions", "%(count)s sessions|other": "%(count)s sessions", + "%(count)s sessions|one": "%(count)s session", "Hide sessions": "Hide sessions", "Jump to read receipt": "Jump to read receipt", "Mention": "Mention", @@ -1787,8 +1807,10 @@ "Try scrolling up in the timeline to see if there are any earlier ones.": "Try scrolling up in the timeline to see if there are any earlier ones.", "Remove recent messages by %(user)s": "Remove recent messages by %(user)s", "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "You are about to remove 1 message by %(user)s. This cannot be undone. Do you wish to continue?", "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.", "Remove %(count)s messages|other": "Remove %(count)s messages", + "Remove %(count)s messages|one": "Remove 1 message", "Remove recent messages": "Remove recent messages", "Ban": "Ban", "Unban this user?": "Unban this user?", @@ -1991,9 +2013,12 @@ "collapse": "collapse", "expand": "expand", "View all %(count)s members|other": "View all %(count)s members", + "View all %(count)s members|one": "View 1 member", "Including %(commaSeparatedMembers)s": "Including %(commaSeparatedMembers)s", "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s members including %(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", "%(count)s people you know have already joined|other": "%(count)s people you know have already joined", + "%(count)s people you know have already joined|one": "%(count)s person you know has already joined", "Zoom out": "Zoom out", "Zoom in": "Zoom in", "Rotate Left": "Rotate Left", @@ -2002,33 +2027,61 @@ "Language Dropdown": "Language Dropdown", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times", + "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)sjoined", "%(oneUser)sjoined %(count)s times|other": "%(oneUser)sjoined %(count)s times", + "%(oneUser)sjoined %(count)s times|one": "%(oneUser)sjoined", "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)sleft %(count)s times", + "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)sleft", "%(oneUser)sleft %(count)s times|other": "%(oneUser)sleft %(count)s times", + "%(oneUser)sleft %(count)s times|one": "%(oneUser)sleft", "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)sjoined and left %(count)s times", + "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)sjoined and left", "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)sjoined and left %(count)s times", + "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)sjoined and left", "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)sleft and rejoined %(count)s times", + "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)sleft and rejoined", "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)sleft and rejoined %(count)s times", + "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sleft and rejoined", "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)srejected their invitations %(count)s times", + "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)srejected their invitations", "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)srejected their invitation %(count)s times", + "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)srejected their invitation", "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "%(severalUsers)shad their invitations withdrawn %(count)s times", + "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)shad their invitations withdrawn", "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)shad their invitation withdrawn %(count)s times", + "%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)shad their invitation withdrawn", "were invited %(count)s times|other": "were invited %(count)s times", + "were invited %(count)s times|one": "were invited", "was invited %(count)s times|other": "was invited %(count)s times", + "was invited %(count)s times|one": "was invited", "were banned %(count)s times|other": "were banned %(count)s times", + "were banned %(count)s times|one": "were banned", "was banned %(count)s times|other": "was banned %(count)s times", + "was banned %(count)s times|one": "was banned", "were unbanned %(count)s times|other": "were unbanned %(count)s times", + "were unbanned %(count)s times|one": "were unbanned", "was unbanned %(count)s times|other": "was unbanned %(count)s times", + "was unbanned %(count)s times|one": "was unbanned", "were kicked %(count)s times|other": "were kicked %(count)s times", + "were kicked %(count)s times|one": "were kicked", "was kicked %(count)s times|other": "was kicked %(count)s times", + "was kicked %(count)s times|one": "was kicked", "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)schanged their name %(count)s times", + "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)schanged their name", "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)schanged their name %(count)s times", + "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)schanged their name", "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)schanged their avatar %(count)s times", + "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)schanged their avatar", "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)schanged their avatar %(count)s times", + "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)schanged their avatar", "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)smade no changes %(count)s times", + "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)smade no changes", "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)smade no changes %(count)s times", + "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)smade no changes", "%(severalUsers)schanged the server ACLs %(count)s times|other": "%(severalUsers)schanged the server ACLs %(count)s times", + "%(severalUsers)schanged the server ACLs %(count)s times|one": "%(severalUsers)schanged the server ACLs", "%(oneUser)schanged the server ACLs %(count)s times|other": "%(oneUser)schanged the server ACLs %(count)s times", + "%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)schanged the server ACLs", "Power level": "Power level", "Custom level": "Custom level", "QR Code": "QR Code", @@ -2063,6 +2116,7 @@ "Matrix rooms": "Matrix rooms", "Not all selected were added": "Not all selected were added", "Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...", "Filter your rooms and spaces": "Filter your rooms and spaces", "Feeling experimental?": "Feeling experimental?", "You can add existing spaces to a space.": "You can add existing spaces to a space.", @@ -2116,6 +2170,7 @@ "Hide": "Hide", "Show": "Show", "Send %(count)s invites|other": "Send %(count)s invites", + "Send %(count)s invites|one": "Send %(count)s invite", "Invite people to join %(communityName)s": "Invite people to join %(communityName)s", "You cannot delete this message. (%(code)s)": "You cannot delete this message. (%(code)s)", "Removing…": "Removing…", @@ -2315,7 +2370,9 @@ "You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages", "Are you sure you want to sign out?": "Are you sure you want to sign out?", "%(count)s members|other": "%(count)s members", + "%(count)s members|one": "%(count)s member", "%(count)s rooms|other": "%(count)s rooms", + "%(count)s rooms|one": "%(count)s room", "You're removing all spaces. Access will default to invite only": "You're removing all spaces. Access will default to invite only", "Select spaces": "Select spaces", "Decide which spaces can access this room. If a space is selected, its members can find and join .": "Decide which spaces can access this room. If a space is selected, its members can find and join .", @@ -2447,6 +2504,7 @@ "These files are too large to upload. The file size limit is %(limit)s.": "These files are too large to upload. The file size limit is %(limit)s.", "Some files are too large to be uploaded. The file size limit is %(limit)s.": "Some files are too large to be uploaded. The file size limit is %(limit)s.", "Upload %(count)s other files|other": "Upload %(count)s other files", + "Upload %(count)s other files|one": "Upload %(count)s other file", "Cancel All": "Cancel All", "Upload Error": "Upload Error", "Verify other login": "Verify other login", @@ -2670,6 +2728,7 @@ "%(creator)s created this DM.": "%(creator)s created this DM.", "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "%(count)s messages deleted.|other": "%(count)s messages deleted.", + "%(count)s messages deleted.|one": "%(count)s message deleted.", "Your Communities": "Your Communities", "Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!", "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.", @@ -2725,12 +2784,15 @@ "Failed to reject invite": "Failed to reject invite", "Drop file here to upload": "Drop file here to upload", "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.", + "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.", "You don't have permission": "You don't have permission", "This room is suggested as a good one to join": "This room is suggested as a good one to join", "Suggested": "Suggested", "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.", "%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s rooms and %(numSpaces)s spaces", + "%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s room and %(numSpaces)s spaces", "%(count)s rooms and 1 space|other": "%(count)s rooms and 1 space", + "%(count)s rooms and 1 space|one": "%(count)s room and 1 space", "Select a room below first": "Select a room below first", "Failed to remove some rooms. Try again later": "Failed to remove some rooms. Try again later", "Removing...": "Removing...", @@ -2784,6 +2846,8 @@ "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Failed to load timeline position": "Failed to load timeline position", "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", + "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", + "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", "Failed to find the general chat for this community": "Failed to find the general chat for this community", "Got an account? Sign in": "Got an account? Sign in", "New here? Create an account": "New here? Create an account", @@ -2798,6 +2862,7 @@ "User menu": "User menu", "Community and user menu": "Community and user menu", "Currently joining %(count)s rooms|other": "Currently joining %(count)s rooms", + "Currently joining %(count)s rooms|one": "Currently joining %(count)s room", "Could not load user profile": "Could not load user profile", "Decrypted event source": "Decrypted event source", "Original event source": "Original event source", From ec173e74e60246d2e4bf096d6345c1eb41c569a1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 27 Jul 2021 20:15:40 +0100 Subject: [PATCH 105/445] Test & Refactor SpaceWatcher to allow all rooms/home change without needing reload --- src/stores/room-list/SpaceWatcher.ts | 40 +++-- test/stores/SpaceStore-test.ts | 68 ++------ test/stores/room-list/SpaceWatcher-test.ts | 186 +++++++++++++++++++++ test/utils/test-utils.ts | 51 ++++++ 4 files changed, 275 insertions(+), 70 deletions(-) create mode 100644 test/stores/room-list/SpaceWatcher-test.ts diff --git a/src/stores/room-list/SpaceWatcher.ts b/src/stores/room-list/SpaceWatcher.ts index 1cec612e6f..fe2eb1e881 100644 --- a/src/stores/room-list/SpaceWatcher.ts +++ b/src/stores/room-list/SpaceWatcher.ts @@ -18,39 +18,47 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { RoomListStoreClass } from "./RoomListStore"; import { SpaceFilterCondition } from "./filters/SpaceFilterCondition"; -import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore"; +import SpaceStore, { UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../SpaceStore"; /** * Watches for changes in spaces to manage the filter on the provided RoomListStore */ export class SpaceWatcher { - private filter: SpaceFilterCondition; + private readonly filter = new SpaceFilterCondition(); + // we track these separately to the SpaceStore as we need to observe transitions private activeSpace: Room = SpaceStore.instance.activeSpace; + private allRoomsInHome: boolean = SpaceStore.instance.allRoomsInHome; constructor(private store: RoomListStoreClass) { - if (!SpaceStore.spacesTweakAllRoomsEnabled) { - this.filter = new SpaceFilterCondition(); + if (!this.allRoomsInHome || this.activeSpace) { this.updateFilter(); store.addFilter(this.filter); } SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated); + SpaceStore.instance.on(UPDATE_HOME_BEHAVIOUR, this.onHomeBehaviourUpdated); } - private onSelectedSpaceUpdated = (activeSpace?: Room) => { - this.activeSpace = activeSpace; + private onSelectedSpaceUpdated = (activeSpace?: Room, allRoomsInHome = this.allRoomsInHome) => { + if (activeSpace === this.activeSpace && allRoomsInHome === this.allRoomsInHome) return; // nop - if (this.filter) { - if (activeSpace || !SpaceStore.spacesTweakAllRoomsEnabled) { - this.updateFilter(); - } else { - this.store.removeFilter(this.filter); - this.filter = null; - } - } else if (activeSpace) { - this.filter = new SpaceFilterCondition(); + const oldActiveSpace = this.activeSpace; + const oldAllRoomsInHome = this.allRoomsInHome; + this.activeSpace = activeSpace; + this.allRoomsInHome = allRoomsInHome; + + if (activeSpace || !allRoomsInHome) { this.updateFilter(); - this.store.addFilter(this.filter); } + + if (oldAllRoomsInHome && !oldActiveSpace) { + this.store.addFilter(this.filter); + } else if (allRoomsInHome && !activeSpace) { + this.store.removeFilter(this.filter); + } + }; + + private onHomeBehaviourUpdated = (allRoomsInHome: boolean) => { + this.onSelectedSpaceUpdated(this.activeSpace, allRoomsInHome); }; private updateFilter = () => { diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index d772a7a658..8b809be95d 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -16,7 +16,6 @@ limitations under the License. import { EventEmitter } from "events"; import { EventType } from "matrix-js-sdk/src/@types/event"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import "./SpaceStore-setup"; // enable space lab @@ -26,31 +25,14 @@ import SpaceStore, { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES, } from "../../src/stores/SpaceStore"; -import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../utils/test-utils"; -import { mkEvent, mkStubRoom, stubClient } from "../test-utils"; -import { EnhancedMap } from "../../src/utils/maps"; +import * as testUtils from "../utils/test-utils"; +import { mkEvent, stubClient } from "../test-utils"; import DMRoomMap from "../../src/utils/DMRoomMap"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; import defaultDispatcher from "../../src/dispatcher/dispatcher"; jest.useFakeTimers(); -const mockStateEventImplementation = (events: MatrixEvent[]) => { - const stateMap = new EnhancedMap>(); - events.forEach(event => { - stateMap.getOrCreate(event.getType(), new Map()).set(event.getStateKey(), event); - }); - - return (eventType: string, stateKey?: string) => { - if (stateKey || stateKey === "") { - return stateMap.get(eventType)?.get(stateKey) || null; - } - return Array.from(stateMap.get(eventType)?.values() || []); - }; -}; - -const emitPromise = (e: EventEmitter, k: string | symbol) => new Promise(r => e.once(k, r)); - const testUserId = "@test:user"; const getUserIdForRoomId = jest.fn(); @@ -87,36 +69,13 @@ describe("SpaceStore", () => { const client = MatrixClientPeg.get(); let rooms = []; - - const mkRoom = (roomId: string) => { - const room = mkStubRoom(roomId, roomId, client); - room.currentState.getStateEvents.mockImplementation(mockStateEventImplementation([])); - rooms.push(room); - return room; - }; - - const mkSpace = (spaceId: string, children: string[] = []) => { - const space = mkRoom(spaceId); - space.isSpaceRoom.mockReturnValue(true); - space.currentState.getStateEvents.mockImplementation(mockStateEventImplementation(children.map(roomId => - mkEvent({ - event: true, - type: EventType.SpaceChild, - room: spaceId, - user: testUserId, - skey: roomId, - content: { via: [] }, - ts: Date.now(), - }), - ))); - return space; - }; - + const mkRoom = (roomId: string) => testUtils.mkRoom(client, roomId, rooms); + const mkSpace = (spaceId: string, children: string[] = []) => testUtils.mkSpace(client, spaceId, rooms, children); const viewRoom = roomId => defaultDispatcher.dispatch({ action: "view_room", room_id: roomId }, true); const run = async () => { client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId)); - await setupAsyncStoreWithClient(store, client); + await testUtils.setupAsyncStoreWithClient(store, client); jest.runAllTimers(); }; @@ -125,7 +84,7 @@ describe("SpaceStore", () => { client.getVisibleRooms.mockReturnValue(rooms = []); }); afterEach(async () => { - await resetAsyncStoreWithClient(store); + await testUtils.resetAsyncStoreWithClient(store); }); describe("static hierarchy resolution tests", () => { @@ -488,7 +447,7 @@ describe("SpaceStore", () => { await run(); expect(store.spacePanelSpaces).toStrictEqual([]); const space = mkSpace(space1); - const prom = emitPromise(store, UPDATE_TOP_LEVEL_SPACES); + const prom = testUtils.emitPromise(store, UPDATE_TOP_LEVEL_SPACES); emitter.emit("Room", space); await prom; expect(store.spacePanelSpaces).toStrictEqual([space]); @@ -501,7 +460,7 @@ describe("SpaceStore", () => { expect(store.spacePanelSpaces).toStrictEqual([space]); space.getMyMembership.mockReturnValue("leave"); - const prom = emitPromise(store, UPDATE_TOP_LEVEL_SPACES); + const prom = testUtils.emitPromise(store, UPDATE_TOP_LEVEL_SPACES); emitter.emit("Room.myMembership", space, "leave", "join"); await prom; expect(store.spacePanelSpaces).toStrictEqual([]); @@ -513,7 +472,7 @@ describe("SpaceStore", () => { expect(store.invitedSpaces).toStrictEqual([]); const space = mkSpace(space1); space.getMyMembership.mockReturnValue("invite"); - const prom = emitPromise(store, UPDATE_INVITED_SPACES); + const prom = testUtils.emitPromise(store, UPDATE_INVITED_SPACES); emitter.emit("Room", space); await prom; expect(store.spacePanelSpaces).toStrictEqual([]); @@ -528,7 +487,7 @@ describe("SpaceStore", () => { expect(store.spacePanelSpaces).toStrictEqual([]); expect(store.invitedSpaces).toStrictEqual([space]); space.getMyMembership.mockReturnValue("join"); - const prom = emitPromise(store, UPDATE_TOP_LEVEL_SPACES); + const prom = testUtils.emitPromise(store, UPDATE_TOP_LEVEL_SPACES); emitter.emit("Room.myMembership", space, "join", "invite"); await prom; expect(store.spacePanelSpaces).toStrictEqual([space]); @@ -543,7 +502,7 @@ describe("SpaceStore", () => { expect(store.spacePanelSpaces).toStrictEqual([]); expect(store.invitedSpaces).toStrictEqual([space]); space.getMyMembership.mockReturnValue("leave"); - const prom = emitPromise(store, UPDATE_INVITED_SPACES); + const prom = testUtils.emitPromise(store, UPDATE_INVITED_SPACES); emitter.emit("Room.myMembership", space, "leave", "invite"); await prom; expect(store.spacePanelSpaces).toStrictEqual([]); @@ -563,7 +522,7 @@ describe("SpaceStore", () => { const invite = mkRoom(invite1); invite.getMyMembership.mockReturnValue("invite"); - const prom = emitPromise(store, space1); + const prom = testUtils.emitPromise(store, space1); emitter.emit("Room", space); await prom; @@ -704,7 +663,8 @@ describe("SpaceStore", () => { mkSpace(space1, [room1, room2, room3]); mkSpace(space2, [room1, room2]); - client.getRoom(room2).currentState.getStateEvents.mockImplementation(mockStateEventImplementation([ + const cliRoom2 = client.getRoom(room2); + cliRoom2.currentState.getStateEvents.mockImplementation(testUtils.mockStateEventImplementation([ mkEvent({ event: true, type: EventType.SpaceParent, diff --git a/test/stores/room-list/SpaceWatcher-test.ts b/test/stores/room-list/SpaceWatcher-test.ts new file mode 100644 index 0000000000..c27088b643 --- /dev/null +++ b/test/stores/room-list/SpaceWatcher-test.ts @@ -0,0 +1,186 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +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 "../SpaceStore-setup"; // enable space lab +import "../../skinned-sdk"; // Must be first for skinning to work +import { SpaceWatcher } from "../../../src/stores/room-list/SpaceWatcher"; +import type { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore"; +import SettingsStore from "../../../src/settings/SettingsStore"; +import SpaceStore, { UPDATE_HOME_BEHAVIOUR } from "../../../src/stores/SpaceStore"; +import { stubClient } from "../../test-utils"; +import { SettingLevel } from "../../../src/settings/SettingLevel"; +import { setupAsyncStoreWithClient } from "../../utils/test-utils"; +import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; +import * as testUtils from "../../utils/test-utils"; +import { SpaceFilterCondition } from "../../../src/stores/room-list/filters/SpaceFilterCondition"; + +let filter: SpaceFilterCondition = null; + +const mockRoomListStore = { + addFilter: f => filter = f, + removeFilter: () => filter = null, +} as unknown as RoomListStoreClass; + +const space1Id = "!space1:server"; +const space2Id = "!space2:server"; + +describe("SpaceWatcher", () => { + stubClient(); + const store = SpaceStore.instance; + const client = MatrixClientPeg.get(); + + let rooms = []; + const mkSpace = (spaceId: string, children: string[] = []) => testUtils.mkSpace(client, spaceId, rooms, children); + + const setShowAllRooms = async (value: boolean) => { + if (store.allRoomsInHome === value) return; + await SettingsStore.setValue("feature_spaces.all_rooms", null, SettingLevel.DEVICE, value); + await testUtils.emitPromise(store, UPDATE_HOME_BEHAVIOUR); + }; + + let space1; + let space2; + + beforeEach(async () => { + filter = null; + store.removeAllListeners(); + await store.setActiveSpace(null); + client.getVisibleRooms.mockReturnValue(rooms = []); + + space1 = mkSpace(space1Id); + space2 = mkSpace(space2Id); + + client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId)); + await setupAsyncStoreWithClient(store, client); + }); + + it("initialises sanely with home behaviour", async () => { + await setShowAllRooms(false); + new SpaceWatcher(mockRoomListStore); + + expect(filter).toBeInstanceOf(SpaceFilterCondition); + }); + + it("initialises sanely with all behaviour", async () => { + await setShowAllRooms(true); + new SpaceWatcher(mockRoomListStore); + + expect(filter).toBeNull(); + }); + + it("sets space=null filter for all -> home transition", async () => { + await setShowAllRooms(true); + new SpaceWatcher(mockRoomListStore); + + await setShowAllRooms(false); + + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBeNull(); + }); + + it("sets filter correctly for all -> space transition", async () => { + await setShowAllRooms(true); + new SpaceWatcher(mockRoomListStore); + + await SpaceStore.instance.setActiveSpace(space1); + + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + }); + + it("removes filter for home -> all transition", async () => { + await setShowAllRooms(false); + new SpaceWatcher(mockRoomListStore); + + await setShowAllRooms(true); + + expect(filter).toBeNull(); + }); + + it("sets filter correctly for home -> space transition", async () => { + await setShowAllRooms(false); + new SpaceWatcher(mockRoomListStore); + + await SpaceStore.instance.setActiveSpace(space1); + + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + }); + + it("removes filter for space -> all transition", async () => { + await setShowAllRooms(true); + new SpaceWatcher(mockRoomListStore); + + await SpaceStore.instance.setActiveSpace(space1); + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + await SpaceStore.instance.setActiveSpace(null); + + expect(filter).toBeNull(); + }); + + it("updates filter correctly for space -> home transition", async () => { + await setShowAllRooms(false); + await SpaceStore.instance.setActiveSpace(space1); + + new SpaceWatcher(mockRoomListStore); + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + await SpaceStore.instance.setActiveSpace(null); + + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(null); + }); + + it("updates filter correctly for space -> space transition", async () => { + await setShowAllRooms(false); + await SpaceStore.instance.setActiveSpace(space1); + + new SpaceWatcher(mockRoomListStore); + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + await SpaceStore.instance.setActiveSpace(space2); + + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space2); + }); + + it("doesn't change filter when changing showAllRooms mode to true", async () => { + await setShowAllRooms(false); + await SpaceStore.instance.setActiveSpace(space1); + + new SpaceWatcher(mockRoomListStore); + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + await setShowAllRooms(true); + + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + }); + + it("doesn't change filter when changing showAllRooms mode to false", async () => { + await setShowAllRooms(true); + await SpaceStore.instance.setActiveSpace(space1); + + new SpaceWatcher(mockRoomListStore); + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + await setShowAllRooms(false); + + expect(filter).toBeInstanceOf(SpaceFilterCondition); + expect(filter["space"]).toBe(space1); + }); +}); diff --git a/test/utils/test-utils.ts b/test/utils/test-utils.ts index af92987a3d..8bc602fe35 100644 --- a/test/utils/test-utils.ts +++ b/test/utils/test-utils.ts @@ -15,7 +15,13 @@ limitations under the License. */ import { MatrixClient } from "matrix-js-sdk/src/client"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { EventType } from "matrix-js-sdk/src/@types/event"; + import { AsyncStoreWithClient } from "../../src/stores/AsyncStoreWithClient"; +import { mkEvent, mkStubRoom } from "../test-utils"; +import { EnhancedMap } from "../../src/utils/maps"; +import { EventEmitter } from "events"; // These methods make some use of some private methods on the AsyncStoreWithClient to simplify getting into a consistent // ready state without needing to wire up a dispatcher and pretend to be a js-sdk client. @@ -31,3 +37,48 @@ export const resetAsyncStoreWithClient = async (store: AsyncStoreWithClient // @ts-ignore await store.onNotReady(); }; + +export const mockStateEventImplementation = (events: MatrixEvent[]) => { + const stateMap = new EnhancedMap>(); + events.forEach(event => { + stateMap.getOrCreate(event.getType(), new Map()).set(event.getStateKey(), event); + }); + + return (eventType: string, stateKey?: string) => { + if (stateKey || stateKey === "") { + return stateMap.get(eventType)?.get(stateKey) || null; + } + return Array.from(stateMap.get(eventType)?.values() || []); + }; +}; + +export const mkRoom = (client: MatrixClient, roomId: string, rooms?: ReturnType[]) => { + const room = mkStubRoom(roomId, roomId, client); + room.currentState.getStateEvents.mockImplementation(mockStateEventImplementation([])); + rooms?.push(room); + return room; +}; + +export const mkSpace = ( + client: MatrixClient, + spaceId: string, + rooms?: ReturnType[], + children: string[] = [], +) => { + const space = mkRoom(client, spaceId, rooms); + space.isSpaceRoom.mockReturnValue(true); + space.currentState.getStateEvents.mockImplementation(mockStateEventImplementation(children.map(roomId => + mkEvent({ + event: true, + type: EventType.SpaceChild, + room: spaceId, + user: "@user:server", + skey: roomId, + content: { via: [] }, + ts: Date.now(), + }), + ))); + return space; +}; + +export const emitPromise = (e: EventEmitter, k: string | symbol) => new Promise(r => e.once(k, r)); From 0a9d3302baf96ed0c3b2d8497fcd44a65475c895 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 27 Jul 2021 21:11:47 +0100 Subject: [PATCH 106/445] Fix home vs all rooms requiring app reload and change default to `home` Consolidate ALL_ROOMS and HOME_SPACE storage Fix behaviour when recalled room is no longer part of the target space Improve tests --- src/components/views/spaces/SpacePanel.tsx | 13 +++- src/settings/Settings.tsx | 3 +- src/stores/SpaceStore.tsx | 75 ++++++++++++++-------- test/stores/SpaceStore-test.ts | 48 ++++++++++++-- 4 files changed, 100 insertions(+), 39 deletions(-) diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 1c4043f150..8223d84dbb 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -137,15 +137,22 @@ const InnerSpacePanel = React.memo(({ children, isPanelCo const [invites, spaces, activeSpace] = useSpaces(); const activeSpaces = activeSpace ? [activeSpace] : []; - const homeNotificationState = SpaceStore.spacesTweakAllRoomsEnabled - ? RoomNotificationStateStore.instance.globalState : SpaceStore.instance.getNotificationState(HOME_SPACE); + let homeTooltip: string; + let homeNotificationState: NotificationState; + if (SpaceStore.instance.allRoomsInHome) { + homeTooltip = _t("All rooms"); + homeNotificationState = RoomNotificationStateStore.instance.globalState; + } else { + homeTooltip = _t("Home"); + homeNotificationState = SpaceStore.instance.getNotificationState(HOME_SPACE); + } return
SpaceStore.instance.setActiveSpace(null)} selected={!activeSpace} - tooltip={SpaceStore.spacesTweakAllRoomsEnabled ? _t("All rooms") : _t("Home")} + tooltip={homeTooltip} notificationState={homeNotificationState} isNarrow={isPanelCollapsed} /> diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 5aa49df8a1..54153b3d75 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -187,8 +187,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { "feature_spaces.all_rooms": { displayName: _td("Show all rooms in Home"), supportedLevels: LEVELS_FEATURE, - default: true, - controller: new ReloadOnChangeController(), + default: false, }, "feature_dnd": { isFeature: true, diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index d064b01257..42ecc25651 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -37,9 +37,8 @@ import { EnhancedMap, mapDiff } from "../utils/maps"; import { setHasDiff } from "../utils/sets"; import RoomViewStore from "./RoomViewStore"; import { Action } from "../dispatcher/actions"; -import { arrayHasDiff } from "../utils/arrays"; +import { arrayHasDiff, arrayHasOrderChange } from "../utils/arrays"; import { objectDiff } from "../utils/objects"; -import { arrayHasOrderChange } from "../utils/arrays"; import { reorderLexicographically } from "../utils/stringOrderField"; import { TAG_ORDER } from "../components/views/rooms/RoomList"; import { shouldShowSpaceSettings } from "../utils/space"; @@ -48,6 +47,7 @@ import { _t } from "../languageHandler"; import GenericToast from "../components/views/toasts/GenericToast"; import Modal from "../Modal"; import InfoDialog from "../components/views/dialogs/InfoDialog"; +import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload"; type SpaceKey = string | symbol; @@ -61,6 +61,7 @@ export const SUGGESTED_ROOMS = Symbol("suggested-rooms"); export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces"); export const UPDATE_INVITED_SPACES = Symbol("invited-spaces"); export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); +export const UPDATE_HOME_BEHAVIOUR = Symbol("home-behaviour"); // Space Room ID/HOME_SPACE will be emitted when a Space's children change export interface ISuggestedRoom extends ISpaceSummaryRoom { @@ -69,12 +70,10 @@ export interface ISuggestedRoom extends ISpaceSummaryRoom { const MAX_SUGGESTED_ROOMS = 20; -// All of these settings cause the page to reload and can be costly if read frequently, so read them here only +// This setting causes the page to reload and can be costly if read frequently, so read it here only const spacesEnabled = SettingsStore.getValue("feature_spaces"); -const spacesTweakAllRoomsEnabled = SettingsStore.getValue("feature_spaces.all_rooms"); -const homeSpaceKey = spacesTweakAllRoomsEnabled ? "ALL_ROOMS" : "HOME_SPACE"; -const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || homeSpaceKey}`; +const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "HOME_SPACE"}`; const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms] return arr.reduce((result, room: Room) => { @@ -102,10 +101,6 @@ const getRoomFn: FetchRoomFn = (room: Room) => { }; export class SpaceStoreClass extends AsyncStoreWithClient { - constructor() { - super(defaultDispatcher, {}); - } - // The spaces representing the roots of the various tree-like hierarchies private rootSpaces: Room[] = []; // The list of rooms not present in any currently joined spaces @@ -122,6 +117,13 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private _invitedSpaces = new Set(); private spaceOrderLocalEchoMap = new Map(); private _restrictedJoinRuleSupport?: IRoomCapability; + private _allRoomsInHome: boolean = SettingsStore.getValue("feature_spaces.all_rooms"); + + constructor() { + super(defaultDispatcher, {}); + + SettingsStore.monitorSetting("feature_spaces.all_rooms", null); + } public get invitedSpaces(): Room[] { return Array.from(this._invitedSpaces); @@ -139,13 +141,16 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return this._suggestedRooms; } + public get allRoomsInHome(): boolean { + return this._allRoomsInHome; + } + public async setActiveRoomInSpace(space: Room | null): Promise { if (space && !space.isSpaceRoom()) return; if (space !== this.activeSpace) await this.setActiveSpace(space); if (space) { - const notificationState = this.getNotificationState(space.roomId); - const roomId = notificationState.getFirstRoomWithNotifications(); + const roomId = this.getNotificationState(space.roomId).getFirstRoomWithNotifications(); defaultDispatcher.dispatch({ action: "view_room", room_id: roomId, @@ -200,7 +205,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // else if the last viewed room in this space is joined then view that // else view space home or home depending on what is being clicked on if (space?.getMyMembership() !== "invite" && - this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join" + this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join" && + this.getSpaceFilteredRoomIds(space).has(roomId) ) { defaultDispatcher.dispatch({ action: "view_room", @@ -377,7 +383,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } public getSpaceFilteredRoomIds = (space: Room | null): Set => { - if (!space && spacesTweakAllRoomsEnabled) { + if (!space && this.allRoomsInHome) { return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId)); } return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set(); @@ -474,7 +480,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }; private showInHomeSpace = (room: Room) => { - if (spacesTweakAllRoomsEnabled) return true; + if (this.allRoomsInHome) return true; if (room.isSpaceRoom()) return false; return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space || DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space @@ -506,7 +512,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const oldFilteredRooms = this.spaceFilteredRooms; this.spaceFilteredRooms = new Map(); - if (!spacesTweakAllRoomsEnabled) { + if (!this.allRoomsInHome) { // put all room invites in the Home Space const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite"); this.spaceFilteredRooms.set(HOME_SPACE, new Set(invites.map(room => room.roomId))); @@ -562,8 +568,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }); this.spaceFilteredRooms.forEach((roomIds, s) => { + if (this.allRoomsInHome && s === HOME_SPACE) return; // we'll be using the global notification state, skip + // Update NotificationStates - this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => { + this.getNotificationState(s).setRooms(visibleRooms.filter(room => { if (!roomIds.has(room.roomId)) return false; if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { @@ -663,7 +671,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // TODO confirm this after implementing parenting behaviour if (room.isSpaceRoom()) { this.onSpaceUpdate(); - } else if (!spacesTweakAllRoomsEnabled) { + } else if (!this.allRoomsInHome) { this.onRoomUpdate(room); } this.emit(room.roomId); @@ -687,7 +695,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (order !== lastOrder) { this.notifyIfOrderChanged(); } - } else if (ev.getType() === EventType.Tag && !spacesTweakAllRoomsEnabled) { + } else if (ev.getType() === EventType.Tag && !this.allRoomsInHome) { // If the room was in favourites and now isn't or the opposite then update its position in the trees const oldTags = lastEv?.getContent()?.tags || {}; const newTags = ev.getContent()?.tags || {}; @@ -698,7 +706,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }; private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => { - if (ev.getType() === EventType.Direct) { + if (!this.allRoomsInHome && ev.getType() === EventType.Direct) { const lastContent = lastEvent.getContent(); const content = ev.getContent(); @@ -733,9 +741,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.matrixClient.removeListener("Room.myMembership", this.onRoom); this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData); this.matrixClient.removeListener("RoomState.events", this.onRoomState); - if (!spacesTweakAllRoomsEnabled) { - this.matrixClient.removeListener("accountData", this.onAccountData); - } + this.matrixClient.removeListener("accountData", this.onAccountData); } await this.reset(); } @@ -746,9 +752,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.matrixClient.on("Room.myMembership", this.onRoom); this.matrixClient.on("Room.accountData", this.onRoomAccountData); this.matrixClient.on("RoomState.events", this.onRoomState); - if (!spacesTweakAllRoomsEnabled) { - this.matrixClient.on("accountData", this.onAccountData); - } + this.matrixClient.on("accountData", this.onAccountData); this.matrixClient.getCapabilities().then(capabilities => { this._restrictedJoinRuleSupport = capabilities @@ -779,7 +783,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // as it will cause you to end up in the wrong room this.setActiveSpace(room, false); } else if ( - (!spacesTweakAllRoomsEnabled || this.activeSpace) && + (!this.allRoomsInHome || this.activeSpace) && !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId) ) { this.switchToRelatedSpace(roomId); @@ -791,17 +795,33 @@ export class SpaceStoreClass extends AsyncStoreWithClient { window.localStorage.setItem(getSpaceContextKey(this.activeSpace), payload.room_id); break; } + case "after_leave_room": if (this._activeSpace && payload.room_id === this._activeSpace.roomId) { this.setActiveSpace(null, false); } break; + case Action.SwitchSpace: if (payload.num === 0) { this.setActiveSpace(null); } else if (this.spacePanelSpaces.length >= payload.num) { this.setActiveSpace(this.spacePanelSpaces[payload.num - 1]); } + break; + + case Action.SettingUpdated: { + const settingUpdatedPayload = payload as SettingUpdatedPayload; + if (settingUpdatedPayload.settingName === "feature_spaces.all_rooms") { + const newValue = SettingsStore.getValue("feature_spaces.all_rooms"); + if (this.allRoomsInHome !== newValue) { + this._allRoomsInHome = newValue; + this.emit(UPDATE_HOME_BEHAVIOUR, this.allRoomsInHome); + this.rebuild(); // rebuild everything + } + } + break; + } } } @@ -872,7 +892,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { export default class SpaceStore { public static spacesEnabled = spacesEnabled; - public static spacesTweakAllRoomsEnabled = spacesTweakAllRoomsEnabled; private static internalInstance = new SpaceStoreClass(); diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 8b809be95d..09005e3d84 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -21,6 +21,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import "./SpaceStore-setup"; // enable space lab import "../skinned-sdk"; // Must be first for skinning to work import SpaceStore, { + UPDATE_HOME_BEHAVIOUR, UPDATE_INVITED_SPACES, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES, @@ -30,6 +31,8 @@ import { mkEvent, stubClient } from "../test-utils"; import DMRoomMap from "../../src/utils/DMRoomMap"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; import defaultDispatcher from "../../src/dispatcher/dispatcher"; +import SettingsStore from "../../src/settings/SettingsStore"; +import { SettingLevel } from "../../src/settings/SettingLevel"; jest.useFakeTimers(); @@ -79,8 +82,16 @@ describe("SpaceStore", () => { jest.runAllTimers(); }; + const setShowAllRooms = async (value: boolean) => { + if (store.allRoomsInHome === value) return; + const emitProm = testUtils.emitPromise(store, UPDATE_HOME_BEHAVIOUR); + await SettingsStore.setValue("feature_spaces.all_rooms", null, SettingLevel.DEVICE, value); + jest.runAllTimers(); // run async dispatch + await emitProm; + }; + beforeEach(() => { - jest.runAllTimers(); + jest.runAllTimers(); // run async dispatch client.getVisibleRooms.mockReturnValue(rooms = []); }); afterEach(async () => { @@ -346,10 +357,16 @@ describe("SpaceStore", () => { expect(store.getSpaceFilteredRoomIds(null).has(invite2)).toBeTruthy(); }); - it("home space does contain rooms/low priority even if they are also shown in a space", () => { + it("all rooms space does contain rooms/low priority even if they are also shown in a space", async () => { + await setShowAllRooms(true); expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeTruthy(); }); + it("home space doesn't contain rooms/low priority if they are also shown in a space", async () => { + await setShowAllRooms(false); + expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeFalsy(); + }); + it("space contains child rooms", () => { const space = client.getRoom(space1); expect(store.getSpaceFilteredRoomIds(space).has(fav1)).toBeTruthy(); @@ -592,20 +609,30 @@ describe("SpaceStore", () => { }); describe("context switching tests", () => { - const fn = jest.spyOn(defaultDispatcher, "dispatch"); + let dispatcherRef; + let currentRoom = null; beforeEach(async () => { [room1, room2, orphan1].forEach(mkRoom); mkSpace(space1, [room1, room2]); mkSpace(space2, [room2]); await run(); + + dispatcherRef = defaultDispatcher.register(payload => { + if (payload.action === "view_room" || payload.action === "view_home_page") { + currentRoom = payload.room_id || null; + } + }); }); afterEach(() => { - fn.mockClear(); localStorage.clear(); + defaultDispatcher.unregister(dispatcherRef); }); - const getCurrentRoom = () => fn.mock.calls.reverse().find(([p]) => p.action === "view_room")?.[0].room_id; + const getCurrentRoom = () => { + jest.runAllTimers(); + return currentRoom; + }; it("last viewed room in target space is the current viewed and in both spaces", async () => { await store.setActiveSpace(client.getRoom(space1)); @@ -642,6 +669,14 @@ describe("SpaceStore", () => { expect(getCurrentRoom()).toBe(space2); }); + it("last viewed room is target space is no longer in that space", async () => { + await store.setActiveSpace(client.getRoom(space1)); + viewRoom(room1); + localStorage.setItem(`mx_space_context_${space2}`, room1); + await store.setActiveSpace(client.getRoom(space2)); + expect(getCurrentRoom()).toBe(space2); // Space home instead of room1 + }); + it("no last viewed room in target space", async () => { await store.setActiveSpace(client.getRoom(space1)); viewRoom(room1); @@ -653,7 +688,7 @@ describe("SpaceStore", () => { await store.setActiveSpace(client.getRoom(space1)); viewRoom(room1); await store.setActiveSpace(null); - expect(fn.mock.calls[fn.mock.calls.length - 1][0]).toStrictEqual({ action: "view_home_page" }); + expect(getCurrentRoom()).toBeNull(); // Home }); }); @@ -707,6 +742,7 @@ describe("SpaceStore", () => { }); it("when switching rooms in the all rooms home space don't switch to related space", async () => { + await setShowAllRooms(true); viewRoom(room2); await store.setActiveSpace(null, false); viewRoom(room1); From 776435f6208e48d9845cbdcbaafc2b2d8bdf1d64 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 27 Jul 2021 21:17:24 +0100 Subject: [PATCH 107/445] Switch all-rooms toggle for spaces to non-feature settings key --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.tsx | 12 ++++++------ src/stores/SpaceStore.tsx | 8 ++++---- test/stores/SpaceStore-setup.ts | 1 - test/stores/SpaceStore-test.ts | 2 +- test/stores/room-list/SpaceWatcher-test.ts | 2 +- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index de432d6177..2cd2a096ad 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -797,7 +797,6 @@ "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.", "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.", "Your feedback will help make spaces better. The more detail you can go into, the better.": "Your feedback will help make spaces better. The more detail you can go into, the better.", - "Show all rooms in Home": "Show all rooms in Home", "Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode", "Send and receive voice messages": "Send and receive voice messages", "Render LaTeX maths in messages": "Render LaTeX maths in messages", @@ -868,6 +867,7 @@ "Manually verify all remote sessions": "Manually verify all remote sessions", "IRC display name width": "IRC display name width", "Show chat effects (animations when receiving e.g. confetti)": "Show chat effects (animations when receiving e.g. confetti)", + "Show all rooms in Home": "Show all rooms in Home", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading logs": "Uploading logs", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 54153b3d75..dfd6f1eec9 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -180,15 +180,10 @@ export const SETTINGS: {[setting: string]: ISetting} = { "The more detail you can go into, the better."), feedbackLabel: "spaces-feedback", extraSettings: [ - "feature_spaces.all_rooms", + "Spaces.all_rooms_in_home", ], }, }, - "feature_spaces.all_rooms": { - displayName: _td("Show all rooms in Home"), - supportedLevels: LEVELS_FEATURE, - default: false, - }, "feature_dnd": { isFeature: true, displayName: _td("Show options to enable 'Do not disturb' mode"), @@ -756,6 +751,11 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_ACCOUNT_SETTINGS, default: null, }, + "Spaces.all_rooms_in_home": { + displayName: _td("Show all rooms in Home"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, [UIFeature.RoomHistorySettings]: { supportedLevels: LEVELS_UI_FEATURE, default: true, diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 42ecc25651..3fc4a1bc6d 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -117,12 +117,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private _invitedSpaces = new Set(); private spaceOrderLocalEchoMap = new Map(); private _restrictedJoinRuleSupport?: IRoomCapability; - private _allRoomsInHome: boolean = SettingsStore.getValue("feature_spaces.all_rooms"); + private _allRoomsInHome: boolean = SettingsStore.getValue("Spaces.all_rooms_in_home"); constructor() { super(defaultDispatcher, {}); - SettingsStore.monitorSetting("feature_spaces.all_rooms", null); + SettingsStore.monitorSetting("Spaces.all_rooms_in_home", null); } public get invitedSpaces(): Room[] { @@ -812,8 +812,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { case Action.SettingUpdated: { const settingUpdatedPayload = payload as SettingUpdatedPayload; - if (settingUpdatedPayload.settingName === "feature_spaces.all_rooms") { - const newValue = SettingsStore.getValue("feature_spaces.all_rooms"); + if (settingUpdatedPayload.settingName === "Spaces.all_rooms_in_home") { + const newValue = SettingsStore.getValue("Spaces.all_rooms_in_home"); if (this.allRoomsInHome !== newValue) { this._allRoomsInHome = newValue; this.emit(UPDATE_HOME_BEHAVIOUR, this.allRoomsInHome); diff --git a/test/stores/SpaceStore-setup.ts b/test/stores/SpaceStore-setup.ts index b9b865e89a..78418d45cc 100644 --- a/test/stores/SpaceStore-setup.ts +++ b/test/stores/SpaceStore-setup.ts @@ -18,4 +18,3 @@ limitations under the License. // SpaceStore reads the SettingsStore which needs the localStorage values set at init time. localStorage.setItem("mx_labs_feature_feature_spaces", "true"); -localStorage.setItem("mx_labs_feature_feature_spaces.all_rooms", "true"); diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 09005e3d84..eb3d5f0b97 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -85,7 +85,7 @@ describe("SpaceStore", () => { const setShowAllRooms = async (value: boolean) => { if (store.allRoomsInHome === value) return; const emitProm = testUtils.emitPromise(store, UPDATE_HOME_BEHAVIOUR); - await SettingsStore.setValue("feature_spaces.all_rooms", null, SettingLevel.DEVICE, value); + await SettingsStore.setValue("Spaces.all_rooms_in_home", null, SettingLevel.DEVICE, value); jest.runAllTimers(); // run async dispatch await emitProm; }; diff --git a/test/stores/room-list/SpaceWatcher-test.ts b/test/stores/room-list/SpaceWatcher-test.ts index c27088b643..c6254349b5 100644 --- a/test/stores/room-list/SpaceWatcher-test.ts +++ b/test/stores/room-list/SpaceWatcher-test.ts @@ -47,7 +47,7 @@ describe("SpaceWatcher", () => { const setShowAllRooms = async (value: boolean) => { if (store.allRoomsInHome === value) return; - await SettingsStore.setValue("feature_spaces.all_rooms", null, SettingLevel.DEVICE, value); + await SettingsStore.setValue("Spaces.all_rooms_in_home", null, SettingLevel.DEVICE, value); await testUtils.emitPromise(store, UPDATE_HOME_BEHAVIOUR); }; From caefefc2c22704fc4f678a680592d885880193d0 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 27 Jul 2021 17:22:49 -0400 Subject: [PATCH 108/445] Add regional indicators to emoji picker Signed-off-by: Robin Townsend --- src/emoji.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/emoji.ts b/src/emoji.ts index 321eae63f6..1445f737d6 100644 --- a/src/emoji.ts +++ b/src/emoji.ts @@ -35,6 +35,9 @@ export const EMOTICON_TO_EMOJI = new Map(); export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(stripVariation(unicode)); +const isRegionalIndicator = (x: string) => + Array.from(x).length === 1 && x >= '\u{1f1e6}' && x <= '\u{1f1ff}'; + const EMOJIBASE_GROUP_ID_TO_CATEGORY = [ "people", // smileys "people", // actually people @@ -72,7 +75,11 @@ export const EMOJI: IEmoji[] = EMOJIBASE.map((emojiData: Omit Date: Tue, 27 Jul 2021 17:35:34 -0400 Subject: [PATCH 109/445] Add more types Signed-off-by: Robin Townsend --- src/emoji.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emoji.ts b/src/emoji.ts index 1445f737d6..e871e0bb58 100644 --- a/src/emoji.ts +++ b/src/emoji.ts @@ -35,7 +35,7 @@ export const EMOTICON_TO_EMOJI = new Map(); export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(stripVariation(unicode)); -const isRegionalIndicator = (x: string) => +const isRegionalIndicator = (x: string): boolean => Array.from(x).length === 1 && x >= '\u{1f1e6}' && x <= '\u{1f1ff}'; const EMOJIBASE_GROUP_ID_TO_CATEGORY = [ From f8106ef39b45212c56f20cbe19fc9ba5daa724a7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Jul 2021 08:39:04 +0100 Subject: [PATCH 110/445] Fix CreateRoomDialog exploding when making public room outside of a space --- src/components/views/dialogs/CreateRoomDialog.tsx | 8 +++++++- src/i18n/strings/en_EN.json | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx index a06f508908..572212a96c 100644 --- a/src/components/views/dialogs/CreateRoomDialog.tsx +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -250,7 +250,7 @@ export default class CreateRoomDialog extends React.Component {   { _t("You can change this at any time from room settings.") }

; - } else if (this.state.joinRule === JoinRule.Public) { + } else if (this.state.joinRule === JoinRule.Public && this.props.parentSpace) { publicPrivateLabel =

{ _t( "Anyone will be able to find and join this room, not just members of .", {}, { @@ -260,6 +260,12 @@ export default class CreateRoomDialog extends React.Component {   { _t("You can change this at any time from room settings.") }

; + } else if (this.state.joinRule === JoinRule.Public) { + publicPrivateLabel =

+ { _t("Anyone will be able to find and join this room.") } +   + { _t("You can change this at any time from room settings.") } +

; } else if (this.state.joinRule === JoinRule.Invite) { publicPrivateLabel =

{ _t( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 102a481f52..1093f478bb 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2195,6 +2195,7 @@ "Everyone in will be able to find and join this room.": "Everyone in will be able to find and join this room.", "You can change this at any time from room settings.": "You can change this at any time from room settings.", "Anyone will be able to find and join this room, not just members of .": "Anyone will be able to find and join this room, not just members of .", + "Anyone will be able to find and join this room.": "Anyone will be able to find and join this room.", "Only people invited will be able to find and join this room.": "Only people invited will be able to find and join this room.", "You can’t disable this later. Bridges & most bots won’t work yet.": "You can’t disable this later. Bridges & most bots won’t work yet.", "Your server requires encryption to be enabled in private rooms.": "Your server requires encryption to be enabled in private rooms.", From 4f0e3d0069d2efc01cd9b0c3fab349bbacdaf0cf Mon Sep 17 00:00:00 2001 From: libexus Date: Wed, 28 Jul 2021 05:49:21 +0000 Subject: [PATCH 111/445] Translated using Weblate (German) Currently translated at 97.5% (3009 of 3085 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 4a2b6521b2..fc347455e0 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -3510,5 +3510,7 @@ "An unknown error occurred": "Ein unbekannter Fehler ist aufgetreten", "Message bubbles": "Nachrichtenblasen", "New layout switcher (with message bubbles)": "Layout ändern erlauben (mit Nachrichtenblasen)", - "New in the Spaces beta": "Neues in der Spaces Beta" + "New in the Spaces beta": "Neues in der Spaces Beta", + "To help space members find and join a private room, go to that room's Security & Privacy settings.": "Um Mitgliedern beim Finden privater Räume zu helfen, öffne die Sicherheitseinstellungen des Raumes.", + "Help space members find private rooms": "Hilf Mitgliedern, private Räume zu finden" } From a34a1f5536209a3b27f30362367639837d059a1c Mon Sep 17 00:00:00 2001 From: Szimszon Date: Tue, 27 Jul 2021 22:10:57 +0000 Subject: [PATCH 112/445] Translated using Weblate (Hungarian) Currently translated at 99.8% (3080 of 3085 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 91 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 55adba87b2..a04811d2c0 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -3490,15 +3490,96 @@ "Use Ctrl + F to search timeline": "Ctrl + F az idővonalon való kereséshez", "User %(userId)s is already invited to the room": "%(userId)s felhasználó már kapott meghívót a szobába", "Integration manager": "Integrációs Menedzser", - "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "A %(brand)sod nem használhat ehhez Integrációs Menedzsert. Kérlek vedd fel a kapcsolatot az adminisztrátorral.", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "A %(brand)s nem használhat Integrációs Menedzsert. Kérem vegye fel a kapcsolatot az adminisztrátorral.", "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg a(z) %(widgetDomain)s oldallal és az Integrációkezelővel.", "Identity server is": "Azonosítási szerver", - "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrációs Menedzser megkapja a konfigurációt, módosíthat kisalkalmazásokat, szobához meghívót küldhet és a hozzáférési szintet beállíthatja helyetted.", - "Use an integration manager to manage bots, widgets, and sticker packs.": "Használj Integrációs Menedzsert a botok, kisalkalmazások és matrica csomagok kezeléséhez.", - "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Használj Integrációs Menedzsert (%(serverName)s) a botok, kisalkalmazások és matrica csomagok kezeléséhez.", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrációs Menedzser megkapja a konfigurációt, módosíthat kisalkalmazásokat, szobához meghívót küldhet és a hozzáférési szintet állíthat be az ön nevében.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Használjon Integrációs Menedzsert a botok, kisalkalmazások és matrica csomagok kezeléséhez.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Használjon Integrációs Menedzsert (%(serverName)s) a botok, kisalkalmazások és matrica csomagok kezeléséhez.", "Identity server": "Azonosító szerver", "Identity server (%(server)s)": "Azonosítási kiszolgáló (%(server)s)", "Could not connect to identity server": "Az Azonosítási Szerverhez nem lehet csatlakozni", "Not a valid identity server (status code %(code)s)": "Az Azonosítási Szerver nem érvényes (státusz kód: %(code)s)", - "Identity server URL must be HTTPS": "Az Azonosítási Szerver URL-jének HTTPS-nek kell lennie" + "Identity server URL must be HTTPS": "Az Azonosítási Szerver URL-jének HTTPS-nek kell lennie", + "Unable to copy a link to the room to the clipboard.": "Ennek a szobának a hivatkozását nem sikerül a vágólapra másolni.", + "Unable to copy room link": "A szoba hivatkozása nem másolható", + "Error downloading audio": "Hiba a hang letöltésekor", + "Please note upgrading will make a new version of the room. All current messages will stay in this archived room.": "Vegye figyelembe, hogy a fejlesztés a szoba új verzióját hozza létre Minden jelenlegi üzenet itt marad az archivált szobában.", + "Automatically invite members from this room to the new one": "Tagok automatikus meghívása ebből a szobából az újba", + "Other spaces or rooms you might not know": "Más terek vagy szobák melyről lehet, hogy nem tud", + "Spaces you know that contain this room": "Terek melyről tudja, hogy ezt a szobát tartalmazzák", + "Search spaces": "Terek keresése", + "Decide which spaces can access this room. If a space is selected, its members can find and join .": "Döntse el melyik terek férhetnek hozzá ehhez a szobához. Ha a tér ki van választva a tagsága megtalálhatja és beléphet ebbe a szobába: .", + "Select spaces": "Terek kiválasztása", + "You're removing all spaces. Access will default to invite only": "Minden teret töröl. A hozzáférés alapállapota „csak meghívóval” lesz", + "User Directory": "Felhasználójegyzék", + "Room visibility": "Szoba láthatóság", + "Visible to space members": "Tér tagság számára látható", + "Public room": "Nyilvános szoba", + "Private room (invite only)": "Privát szoba (csak meghívóval)", + "Create a room": "Szoba létrehozása", + "Only people invited will be able to find and join this room.": "Csak a meghívott emberek fogják megtalálni és tudnak belépni a szobába.", + "Anyone will be able to find and join this room, not just members of .": "Bárki megtalálhatja és beléphet a szobába, nem csak tér tagsága.", + "You can change this at any time from room settings.": "A szoba beállításokban ezt bármikor megváltoztathatja.", + "Everyone in will be able to find and join this room.": " téren bárki megtalálhatja és beléphet a szobába.", + "Share content": "Tartalom megosztása", + "Application window": "Alkalmazás ablak", + "Share entire screen": "A teljes képernyő megosztása", + "Image": "Kép", + "Sticker": "Matrica", + "Downloading": "Letöltés", + "The call is in an unknown state!": "A hívás ismeretlen állapotban van!", + "You missed this call": "Elmulasztotta a hívást", + "This call has failed": "Hívás nem sikerült", + "Unknown failure: %(reason)s)": "Ismeretlen hiba: %(reason)s)", + "An unknown error occurred": "Ismeretlen hiba történt", + "Their device couldn't start the camera or microphone": "A másik fél eszköze nem képes használni a kamerát vagy a mikrofont", + "Connection failed": "Kapcsolódás sikertelen", + "Could not connect media": "Média kapcsolat nem hozható létre", + "They didn't pick up": "Nem vették fel", + "This call has ended": "Hívás befejeződött", + "Call again": "Hívás újra", + "Call back": "Visszahívás", + "They declined this call": "Elutasították a hívást", + "You declined this call": "Elutasította ezt a hívást", + "Connected": "Kapcsolódva", + "The voice message failed to upload.": "A hangüzenet feltöltése sikertelen.", + "Copy Room Link": "Szoba hivatkozás másolása", + "You can now share your screen by pressing the \"screen share\" button during a call. You can even do this in audio calls if both sides support it!": "Már megoszthatja a képernyőjét hívás közben a \"képernyő megosztás\" gombra kattintva. Még hanghívás közben is működik ha mind a két fél támogatja.", + "Screen sharing is here!": "Képernyőmegosztás itt van!", + "Access": "Hozzáférés", + "People with supported clients will be able to join the room without having a registered account.": "Emberek támogatott kliensekkel, még regisztrált fiók nélkül is, beléphetnek a szobába.", + "Decide who can join %(roomName)s.": "Döntse el ki léphet be ide: %(roomName)s.", + "Space members": "Tér tagság", + "Anyone in a space can find and join. You can select multiple spaces.": "A téren bárki megtalálhatja és beléphet. Több teret is kiválaszthat egyidejűleg.", + "Anyone in %(spaceName)s can find and join. You can select other spaces too.": "%(spaceName)s téren bárki megtalálhatja és beléphet. Kiválaszthat más tereket is.", + "Spaces with access": "Terek hozzáféréssel", + "Anyone in a space can find and join. Edit which spaces can access here.": "A téren bárki megtalálhatja és beléphet. Szerkessze meg melyik tér férhet hozzá ehhez.", + "Currently, %(count)s spaces have access|other": "Jelenleg %(count)s tér rendelkezik hozzáféréssel", + "& %(count)s more|other": "és még %(count)s", + "Upgrade required": "Fejlesztés szükséges", + "Anyone can find and join.": "Bárki megtalálhatja és beléphet.", + "Only invited people can join.": "Csak a meghívott emberek léphetnek be.", + "Private (invite only)": "Privát (csak meghívóval)", + "This upgrade will allow members of selected spaces access to this room without an invite.": "Ehhez a szobához ez a fejlesztés hozzáférést ad a kijelölt térhez tartozó tagoknak meghívó nélkül is.", + "Message bubbles": "Üzenet buborékok", + "IRC": "IRC", + "There was an error loading your notification settings.": "Az értesítés beállítások betöltésénél hiba történt.", + "Mentions & keywords": "Megemlítések és kulcsszavak", + "Global": "Globális", + "New keyword": "Új kulcsszó", + "Keyword": "Kulcsszó", + "Enable email notifications for %(email)s": "E-mail értesítés engedélyezése ehhez az e-mail címhez: %(email)s", + "Enable for this account": "Engedélyezés ennél a fióknál", + "An error occurred whilst saving your notification preferences.": "Hiba történt az értesítési beállításai mentése közben.", + "Error saving notification preferences": "Hiba az értesítési beállítások mentésekor", + "Messages containing keywords": "Az üzenetek kulcsszavakat tartalmaznak", + "Your camera is still enabled": "Az ön kamerája még be van kapcsolva", + "Your camera is turned off": "Az ön kamerája ki van kapcsolva", + "%(sharerName)s is presenting": "%(sharerName)s tartja a bemutatót", + "You are presenting": "Ön tartja a bemutatót", + "New layout switcher (with message bubbles)": "Új kinézet váltó (üzenet buborékokkal)", + "New in the Spaces beta": "Újdonság a béta Terekben", + "Transfer Failed": "Átadás sikertelen", + "Unable to transfer call": "A hívás átadása nem lehetséges" } From 8f48044b65b3890ab51a55a39bbb95765dcfb561 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 28 Jul 2021 01:57:48 +0000 Subject: [PATCH 113/445] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3085 of 3085 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 74234746f6..b8d0d3f1c7 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -3583,5 +3583,18 @@ "To help space members find and join a private room, go to that room's Security & Privacy settings.": "要協助空間成員尋找並加入私人聊天室,請到該聊天室的「安全與隱私」設定。", "Help space members find private rooms": "協助空間成員尋找私人聊天室", "Help people in spaces to find and join private rooms": "協助空間中的夥伴尋找並加入私人聊天室", - "New in the Spaces beta": "Spaces 測試版的新功能" + "New in the Spaces beta": "Spaces 測試版的新功能", + "Share content": "分享內容", + "Application window": "應用程式視窗", + "Share entire screen": "分享整個螢幕", + "They didn't pick up": "他們未接聽", + "Call again": "重撥", + "They declined this call": "他們回絕了此通話", + "You declined this call": "您回絕了此通話", + "You can now share your screen by pressing the \"screen share\" button during a call. You can even do this in audio calls if both sides support it!": "您現在可以透過在通話中按下「畫面分享」按鈕來分享您的畫面了。如果雙方都支援,您甚至可以在音訊通話中使用此功能!", + "Screen sharing is here!": "畫面分享在此!", + "Your camera is still enabled": "您的攝影機仍為啟用狀態", + "Your camera is turned off": "您的攝影機已關閉", + "You are presenting": "您正在出席", + "%(sharerName)s is presenting": "%(sharerName)s 正在出席" } From 58f607d99cd617b4ef93c0fb3aed70cc65afacc1 Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Tue, 27 Jul 2021 14:43:04 +0000 Subject: [PATCH 114/445] Translated using Weblate (Czech) Currently translated at 100.0% (3085 of 3085 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ --- src/i18n/strings/cs.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 940ab1a79a..8b984c5652 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -3502,5 +3502,14 @@ "They didn't pick up": "Nezvedli to", "Call again": "Volat znova", "They declined this call": "Odmítli tento hovor", - "You declined this call": "Odmítli jste tento hovor" + "You declined this call": "Odmítli jste tento hovor", + "Share content": "Sdílet obsah", + "Application window": "Okno aplikace", + "Share entire screen": "Sdílet celou obrazovku", + "You can now share your screen by pressing the \"screen share\" button during a call. You can even do this in audio calls if both sides support it!": "Nyní můžete sdílet obrazovku stisknutím tlačítka \"sdílení obrazovky\" během hovoru. Můžete tak učinit i při zvukových hovorech, pokud to obě strany podporují!", + "Screen sharing is here!": "Sdílení obrazovky je tu!", + "Your camera is still enabled": "Vaše kamera je stále zapnutá", + "Your camera is turned off": "Vaše kamera je vypnutá", + "%(sharerName)s is presenting": "%(sharerName)s prezentuje", + "You are presenting": "Prezentujete" } From 4d8f4c8d158636f4f6d9f331d2956667f16b3fe2 Mon Sep 17 00:00:00 2001 From: XoseM Date: Wed, 28 Jul 2021 04:53:58 +0000 Subject: [PATCH 115/445] Translated using Weblate (Galician) Currently translated at 98.4% (3036 of 3085 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/ --- src/i18n/strings/gl.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 856a783497..3a3271c4b7 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3540,5 +3540,9 @@ "IRC": "IRC", "New layout switcher (with message bubbles)": "Nova disposición do control (con burbullas con mensaxes)", "Image": "Imaxe", - "Sticker": "Adhesivo" + "Sticker": "Adhesivo", + "To help space members find and join a private room, go to that room's Security & Privacy settings.": "Axudarlle aos membros do espazo a que atopen e se unan a salas privadas, vaite aos axustes de Seguridade e Privacidade desa sala.", + "Help space members find private rooms": "Axudarlle aos membros do espazo a que atopen salas privadas", + "Help people in spaces to find and join private rooms": "Axudarlle ás persoas en espazos que atopen e se unan a salas privadas", + "New in the Spaces beta": "Novo na beta de Espazos" } From 1d81bdc6f9a676d075e6ad83b055b4ee43080a86 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 09:37:08 +0100 Subject: [PATCH 116/445] Interface changes and anonymity fixes --- src/Lifecycle.ts | 10 +- src/PosthogAnalytics.ts | 118 +++++++++--------- src/components/structures/MatrixChat.tsx | 13 +- .../tabs/user/SecurityUserSettingsTab.js | 4 +- src/settings/Settings.tsx | 8 ++ .../PseudonymousAnalyticsController.ts | 26 ++++ test/PosthogAnalytics-test.ts | 44 ++++--- 7 files changed, 124 insertions(+), 99 deletions(-) create mode 100644 src/settings/controllers/PseudonymousAnalyticsController.ts diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index c27c774cd7..b0a521d886 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -48,7 +48,7 @@ import { Jitsi } from "./widgets/Jitsi"; import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY } from "./BasePlatform"; import ThreepidInviteStore from "./stores/ThreepidInviteStore"; import CountlyAnalytics from "./CountlyAnalytics"; -import { Anonymity, getAnalytics, getPlatformProperties } from "./PosthogAnalytics"; +import { getAnalytics } from "./PosthogAnalytics"; import CallHandler from './CallHandler'; import LifecycleCustomisations from "./customisations/Lifecycle"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; @@ -574,13 +574,7 @@ async function doSetLoggedIn( await abortLogin(); } - if (SettingsStore.getValue("analyticsOptIn")) { - const analytics = getAnalytics(); - analytics.setAnonymity(Anonymity.Pseudonymous); - await analytics.identifyUser(credentials.userId); - } else { - getAnalytics().setAnonymity(Anonymity.Anonymous); - } + getAnalytics().updateAnonymityFromSettings(credentials.userId); Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl); diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index cdb23e582c..fa530b5309 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -85,14 +85,10 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p export class PosthogAnalytics { private anonymity = Anonymity.Anonymous; private posthog?: PostHog = null; - - // set true during init() if posthog config is present + // set true during the constructor if posthog config is present, otherwise false private enabled = false; - - // set to true after init() has been called - private initialised = false; - private static _instance = null; + private platformSuperProperties = {}; public static instance(): PosthogAnalytics { if (!this._instance) { @@ -103,10 +99,6 @@ export class PosthogAnalytics { constructor(posthog: PostHog) { this.posthog = posthog; - } - - public init(anonymity: Anonymity) { - this.anonymity = anonymity; const posthogConfig = SdkConfig.get()["posthog"]; if (posthogConfig) { this.posthog.init(posthogConfig.projectApiKey, { @@ -123,7 +115,6 @@ export class PosthogAnalytics { sanitize_properties: this.sanitizeProperties.bind(this), respect_dnt: true, }); - this.initialised = true; this.enabled = true; } else { this.enabled = false; @@ -159,19 +150,39 @@ export class PosthogAnalytics { return properties; } - public async identifyUser(userId: string) { - if (this.anonymity == Anonymity.Anonymous) return; - this.posthog.identify(await hashHex(userId)); + private static getAnonymityFromSettings(): Anonymity { + // determine the current anonymity level based on curernt user settings + + // "Send anonymous usage data which helps us improve Element. This will use a cookie." + const analyticsOptIn = SettingsStore.getValue("analyticsOptIn"); + + // "Send pseudonymous usage data which helps us improve Element. This will use a cookie." + // + // Currently, this is only a labs flag, for testing purposes. + const pseudonumousOptIn = SettingsStore.getValue("feature_pseudonymousAnalyticsOptIn"); + + let anonymity; + if (pseudonumousOptIn) { + anonymity = Anonymity.Pseudonymous; + } else if (analyticsOptIn) { + anonymity = Anonymity.Anonymous; + } else { + anonymity = Anonymity.Disabled; + } + + return anonymity; } - public registerSuperProperties(properties) { - if (this.enabled) { - this.posthog.register(properties); + public async identifyUser(userId: string) { + if (this.anonymity == Anonymity.Pseudonymous) { + this.posthog.identify(await hashHex(userId)); } } - public isInitialised() { - return this.initialised; + private registerSuperProperties(properties) { + if (this.enabled) { + this.posthog.register(properties); + } } public isEnabled() { @@ -179,6 +190,13 @@ export class PosthogAnalytics { } public setAnonymity(anonymity: Anonymity) { + if (this.enabled && (anonymity == Anonymity.Disabled || anonymity == Anonymity.Anonymous)) { + // when transitioning to Disabled or Anonymous ensure we clear out any prior state + // set in posthog e.g. distinct ID + this.posthog.reset(); + // Restore any previously set platform super properties + this.registerSuperProperties(this.platformSuperProperties); + } this.anonymity = anonymity; } @@ -194,9 +212,6 @@ export class PosthogAnalytics { } private async capture(eventName: string, properties: posthog.Properties) { - if (!this.initialised) { - throw Error("Tried to track event before PoshogAnalytics.init has completed"); - } if (!this.enabled) { return; } @@ -239,45 +254,36 @@ export class PosthogAnalytics { durationMs, }); } -} -export async function getPlatformProperties() { - const platform = PlatformPeg.get(); - let appVersion; - try { - appVersion = await platform.getAppVersion(); - } catch (e) { - // this happens if no version is set i.e. in dev - appVersion = "unknown"; + public async updatePlatformSuperProperties() { + this.platformSuperProperties = await PosthogAnalytics.getPlatformProperties(); + this.registerSuperProperties(this.platformSuperProperties); } - return { - appVersion, - appPlatform: platform.getHumanReadableName(), - }; + private static async getPlatformProperties() { + const platform = PlatformPeg.get(); + let appVersion; + try { + appVersion = await platform.getAppVersion(); + } catch (e) { + // this happens if no version is set i.e. in dev + appVersion = "unknown"; + } + + return { + appVersion, + appPlatform: platform.getHumanReadableName(), + }; + } + + public async updateAnonymityFromSettings(userId?: string) { + this.setAnonymity(PosthogAnalytics.getAnonymityFromSettings()); + if (userId && this.getAnonymity() == Anonymity.Pseudonymous) { + await this.identifyUser(userId); + } + } } export function getAnalytics(): PosthogAnalytics { return PosthogAnalytics.instance(); } - -export function getAnonymityFromSettings(): Anonymity { - // determine the current anonymity level based on curernt user settings - - // "Send anonymous usage data which helps us improve Element. This will use a cookie." - const analyticsOptIn = SettingsStore.getValue("analyticsOptIn"); - - // "Send pseudonymous usage data which helps us improve Element. This will use a cookie." - const pseudonumousOptIn = SettingsStore.getValue("pseudonymousAnalyticsOptIn"); - - let anonymity; - if (pseudonumousOptIn) { - anonymity = Anonymity.Pseudonymous; - } else if (analyticsOptIn) { - anonymity = Anonymity.Anonymous; - } else { - anonymity = Anonymity.Disabled; - } - - return anonymity; -} diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index bd54b0ebc9..1a477970fa 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -107,7 +107,7 @@ import UIStore, { UI_EVENTS } from "../../stores/UIStore"; import SoftLogout from './auth/SoftLogout'; import { makeRoomPermalink } from "../../utils/permalinks/Permalinks"; import { copyPlaintext } from "../../utils/strings"; -import { Anonymity, getAnalytics, getAnonymityFromSettings, getPlatformProperties } from '../../PosthogAnalytics'; +import { getAnalytics } from '../../PosthogAnalytics'; /** constants for MatrixChat.state.view */ export enum Views { @@ -390,10 +390,8 @@ export default class MatrixChat extends React.PureComponent { } const analytics = getAnalytics(); - analytics.init(getAnonymityFromSettings()); - // note this requires a network request in the browser, so some events can potentially - // before before registerSuperProperties has been called - getPlatformProperties().then((properties) => analytics.registerSuperProperties(properties)); + analytics.updateAnonymityFromSettings(); + analytics.updatePlatformSuperProperties(); CountlyAnalytics.instance.enable(/* anonymous = */ true); } @@ -831,11 +829,6 @@ export default class MatrixChat extends React.PureComponent { if (CountlyAnalytics.instance.canEnable()) { CountlyAnalytics.instance.enable(/* anonymous = */ false); } - getAnalytics().setAnonymity(Anonymity.Pseudonymous); - // TODO: this is an async call and we're not waiting for it to complete - - // so potentially an event could be fired prior to it completing and would be - // missing the user identification. - getAnalytics().identifyUser(MatrixClientPeg.get().getUserId()); break; case 'reject_cookies': SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false); diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index 15b4992cd8..670e2ec757 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -36,7 +36,7 @@ import { UIFeature } from "../../../../../settings/UIFeature"; import { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel"; import CountlyAnalytics from "../../../../../CountlyAnalytics"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; -import { Anonymity, getAnalytics } from "../../../../../PosthogAnalytics"; +import { getAnalytics } from "../../../../../PosthogAnalytics"; export class IgnoredUser extends React.Component { static propTypes = { @@ -107,7 +107,7 @@ export default class SecurityUserSettingsTab extends React.Component { _updateAnalytics = (checked) => { checked ? Analytics.enable() : Analytics.disable(); CountlyAnalytics.instance.enable(/* anonymous = */ !checked); - getAnalytics().setAnonymity(checked ? Anonymity.Pseudonymous : Anonymity.Anonymous); + getAnalytics().updateAnonymityFromSettings(MatrixClientPeg.get().getUserId()); }; _onExportE2eKeysClicked = () => { diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 830ea9e32e..db0cb05c9f 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -41,6 +41,7 @@ import { Layout } from "./Layout"; import ReducedMotionController from './controllers/ReducedMotionController'; import IncompatibleController from "./controllers/IncompatibleController"; import SdkConfig from "../SdkConfig"; +import PseudonymousAnalyticsController from './controllers/PseudonymousAnalyticsController'; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = [ @@ -297,6 +298,13 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_pseudonymousAnalyticsOptIn": { + isFeature: true, + supportedLevels: LEVELS_FEATURE, + displayName: _td('Send pseudonymous analytics data'), + default: false, + controller: new PseudonymousAnalyticsController(), + }, "advancedRoomListLogging": { // TODO: Remove flag before launch: https://github.com/vector-im/element-web/issues/14231 displayName: _td("Enable advanced debugging for the room list"), diff --git a/src/settings/controllers/PseudonymousAnalyticsController.ts b/src/settings/controllers/PseudonymousAnalyticsController.ts new file mode 100644 index 0000000000..d55efe3c74 --- /dev/null +++ b/src/settings/controllers/PseudonymousAnalyticsController.ts @@ -0,0 +1,26 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +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 SettingController from "./SettingController"; +import { SettingLevel } from "../SettingLevel"; +import { getAnalytics } from "../../PosthogAnalytics"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; + +export default class PseudonymousAnalyticsController extends SettingController { + public onChange(level: SettingLevel, roomId: string, newValue: any) { + getAnalytics().updateAnonymityFromSettings(MatrixClientPeg.get().getUserId()); + } +} diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index 7d81b6e86d..f726fe0b13 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -7,11 +7,15 @@ class FakePosthog { public capture; public init; public identify; + public reset; + public register; constructor() { this.capture = jest.fn(); this.init = jest.fn(); this.identify = jest.fn(); + this.reset = jest.fn(); + this.register = jest.fn(); } } @@ -37,12 +41,11 @@ export interface ITestRoomEvent extends IRoomEvent { } describe("PosthogAnalytics", () => { - let analytics: PosthogAnalytics; let fakePosthog: FakePosthog; beforeEach(() => { fakePosthog = new FakePosthog(); - analytics = new PosthogAnalytics(fakePosthog); + window.crypto = { subtle: crypto.webcrypto.subtle, }; @@ -53,26 +56,28 @@ describe("PosthogAnalytics", () => { }); describe("Initialisation", () => { - it("Should not initialise if config is not set", async () => { + it("Should not be enabled without config being set", () => { jest.spyOn(SdkConfig, "get").mockReturnValue({}); - analytics.init(Anonymity.Pseudonymous); + const analytics = new PosthogAnalytics(fakePosthog); expect(analytics.isEnabled()).toBe(false); }); - it("Should initialise if config is set", async () => { + it("Should be enabled if config is set", () => { jest.spyOn(SdkConfig, "get").mockReturnValue({ posthog: { projectApiKey: "foo", apiHost: "bar", }, }); - analytics.init(Anonymity.Pseudonymous); - expect(analytics.isInitialised()).toBe(true); + const analytics = new PosthogAnalytics(fakePosthog); + analytics.setAnonymity(Anonymity.Pseudonymous); expect(analytics.isEnabled()).toBe(true); }); }); describe("Tracking", () => { + let analytics: PosthogAnalytics; + beforeEach(() => { jest.spyOn(SdkConfig, "get").mockReturnValue({ posthog: { @@ -80,10 +85,12 @@ describe("PosthogAnalytics", () => { apiHost: "bar", }, }); + + analytics = new PosthogAnalytics(fakePosthog); }); it("Should pass trackAnonymousEvent() to posthog", async () => { - analytics.init(Anonymity.Pseudonymous); + analytics.setAnonymity(Anonymity.Pseudonymous); await analytics.trackAnonymousEvent("jest_test_event", { foo: "bar", }); @@ -92,7 +99,7 @@ describe("PosthogAnalytics", () => { }); it("Should pass trackRoomEvent to posthog", async () => { - analytics.init(Anonymity.Pseudonymous); + analytics.setAnonymity(Anonymity.Pseudonymous); const roomId = "42"; await analytics.trackRoomEvent("jest_test_event", roomId, { foo: "bar", @@ -104,7 +111,7 @@ describe("PosthogAnalytics", () => { }); it("Should pass trackPseudonymousEvent() to posthog", async () => { - analytics.init(Anonymity.Pseudonymous); + analytics.setAnonymity(Anonymity.Pseudonymous); await analytics.trackPseudonymousEvent("jest_test_pseudo_event", { foo: "bar", }); @@ -112,17 +119,8 @@ describe("PosthogAnalytics", () => { expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar"); }); - it("Should blow up if not inititalised prior to tracking", async () => { - const fn = () => { - return analytics.trackAnonymousEvent("jest_test_event", { - foo: "bar", - }); - }; - await expect(fn()).rejects.toThrow(); - }); - it("Should not track pseudonymous messages if anonymous", async () => { - analytics.init(Anonymity.Anonymous); + analytics.setAnonymity(Anonymity.Anonymous); await analytics.trackPseudonymousEvent("jest_test_event", { foo: "bar", }); @@ -130,7 +128,7 @@ describe("PosthogAnalytics", () => { }); it("Should not track any events if disabled", async () => { - analytics.init(Anonymity.Disabled); + analytics.setAnonymity(Anonymity.Disabled); await analytics.trackPseudonymousEvent("jest_test_event", { foo: "bar", }); @@ -181,14 +179,14 @@ bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`); }); it("Should identify the user to posthog if pseudonymous", async () => { - analytics.init(Anonymity.Pseudonymous); + analytics.setAnonymity(Anonymity.Pseudonymous); await analytics.identifyUser("foo"); expect(fakePosthog.identify.mock.calls[0][0]) .toBe("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"); }); it("Should not identify the user to posthog if anonymous", async () => { - analytics.init(Anonymity.Anonymous); + analytics.setAnonymity(Anonymity.Anonymous); await analytics.identifyUser("foo"); expect(fakePosthog.identify.mock.calls.length).toBe(0); }); From 42444d4bb604ddc0f007f78bff3aad14d3703473 Mon Sep 17 00:00:00 2001 From: jelv Date: Wed, 28 Jul 2021 08:22:54 +0000 Subject: [PATCH 117/445] Translated using Weblate (Dutch) Currently translated at 100.0% (3086 of 3086 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nl/ --- src/i18n/strings/nl.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 54cd4c6f66..eb33eca3c3 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -3472,5 +3472,15 @@ "Decide which spaces can access this room. If a space is selected, its members can find and join .": "Kies welke spaces toegang hebben tot dit gesprek. Als een space is geselecteerd kunnen deze leden vinden en aan deelnemen.", "Select spaces": "Spaces selecteren", "You're removing all spaces. Access will default to invite only": "U verwijderd alle spaces. De toegang zal standaard alleen op uitnodiging zijn", - "Room visibility": "Gesprekszichtbaarheid" + "Room visibility": "Gesprekszichtbaarheid", + "Anyone will be able to find and join this room.": "Iedereen kan de kamer vinden en aan deelnemen.", + "Share content": "Deel inhoud", + "Application window": "Deel een app", + "Share entire screen": "Deel uw gehele scherm", + "You can now share your screen by pressing the \"screen share\" button during a call. You can even do this in audio calls if both sides support it!": "U kunt uw schermdelen door te klikken op schermdelen-knop tijdens een oproep. U kunt dit zelfs doen tijdens een audiogesprek als de ontvanger het ook ondersteund!", + "Screen sharing is here!": "Schermdelen is hier!", + "Your camera is still enabled": "Uw camera is nog ingeschakeld", + "Your camera is turned off": "Uw camera staat uit", + "%(sharerName)s is presenting": "%(sharerName)s is aan het presenteren", + "You are presenting": "U bent aan het presenteren" } From ab7d38717c1fc5046a4d581c4ccb964ec0af2507 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 27 Jul 2021 16:24:05 +0200 Subject: [PATCH 118/445] Restore padding for single person state events --- res/css/views/rooms/_EventTile.scss | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 4a419244ff..808af30329 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -132,15 +132,6 @@ $hover-select-border: 4px; } } - &.mx_EventTile_info .mx_EventTile_line, - & ~ .mx_EventListSummary > :not(.mx_EventTile) .mx_EventTile_avatar ~ .mx_EventTile_line { - padding-left: calc($left-gutter + 18px); - } - - & ~ .mx_EventListSummary .mx_EventTile_line { - padding-left: calc($left-gutter); - } - &.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { padding-left: calc($left-gutter + 18px - $hover-select-border); } @@ -280,6 +271,15 @@ $hover-select-border: 4px; } } +.mx_EventTile:not([data-layout=bubble]).mx_EventTile_info .mx_EventTile_line, +.mx_EventListSummary:not([data-layout=bubble]) > :not(.mx_EventTile) .mx_EventTile_avatar ~ .mx_EventTile_line { + padding-left: calc($left-gutter + 18px); +} + +.mx_EventListSummary:not([data-layout=bubble]) .mx_EventTile_line { + padding-left: calc($left-gutter); +} + /* all the overflow-y: hidden; are to trap Zalgos - but they introduce an implicit overflow-x: auto. so make that explicitly hidden too to avoid random From a6df687196916cb3b31a4fa0cc52cbebed1bb939 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 09:54:37 +0100 Subject: [PATCH 119/445] Tidy up interface and add some comments --- src/PosthogAnalytics.ts | 102 ++++++++++++++++++++++------------ test/PosthogAnalytics-test.ts | 2 +- 2 files changed, 66 insertions(+), 38 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index fa530b5309..6329598685 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -20,11 +20,12 @@ export enum Anonymity { // If an event extends IPseudonymousEvent, the event contains pseudonymous data // that won't be sent unless the user has explicitly consented to pseudonymous tracking. -// For example, hashed user IDs or room IDs. +// For example, it might contain hashed user IDs or room IDs. +// Such events will be automatically dropped if PosthogAnalytics.anonymity isn't set to Pseudonymous. export interface IPseudonymousEvent extends IEvent {} -// If an event extends IAnonymousEvent, the event strictly contains *only* anonymous data which -// may be sent without explicit user consent. +// If an event extends IAnonymousEvent, the event strictly contains *only* anonymous data; +// i.e. no identifiers that can be associated with the user. export interface IAnonymousEvent extends IEvent {} export interface IRoomEvent extends IPseudonymousEvent { @@ -83,6 +84,23 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p } export class PosthogAnalytics { + /* Wrapper for Posthog analytics. + * + * 3 modes of anonymity are supported, governed by this.anonymity + * - Anonymity.Disabled means *no data* is passed to posthog + * - Anonymity.Anonymous means all identifers will be redacted before being passed to posthog + * - Anonymity.Pseudonymous means all identifiers will be hashed via SHA-256 before being passed + * to Posthog + * + * To update anonymity, call updateAnonymityFromSettings() or you can set it directly via setAnonymity(). + * + * To pass an event to Posthog: + * + * 1. Declare a type for the event, extending IAnonymousEvent, IPseudonymousEvent or IRoomEvent. + * 2. Call the appropriate track*() method. Pseudonymous events will be dropped when anonymity is + * Anonymous or Disabled; Anonymous events will be dropped when anonymity is Disabled. + */ + private anonymity = Anonymity.Anonymous; private posthog?: PostHog = null; // set true during the constructor if posthog config is present, otherwise false @@ -156,9 +174,9 @@ export class PosthogAnalytics { // "Send anonymous usage data which helps us improve Element. This will use a cookie." const analyticsOptIn = SettingsStore.getValue("analyticsOptIn"); - // "Send pseudonymous usage data which helps us improve Element. This will use a cookie." + // (proposed wording) "Send pseudonymous usage data which helps us improve Element. This will use a cookie." // - // Currently, this is only a labs flag, for testing purposes. + // TODO: Currently, this is only a labs flag, for testing purposes. const pseudonumousOptIn = SettingsStore.getValue("feature_pseudonymousAnalyticsOptIn"); let anonymity; @@ -173,23 +191,46 @@ export class PosthogAnalytics { return anonymity; } - public async identifyUser(userId: string) { - if (this.anonymity == Anonymity.Pseudonymous) { - this.posthog.identify(await hashHex(userId)); - } - } - private registerSuperProperties(properties) { if (this.enabled) { this.posthog.register(properties); } } + private static async getPlatformProperties() { + const platform = PlatformPeg.get(); + let appVersion; + try { + appVersion = await platform.getAppVersion(); + } catch (e) { + // this happens if no version is set i.e. in dev + appVersion = "unknown"; + } + + return { + appVersion, + appPlatform: platform.getHumanReadableName(), + }; + } + + private async capture(eventName: string, properties: posthog.Properties) { + if (!this.enabled) { + return; + } + const { origin, hash, pathname } = window.location; + properties['$redacted_current_url'] = await getRedactedCurrentLocation( + origin, hash, pathname, this.anonymity); + this.posthog.capture(eventName, properties); + } + public isEnabled() { return this.enabled; } public setAnonymity(anonymity: Anonymity) { + // Update this.anonymity. + // This is public for testing purposes, typically you want to call updateAnonymityFromSettings + // to ensure this value is in step with the user's settings. if (this.enabled && (anonymity == Anonymity.Disabled || anonymity == Anonymity.Anonymous)) { // when transitioning to Disabled or Anonymous ensure we clear out any prior state // set in posthog e.g. distinct ID @@ -200,6 +241,12 @@ export class PosthogAnalytics { this.anonymity = anonymity; } + public async identifyUser(userId: string) { + if (this.anonymity == Anonymity.Pseudonymous) { + this.posthog.identify(await hashHex(userId)); + } + } + public getAnonymity() { return this.anonymity; } @@ -211,16 +258,6 @@ export class PosthogAnalytics { this.setAnonymity(Anonymity.Anonymous); } - private async capture(eventName: string, properties: posthog.Properties) { - if (!this.enabled) { - return; - } - const { origin, hash, pathname } = window.location; - properties['$redacted_current_url'] = await getRedactedCurrentLocation( - origin, hash, pathname, this.anonymity); - this.posthog.capture(eventName, properties); - } - public async trackPseudonymousEvent( eventName: E["eventName"], properties: E["properties"], @@ -256,27 +293,18 @@ export class PosthogAnalytics { } public async updatePlatformSuperProperties() { + // Update super properties in posthog with our platform (app version, platform). + // These properties will be subsequently passed in every event. + // + // This only needs to be done once per page lifetime. Note that getPlatformProperties + // is async and can involve a network request if we are running in a browser. this.platformSuperProperties = await PosthogAnalytics.getPlatformProperties(); this.registerSuperProperties(this.platformSuperProperties); } - private static async getPlatformProperties() { - const platform = PlatformPeg.get(); - let appVersion; - try { - appVersion = await platform.getAppVersion(); - } catch (e) { - // this happens if no version is set i.e. in dev - appVersion = "unknown"; - } - - return { - appVersion, - appPlatform: platform.getHumanReadableName(), - }; - } - public async updateAnonymityFromSettings(userId?: string) { + // Update this.anonymity based on the user's analytics opt-in settings + // Identify the user (via hashed user ID) to posthog if anonymity is pseudonmyous this.setAnonymity(PosthogAnalytics.getAnonymityFromSettings()); if (userId && this.getAnonymity() == Anonymity.Pseudonymous) { await this.identifyUser(userId); diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index f726fe0b13..a0cfec2406 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -172,7 +172,7 @@ bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`); expect(location).toBe("https://foo.bar/#///"); }); - it("Should currently handle an empty hash", async () => { + it("Should handle an empty hash", async () => { const location = await getRedactedCurrentLocation( "https://foo.bar", "", "/", Anonymity.Anonymous); expect(location).toBe("https://foo.bar/"); From 4048cb3c37132596ae52a3b5c2f336201213a3ac Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 10:20:16 +0100 Subject: [PATCH 120/445] Default to Anonymous tracking when no OptIn setting is present --- src/PosthogAnalytics.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 6329598685..bce6548cb3 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -172,17 +172,19 @@ export class PosthogAnalytics { // determine the current anonymity level based on curernt user settings // "Send anonymous usage data which helps us improve Element. This will use a cookie." - const analyticsOptIn = SettingsStore.getValue("analyticsOptIn"); + const analyticsOptIn = SettingsStore.getValue("analyticsOptIn", null, true); // (proposed wording) "Send pseudonymous usage data which helps us improve Element. This will use a cookie." // // TODO: Currently, this is only a labs flag, for testing purposes. - const pseudonumousOptIn = SettingsStore.getValue("feature_pseudonymousAnalyticsOptIn"); + const pseudonumousOptIn = SettingsStore.getValue("feature_pseudonymousAnalyticsOptIn", null, true); let anonymity; if (pseudonumousOptIn) { anonymity = Anonymity.Pseudonymous; - } else if (analyticsOptIn) { + } else if (analyticsOptIn || analyticsOptIn === null) { + // If no analyticsOptIn has been set (i.e. before the user has logged in, or if they haven't answered the + // opt-in question, assume Anonymous) anonymity = Anonymity.Anonymous; } else { anonymity = Anonymity.Disabled; From c206127f68f8ab72a42fa55d38aeb88604fd84b8 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 10:45:03 +0100 Subject: [PATCH 121/445] Track screen name when tracking page view --- src/PosthogAnalytics.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index bce6548cb3..e28972060a 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -36,6 +36,7 @@ interface IPageView extends IAnonymousEvent { eventName: "$pageview", properties: { durationMs?: number + screen?: string } } @@ -289,8 +290,17 @@ export class PosthogAnalytics { } public async trackPageView(durationMs: number) { + const hash = window.location.hash; + + let screen = null; + const split = hash.split("/"); + if (split.length >= 2) { + screen = split[1]; + } + await this.trackAnonymousEvent("$pageview", { durationMs, + screen, }); } From c3e715c1ca525beff68c8fa03ecbe64c4d8df6f3 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 10:45:32 +0100 Subject: [PATCH 122/445] i18n --- src/i18n/strings/en_EN.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2790e17eed..403374edb4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -821,6 +821,7 @@ "Show message previews for reactions in DMs": "Show message previews for reactions in DMs", "Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms", "Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices", + "Send pseudonymous analytics data": "Send pseudonymous analytics data", "Enable advanced debugging for the room list": "Enable advanced debugging for the room list", "Show info about bridges in room settings": "Show info about bridges in room settings", "Font size": "Font size", From 8ef18d0f9a97067676741349710853faac16fc07 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 11:01:59 +0100 Subject: [PATCH 123/445] Add module level comment about anonymity behaviour --- src/PosthogAnalytics.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index e28972060a..0435a0f22c 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -3,6 +3,21 @@ import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; import SettingsStore from './settings/SettingsStore'; +/* Posthog analytics tracking. + * + * Anonymity behaviour is as follows: + * + * - If Posthog isn't configured in `config.json`, events are not sent. + * - If [Do Not Track](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack) is + * enabled, events are not sent (this detection is built into posthog and turned on via the + * `respect_dnt` flag being passed to `posthog.init`). + * - If the `feature_pseudonymousAnalyticsOptIn` labs flag is `true`, track pseudonomously, i.e. + * hash all matrix identifiers in tracking events (user IDs, room IDs etc) using SHA-256. + * - Otherwise, if the existing `analyticsOptIn` flag is `true`, or not present (i.e. prior to + * logging in), track anonymously, i.e. redact all matrix identifiers in tracking events. + * - If both flags are false, events are not sent. +*/ + interface IEvent { // The event name that will be used by PostHog. // TODO: standard format (camel case? snake? UpperCase?) @@ -86,7 +101,6 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p export class PosthogAnalytics { /* Wrapper for Posthog analytics. - * * 3 modes of anonymity are supported, governed by this.anonymity * - Anonymity.Disabled means *no data* is passed to posthog * - Anonymity.Anonymous means all identifers will be redacted before being passed to posthog From 3ca9f0763f39db1757928f4503161ceee76199d2 Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Wed, 28 Jul 2021 09:40:35 +0000 Subject: [PATCH 124/445] Translated using Weblate (Czech) Currently translated at 100.0% (3086 of 3086 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ --- src/i18n/strings/cs.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 8b984c5652..d4b3b5fd2c 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -3511,5 +3511,6 @@ "Your camera is still enabled": "Vaše kamera je stále zapnutá", "Your camera is turned off": "Vaše kamera je vypnutá", "%(sharerName)s is presenting": "%(sharerName)s prezentuje", - "You are presenting": "Prezentujete" + "You are presenting": "Prezentujete", + "Anyone will be able to find and join this room.": "Kdokoliv může najít tuto místnost a připojit se k ní." } From c697079eb42727f84cfb6b9f79d73931710d10b9 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 11:22:40 +0100 Subject: [PATCH 125/445] Fix import --- test/PosthogAnalytics-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index a0cfec2406..095e216262 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -1,7 +1,7 @@ import { Anonymity, getRedactedCurrentLocation, IAnonymousEvent, IPseudonymousEvent, IRoomEvent, PosthogAnalytics } from '../src/PosthogAnalytics'; import SdkConfig from '../src/SdkConfig'; -const crypto = require('crypto'); +import crypto = require('crypto'); class FakePosthog { public capture; From 7c62386915b2e027d260c77d700610bea16791ef Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 11:28:19 +0100 Subject: [PATCH 126/445] lint --- src/PosthogAnalytics.ts | 14 +++++++------- test/PosthogAnalytics-test.ts | 18 +++++++++--------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 0435a0f22c..ee45f57e70 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -24,7 +24,7 @@ interface IEvent { eventName: string; // The properties of the event that will be stored in PostHog. - properties: {} + properties: {}; } export enum Anonymity { @@ -44,19 +44,19 @@ export interface IPseudonymousEvent extends IEvent {} export interface IAnonymousEvent extends IEvent {} export interface IRoomEvent extends IPseudonymousEvent { - hashedRoomId: string + hashedRoomId: string; } interface IPageView extends IAnonymousEvent { - eventName: "$pageview", + eventName: "$pageview"; properties: { - durationMs?: number - screen?: string - } + durationMs?: number; + screen?: string; + }; } export interface IWelcomeScreenLoad extends IAnonymousEvent { - eventName: "welcome_screen_load", + eventName: "welcome_screen_load"; } const hashHex = async (input: string): Promise => { diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index 095e216262..920f449bab 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -20,24 +20,24 @@ class FakePosthog { } export interface ITestEvent extends IAnonymousEvent { - key: "jest_test_event", + key: "jest_test_event"; properties: { - foo: string - } + foo: string; + }; } export interface ITestPseudonymousEvent extends IPseudonymousEvent { - key: "jest_test_pseudo_event", + key: "jest_test_pseudo_event"; properties: { - foo: string - } + foo: string; + }; } export interface ITestRoomEvent extends IRoomEvent { - key: "jest_test_room_event", + key: "jest_test_room_event"; properties: { - foo: string - } + foo: string; + }; } describe("PosthogAnalytics", () => { From 7cf28de9c90a53708821ce4a7cb82752f4401502 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 11:32:17 +0100 Subject: [PATCH 127/445] take 2 at fixing import --- test/PosthogAnalytics-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index 920f449bab..57eb8ed72b 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -1,7 +1,7 @@ import { Anonymity, getRedactedCurrentLocation, IAnonymousEvent, IPseudonymousEvent, IRoomEvent, PosthogAnalytics } from '../src/PosthogAnalytics'; import SdkConfig from '../src/SdkConfig'; -import crypto = require('crypto'); +import crypto from 'crypto'; class FakePosthog { public capture; From d96e7e3375d64931af8eb9bebf49664a3fde0df6 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 12:08:55 +0100 Subject: [PATCH 128/445] Add transitive dev dependencies of posthog This is needed during tsc lint as posthog imports types from these libraries into its type definitions --- package.json | 4 +++- yarn.lock | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c0062db46c..084f413605 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "what-input": "^5.2.10", "zxcvbn": "^4.4.2" }, - "devDependencies": { + "devDependencies": { "@babel/cli": "^7.12.10", "@babel/core": "^7.12.10", "@babel/eslint-parser": "^7.12.10", @@ -125,6 +125,7 @@ "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", "@peculiar/webcrypto": "^1.1.4", "@sinonjs/fake-timers": "^7.0.2", + "@sentry/types": "^6.2.2", "@types/classnames": "^2.2.11", "@types/commonmark": "^0.27.4", "@types/counterpart": "^0.18.1", @@ -167,6 +168,7 @@ "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", "react-test-renderer": "^17.0.2", "rimraf": "^3.0.2", + "rrweb": "^0.9.9", "stylelint": "^13.9.0", "stylelint-config-standard": "^20.0.0", "stylelint-scss": "^3.18.0", diff --git a/yarn.lock b/yarn.lock index c4d1456612..78f4838a09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1352,6 +1352,11 @@ tslib "^2.2.0" webcrypto-core "^1.2.0" +"@sentry/types@^6.2.2": + version "6.10.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.10.0.tgz#6b1f44e5ed4dbc2710bead24d1b32fb08daf04e1" + integrity sha512-M7s0JFgG7/6/yNVYoPUbxzaXDhnzyIQYRRJJKRaTD77YO4MHvi4Ke8alBWqD5fer0cPIfcSkBqa9BLdqRqcMWw== + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -1448,6 +1453,11 @@ resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.1.tgz#b1b784d9e54d9879f0a8cb12f2caedab65430fe8" integrity sha512-PRuFlBBkvdDOtxlIASzTmkEFar+S66Ek48NVVTWMUjtJAdn5vyMSN8y6IZIoIymGpR36q2nZbIYazBWyFxL+IQ== +"@types/css-font-loading-module@0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.4.tgz#94a835e27d1af444c65cba88523533c174463d64" + integrity sha512-ENdXf7MW4m9HeDojB2Ukbi7lYMIuQNBHVf98dbzaiG4EEJREBd6oleVAjrLRCrp7dm6CK1mmdmU9tcgF61acbw== + "@types/css-font-loading-module@^0.0.6": version "0.0.6" resolved "https://registry.yarnpkg.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.6.tgz#1ac3417ed31eeb953134d29b56bca921644b87c0" @@ -1790,6 +1800,11 @@ object.fromentries "^2.0.0" prop-types "^15.7.0" +"@xstate/fsm@^1.4.0": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@xstate/fsm/-/fsm-1.6.1.tgz#c92972b835540c4e3c5e14277f40dbcbdaee9571" + integrity sha512-xYKDNuPR36/fUK+jmhM+oauBmbdUAfuJKnDjg3/7NbN+Pj03TX7e94LXnzkwGgAR+U/HWoMqM5UPTuGIYfIx9g== + abab@^2.0.3, abab@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" @@ -3601,7 +3616,7 @@ fbjs@^0.8.4: setimmediate "^1.0.5" ua-parser-js "^0.7.18" -fflate@^0.4.1: +fflate@^0.4.1, fflate@^0.4.4: version "0.4.8" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== @@ -5639,6 +5654,11 @@ minimist@>=1.2.2, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +mitt@^1.1.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-1.2.0.tgz#cb24e6569c806e31bd4e3995787fe38a04fdf90d" + integrity sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw== + mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" @@ -6841,6 +6861,22 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rrweb-snapshot@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.1.7.tgz#92a3b47b1112a1b566c2fae2edb02fa48a6f6653" + integrity sha512-+f2kCCvIQ1hbEeCWnV7mPVPDEdWEExqwcYqMd/r1nfK52QE7qU52jefUOyTe85Vy67rZGqWnfK/B25e/OTSgYg== + +rrweb@^0.9.9: + version "0.9.14" + resolved "https://registry.yarnpkg.com/rrweb/-/rrweb-0.9.14.tgz#09bec604fc44c74801e4fe910606e5a6cde008ec" + integrity sha512-nm2rrVNoyWFPrbGQmcvTTlA7XjbbgPIgO7qsW0Zyr5iOURIFJDGPHFmOVLRyLpWiriVtEoXh6a+x+D1sj+qwWg== + dependencies: + "@types/css-font-loading-module" "0.0.4" + "@xstate/fsm" "^1.4.0" + fflate "^0.4.4" + mitt "^1.1.3" + rrweb-snapshot "^1.0.3" + rst-selector-parser@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" From 55e8173ee9011e6dff733090f6671639244be692 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 12:12:10 +0100 Subject: [PATCH 129/445] remove whitespace --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 084f413605..e5ecbb31a9 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "what-input": "^5.2.10", "zxcvbn": "^4.4.2" }, - "devDependencies": { + "devDependencies": { "@babel/cli": "^7.12.10", "@babel/core": "^7.12.10", "@babel/eslint-parser": "^7.12.10", From 3ff7de3c967a7937830fcddf5e6785b1e0c5ff5a Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 13:43:06 +0100 Subject: [PATCH 130/445] Mock SHA-256 to avoid problems loading crypto on Node 14 --- test/PosthogAnalytics-test.ts | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index 57eb8ed72b..b7fae3c196 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -1,7 +1,6 @@ import { Anonymity, getRedactedCurrentLocation, IAnonymousEvent, IPseudonymousEvent, IRoomEvent, PosthogAnalytics } from '../src/PosthogAnalytics'; import SdkConfig from '../src/SdkConfig'; -import crypto from 'crypto'; class FakePosthog { public capture; @@ -42,12 +41,37 @@ export interface ITestRoomEvent extends IRoomEvent { describe("PosthogAnalytics", () => { let fakePosthog: FakePosthog; + const shaHashes = { + "42": "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049", + "some": "a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b", + "pii": "bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4", + "foo": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", + }; beforeEach(() => { fakePosthog = new FakePosthog(); window.crypto = { - subtle: crypto.webcrypto.subtle, + subtle: { + digest: async (_, encodedMessage) => { + const message = new TextDecoder().decode(encodedMessage); + const hexHash = shaHashes[message]; + const bytes = []; + for (let c = 0; c < hexHash.length; c += 2) { + bytes.push(parseInt(hexHash.substr(c, 2), 16)); + } + return bytes; + }, + /*console.log(message); + const digest = sha256(new WordArray(message)); + const digestBuf = new ArrayBuffer(digest.words.length * 4); + console.log(digest); + const view = new Uint32Array(digestBuf); + for (let i = 0; i < digest.words.length; i++) { + view[i] = digest.words[i]; + } + return digestBuf*/ + }, }; }); @@ -135,7 +159,7 @@ describe("PosthogAnalytics", () => { await analytics.trackAnonymousEvent("jest_test_event", { foo: "bar", }); - await analytics.trackRoomEvent("room id", "jest_test_room_event", { + await analytics.trackRoomEvent("room id", "foo", { foo: "bar", }); await analytics.trackPageView(200); From 6bb539d77421333cb92d735d667f5f46d2859555 Mon Sep 17 00:00:00 2001 From: XoseM Date: Wed, 28 Jul 2021 12:33:34 +0000 Subject: [PATCH 131/445] Translated using Weblate (Galician) Currently translated at 100.0% (3086 of 3086 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/ --- src/i18n/strings/gl.json | 52 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 3a3271c4b7..f873f8dfac 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3544,5 +3544,55 @@ "To help space members find and join a private room, go to that room's Security & Privacy settings.": "Axudarlle aos membros do espazo a que atopen e se unan a salas privadas, vaite aos axustes de Seguridade e Privacidade desa sala.", "Help space members find private rooms": "Axudarlle aos membros do espazo a que atopen salas privadas", "Help people in spaces to find and join private rooms": "Axudarlle ás persoas en espazos que atopen e se unan a salas privadas", - "New in the Spaces beta": "Novo na beta de Espazos" + "New in the Spaces beta": "Novo na beta de Espazos", + "Error downloading audio": "Erro ao descargar o audio", + "Please note upgrading will make a new version of the room. All current messages will stay in this archived room.": "Ten en conta que a actualización creará unha nova versión da sala. Tódalas mensaxes actuais permanecerán nesta sala arquivada.", + "Automatically invite members from this room to the new one": "Convidar automáticamente membros desta sala á nova sala", + "These are likely ones other room admins are a part of.": "Probablemente estas son salas das que forman parte outras administradoras da sala.", + "Other spaces or rooms you might not know": "Outros espazos ou salas que poderías coñecer", + "Spaces you know that contain this room": "Espazos que coñeces que conteñen a esta sala", + "Search spaces": "Buscar espazos", + "Decide which spaces can access this room. If a space is selected, its members can find and join .": "Decide que espazos poderán acceder a esta sala. Se un espazo é elexido, os seus membros poderán atopar e unirse a .", + "Select spaces": "Elixe espazos", + "You're removing all spaces. Access will default to invite only": "Vas eliminar tódolos espazos. Por defecto o acceso cambiará a só por convite", + "Room visibility": "Visibilidade da sala", + "Visible to space members": "Visible para membros do espazo", + "Public room": "Sala pública", + "Private room (invite only)": "Sala privada (só con convite)", + "Create a room": "Crear unha sala", + "Only people invited will be able to find and join this room.": "Só as persoas convidadas poderán atopar e unirse a esta sala.", + "Anyone will be able to find and join this room.": "Calquera poderá atopar e unirse a esta sala.", + "Anyone will be able to find and join this room, not just members of .": "Calquera poderá atopar e unirse a esta sala, non só os membros de .", + "You can change this at any time from room settings.": "Podes cambiar isto en calquera momento nos axustes da sala.", + "Everyone in will be able to find and join this room.": "Todas en poderán atopar e unirse a esta sala.", + "Share content": "Compartir contido", + "Application window": "Ventá da aplicación", + "Share entire screen": "Compartir pantalla completa", + "They didn't pick up": "Non respondeu", + "Call again": "Chamar outra vez", + "They declined this call": "Rexeitou esta chamada", + "You declined this call": "Rexeitaches esta chamada", + "The voice message failed to upload.": "Fallou a subida da mensaxe de voz.", + "You can now share your screen by pressing the \"screen share\" button during a call. You can even do this in audio calls if both sides support it!": "Podes compartir a túa pantalla premendo no botón \"compartir pantalla\" durante unha chamada. Incluso podes facelo nas chamadas de audio se as dúas partes teñen soporte!", + "Screen sharing is here!": "Aquí tes a compartición de pantalla!", + "Access": "Acceder", + "People with supported clients will be able to join the room without having a registered account.": "As persoas con clientes habilitados poderán unirse a sala sen ter que posuir unha conta rexistrada.", + "Decide who can join %(roomName)s.": "Decidir quen pode unirse a %(roomName)s.", + "Space members": "Membros do espazo", + "Anyone in a space can find and join. You can select multiple spaces.": "Calquera nun espazo pode atopar e unirse. Podes elexir múltiples espazos.", + "Anyone in %(spaceName)s can find and join. You can select other spaces too.": "Calquera en %(spaceName)s pode atopar e unirse. Podes elexir outros espazos tamén.", + "Spaces with access": "Espazos con acceso", + "Anyone in a space can find and join. Edit which spaces can access here.": "Calquera nun espazo pode atopala e unirse. Editar que espazos poden acceder aquí.", + "Currently, %(count)s spaces have access|other": "Actualmente, %(count)s espazos teñen acceso", + "& %(count)s more|other": "e %(count)s máis", + "Upgrade required": "Actualización requerida", + "Anyone can find and join.": "Calquera pode atopala e unirse.", + "Only invited people can join.": "Só se poden unir persoas con convite.", + "Private (invite only)": "Privada (só con convite)", + "This upgrade will allow members of selected spaces access to this room without an invite.": "Esta actualización permitirá que os membros dos espazos seleccionados teñan acceso á sala sen precisar convite.", + "Your camera is still enabled": "A túa cámara aínda está acendida", + "Your camera is turned off": "A túa cámara está apagada", + "%(sharerName)s is presenting": "%(sharerName)s estase presentando", + "You are presenting": "Estaste a presentar", + "This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.": "Esto facilita que as salas permanezan privadas respecto do espazo, mais permitindo que as persoas do espazo as atopen e se unan a elas. Tódalas novas salas do espazo terán esta opción dispoñible." } From 072e9a30cb6bcf869eac69e8d12b50f5861353f0 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 28 Jul 2021 10:30:27 +0000 Subject: [PATCH 132/445] Translated using Weblate (Albanian) Currently translated at 99.7% (3079 of 3086 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 45f76a4a61..e8bb3fd855 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -3568,5 +3568,20 @@ "To help space members find and join a private room, go to that room's Security & Privacy settings.": "Që të ndihmoni anëtarë hapësirash të gjejnë dhe hyjnë në një dhomë private, kaloni te rregullimet e Sigurisë & Privatësisë së dhomës.", "Help space members find private rooms": "Ndihmoni anëtarë hapësirash të gjejnë dhoma private", "Help people in spaces to find and join private rooms": "Ndihmoni persona në hapësira të gjejnë dhe hyjnë në dhoma private", - "New in the Spaces beta": "E re në Hapësira beta" + "New in the Spaces beta": "E re në Hapësira beta", + "You're removing all spaces. Access will default to invite only": "Po hiqni krejt hapësirat. Hyrja do të kthehet te parazgjedhja, pra vetëm me ftesa", + "Anyone will be able to find and join this room.": "Gjithkush do të jetë në gjendje të gjejë dhe hyjë në këtë dhomë.", + "Share content": "Ndani lëndë", + "Application window": "Dritare aplikacioni", + "Share entire screen": "Nda krejt ekranin", + "They didn't pick up": "S’iu përgjigjën", + "Call again": "Thirre prapë", + "They declined this call": "E hodhën poshtë këtë thirrje", + "You declined this call": "E hodhët poshtë këtë thirrje", + "You can now share your screen by pressing the \"screen share\" button during a call. You can even do this in audio calls if both sides support it!": "Tani mund t’u tregoni të tjerëve ekranin tuaj duke shtypur butonin “ndarje ekrani” gjatë thirrjes. Këtë mund ta bëni edhe në thirrje audio, nëse mbulohet nga të dy palët!", + "Screen sharing is here!": "Ndarja e ekranit me të tjerë erdhi!", + "Your camera is still enabled": "Kamera juaj është ende e aktivizuar", + "Your camera is turned off": "Kamera juaj është e fikur", + "%(sharerName)s is presenting": "%(sharerName)s përfaqëson", + "You are presenting": "Përfaqësoni" } From df7ebb2e7ce5894afd349f3a1f8cf930f80f5f90 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:00:37 +0100 Subject: [PATCH 133/445] Remove commented out block --- test/PosthogAnalytics-test.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index b7fae3c196..9b8e703c8e 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -62,15 +62,6 @@ describe("PosthogAnalytics", () => { } return bytes; }, - /*console.log(message); - const digest = sha256(new WordArray(message)); - const digestBuf = new ArrayBuffer(digest.words.length * 4); - console.log(digest); - const view = new Uint32Array(digestBuf); - for (let i = 0; i < digest.words.length; i++) { - view[i] = digest.words[i]; - } - return digestBuf*/ }, }; }); From 868d92781d402a46a5ec758c8ad10a77ef11f27d Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:01:21 +0100 Subject: [PATCH 134/445] Add copyright header --- src/PosthogAnalytics.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index ee45f57e70..9628ed1e4e 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -1,3 +1,19 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +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 posthog, { PostHog } from 'posthog-js'; import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; From d5bef53f8bc9f7c523afdc78c3c0c3500e268a30 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:03:17 +0100 Subject: [PATCH 135/445] Use snake case for feature name --- src/PosthogAnalytics.ts | 4 ++-- src/settings/Settings.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 9628ed1e4e..15bd10ad67 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -27,7 +27,7 @@ import SettingsStore from './settings/SettingsStore'; * - If [Do Not Track](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack) is * enabled, events are not sent (this detection is built into posthog and turned on via the * `respect_dnt` flag being passed to `posthog.init`). - * - If the `feature_pseudonymousAnalyticsOptIn` labs flag is `true`, track pseudonomously, i.e. + * - If the `feature_pseudonymous_analytics_opt_in` labs flag is `true`, track pseudonomously, i.e. * hash all matrix identifiers in tracking events (user IDs, room IDs etc) using SHA-256. * - Otherwise, if the existing `analyticsOptIn` flag is `true`, or not present (i.e. prior to * logging in), track anonymously, i.e. redact all matrix identifiers in tracking events. @@ -208,7 +208,7 @@ export class PosthogAnalytics { // (proposed wording) "Send pseudonymous usage data which helps us improve Element. This will use a cookie." // // TODO: Currently, this is only a labs flag, for testing purposes. - const pseudonumousOptIn = SettingsStore.getValue("feature_pseudonymousAnalyticsOptIn", null, true); + const pseudonumousOptIn = SettingsStore.getValue("feature_pseudonymous_analytics_opt_in", null, true); let anonymity; if (pseudonumousOptIn) { diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 810d8bb323..c287a3fd9d 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -299,7 +299,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_FEATURE, default: false, }, - "feature_pseudonymousAnalyticsOptIn": { + "feature_pseudonymous_analytics_opt_in": { isFeature: true, supportedLevels: LEVELS_FEATURE, displayName: _td('Send pseudonymous analytics data'), From 91e65534fa270fc22f62e30f77585ac1f67689c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 28 Jul 2021 15:04:33 +0200 Subject: [PATCH 136/445] await setState to avoid races where we would try to play media without an HTMLVideoElement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/VideoFeed.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index fef3aa0691..ad5b6f42fd 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -140,16 +140,16 @@ export default class VideoFeed extends React.Component { // seem to be necessary - Šimon } - private onNewStream = () => { - this.setState({ + private onNewStream = async () => { + await this.setState({ audioMuted: this.props.feed.isAudioMuted(), videoMuted: this.props.feed.isVideoMuted(), }); this.playMedia(); }; - private onMuteStateChanged = () => { - this.setState({ + private onMuteStateChanged = async () => { + await this.setState({ audioMuted: this.props.feed.isAudioMuted(), videoMuted: this.props.feed.isVideoMuted(), }); From 7c4e3efbff953c100efcd30e813c2447f9527775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 28 Jul 2021 15:11:31 +0200 Subject: [PATCH 137/445] Extend PureComponent to avoid unnecessary renders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/VideoFeed.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index ad5b6f42fd..41c6b5185c 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -47,7 +47,7 @@ interface IState { } @replaceableComponent("views.voip.VideoFeed") -export default class VideoFeed extends React.Component { +export default class VideoFeed extends React.PureComponent { private element: HTMLVideoElement; constructor(props: IProps) { From a09e046c18d62dd3f14f18b6bf991f646bcad9d8 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:11:55 +0100 Subject: [PATCH 138/445] Update test/PosthogAnalytics-test.ts Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- test/PosthogAnalytics-test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index 9b8e703c8e..d80f2946c3 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -1,5 +1,11 @@ -import { Anonymity, getRedactedCurrentLocation, IAnonymousEvent, IPseudonymousEvent, IRoomEvent, - PosthogAnalytics } from '../src/PosthogAnalytics'; +import { + Anonymity, + getRedactedCurrentLocation, + IAnonymousEvent, + IPseudonymousEvent, + IRoomEvent, + PosthogAnalytics, +} from '../src/PosthogAnalytics'; import SdkConfig from '../src/SdkConfig'; class FakePosthog { From ecbc536a3eb67bee130793a396fa2693ceb02f74 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:15:27 +0100 Subject: [PATCH 139/445] Add copyright header --- test/PosthogAnalytics-test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index d80f2946c3..6cb1743051 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -1,3 +1,19 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +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 { Anonymity, getRedactedCurrentLocation, @@ -6,6 +22,7 @@ import { IRoomEvent, PosthogAnalytics, } from '../src/PosthogAnalytics'; + import SdkConfig from '../src/SdkConfig'; class FakePosthog { From da3bf5a097b437b912d5d1ad78e00e5f0aceb7c2 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:14:15 +0100 Subject: [PATCH 140/445] rename knownScreens -> whitelistedScreens --- src/PosthogAnalytics.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 15bd10ad67..66f17a4937 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -83,7 +83,7 @@ const hashHex = async (input: string): Promise => { return [...new Uint8Array(digestBuf)].map((b: number) => b.toString(16).padStart(2, "0")).join(""); }; -const knownScreens = new Set([ +const whitelistedScreens = new Set([ "register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory", "start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group", ]); @@ -102,7 +102,7 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p } else { let [_, screen, ...parts] = hash.split("/"); - if (!knownScreens.has(screen)) { + if (!whitelistedScreens.has(screen)) { screen = ""; } From 90c848efe2ef3ec3bac0d698b217a54e1ceaa2b6 Mon Sep 17 00:00:00 2001 From: Bog Rol Date: Wed, 28 Jul 2021 12:58:39 +0000 Subject: [PATCH 141/445] Translated using Weblate (Hungarian) Currently translated at 99.8% (3077 of 3083 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index a04811d2c0..a7c09c0fec 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -1318,7 +1318,7 @@ "Use an email address to recover your account": "A felhasználói fiók visszaszerzése e-mail címmel", "Enter email address (required on this homeserver)": "E-mail cím megadása (ezen a matrix szerveren kötelező)", "Doesn't look like a valid email address": "Az e-mail cím nem tűnik érvényesnek", - "Enter password": "Jelszó megadása", + "Enter password": "Adja meg a jelszót", "Password is allowed, but unsafe": "A jelszó engedélyezett, de nem biztonságos", "Nice, strong password!": "Szép, erős jelszó!", "Passwords don't match": "A jelszavak nem egyeznek meg", From 9420b81eebdfc2b4edb3d6dcf0f179c4d29e358c Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:22:32 +0100 Subject: [PATCH 142/445] Rename mysterious _ to beforeFirstSlash --- src/PosthogAnalytics.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 66f17a4937..7331b2edd1 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -100,7 +100,7 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p if (hash == "") { hashStr = ""; } else { - let [_, screen, ...parts] = hash.split("/"); + let [beforeFirstSlash, screen, ...parts] = hash.split("/"); if (!whitelistedScreens.has(screen)) { screen = ""; @@ -110,7 +110,7 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p parts[i] = anonymity === Anonymity.Anonymous ? `` : await hashHex(parts[i]); } - hashStr = `${_}/${screen}/${parts.join("/")}`; + hashStr = `${beforeFirstSlash}/${screen}/${parts.join("/")}`; } return origin + pathname + hashStr; } From 60bc283455f22e3bbf640b538e9590a28dea4ef7 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:22:57 +0100 Subject: [PATCH 143/445] Add return type to getRedactedCurrentLocation --- src/PosthogAnalytics.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 7331b2edd1..9ba33e37c1 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -88,7 +88,12 @@ const whitelistedScreens = new Set([ "start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group", ]); -export async function getRedactedCurrentLocation(origin: string, hash: string, pathname: string, anonymity: Anonymity) { +export async function getRedactedCurrentLocation( + origin: string, + hash: string, + pathname: string, + anonymity: Anonymity, +): Promise { // Redact PII from the current location. // If anonymous is true, redact entirely, if false, substitute it with a hash. // For known screens, assumes a URL structure of //might/be/pii From a687bab52f6e5bcd66b9883a9a519b1fff54714e Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:38:41 +0100 Subject: [PATCH 144/445] Use readonly shorthand for posthog param --- src/PosthogAnalytics.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 9ba33e37c1..d10ea01f4e 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -138,7 +138,6 @@ export class PosthogAnalytics { */ private anonymity = Anonymity.Anonymous; - private posthog?: PostHog = null; // set true during the constructor if posthog config is present, otherwise false private enabled = false; private static _instance = null; @@ -151,8 +150,7 @@ export class PosthogAnalytics { return this._instance; } - constructor(posthog: PostHog) { - this.posthog = posthog; + constructor(private readonly posthog: PostHog) { const posthogConfig = SdkConfig.get()["posthog"]; if (posthogConfig) { this.posthog.init(posthogConfig.projectApiKey, { From 07b9d6b30b7468c6030cf5be71e89ec1854d9494 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Jul 2021 14:39:19 +0100 Subject: [PATCH 145/445] Fix styling of setting flag descriptions in preferences and add description to spaces all/home setting and make it an account setting rather than device one and hide it from the Beta card --- res/css/views/settings/tabs/_SettingsTab.scss | 7 +++++++ .../settings/tabs/user/PreferencesUserSettingsTab.tsx | 10 ++++++++++ src/i18n/strings/en_EN.json | 1 + src/settings/Settings.tsx | 6 ++---- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index 0d679af4e5..804a06186d 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -73,6 +73,13 @@ limitations under the License. padding-right: 10px; } +.mx_SettingsTab_section .mx_SettingsFlag .mx_SettingsFlag_microcopy { + margin-top: 4px; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; +} + .mx_SettingsTab_section .mx_SettingsFlag .mx_ToggleSwitch { float: right; } diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 2e5db59d9b..53d8d41f69 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -26,6 +26,7 @@ import { replaceableComponent } from "../../../../../utils/replaceableComponent" import SettingsFlag from '../../../elements/SettingsFlag'; import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts"; import AccessibleButton from "../../../elements/AccessibleButton"; +import SpaceStore from "../../../../../stores/SpaceStore"; interface IState { autoLaunch: boolean; @@ -47,6 +48,10 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta 'breadcrumbs', ]; + static SPACES_SETTINGS = [ + "Spaces.all_rooms_in_home", + ]; + static KEYBINDINGS_SETTINGS = [ 'ctrlFForSearch', ]; @@ -231,6 +236,11 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta { this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }

+ { SpaceStore.spacesEnabled &&
+ { _t("Spaces") } + { this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS) } +
} +
{ _t("Keyboard shortcuts") } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2cd2a096ad..4a728b72b6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -868,6 +868,7 @@ "IRC display name width": "IRC display name width", "Show chat effects (animations when receiving e.g. confetti)": "Show chat effects (animations when receiving e.g. confetti)", "Show all rooms in Home": "Show all rooms in Home", + "All rooms you're in will appear in Home.": "All rooms you're in will appear in Home.", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading logs": "Uploading logs", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index dfd6f1eec9..290f5de789 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -179,9 +179,6 @@ export const SETTINGS: {[setting: string]: ISetting} = { feedbackSubheading: _td("Your feedback will help make spaces better. " + "The more detail you can go into, the better."), feedbackLabel: "spaces-feedback", - extraSettings: [ - "Spaces.all_rooms_in_home", - ], }, }, "feature_dnd": { @@ -753,7 +750,8 @@ export const SETTINGS: {[setting: string]: ISetting} = { }, "Spaces.all_rooms_in_home": { displayName: _td("Show all rooms in Home"), - supportedLevels: LEVELS_FEATURE, + description: _td("All rooms you're in will appear in Home."), + supportedLevels: LEVELS_ACCOUNT_SETTINGS, default: false, }, [UIFeature.RoomHistorySettings]: { From df6d772d8d1df34988884d2af3acb6664657f8e0 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:40:32 +0100 Subject: [PATCH 146/445] Pin posthog version We'd like to manually review each posthog change to avoid unanticipated tracking leakages; each upgrade should include reviewing the data coming in on events --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5ecbb31a9..bd989ac9bc 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "pako": "^2.0.3", "parse5": "^6.0.1", "png-chunks-extract": "^1.0.0", - "posthog-js": "^1.12.1", + "posthog-js": "1.12.1", "prop-types": "^15.7.2", "qrcode": "^1.4.4", "re-resizable": "^6.9.0", From 279871ce01f6e9c01dc5d5f691bcb01b0afb9c90 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:44:16 +0100 Subject: [PATCH 147/445] Add types --- src/PosthogAnalytics.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index d10ea01f4e..345f778c89 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -120,6 +120,11 @@ export async function getRedactedCurrentLocation( return origin + pathname + hashStr; } +interface PlatformProperties { + appVersion: string, + appPlatform: string +} + export class PosthogAnalytics { /* Wrapper for Posthog analytics. * 3 modes of anonymity are supported, governed by this.anonymity @@ -227,13 +232,13 @@ export class PosthogAnalytics { return anonymity; } - private registerSuperProperties(properties) { + private registerSuperProperties(properties: posthog.Properties) { if (this.enabled) { this.posthog.register(properties); } } - private static async getPlatformProperties() { + private static async getPlatformProperties(): Promise { const platform = PlatformPeg.get(); let appVersion; try { From ce80e5a4639cd1d698e58e229c609a50e0870743 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:44:28 +0100 Subject: [PATCH 148/445] Remove superfluous unused argument --- src/PosthogAnalytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 345f778c89..7aae756894 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -178,7 +178,7 @@ export class PosthogAnalytics { } } - private sanitizeProperties(properties: posthog.Properties, _: string): posthog.Properties { + private sanitizeProperties(properties: posthog.Properties): posthog.Properties { // Callback from posthog to sanitize properties before sending them to the server. // // Here we sanitize posthog's built in properties which leak PII e.g. url reporting. From 537ce40f429c0284c7ba837f6e7912238242b9cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 28 Jul 2021 16:32:55 +0200 Subject: [PATCH 149/445] Add a TODO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/VideoFeed.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 41c6b5185c..9975f70d62 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -46,6 +46,7 @@ interface IState { videoMuted: boolean; } +// TODO: We shouldn't be calling playMedia() all the time @replaceableComponent("views.voip.VideoFeed") export default class VideoFeed extends React.PureComponent { private element: HTMLVideoElement; From 13ef819ba6a36e3c3d39f4de373a32ec284495d2 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 16:42:56 +0100 Subject: [PATCH 150/445] isEnabled returns a boolean --- src/PosthogAnalytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 7aae756894..7e7703a9aa 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -264,7 +264,7 @@ export class PosthogAnalytics { this.posthog.capture(eventName, properties); } - public isEnabled() { + public isEnabled(): boolean { return this.enabled; } From b1bd5f57a4deb3674e59179e0e72962dc99d1edd Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 16:43:13 +0100 Subject: [PATCH 151/445] Document IEvent.properties, fix IWelcomeScreenLoad IEvent.properties is a placeholder that needs to be overriden by extenders for type validation to take place. IWelcomeScreenLoad should have had properties declared for it. Because it didn't, a faulty call using it was possible. --- src/PosthogAnalytics.ts | 11 ++++++----- src/components/views/auth/Welcome.tsx | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 7e7703a9aa..63bfbda72e 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -32,15 +32,15 @@ import SettingsStore from './settings/SettingsStore'; * - Otherwise, if the existing `analyticsOptIn` flag is `true`, or not present (i.e. prior to * logging in), track anonymously, i.e. redact all matrix identifiers in tracking events. * - If both flags are false, events are not sent. -*/ + */ interface IEvent { - // The event name that will be used by PostHog. - // TODO: standard format (camel case? snake? UpperCase?) + // The event name that will be used by PostHog. Event names should use snake_case. eventName: string; - // The properties of the event that will be stored in PostHog. - properties: {}; + // The properties of the event that will be stored in PostHog. This is just a placeholder, + // extending interfaces must override this with a concrete definition to do type validation. + properties: {} } export enum Anonymity { @@ -73,6 +73,7 @@ interface IPageView extends IAnonymousEvent { export interface IWelcomeScreenLoad extends IAnonymousEvent { eventName: "welcome_screen_load"; + properties: Record; } const hashHex = async (input: string): Promise => { diff --git a/src/components/views/auth/Welcome.tsx b/src/components/views/auth/Welcome.tsx index 4ba603eaf4..75bbe15411 100644 --- a/src/components/views/auth/Welcome.tsx +++ b/src/components/views/auth/Welcome.tsx @@ -76,6 +76,6 @@ export default class Welcome extends React.PureComponent { } componentDidMount() { - getAnalytics().trackAnonymousEvent("welcome_screen_load", { foo: "bar" }); + getAnalytics().trackAnonymousEvent("welcome_screen_load", {}); } } From e5d36e9a81a387022094db844c9a66835b285a2d Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 17:02:26 +0100 Subject: [PATCH 152/445] Use arrow function instead of bind --- src/PosthogAnalytics.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 63bfbda72e..8f338f6012 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -170,7 +170,7 @@ export class PosthogAnalytics { // // To raise this manually, just call .capture("$pageview") or posthog.capture_pageview. capture_pageview: false, - sanitize_properties: this.sanitizeProperties.bind(this), + sanitize_properties: this.sanitizeProperties, respect_dnt: true, }); this.enabled = true; @@ -179,7 +179,7 @@ export class PosthogAnalytics { } } - private sanitizeProperties(properties: posthog.Properties): posthog.Properties { + private sanitizeProperties = (properties: posthog.Properties): posthog.Properties => { // Callback from posthog to sanitize properties before sending them to the server. // // Here we sanitize posthog's built in properties which leak PII e.g. url reporting. @@ -206,7 +206,7 @@ export class PosthogAnalytics { } return properties; - } + }; private static getAnonymityFromSettings(): Anonymity { // determine the current anonymity level based on curernt user settings From 0a951501b2f90c62419fcbd43af6f36616f59f74 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 17:04:18 +0100 Subject: [PATCH 153/445] lint --- src/PosthogAnalytics.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 8f338f6012..6d15ca79ce 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -40,7 +40,7 @@ interface IEvent { // The properties of the event that will be stored in PostHog. This is just a placeholder, // extending interfaces must override this with a concrete definition to do type validation. - properties: {} + properties: {}; } export enum Anonymity { @@ -122,8 +122,8 @@ export async function getRedactedCurrentLocation( } interface PlatformProperties { - appVersion: string, - appPlatform: string + appVersion: string; + appPlatform: string; } export class PosthogAnalytics { From e4722ee4578dc7d06999c68b7e321731c598b1d8 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 17:14:36 +0100 Subject: [PATCH 154/445] Override posthog type definitions to point to a locally fixed type definition file Posthog's type definitions refer to types in transitive dependencies we don't want to include. Clone posthog.d.ts locally, remove the offending types from it, and provide an overriding mapping in tsconfig. If this proves annoying to maintain, posthog.d.ts could just be an empty file. --- package.json | 2 - src/@types/posthog.d.ts | 739 ++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 9 +- yarn.lock | 40 +-- 4 files changed, 748 insertions(+), 42 deletions(-) create mode 100644 src/@types/posthog.d.ts diff --git a/package.json b/package.json index bd989ac9bc..6adb6ce004 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,6 @@ "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", "@peculiar/webcrypto": "^1.1.4", "@sinonjs/fake-timers": "^7.0.2", - "@sentry/types": "^6.2.2", "@types/classnames": "^2.2.11", "@types/commonmark": "^0.27.4", "@types/counterpart": "^0.18.1", @@ -168,7 +167,6 @@ "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", "react-test-renderer": "^17.0.2", "rimraf": "^3.0.2", - "rrweb": "^0.9.9", "stylelint": "^13.9.0", "stylelint-config-standard": "^20.0.0", "stylelint-scss": "^3.18.0", diff --git a/src/@types/posthog.d.ts b/src/@types/posthog.d.ts new file mode 100644 index 0000000000..1108e2c6df --- /dev/null +++ b/src/@types/posthog.d.ts @@ -0,0 +1,739 @@ +// Type definitions for exported methods + +declare class posthog { + /** + * This function initializes a new instance of the PostHog capturing object. + * All new instances are added to the main posthog object as sub properties (such as + * posthog.library_name) and also returned by this function. To define a + * second instance on the page, you would call: + * + * posthog.init('new token', { your: 'config' }, 'library_name'); + * + * and use it like so: + * + * posthog.library_name.capture(...); + * + * @param {String} token Your PostHog API token + * @param {Object} [config] A dictionary of config options to override. See a list of default config options. + * @param {String} [name] The name for the new posthog instance that you want created + */ + static init(token: string, config?: posthog.Config, name?: string): posthog + + /** + * Clears super properties and generates a new random distinct_id for this instance. + * Useful for clearing data when a user logs out. + */ + static reset(reset_device_id?: boolean): void + + /** + * Capture an event. This is the most important and + * frequently used PostHog function. + * + * ### Usage: + * + * // capture an event named 'Registered' + * posthog.capture('Registered', {'Gender': 'Male', 'Age': 21}); + * + * // capture an event using navigator.sendBeacon + * posthog.capture('Left page', {'duration_seconds': 35}, {transport: 'sendBeacon'}); + * + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Object} [options] Optional configuration for this capture request. + * @param {String} [options.transport] Transport method for network request ('XHR' or 'sendBeacon'). + */ + static capture( + event_name: string, + properties?: posthog.Properties, + options?: { transport: 'XHR' | 'sendBeacon' } + ): posthog.CaptureResult + + /** + * Capture a page view event, which is currently ignored by the server. + * This function is called by default on page load unless the + * capture_pageview configuration variable is false. + * + * @param {String} [page] The url of the page to record. If you don't include this, it defaults to the current url. + * @api private + */ + static capture_pageview(page?: string): void + + /** + * Register a set of super properties, which are included with all + * events. This will overwrite previous super property values. + * + * ### Usage: + * + * // register 'Gender' as a super property + * posthog.register({'Gender': 'Female'}); + * + * // register several super properties when a user signs up + * posthog.register({ + * 'Email': 'jdoe@example.com', + * 'Account Type': 'Free' + * }); + * + * @param {Object} properties An associative array of properties to store about the user + * @param {Number} [days] How many days since the user's last visit to store the super properties + */ + static register(properties: posthog.Properties, days?: number): void + + /** + * Register a set of super properties only once. This will not + * overwrite previous super property values, unlike register(). + * + * ### Usage: + * + * // register a super property for the first time only + * posthog.register_once({ + * 'First Login Date': new Date().toISOString() + * }); + * + * ### Notes: + * + * If default_value is specified, current super properties + * with that value will be overwritten. + * + * @param {Object} properties An associative array of properties to store about the user + * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None' + * @param {Number} [days] How many days since the users last visit to store the super properties + */ + static register_once(properties: posthog.Properties, default_value?: posthog.Property, days?: number): void + + /** + * Delete a super property stored with the current user. + * + * @param {String} property The name of the super property to remove + */ + static unregister(property: string): void + + /** + * Identify a user with a unique ID instead of a PostHog + * randomly generated distinct_id. If the method is never called, + * then unique visitors will be identified by a UUID generated + * the first time they visit the site. + * + * If user properties are passed, they are also sent to posthog. + * + * ### Usage: + * + * posthog.identify('[user unique id]') + * posthog.identify('[user unique id]', { email: 'john@example.com' }) + * posthog.identify('[user unique id]', {}, { referral_code: '12345' }) + * + * ### Notes: + * + * You can call this function to overwrite a previously set + * unique ID for the current user. PostHog cannot translate + * between IDs at this time, so when you change a user's ID + * they will appear to be a new user. + * + * When used alone, posthog.identify will change the user's + * distinct_id to the unique ID provided. When used in tandem + * with posthog.alias, it will allow you to identify based on + * unique ID and map that back to the original, anonymous + * distinct_id given to the user upon her first arrival to your + * site (thus connecting anonymous pre-signup activity to + * post-signup activity). Though the two work together, do not + * call identify() at the same time as alias(). Calling the two + * at the same time can cause a race condition, so it is best + * practice to call identify on the original, anonymous ID + * right after you've aliased it. + * + * @param {String} [unique_id] A string that uniquely identifies a user. If not provided, the distinct_id currently in the persistent store (cookie or localStorage) will be used. + * @param {Object} [userProperties] Optional: An associative array of properties to store about the user + * @param {Object} [userPropertiesToSetOnce] Optional: An associative array of properties to store about the user. If property is previously set, this does not override that value. + */ + static identify( + unique_id?: string, + userPropertiesToSet?: posthog.Properties, + userPropertiesToSetOnce?: posthog.Properties + ): void + + /** + * Create an alias, which PostHog will use to link two distinct_ids going forward (not retroactively). + * Multiple aliases can map to the same original ID, but not vice-versa. Aliases can also be chained - the + * following is a valid scenario: + * + * posthog.alias('new_id', 'existing_id'); + * ... + * posthog.alias('newer_id', 'new_id'); + * + * If the original ID is not passed in, we will use the current distinct_id - probably the auto-generated GUID. + * + * ### Notes: + * + * The best practice is to call alias() when a unique ID is first created for a user + * (e.g., when a user first registers for an account and provides an email address). + * alias() should never be called more than once for a given user, except to + * chain a newer ID to a previously new ID, as described above. + * + * @param {String} alias A unique identifier that you want to use for this user in the future. + * @param {String} [original] The current identifier being used for this user. + */ + static alias(alias: string, original?: string): posthog.CaptureResult | number + + /** + * Update the configuration of a posthog library instance. + * + * The default config is: + * + * { + * // HTTP method for capturing requests + * api_method: 'POST' + * + * // transport for sending requests ('XHR' or 'sendBeacon') + * // NB: sendBeacon should only be used for scenarios such as + * // page unload where a "best-effort" attempt to send is + * // acceptable; the sendBeacon API does not support callbacks + * // or any way to know the result of the request. PostHog + * // capturing via sendBeacon will not support any event- + * // batching or retry mechanisms. + * api_transport: 'XHR' + * + * // Automatically capture clicks, form submissions and change events + * autocapture: true + * + * // Capture rage clicks (beta) - useful for session recording + * rageclick: false + * + * // super properties cookie expiration (in days) + * cookie_expiration: 365 + * + * // super properties span subdomains + * cross_subdomain_cookie: true + * + * // debug mode + * debug: false + * + * // if this is true, the posthog cookie or localStorage entry + * // will be deleted, and no user persistence will take place + * disable_persistence: false + * + * // if this is true, PostHog will automatically determine + * // City, Region and Country data using the IP address of + * //the client + * ip: true + * + * // opt users out of capturing by this PostHog instance by default + * opt_out_capturing_by_default: false + * + * // opt users out of browser data storage by this PostHog instance by default + * opt_out_persistence_by_default: false + * + * // persistence mechanism used by opt-in/opt-out methods - cookie + * // or localStorage - falls back to cookie if localStorage is unavailable + * opt_out_capturing_persistence_type: 'localStorage' + * + * // customize the name of cookie/localStorage set by opt-in/opt-out methods + * opt_out_capturing_cookie_prefix: null + * + * // type of persistent store for super properties (cookie/ + * // localStorage) if set to 'localStorage', any existing + * // posthog cookie value with the same persistence_name + * // will be transferred to localStorage and deleted + * persistence: 'cookie' + * + * // name for super properties persistent store + * persistence_name: '' + * + * // names of properties/superproperties which should never + * // be sent with capture() calls + * property_blacklist: [] + * + * // if this is true, posthog cookies will be marked as + * // secure, meaning they will only be transmitted over https + * secure_cookie: false + * + * // should we capture a page view on page load + * capture_pageview: true + * + * // if you set upgrade to be true, the library will check for + * // a cookie from our old js library and import super + * // properties from it, then the old cookie is deleted + * // The upgrade config option only works in the initialization, + * // so make sure you set it when you create the library. + * upgrade: false + * + * // extra HTTP request headers to set for each API request, in + * // the format {'Header-Name': value} + * xhr_headers: {} + * + * // protocol for fetching in-app message resources, e.g. + * // 'https://' or 'http://'; defaults to '//' (which defers to the + * // current page's protocol) + * inapp_protocol: '//' + * + * // whether to open in-app message link in new tab/window + * inapp_link_new_window: false + * + * // a set of rrweb config options that PostHog users can configure + * // see https://github.com/rrweb-io/rrweb/blob/master/guide.md + * session_recording: { + * blockClass: 'ph-no-capture', + * blockSelector: null, + * ignoreClass: 'ph-ignore-input', + * maskAllInputs: false, + * maskInputOptions: {}, + * maskInputFn: null, + * slimDOMOptions: {}, + * collectFonts: false + * } + * + * // prevent autocapture from capturing any attribute names on elements + * mask_all_element_attributes: false + * + * // prevent autocapture from capturing textContent on all elements + * mask_all_text: false + * + * // will disable requests to the /decide endpoint (please review documentation for details) + * // autocapture, feature flags, compression and session recording will be disabled when set to `true` + * advanced_disable_decide: false + * + * } + * + * + * @param {Object} config A dictionary of new configuration values to update + */ + static set_config(config: posthog.Config): void + + /** + * returns the current config object for the library. + */ + static get_config(prop_name: T): posthog.Config[T] + + /** + * Returns the value of the super property named property_name. If no such + * property is set, get_property() will return the undefined value. + * + * ### Notes: + * + * get_property() can only be called after the PostHog library has finished loading. + * init() has a loaded function available to handle this automatically. For example: + * + * // grab value for 'user_id' after the posthog library has loaded + * posthog.init('YOUR PROJECT TOKEN', { + * loaded: function(posthog) { + * user_id = posthog.get_property('user_id'); + * } + * }); + * + * @param {String} property_name The name of the super property you want to retrieve + */ + static get_property(property_name: string): posthog.Property | undefined + + /** + * Returns the current distinct id of the user. This is either the id automatically + * generated by the library or the id that has been passed by a call to identify(). + * + * ### Notes: + * + * get_distinct_id() can only be called after the PostHog library has finished loading. + * init() has a loaded function available to handle this automatically. For example: + * + * // set distinct_id after the posthog library has loaded + * posthog.init('YOUR PROJECT TOKEN', { + * loaded: function(posthog) { + * distinct_id = posthog.get_distinct_id(); + * } + * }); + */ + static get_distinct_id(): string + + /** + * Opt the user out of data capturing and cookies/localstorage for this PostHog instance + * + * ### Usage + * + * // opt user out + * posthog.opt_out_capturing(); + * + * // opt user out with different cookie configuration from PostHog instance + * posthog.opt_out_capturing({ + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {boolean} [options.clear_persistence=true] If true, will delete all data stored by the sdk in persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this PostHog instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this PostHog instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this PostHog instance's config) + */ + static opt_out_capturing(options?: posthog.OptInOutCapturingOptions): void + + /** + * Opt the user in to data capturing and cookies/localstorage for this PostHog instance + * + * ### Usage + * + * // opt user in + * posthog.opt_in_capturing(); + * + * // opt user in with specific event name, properties, cookie configuration + * posthog.opt_in_capturing({ + * capture_event_name: 'User opted in', + * capture_event_properties: { + * 'Email': 'jdoe@example.com' + * }, + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {function} [options.capture] Function used for capturing a PostHog event to record the opt-in action (default is this PostHog instance's capture method) + * @param {string} [options.capture_event_name=$opt_in] Event name to be used for capturing the opt-in action + * @param {Object} [options.capture_properties] Set of properties to be captured along with the opt-in action + * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this PostHog instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this PostHog instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this PostHog instance's config) + */ + static opt_in_capturing(options?: posthog.OptInOutCapturingOptions): void + + /** + * Check whether the user has opted out of data capturing and cookies/localstorage for this PostHog instance + * + * ### Usage + * + * const has_opted_out = posthog.has_opted_out_capturing(); + * // use has_opted_out value + * + * @param {Object} [options] A dictionary of config options to override + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @returns {boolean} current opt-out status + */ + static has_opted_out_capturing(options?: posthog.HasOptedInOutCapturingOptions): boolean + + /** + * Check whether the user has opted in to data capturing and cookies/localstorage for this PostHog instance + * + * ### Usage + * + * const has_opted_in = posthog.has_opted_in_capturing(); + * // use has_opted_in value + * + * @param {Object} [options] A dictionary of config options to override + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @returns {boolean} current opt-in status + */ + static has_opted_in_capturing(options?: posthog.HasOptedInOutCapturingOptions): boolean + + /** + * Clear the user's opt in/out status of data capturing and cookies/localstorage for this PostHog instance + * + * ### Usage + * + * // clear user's opt-in/out status + * posthog.clear_opt_in_out_capturing(); + * + * // clear user's opt-in/out status with specific cookie configuration - should match + * // configuration used when opt_in_capturing/opt_out_capturing methods were called. + * posthog.clear_opt_in_out_capturing({ + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this PostHog instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this PostHog instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this PostHog instance's config) + */ + static clear_opt_in_out_capturing(options?: posthog.ClearOptInOutCapturingOptions): void + + /* + * See if feature flag is enabled for user. + * + * ### Usage: + * + * if(posthog.isFeatureEnabled('beta-feature')) { // do something } + * + * @param {Object|String} prop Key of the feature flag. + * @param {Object|String} options (optional) If {send_event: false}, we won't send an $feature_flag_call event to PostHog. + */ + static isFeatureEnabled(key: string, options?: posthog.isFeatureEnabledOptions): boolean + + /* + * See if feature flags are available. + * + * ### Usage: + * + * posthog.onFeatureFlags(function(featureFlags) { // do something }) + * + * @param {Function} [callback] The callback function will be called once the feature flags are ready. It'll return a list of feature flags enabled for the user. + */ + static onFeatureFlags(callback: (flags: string[]) => void): false | undefined + + /* + * Reload all feature flags for the user. + * + * ### Usage: + * + * posthog.reloadFeatureFlags() + */ + static reloadFeatureFlags(): void + + static toString(): string + + /* Will log all capture requests to the Javascript console, including event properties for easy debugging */ + static debug(): void + + /* + * Starts session recording and updates disable_session_recording to false. + * Used for manual session recording management. By default, session recording is enabled and + * starts automatically. + * + * ### Usage: + * + * posthog.startSessionRecording() + */ + static startSessionRecording(): void + + /* + * Stops session recording and updates disable_session_recording to true. + * + * ### Usage: + * + * posthog.stopSessionRecording() + */ + static stopSessionRecording(): void + + /* + * Check if session recording is currently running. + * + * ### Usage: + * + * const isSessionRecordingOn = posthog.sessionRecordingStarted() + */ + static sessionRecordingStarted(): boolean +} + +declare namespace posthog { + /* eslint-disable @typescript-eslint/no-explicit-any */ + type Property = any; + type Properties = Record; + type CaptureResult = { event: string; properties: Properties } | undefined; + type CaptureCallback = (response: any, data: any) => void; + /* eslint-enable @typescript-eslint/no-explicit-any */ + + interface Config { + api_host?: string + api_method?: string + api_transport?: string + autocapture?: boolean + rageclick?: boolean + cdn?: string + cross_subdomain_cookie?: boolean + persistence?: 'localStorage' | 'cookie' | 'memory' + persistence_name?: string + cookie_name?: string + loaded?: (posthog_instance: typeof posthog) => void + store_google?: boolean + save_referrer?: boolean + test?: boolean + verbose?: boolean + img?: boolean + capture_pageview?: boolean + debug?: boolean + cookie_expiration?: number + upgrade?: boolean + disable_session_recording?: boolean + disable_persistence?: boolean + disable_cookie?: boolean + secure_cookie?: boolean + ip?: boolean + opt_out_capturing_by_default?: boolean + opt_out_persistence_by_default?: boolean + opt_out_capturing_persistence_type?: 'localStorage' | 'cookie' + opt_out_capturing_cookie_prefix?: string | null + respect_dnt?: boolean + property_blacklist?: string[] + xhr_headers?: { [header_name: string]: string } + inapp_protocol?: string + inapp_link_new_window?: boolean + request_batching?: boolean + sanitize_properties?: (properties: posthog.Properties, event_name: string) => posthog.Properties + properties_string_max_length?: number + mask_all_element_attributes?: boolean + mask_all_text?: boolean + advanced_disable_decide?: boolean + } + + interface OptInOutCapturingOptions { + clear_persistence: boolean + persistence_type: string + cookie_prefix: string + cookie_expiration: number + cross_subdomain_cookie: boolean + secure_cookie: boolean + } + + interface HasOptedInOutCapturingOptions { + persistence_type: string + cookie_prefix: string + } + + interface ClearOptInOutCapturingOptions { + enable_persistence: boolean + persistence_type: string + cookie_prefix: string + cookie_expiration: number + cross_subdomain_cookie: boolean + secure_cookie: boolean + } + + interface isFeatureEnabledOptions { + send_event: boolean + } + + export class persistence { + static properties(): posthog.Properties + + static load(): void + + static save(): void + + static remove(): void + + static clear(): void + + /** + * @param {Object} props + * @param {*=} default_value + * @param {number=} days + */ + static register_once(props: Properties, default_value?: Property, days?: number): boolean + + /** + * @param {Object} props + * @param {number=} days + */ + static register(props: posthog.Properties, days?: number): boolean + + static unregister(prop: string): void + + static update_campaign_params(): void + + static update_search_keyword(referrer: string): void + + static update_referrer_info(referrer: string): void + + static get_referrer_info(): posthog.Properties + + static safe_merge(props: posthog.Properties): posthog.Properties + + static update_config(config: posthog.Config): void + + static set_disabled(disabled: boolean): void + + static set_cross_subdomain(cross_subdomain: boolean): void + + static get_cross_subdomain(): boolean + + static set_secure(secure: boolean): void + + static set_event_timer(event_name: string, timestamp: Date): void + + static remove_event_timer(event_name: string): Date | undefined + } + + export class people { + /* + * Set properties on a user record. + * + * ### Usage: + * + * posthog.people.set('gender', 'm'); + * + * // or set multiple properties at once + * posthog.people.set({ + * 'Company': 'Acme', + * 'Plan': 'Premium', + * 'Upgrade date': new Date() + * }); + * // properties can be strings, integers, dates, or lists + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [to] A value to set on the given property name + * @param {Function} [callback] If provided, the callback will be called after capturing the event. + */ + static set( + prop: posthog.Properties | string, + to?: posthog.Property, + callback?: posthog.CaptureCallback + ): posthog.Properties + + /* + * Set properties on a user record, only if they do not yet exist. + * This will not overwrite previous people property values, unlike + * people.set(). + * + * ### Usage: + * + * posthog.people.set_once('First Login Date', new Date()); + * + * // or set multiple properties at once + * posthog.people.set_once({ + * 'First Login Date': new Date(), + * 'Starting Plan': 'Premium' + * }); + * + * // properties can be strings, integers or dates + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [to] A value to set on the given property name + * @param {Function} [callback] If provided, the callback will be called after capturing the event. + */ + static set_once( + prop: posthog.Properties | string, + to?: posthog.Property, + callback?: posthog.CaptureCallback + ): posthog.Properties + + static toString(): string + } + + export class featureFlags { + static getFlags(): string[] + + static reloadFeatureFlags(): void + + /* + * See if feature flag is enabled for user. + * + * ### Usage: + * + * if(posthog.isFeatureEnabled('beta-feature')) { // do something } + * + * @param {Object|String} prop Key of the feature flag. + * @param {Object|String} options (optional) If {send_event: false}, we won't send an $feature_flag_call event to PostHog. + */ + static isFeatureEnabled(key: string, options?: { send_event?: boolean }): boolean + + /* + * See if feature flags are available. + * + * ### Usage: + * + * posthog.onFeatureFlags(function(featureFlags) { // do something }) + * + * @param {Function} [callback] The callback function will be called once the feature flags are ready. It'll return a list of feature flags enabled for the user. + */ + static onFeatureFlags(callback: (flags: string[]) => void): false | undefined + } + + export class feature_flags extends featureFlags {} +} + +export type PostHog = typeof posthog; + +export default posthog; diff --git a/tsconfig.json b/tsconfig.json index b139e8e8d1..b982d40b07 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,10 +22,15 @@ "es2019", "dom", "dom.iterable" - ] + ], + "paths": { + "posthog-js": [ + "./src/@types/posthog.d.ts" + ] + } }, "include": [ "./src/**/*.ts", "./src/**/*.tsx" - ] + ], } diff --git a/yarn.lock b/yarn.lock index 78f4838a09..633bf99ee6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1352,11 +1352,6 @@ tslib "^2.2.0" webcrypto-core "^1.2.0" -"@sentry/types@^6.2.2": - version "6.10.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.10.0.tgz#6b1f44e5ed4dbc2710bead24d1b32fb08daf04e1" - integrity sha512-M7s0JFgG7/6/yNVYoPUbxzaXDhnzyIQYRRJJKRaTD77YO4MHvi4Ke8alBWqD5fer0cPIfcSkBqa9BLdqRqcMWw== - "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -1453,11 +1448,6 @@ resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.1.tgz#b1b784d9e54d9879f0a8cb12f2caedab65430fe8" integrity sha512-PRuFlBBkvdDOtxlIASzTmkEFar+S66Ek48NVVTWMUjtJAdn5vyMSN8y6IZIoIymGpR36q2nZbIYazBWyFxL+IQ== -"@types/css-font-loading-module@0.0.4": - version "0.0.4" - resolved "https://registry.yarnpkg.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.4.tgz#94a835e27d1af444c65cba88523533c174463d64" - integrity sha512-ENdXf7MW4m9HeDojB2Ukbi7lYMIuQNBHVf98dbzaiG4EEJREBd6oleVAjrLRCrp7dm6CK1mmdmU9tcgF61acbw== - "@types/css-font-loading-module@^0.0.6": version "0.0.6" resolved "https://registry.yarnpkg.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.6.tgz#1ac3417ed31eeb953134d29b56bca921644b87c0" @@ -1800,11 +1790,6 @@ object.fromentries "^2.0.0" prop-types "^15.7.0" -"@xstate/fsm@^1.4.0": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@xstate/fsm/-/fsm-1.6.1.tgz#c92972b835540c4e3c5e14277f40dbcbdaee9571" - integrity sha512-xYKDNuPR36/fUK+jmhM+oauBmbdUAfuJKnDjg3/7NbN+Pj03TX7e94LXnzkwGgAR+U/HWoMqM5UPTuGIYfIx9g== - abab@^2.0.3, abab@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" @@ -3616,7 +3601,7 @@ fbjs@^0.8.4: setimmediate "^1.0.5" ua-parser-js "^0.7.18" -fflate@^0.4.1, fflate@^0.4.4: +fflate@^0.4.1: version "0.4.8" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== @@ -5654,11 +5639,6 @@ minimist@>=1.2.2, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -mitt@^1.1.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mitt/-/mitt-1.2.0.tgz#cb24e6569c806e31bd4e3995787fe38a04fdf90d" - integrity sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw== - mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" @@ -6274,7 +6254,7 @@ postcss@^8.0.2: nanoid "^3.1.23" source-map-js "^0.6.2" -posthog-js@^1.12.1: +posthog-js@1.12.1: version "1.12.1" resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.12.1.tgz#97834ee2574f34ffb5db2f5b07452c847e3c4d27" integrity sha512-Y3lzcWkS8xFY6Ryj3I4ees7qWP2WGkLw0Arcbk5xaT0+5YlA6UC2jlL/+fN9bz/Bl62EoN3BML901Cuot/QNjg== @@ -6861,22 +6841,6 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" -rrweb-snapshot@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.1.7.tgz#92a3b47b1112a1b566c2fae2edb02fa48a6f6653" - integrity sha512-+f2kCCvIQ1hbEeCWnV7mPVPDEdWEExqwcYqMd/r1nfK52QE7qU52jefUOyTe85Vy67rZGqWnfK/B25e/OTSgYg== - -rrweb@^0.9.9: - version "0.9.14" - resolved "https://registry.yarnpkg.com/rrweb/-/rrweb-0.9.14.tgz#09bec604fc44c74801e4fe910606e5a6cde008ec" - integrity sha512-nm2rrVNoyWFPrbGQmcvTTlA7XjbbgPIgO7qsW0Zyr5iOURIFJDGPHFmOVLRyLpWiriVtEoXh6a+x+D1sj+qwWg== - dependencies: - "@types/css-font-loading-module" "0.0.4" - "@xstate/fsm" "^1.4.0" - fflate "^0.4.4" - mitt "^1.1.3" - rrweb-snapshot "^1.0.3" - rst-selector-parser@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" From 7b4a7711b2c05b68fcabdae88b3674046c55036a Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 17:20:22 +0100 Subject: [PATCH 155/445] Declare return types for all public methods, even void ones --- src/PosthogAnalytics.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 6d15ca79ce..d5bb12621d 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -269,7 +269,7 @@ export class PosthogAnalytics { return this.enabled; } - public setAnonymity(anonymity: Anonymity) { + public setAnonymity(anonymity: Anonymity): void { // Update this.anonymity. // This is public for testing purposes, typically you want to call updateAnonymityFromSettings // to ensure this value is in step with the user's settings. @@ -283,17 +283,17 @@ export class PosthogAnalytics { this.anonymity = anonymity; } - public async identifyUser(userId: string) { + public async identifyUser(userId: string): Promise { if (this.anonymity == Anonymity.Pseudonymous) { this.posthog.identify(await hashHex(userId)); } } - public getAnonymity() { + public getAnonymity(): Anonymity { return this.anonymity; } - public logout() { + public logout(): void { if (this.enabled) { this.posthog.reset(); } @@ -311,7 +311,7 @@ export class PosthogAnalytics { public async trackAnonymousEvent( eventName: E["eventName"], properties: E["properties"], - ) { + ): Promise { if (this.anonymity == Anonymity.Disabled) return; await this.capture(eventName, properties); } @@ -320,7 +320,7 @@ export class PosthogAnalytics { eventName: E["eventName"], roomId: string, properties: Omit, - ) { + ): Promise { const updatedProperties = { ...properties, hashedRoomId: roomId ? await hashHex(roomId) : null, @@ -328,7 +328,7 @@ export class PosthogAnalytics { await this.trackPseudonymousEvent(eventName, updatedProperties); } - public async trackPageView(durationMs: number) { + public async trackPageView(durationMs: number): Promise { const hash = window.location.hash; let screen = null; @@ -343,7 +343,7 @@ export class PosthogAnalytics { }); } - public async updatePlatformSuperProperties() { + public async updatePlatformSuperProperties(): Promise { // Update super properties in posthog with our platform (app version, platform). // These properties will be subsequently passed in every event. // @@ -353,7 +353,7 @@ export class PosthogAnalytics { this.registerSuperProperties(this.platformSuperProperties); } - public async updateAnonymityFromSettings(userId?: string) { + public async updateAnonymityFromSettings(userId?: string): Promise { // Update this.anonymity based on the user's analytics opt-in settings // Identify the user (via hashed user ID) to posthog if anonymity is pseudonmyous this.setAnonymity(PosthogAnalytics.getAnonymityFromSettings()); From d401789f9ec3e284053bd5d8b00bf0723582c469 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 17:26:24 +0100 Subject: [PATCH 156/445] Ignore eslint conventions in disastrous posthog type definitions --- src/@types/posthog.d.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/@types/posthog.d.ts b/src/@types/posthog.d.ts index 1108e2c6df..1ca475cd3b 100644 --- a/src/@types/posthog.d.ts +++ b/src/@types/posthog.d.ts @@ -1,3 +1,12 @@ +// A clone of the type definitions from posthog-js, stripped of references to transitive +// dependencies which we don't actually use, so that we don't need to install them. +// +// Original file lives in node_modules/posthog/dist/module.d.ts + +/* eslint-disable @typescript-eslint/member-delimiter-style */ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable camelcase */ + // Type definitions for exported methods declare class posthog { From cdf0d98c3fca269a0be32fb6caaf12846dc6bd70 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Jul 2021 17:39:02 +0100 Subject: [PATCH 157/445] Fix IconizedContextMenuCheckbox layout --- .../views/context_menus/_IconizedContextMenu.scss | 13 +++++++++---- .../views/context_menus/IconizedContextMenu.tsx | 5 ++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/res/css/views/context_menus/_IconizedContextMenu.scss b/res/css/views/context_menus/_IconizedContextMenu.scss index 204435995f..f83699b505 100644 --- a/res/css/views/context_menus/_IconizedContextMenu.scss +++ b/res/css/views/context_menus/_IconizedContextMenu.scss @@ -145,12 +145,17 @@ limitations under the License. } } - .mx_IconizedContextMenu_checked { + .mx_IconizedContextMenu_checked, + .mx_IconizedContextMenu_unchecked { margin-left: 16px; margin-right: -5px; + } - &::before { - mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); - } + .mx_IconizedContextMenu_checked::before { + mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); + } + + .mx_IconizedContextMenu_unchecked::before { + content: unset; } } diff --git a/src/components/views/context_menus/IconizedContextMenu.tsx b/src/components/views/context_menus/IconizedContextMenu.tsx index 1d822fd246..7ad07f0466 100644 --- a/src/components/views/context_menus/IconizedContextMenu.tsx +++ b/src/components/views/context_menus/IconizedContextMenu.tsx @@ -86,7 +86,10 @@ export const IconizedContextMenuCheckbox: React.FC = ({ > { label } - { active && } + ; }; From b3a28bde8966e3b07106445de3d89312c5186f6d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Jul 2021 17:39:56 +0100 Subject: [PATCH 158/445] Factor out useEventEmitterState hook --- src/components/views/spaces/SpacePanel.tsx | 15 +++++++++------ src/hooks/useEventEmitter.ts | 13 ++++++++++++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 8223d84dbb..3bb8d8e3d2 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -117,12 +117,15 @@ const SpaceButton: React.FC = ({ }; const useSpaces = (): [Room[], Room[], Room | null] => { - const [invites, setInvites] = useState(SpaceStore.instance.invitedSpaces); - useEventEmitter(SpaceStore.instance, UPDATE_INVITED_SPACES, setInvites); - const [spaces, setSpaces] = useState(SpaceStore.instance.spacePanelSpaces); - useEventEmitter(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, setSpaces); - const [activeSpace, setActiveSpace] = useState(SpaceStore.instance.activeSpace); - useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, setActiveSpace); + const invites = useEventEmitterState(SpaceStore.instance, UPDATE_INVITED_SPACES, () => { + return SpaceStore.instance.invitedSpaces; + }); + const spaces = useEventEmitterState(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, () => { + return SpaceStore.instance.spacePanelSpaces; + }); + const activeSpace = useEventEmitterState(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => { + return SpaceStore.instance.activeSpace; + }); return [invites, spaces, activeSpace]; }; diff --git a/src/hooks/useEventEmitter.ts b/src/hooks/useEventEmitter.ts index a81bba5699..74b23f0198 100644 --- a/src/hooks/useEventEmitter.ts +++ b/src/hooks/useEventEmitter.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useRef, useEffect } from "react"; +import { useRef, useEffect, useState, useCallback } from "react"; import type { EventEmitter } from "events"; type Handler = (...args: any[]) => void; @@ -48,3 +48,14 @@ export const useEventEmitter = (emitter: EventEmitter, eventName: string | symbo [eventName, emitter], // Re-run if eventName or emitter changes ); }; + +type Mapper = (...args: any[]) => T; + +export const useEventEmitterState = (emitter: EventEmitter, eventName: string | symbol, fn: Mapper): T => { + const [value, setValue] = useState(fn()); + const handler = useCallback((...args: any[]) => { + setValue(fn(...args)); + }, [fn]); + useEventEmitter(emitter, eventName, handler); + return value; +}; From 67ef263940b990959d435deec37b60415d9596e4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Jul 2021 17:40:33 +0100 Subject: [PATCH 159/445] Refactor SpaceButton to be more reusable and add context menu to Home button --- res/css/structures/_SpacePanel.scss | 2 +- src/components/structures/ContextMenu.tsx | 10 +- .../views/context_menus/SpaceContextMenu.tsx | 201 ++++++++++ src/components/views/spaces/SpacePanel.tsx | 242 ++++++------ .../views/spaces/SpaceTreeLevel.tsx | 366 ++++++------------ src/i18n/strings/en_EN.json | 21 +- 6 files changed, 444 insertions(+), 398 deletions(-) create mode 100644 src/components/views/context_menus/SpaceContextMenu.tsx diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index e64057d16c..9d9c3ff8ab 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -297,7 +297,7 @@ $activeBorderColor: $secondary-fg-color; .mx_SpaceButton:hover, .mx_SpaceButton:focus-within, .mx_SpaceButton_hasMenuOpen { - &:not(.mx_SpaceButton_home):not(.mx_SpaceButton_invite) { + &:not(.mx_SpaceButton_invite) { // Hide the badge container on hover because it'll be a menu button .mx_SpacePanel_badgeContainer { width: 0; diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 407dc6f04c..0822d3768b 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { CSSProperties, RefObject, useRef, useState } from "react"; +import React, { CSSProperties, RefObject, SyntheticEvent, useRef, useState } from "react"; import ReactDOM from "react-dom"; import classNames from "classnames"; @@ -461,10 +461,14 @@ type ContextMenuTuple = [boolean, RefObject, () => void, () => void, (val: export const useContextMenu = (): ContextMenuTuple => { const button = useRef(null); const [isOpen, setIsOpen] = useState(false); - const open = () => { + const open = (ev?: SyntheticEvent) => { + ev?.preventDefault(); + ev?.stopPropagation(); setIsOpen(true); }; - const close = () => { + const close = (ev?: SyntheticEvent) => { + ev?.preventDefault(); + ev?.stopPropagation(); setIsOpen(false); }; diff --git a/src/components/views/context_menus/SpaceContextMenu.tsx b/src/components/views/context_menus/SpaceContextMenu.tsx new file mode 100644 index 0000000000..1555870f26 --- /dev/null +++ b/src/components/views/context_menus/SpaceContextMenu.tsx @@ -0,0 +1,201 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useContext } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { EventType } from "matrix-js-sdk/src/@types/event"; + +import { + IProps as IContextMenuProps, +} from "../../structures/ContextMenu"; +import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu"; +import { _t } from "../../../languageHandler"; +import { + shouldShowSpaceSettings, + showAddExistingRooms, + showCreateNewRoom, + showSpaceInvite, + showSpaceSettings, +} from "../../../utils/space"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { ButtonEvent } from "../elements/AccessibleButton"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; +import RoomViewStore from "../../../stores/RoomViewStore"; +import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; +import { Action } from "../../../dispatcher/actions"; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; + +interface IProps extends IContextMenuProps { + space: Room; +} + +const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => { + const cli = useContext(MatrixClientContext); + const userId = cli.getUserId(); + + let inviteOption; + if (space.getJoinRule() === "public" || space.canInvite(userId)) { + const onInviteClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showSpaceInvite(space); + onFinished(); + }; + + inviteOption = ( + + ); + } + + let settingsOption; + let leaveSection; + if (shouldShowSpaceSettings(space)) { + const onSettingsClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showSpaceSettings(space); + onFinished(); + }; + + settingsOption = ( + + ); + } else { + const onLeaveClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + defaultDispatcher.dispatch({ + action: "leave_room", + room_id: space.roomId, + }); + onFinished(); + }; + + leaveSection = + + ; + } + + const canAddRooms = space.currentState.maySendStateEvent(EventType.SpaceChild, userId); + + let newRoomSection; + if (space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { + const onNewRoomClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showCreateNewRoom(space); + onFinished(); + }; + + const onAddExistingRoomClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showAddExistingRooms(space); + onFinished(); + }; + + newRoomSection = + + + ; + } + + const onMembersClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + if (!RoomViewStore.getRoomId()) { + defaultDispatcher.dispatch({ + action: "view_room", + room_id: space.roomId, + }, true); + } + + defaultDispatcher.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.SpaceMemberList, + refireParams: { space: space }, + }); + onFinished(); + }; + + const onExploreRoomsClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + defaultDispatcher.dispatch({ + action: "view_room", + room_id: space.roomId, + }); + onFinished(); + }; + + return +
+ { space.name } +
+ + { inviteOption } + + { settingsOption } + + + { newRoomSection } + { leaveSection } +
; +}; + +export default SpaceContextMenu; + diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 3bb8d8e3d2..a339cb8132 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -14,107 +14,35 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react"; -import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; +import React, { ComponentProps, Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react"; +import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd"; import classNames from "classnames"; import { Room } from "matrix-js-sdk/src/models/room"; import { _t } from "../../../languageHandler"; -import RoomAvatar from "../avatars/RoomAvatar"; import { useContextMenu } from "../../structures/ContextMenu"; import SpaceCreateMenu from "./SpaceCreateMenu"; -import { SpaceItem } from "./SpaceTreeLevel"; +import { SpaceButton, SpaceItem } from "./SpaceTreeLevel"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; -import { useEventEmitter } from "../../../hooks/useEventEmitter"; +import { useEventEmitterState } from "../../../hooks/useEventEmitter"; import SpaceStore, { HOME_SPACE, + UPDATE_HOME_BEHAVIOUR, UPDATE_INVITED_SPACES, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES, } from "../../../stores/SpaceStore"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; -import NotificationBadge from "../rooms/NotificationBadge"; -import { - RovingAccessibleButton, - RovingAccessibleTooltipButton, - RovingTabIndexProvider, -} from "../../../accessibility/RovingTabIndex"; +import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import { Key } from "../../../Keyboard"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; -import { NotificationState } from "../../../stores/notifications/NotificationState"; - -interface IButtonProps { - space?: Room; - className?: string; - selected?: boolean; - tooltip?: string; - notificationState?: NotificationState; - isNarrow?: boolean; - onClick(): void; -} - -const SpaceButton: React.FC = ({ - space, - className, - selected, - onClick, - tooltip, - notificationState, - isNarrow, - children, -}) => { - const classes = classNames("mx_SpaceButton", className, { - mx_SpaceButton_active: selected, - mx_SpaceButton_narrow: isNarrow, - }); - - let avatar =
; - if (space) { - avatar = ; - } - - let notifBadge; - if (notificationState) { - notifBadge =
- SpaceStore.instance.setActiveRoomInSpace(space)} - forceCount={false} - notification={notificationState} - /> -
; - } - - let button; - if (isNarrow) { - button = ( - -
- { avatar } - { notifBadge } - { children } -
-
- ); - } else { - button = ( - -
- { avatar } - { tooltip } - { notifBadge } - { children } -
-
- ); - } - - return
  • - { button } -
  • ; -}; +import SpaceContextMenu from "../context_menus/SpaceContextMenu"; +import IconizedContextMenu, { + IconizedContextMenuCheckbox, + IconizedContextMenuOptionList, +} from "../context_menus/IconizedContextMenu"; +import SettingsStore from "../../../settings/SettingsStore"; +import { SettingLevel } from "../../../settings/SettingLevel"; const useSpaces = (): [Room[], Room[], Room | null] => { const invites = useEventEmitterState(SpaceStore.instance, UPDATE_INVITED_SPACES, () => { @@ -135,30 +63,108 @@ interface IInnerSpacePanelProps { setPanelCollapsed: Dispatch>; } +const HomeButtonContextMenu = ({ onFinished, ...props }: ComponentProps) => { + const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => { + return SpaceStore.instance.allRoomsInHome; + }); + + return +
    + { _t("Home") } +
    + + { + SettingsStore.setValue("Spaces.all_rooms_in_home", null, SettingLevel.ACCOUNT, !allRoomsInHome); + }} + /> + +
    ; +}; + +interface IHomeButtonProps { + selected: boolean; + isPanelCollapsed: boolean; +} + +const HomeButton = ({ selected, isPanelCollapsed }: IHomeButtonProps) => { + const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => { + return SpaceStore.instance.allRoomsInHome; + }); + + return
  • + SpaceStore.instance.setActiveSpace(null)} + selected={selected} + label={allRoomsInHome ? _t("All rooms") : _t("Home")} + notificationState={allRoomsInHome + ? RoomNotificationStateStore.instance.globalState + : SpaceStore.instance.getNotificationState(HOME_SPACE)} + isNarrow={isPanelCollapsed} + ContextMenuComponent={HomeButtonContextMenu} + contextMenuTooltip={_t("Options")} + /> +
  • ; +}; + +const CreateSpaceButton = ({ + isPanelCollapsed, + setPanelCollapsed, +}: Pick) => { + // We don't need the handle as we position the menu in a constant location + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); + + useEffect(() => { + if (!isPanelCollapsed && menuDisplayed) { + closeMenu(); + } + }, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps + + let contextMenu = null; + if (menuDisplayed) { + contextMenu = ; + } + + const onNewClick = menuDisplayed ? closeMenu : () => { + if (!isPanelCollapsed) setPanelCollapsed(true); + openMenu(); + }; + + return
  • + + + { contextMenu } +
  • ; +}; + // Optimisation based on https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md#recommended-droppable--performance-optimisation const InnerSpacePanel = React.memo(({ children, isPanelCollapsed, setPanelCollapsed }) => { const [invites, spaces, activeSpace] = useSpaces(); const activeSpaces = activeSpace ? [activeSpace] : []; - let homeTooltip: string; - let homeNotificationState: NotificationState; - if (SpaceStore.instance.allRoomsInHome) { - homeTooltip = _t("All rooms"); - homeNotificationState = RoomNotificationStateStore.instance.globalState; - } else { - homeTooltip = _t("Home"); - homeNotificationState = SpaceStore.instance.getNotificationState(HOME_SPACE); - } - return
    - SpaceStore.instance.setActiveSpace(null)} - selected={!activeSpace} - tooltip={homeTooltip} - notificationState={homeNotificationState} - isNarrow={isPanelCollapsed} - /> + { invites.map(s => ( (({ children, isPanelCo )) } { children } +
    ; }); const SpacePanel = () => { - // We don't need the handle as we position the menu in a constant location - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); const [isPanelCollapsed, setPanelCollapsed] = useState(true); - useEffect(() => { - if (!isPanelCollapsed && menuDisplayed) { - closeMenu(); - } - }, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps - - let contextMenu = null; - if (menuDisplayed) { - contextMenu = ; - } - const onKeyDown = (ev: React.KeyboardEvent) => { let handled = true; @@ -269,11 +262,6 @@ const SpacePanel = () => { } }; - const onNewClick = menuDisplayed ? closeMenu : () => { - if (!isPanelCollapsed) setPanelCollapsed(true); - openMenu(); - }; - return ( { if (!result.destination) return; // dropped outside the list @@ -301,15 +289,6 @@ const SpacePanel = () => { > { provided.placeholder } - - ) } @@ -318,7 +297,6 @@ const SpacePanel = () => { onClick={() => setPanelCollapsed(!isPanelCollapsed)} title={isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel")} /> - { contextMenu } ) } diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 90584a5361..bb2184853e 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -14,7 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, InputHTMLAttributes, LegacyRef } from "react"; +import React, { + createRef, + MouseEvent, + InputHTMLAttributes, + LegacyRef, + ComponentProps, + ComponentType, +} from "react"; import classNames from "classnames"; import { Room } from "matrix-js-sdk/src/models/room"; @@ -23,31 +30,104 @@ import SpaceStore from "../../../stores/SpaceStore"; import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore"; import NotificationBadge from "../rooms/NotificationBadge"; import { RovingAccessibleTooltipButton } from "../../../accessibility/roving/RovingAccessibleTooltipButton"; -import IconizedContextMenu, { - IconizedContextMenuOption, - IconizedContextMenuOptionList, -} from "../context_menus/IconizedContextMenu"; import { _t } from "../../../languageHandler"; import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; -import { toRightOf } from "../../structures/ContextMenu"; -import { - shouldShowSpaceSettings, - showAddExistingRooms, - showCreateNewRoom, - showSpaceInvite, - showSpaceSettings, -} from "../../../utils/space"; +import { toRightOf, useContextMenu } from "../../structures/ContextMenu"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; -import defaultDispatcher from "../../../dispatcher/dispatcher"; -import { Action } from "../../../dispatcher/actions"; -import RoomViewStore from "../../../stores/RoomViewStore"; -import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; -import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; -import { EventType } from "matrix-js-sdk/src/@types/event"; +import AccessibleButton from "../elements/AccessibleButton"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { NotificationColor } from "../../../stores/notifications/NotificationColor"; import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager"; +import { NotificationState } from "../../../stores/notifications/NotificationState"; +import SpaceContextMenu from "../context_menus/SpaceContextMenu"; + +interface IButtonProps extends Omit, "title"> { + space?: Room; + className?: string; + selected?: boolean; + label: string; + contextMenuTooltip?: string; + notificationState?: NotificationState; + isNarrow?: boolean; + avatarSize?: number; + ContextMenuComponent?: ComponentType>; + onClick(ev: MouseEvent): void; +} + +export const SpaceButton: React.FC = ({ + space, + className, + selected, + onClick, + label, + contextMenuTooltip, + notificationState, + avatarSize, + isNarrow, + children, + ContextMenuComponent, + ...props +}) => { + const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); + + let avatar =
    ; + if (space) { + avatar = ; + } + + let notifBadge; + if (notificationState) { + notifBadge =
    + SpaceStore.instance.setActiveRoomInSpace(space || null)} + forceCount={false} + notification={notificationState} + /> +
    ; + } + + let contextMenu: JSX.Element; + if (menuDisplayed && ContextMenuComponent) { + contextMenu = ; + } + + return ( + + { children } +
    + { avatar } + { !isNarrow && { label } } + { notifBadge } + + { ContextMenuComponent && } + + { contextMenu } +
    +
    + ); +}; interface IItemProps extends InputHTMLAttributes { space?: Room; @@ -61,7 +141,6 @@ interface IItemProps extends InputHTMLAttributes { interface IItemState { collapsed: boolean; - contextMenuPosition: Pick; childSpaces: Room[]; } @@ -81,7 +160,6 @@ export class SpaceItem extends React.PureComponent { this.state = { collapsed: collapsed, - contextMenuPosition: null, childSpaces: this.childSpaces, }; @@ -124,19 +202,6 @@ export class SpaceItem extends React.PureComponent { evt.stopPropagation(); }; - private onContextMenu = (ev: React.MouseEvent) => { - if (this.props.space.getMyMembership() !== "join") return; - ev.preventDefault(); - ev.stopPropagation(); - this.setState({ - contextMenuPosition: { - right: ev.clientX, - top: ev.clientY, - height: 0, - }, - }); - }; - private onKeyDown = (ev: React.KeyboardEvent) => { let handled = true; const action = getKeyBindingsManager().getRoomListAction(ev); @@ -180,188 +245,6 @@ export class SpaceItem extends React.PureComponent { SpaceStore.instance.setActiveSpace(this.props.space); }; - private onMenuOpenClick = (ev: React.MouseEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - const target = ev.target as HTMLButtonElement; - this.setState({ contextMenuPosition: target.getBoundingClientRect() }); - }; - - private onMenuClose = () => { - this.setState({ contextMenuPosition: null }); - }; - - private onInviteClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - showSpaceInvite(this.props.space); - this.setState({ contextMenuPosition: null }); // also close the menu - }; - - private onSettingsClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - showSpaceSettings(this.props.space); - this.setState({ contextMenuPosition: null }); // also close the menu - }; - - private onLeaveClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - defaultDispatcher.dispatch({ - action: "leave_room", - room_id: this.props.space.roomId, - }); - this.setState({ contextMenuPosition: null }); // also close the menu - }; - - private onNewRoomClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - showCreateNewRoom(this.props.space); - this.setState({ contextMenuPosition: null }); // also close the menu - }; - - private onAddExistingRoomClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - showAddExistingRooms(this.props.space); - this.setState({ contextMenuPosition: null }); // also close the menu - }; - - private onMembersClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - if (!RoomViewStore.getRoomId()) { - defaultDispatcher.dispatch({ - action: "view_room", - room_id: this.props.space.roomId, - }, true); - } - - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.SpaceMemberList, - refireParams: { space: this.props.space }, - }); - this.setState({ contextMenuPosition: null }); // also close the menu - }; - - private onExploreRoomsClick = (ev: ButtonEvent) => { - ev.preventDefault(); - ev.stopPropagation(); - - defaultDispatcher.dispatch({ - action: "view_room", - room_id: this.props.space.roomId, - }); - this.setState({ contextMenuPosition: null }); // also close the menu - }; - - private renderContextMenu(): React.ReactElement { - if (this.props.space.getMyMembership() !== "join") return null; - - let contextMenu = null; - if (this.state.contextMenuPosition) { - const userId = this.context.getUserId(); - - let inviteOption; - if (this.props.space.getJoinRule() === "public" || this.props.space.canInvite(userId)) { - inviteOption = ( - - ); - } - - let settingsOption; - let leaveSection; - if (shouldShowSpaceSettings(this.props.space)) { - settingsOption = ( - - ); - } else { - leaveSection = - - ; - } - - const canAddRooms = this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId); - - let newRoomSection; - if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { - newRoomSection = - - - ; - } - - contextMenu = -
    - { this.props.space.name } -
    - - { inviteOption } - - { settingsOption } - - - { newRoomSection } - { leaveSection } -
    ; - } - - return ( - - - { contextMenu } - - ); - } - render() { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { space, activeSpaces, isNested, isPanelCollapsed, onExpand, parents, innerRef, @@ -369,7 +252,6 @@ export class SpaceItem extends React.PureComponent { const collapsed = this.isCollapsed; - const isActive = activeSpaces.includes(space); const itemClasses = classNames(this.props.className, { "mx_SpaceItem": true, "mx_SpaceItem_narrow": isPanelCollapsed, @@ -378,12 +260,7 @@ export class SpaceItem extends React.PureComponent { }); const isInvite = space.getMyMembership() === "invite"; - const classes = classNames("mx_SpaceButton", { - mx_SpaceButton_active: isActive, - mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition, - mx_SpaceButton_narrow: isPanelCollapsed, - mx_SpaceButton_invite: isInvite, - }); + const notificationState = isInvite ? StaticNotificationState.forSymbol("!", NotificationColor.Red) : SpaceStore.instance.getNotificationState(space.roomId); @@ -398,19 +275,6 @@ export class SpaceItem extends React.PureComponent { />; } - let notifBadge; - if (notificationState) { - notifBadge =
    - SpaceStore.instance.setActiveRoomInSpace(space)} - forceCount={false} - notification={notificationState} - /> -
    ; - } - - const avatarSize = isNested ? 24 : 32; - const toggleCollapseButton = this.state.childSpaces?.length ? { return (
  • - { toggleCollapseButton } -
    - - { !isPanelCollapsed && { space.name } } - { notifBadge } - { this.renderContextMenu() } -
    -
    + { childItems }
  • diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4a728b72b6..fc6a58708d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1019,8 +1019,10 @@ "Address": "Address", "Creating...": "Creating...", "Create": "Create", - "All rooms": "All rooms", "Home": "Home", + "Show all rooms in home": "Show all rooms in home", + "All rooms": "All rooms", + "Options": "Options", "Expand space panel": "Expand space panel", "Collapse space panel": "Collapse space panel", "Click to copy": "Click to copy", @@ -1050,16 +1052,9 @@ "Preview Space": "Preview Space", "Allow people to preview your space before they join.": "Allow people to preview your space before they join.", "Recommended for public spaces.": "Recommended for public spaces.", - "Settings": "Settings", - "Leave space": "Leave space", - "Create new room": "Create new room", - "Add existing room": "Add existing room", - "Members": "Members", - "Manage & explore rooms": "Manage & explore rooms", - "Explore rooms": "Explore rooms", - "Space options": "Space options", "Expand": "Expand", "Collapse": "Collapse", + "Space options": "Space options", "Remove": "Remove", "This bridge was provisioned by .": "This bridge was provisioned by .", "This bridge is managed by .": "This bridge is managed by .", @@ -1583,8 +1578,11 @@ "Start chat": "Start chat", "Rooms": "Rooms", "Add room": "Add room", + "Create new room": "Create new room", "You do not have permissions to create new rooms in this space": "You do not have permissions to create new rooms in this space", + "Add existing room": "Add existing room", "You do not have permissions to add rooms to this space": "You do not have permissions to add rooms to this space", + "Explore rooms": "Explore rooms", "Explore community rooms": "Explore community rooms", "Explore public rooms": "Explore public rooms", "Low priority": "Low priority", @@ -1662,6 +1660,7 @@ "Low Priority": "Low Priority", "Invite People": "Invite People", "Copy Room Link": "Copy Room Link", + "Settings": "Settings", "Leave Room": "Leave Room", "Room options": "Room options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", @@ -1755,13 +1754,13 @@ "The homeserver the user you’re verifying is connected to": "The homeserver the user you’re verifying is connected to", "Yours, or the other users’ internet connection": "Yours, or the other users’ internet connection", "Yours, or the other users’ session": "Yours, or the other users’ session", + "Members": "Members", "Nothing pinned, yet": "Nothing pinned, yet", "If you have permissions, open the menu on any message and select Pin to stick them here.": "If you have permissions, open the menu on any message and select Pin to stick them here.", "Pinned messages": "Pinned messages", "Room Info": "Room Info", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", "Unpin a widget to view it in this panel": "Unpin a widget to view it in this panel", - "Options": "Options", "Set my room layout for everyone": "Set my room layout for everyone", "Widgets": "Widgets", "Edit widgets, bridges & bots": "Edit widgets, bridges & bots", @@ -2563,6 +2562,8 @@ "Source URL": "Source URL", "Collapse reply thread": "Collapse reply thread", "Report": "Report", + "Leave space": "Leave space", + "Manage & explore rooms": "Manage & explore rooms", "Clear status": "Clear status", "Update status": "Update status", "Set status": "Set status", From 07eaee25d29542a4fce9d02a976751ffc3e9c9a3 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 17:54:35 +0100 Subject: [PATCH 160/445] Default properties to {} to avoid passing it superfluously for events with no properties --- src/PosthogAnalytics.ts | 4 ++-- src/components/views/auth/Welcome.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index d5bb12621d..c8e156f2fa 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -302,7 +302,7 @@ export class PosthogAnalytics { public async trackPseudonymousEvent( eventName: E["eventName"], - properties: E["properties"], + properties: E["properties"] = {}, ) { if (this.anonymity == Anonymity.Anonymous || this.anonymity == Anonymity.Disabled) return; await this.capture(eventName, properties); @@ -310,7 +310,7 @@ export class PosthogAnalytics { public async trackAnonymousEvent( eventName: E["eventName"], - properties: E["properties"], + properties: E["properties"] = {}, ): Promise { if (this.anonymity == Anonymity.Disabled) return; await this.capture(eventName, properties); diff --git a/src/components/views/auth/Welcome.tsx b/src/components/views/auth/Welcome.tsx index 75bbe15411..7c405b0835 100644 --- a/src/components/views/auth/Welcome.tsx +++ b/src/components/views/auth/Welcome.tsx @@ -76,6 +76,6 @@ export default class Welcome extends React.PureComponent { } componentDidMount() { - getAnalytics().trackAnonymousEvent("welcome_screen_load", {}); + getAnalytics().trackAnonymousEvent("welcome_screen_load"); } } From 9fb1c8e4cd74c618594bf6e84906961cd4e27b74 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Jul 2021 19:33:07 +0100 Subject: [PATCH 161/445] Iterate PR --- res/css/structures/_SpacePanel.scss | 8 ++++++++ src/components/views/spaces/SpacePanel.tsx | 4 ++-- src/i18n/strings/en_EN.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index 9d9c3ff8ab..1dea6332f5 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -368,6 +368,14 @@ $activeBorderColor: $secondary-fg-color; .mx_SpacePanel_iconExplore::before { mask-image: url('$(res)/img/element-icons/roomlist/browse.svg'); } + + .mx_SpacePanel_noIcon { + display: none; + + & + .mx_IconizedContextMenu_label { + padding-left: 5px !important; // override default iconized label style to align with header + } + } } diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index a339cb8132..bbe27ced75 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -79,8 +79,8 @@ const HomeButtonContextMenu = ({ onFinished, ...props }: ComponentProps { SettingsStore.setValue("Spaces.all_rooms_in_home", null, SettingLevel.ACCOUNT, !allRoomsInHome); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ae14c6ed9d..b0752ab0bd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1023,7 +1023,7 @@ "Creating...": "Creating...", "Create": "Create", "Home": "Home", - "Show all rooms in home": "Show all rooms in home", + "Show all rooms": "Show all rooms", "All rooms": "All rooms", "Options": "Options", "Expand space panel": "Expand space panel", From f16b1d46b72430715a75bac495b6ac19e94efe44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 28 Jul 2021 20:58:24 +0200 Subject: [PATCH 162/445] Fix sizing issue of the screen picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/elements/_DesktopCapturerSourcePicker.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/res/css/views/elements/_DesktopCapturerSourcePicker.scss b/res/css/views/elements/_DesktopCapturerSourcePicker.scss index 49a0a44417..bd81aafef3 100644 --- a/res/css/views/elements/_DesktopCapturerSourcePicker.scss +++ b/res/css/views/elements/_DesktopCapturerSourcePicker.scss @@ -35,7 +35,6 @@ limitations under the License. .mx_desktopCapturerSourcePicker_source_thumbnail { margin: 4px; padding: 4px; - width: 312px; border-width: 2px; border-radius: 8px; border-style: solid; @@ -53,6 +52,5 @@ limitations under the License. white-space: nowrap; text-overflow: ellipsis; overflow: hidden; - width: 312px; } } From ae647658706a87c352b7e120423ebc87f33d8dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 29 Jul 2021 08:45:32 +0200 Subject: [PATCH 163/445] playMedia only if necessary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/VideoFeed.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 9975f70d62..af2fd92016 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -22,6 +22,7 @@ import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed'; import { logger } from 'matrix-js-sdk/src/logger'; import MemberAvatar from "../avatars/MemberAvatar"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { objectHasDiff } from '../../../utils/objects'; interface IProps { call: MatrixCall; @@ -46,7 +47,6 @@ interface IState { videoMuted: boolean; } -// TODO: We shouldn't be calling playMedia() all the time @replaceableComponent("views.voip.VideoFeed") export default class VideoFeed extends React.PureComponent { private element: HTMLVideoElement; @@ -69,8 +69,10 @@ export default class VideoFeed extends React.PureComponent { this.updateFeed(this.props.feed, null); } - componentDidUpdate(prevProps: IProps) { + componentDidUpdate(prevProps: IProps, prevState: IState) { this.updateFeed(prevProps.feed, this.props.feed); + // If the mutes state has changed, we try to playMedia() + if (prevState.videoMuted !== this.state.videoMuted) this.playMedia(); } static getDerivedStateFromProps(props: IProps) { @@ -142,7 +144,7 @@ export default class VideoFeed extends React.PureComponent { } private onNewStream = async () => { - await this.setState({ + this.setState({ audioMuted: this.props.feed.isAudioMuted(), videoMuted: this.props.feed.isVideoMuted(), }); @@ -150,11 +152,10 @@ export default class VideoFeed extends React.PureComponent { }; private onMuteStateChanged = async () => { - await this.setState({ + this.setState({ audioMuted: this.props.feed.isAudioMuted(), videoMuted: this.props.feed.isVideoMuted(), }); - this.playMedia(); }; private onResize = (e) => { From 152168ef2ddc99a598fcebbbd517560e053fa850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 29 Jul 2021 10:20:59 +0200 Subject: [PATCH 164/445] Add mic mute icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/img/voip/mic-muted.svg | 5 +++++ res/img/voip/mic-unmuted.svg | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 res/img/voip/mic-muted.svg create mode 100644 res/img/voip/mic-unmuted.svg diff --git a/res/img/voip/mic-muted.svg b/res/img/voip/mic-muted.svg new file mode 100644 index 0000000000..0cb7ad1c9e --- /dev/null +++ b/res/img/voip/mic-muted.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/voip/mic-unmuted.svg b/res/img/voip/mic-unmuted.svg new file mode 100644 index 0000000000..8334cafa0a --- /dev/null +++ b/res/img/voip/mic-unmuted.svg @@ -0,0 +1,4 @@ + + + + From fbde00e22737189cf1f242f19482dc8385f3dae3 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Wed, 28 Jul 2021 19:58:54 +0000 Subject: [PATCH 165/445] Translated using Weblate (Hungarian) Currently translated at 99.9% (3082 of 3083 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index a7c09c0fec..c065c31323 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -3581,5 +3581,10 @@ "New layout switcher (with message bubbles)": "Új kinézet váltó (üzenet buborékokkal)", "New in the Spaces beta": "Újdonság a béta Terekben", "Transfer Failed": "Átadás sikertelen", - "Unable to transfer call": "A hívás átadása nem lehetséges" + "Unable to transfer call": "A hívás átadása nem lehetséges", + "Anyone will be able to find and join this room.": "Bárki megtalálhatja és beléphet ebbe a szobába.", + "This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.": "A szobák egyszerűbben maradhatnak privátok a téren kívül, amíg a tér tagsága megtalálhatja és beléphet oda. Minden új szoba a téren rendelkezik ezzel a beállítási lehetőséggel.", + "To help space members find and join a private room, go to that room's Security & Privacy settings.": "Ahhoz hogy segíthessen a tér tagságának privát szobák megtalálásában és a belépésben, lépjen be a szoba Biztonság és adatvédelem beállításaiba.", + "Help space members find private rooms": "Segítsen a tér tagságának privát szobák megtalálásában", + "Help people in spaces to find and join private rooms": "Segítsen a téren az embereknek privát szobák megtalálásába és a belépésben" } From 65d753348b6ac977752395603126b18191e0ca5f Mon Sep 17 00:00:00 2001 From: Bog Rol Date: Wed, 28 Jul 2021 13:33:04 +0000 Subject: [PATCH 166/445] Translated using Weblate (Hungarian) Currently translated at 99.9% (3082 of 3083 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index c065c31323..876eeea1ef 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -77,7 +77,7 @@ "Command error": "Parancs hiba", "Commands": "Parancsok", "Confirm password": "Jelszó megerősítése", - "Create Room": "Szoba készítése", + "Create Room": "Szoba létrehozása", "Cryptography": "Titkosítás", "Current password": "Jelenlegi jelszó", "Custom": "Egyedi", @@ -1503,7 +1503,7 @@ "View": "Nézet", "Find a room…": "Szoba keresése…", "Find a room… (e.g. %(exampleRoom)s)": "Szoba keresése… (pl.: %(exampleRoom)s)", - "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Ha nem találod a szobát amit keresel kérj egy meghívót vagy Készíts egy új szobát.", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Ha nem találod a szobát amit keresel, kérj egy meghívót vagy készíts egy új szobát.", "Explore rooms": "Szobák felderítése", "Verify the link in your inbox": "Ellenőrizd a hivatkozást a bejövő leveleid között", "Complete": "Kiegészít", @@ -1518,12 +1518,12 @@ "e.g. my-room": "pl.: szobam", "Please enter a name for the room": "Kérlek adj meg egy nevet a szobához", "This room is private, and can only be joined by invitation.": "A szoba zárt, csak meghívóval lehet belépni.", - "Create a public room": "Nyilvános szoba készítése", - "Create a private room": "Zárt szoba készítése", + "Create a public room": "Nyilvános szoba létrehozása", + "Create a private room": "Privát szoba létrehozása", "Topic (optional)": "Téma (nem kötelező)", "Make this room public": "A szoba legyen nyilvános", - "Hide advanced": "Haladó elrejtése", - "Show advanced": "Speciális megjelenítése", + "Hide advanced": "Speciális beállítások elrejtése", + "Show advanced": "Speciális beállítások megjelenítése", "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Más szervereken lévő felhasználók belépésének letiltása-csak helyi szoba (Ezt a beállítást később nem lehet megváltoztatni!)", "Close dialog": "Ablak bezárása", "Show previews/thumbnails for images": "Előnézet/bélyegkép mutatása a képekhez", @@ -1736,7 +1736,7 @@ "Show more": "Több megjelenítése", "Recent Conversations": "Legújabb Beszélgetések", "Direct Messages": "Közvetlen Beszélgetések", - "Go": "Menj", + "Go": "Meghívás", "Show info about bridges in room settings": "Híd információk megmutatása a szoba beállításoknál", "This bridge is managed by .": "Ezt a hidat ez a felhasználó kezeli: .", "Suggestions": "Javaslatok", @@ -2113,7 +2113,7 @@ "Liberate your communication": "Kommunikálj szabadon", "Send a Direct Message": "Közvetlen üzenet küldése", "Explore Public Rooms": "Nyilvános szobák felfedezése", - "Create a Group Chat": "Készíts Csoportos Beszélgetést", + "Create a Group Chat": "Készíts csoportos beszélgetést", "Self-verification request": "Ön ellenőrzés kérése", "Cancel replying to a message": "Üzenet válasz megszakítása", "Confirm adding email": "E-mail hozzáadásának megerősítése", @@ -2446,7 +2446,7 @@ "Cross-signing and secret storage are ready for use.": "Az eszközök közti hitelesítés és a biztonsági tároló kész a használatra.", "Cross-signing is ready for use, but secret storage is currently not being used to backup your keys.": "Az eszközök közti hitelesítés kész a használatra, de a biztonsági tároló nincs használva a kulcsok mentéséhez.", "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Privát szobák csak meghívóval találhatók meg és meghívóval lehet belépni. A nyilvános szobákat bárki megtalálhatja és be is léphet.", - "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Privát szobák csak meghívóval találhatók meg és meghívóval lehet belépni. A nyilvános szobákat a közösség bármely tagja megtalálhatja és be is léphet.", + "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "A privát szobák csak meghívóval találhatók meg és csak meghívóval lehet belépni. A nyilvános szobákat a közösség bármely tagja megtalálhatja és be is léphet.", "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "Beállíthatod, ha a szobát csak egy belső csoport használja majd a matrix szervereden. Ezt később nem lehet megváltoztatni.", "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "Ne engedélyezd ezt, ha a szobát külső csapat is használja másik matrix szerverről. Később nem lehet megváltoztatni.", "Block anyone not part of %(serverName)s from ever joining this room.": "A szobába ne léphessenek be azok, akik nem ezen a szerveren vannak: %(serverName)s.", @@ -3373,7 +3373,7 @@ "Kick, ban, or invite people to this room, and make you leave": "Kirúgni, kitiltani vagy meghívni embereket ebbe a szobába és, hogy ön elhagyja a szobát", "Currently joining %(count)s rooms|one": "%(count)s szobába lép be", "Currently joining %(count)s rooms|other": "%(count)s szobába lép be", - "No results for \"%(query)s\"": "Nincs találat ehhez: %(query)s", + "No results for \"%(query)s\"": "Nincs találat erre: %(query)s", "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Próbáljon ki más szavakat vagy keressen elgépelést. Néhány találat azért nem látszik, mert privát és meghívóra van szüksége, hogy csatlakozhasson.", "The user you called is busy.": "A hívott felhasználó foglalt.", "User Busy": "Felhasználó foglalt", From 9a796149c6054b1288192b032d90a559772d9d51 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 28 Jul 2021 23:18:29 +0000 Subject: [PATCH 167/445] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3083 of 3083 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index b8d0d3f1c7..1b95037d2b 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -3596,5 +3596,6 @@ "Your camera is still enabled": "您的攝影機仍為啟用狀態", "Your camera is turned off": "您的攝影機已關閉", "You are presenting": "您正在出席", - "%(sharerName)s is presenting": "%(sharerName)s 正在出席" + "%(sharerName)s is presenting": "%(sharerName)s 正在出席", + "Anyone will be able to find and join this room.": "任何人都可以找到並加入此聊天室。" } From 7f6cf29766bc81d0dda92e65de9ecdc7a16c3c4e Mon Sep 17 00:00:00 2001 From: Dariusz Niemczyk Date: Thu, 29 Jul 2021 12:39:32 +0200 Subject: [PATCH 168/445] Fix grecaptcha regression --- src/components/views/auth/CaptchaForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/auth/CaptchaForm.tsx b/src/components/views/auth/CaptchaForm.tsx index b1c09f2b22..97f45167a8 100644 --- a/src/components/views/auth/CaptchaForm.tsx +++ b/src/components/views/auth/CaptchaForm.tsx @@ -103,8 +103,8 @@ export default class CaptchaForm extends React.Component Date: Thu, 29 Jul 2021 09:01:37 +0000 Subject: [PATCH 169/445] Translated using Weblate (Estonian) Currently translated at 98.8% (3047 of 3083 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ --- src/i18n/strings/et.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index bb7bd575ef..e47fe72f4f 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -3521,5 +3521,18 @@ "The voice message failed to upload.": "Häälsõnumi üleslaadimine ei õnnestunud.", "Everyone in will be able to find and join this room.": "Kõik kogukonna liikmed saavad seda jututuba leida ning võivad temaga liituda.", "You can change this at any time from room settings.": "Sa saad seda alati jututoa seadistustest muuta.", - "Anyone will be able to find and join this room, not just members of .": "Mitte ainult kogukonna liikmed, vaid kõik saavad seda jututuba leida ja võivad temaga liituda." + "Anyone will be able to find and join this room, not just members of .": "Mitte ainult kogukonna liikmed, vaid kõik saavad seda jututuba leida ja võivad temaga liituda.", + "You declined this call": "Sina keeldusid kõnest", + "They declined this call": "Teine osapool keeldus kõnest", + "Call again": "Helista uuesti", + "They didn't pick up": "Teine osapool ei võtnud kõnet vastu", + "You are presenting": "Sina esitad", + "%(sharerName)s is presenting": "%(sharerName)s esitab", + "Your camera is turned off": "Sinu seadme kaamera on välja lülitatud", + "Your camera is still enabled": "Sinu seadme kaamera on jätkuvalt kasutusel", + "Screen sharing is here!": "Meil on nüüd olemas ekraanijagamine!", + "Share entire screen": "Jaga tervet ekraani", + "Application window": "Rakenduse aken", + "Share content": "Jaga sisu", + "Anyone will be able to find and join this room.": "Kõik saavad seda jututuba leida ja temaga liituda." } From cb89dd408c260a14bd9726fd65fe3f6f5acf2ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 29 Jul 2021 15:05:26 +0200 Subject: [PATCH 170/445] Use mic mute icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/voip/_CallView.scss | 10 +++- res/css/views/voip/_CallViewSidebar.scss | 11 +++++ res/css/views/voip/_VideoFeed.scss | 48 +++++++++++++++--- src/components/views/voip/VideoFeed.tsx | 63 ++++++++++++++++-------- 4 files changed, 103 insertions(+), 29 deletions(-) diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 104e2993d8..eff865f20c 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -76,16 +76,22 @@ limitations under the License. &.mx_VideoFeed_voice { // We don't want to collide with the call controls that have 52px of height - padding-bottom: 52px; + margin-bottom: 52px; background-color: $inverted-bg-color; display: flex; justify-content: center; align-items: center; } - &.mx_VideoFeed_video { + .mx_VideoFeed_video { + height: 100%; background-color: #000; } + + .mx_VideoFeed_mic { + left: 10px; + bottom: 10px; + } } } diff --git a/res/css/views/voip/_CallViewSidebar.scss b/res/css/views/voip/_CallViewSidebar.scss index 79bf3cbf09..892a137a32 100644 --- a/res/css/views/voip/_CallViewSidebar.scss +++ b/res/css/views/voip/_CallViewSidebar.scss @@ -35,12 +35,23 @@ limitations under the License. width: 100%; &.mx_VideoFeed_voice { + border-radius: 4px; + display: flex; align-items: center; justify-content: center; aspect-ratio: 16 / 9; } + + .mx_VideoFeed_video { + border-radius: 4px; + } + + .mx_VideoFeed_mic { + left: 6px; + bottom: 6px; + } } &.mx_CallViewSidebar_pipMode { diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss index 07a4a0e530..3a0f62636e 100644 --- a/res/css/views/voip/_VideoFeed.scss +++ b/res/css/views/voip/_VideoFeed.scss @@ -15,18 +15,52 @@ limitations under the License. */ .mx_VideoFeed { - border-radius: 4px; - + overflow: hidden; + position: relative; &.mx_VideoFeed_voice { background-color: $inverted-bg-color; } - &.mx_VideoFeed_video { + .mx_VideoFeed_video { + width: 100%; background-color: transparent; + + &.mx_VideoFeed_video_mirror { + transform: scale(-1, 1); + } + } + + .mx_VideoFeed_mic { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + + width: 24px; + height: 24px; + + background-color: rgba(0, 0, 0, 0.5); // Same on both themes + border-radius: 100%; + + &::before { + position: absolute; + content: ""; + width: 16px; + height: 16px; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + background-color: white; // Same on both themes + border-radius: 7px; + } + + &.mx_VideoFeed_mic_muted::before { + mask-image: url('$(res)/img/voip/mic-muted.svg'); + } + + &.mx_VideoFeed_mic_unmuted::before { + mask-image: url('$(res)/img/voip/mic-unmuted.svg'); + } } } - -.mx_VideoFeed_mirror { - transform: scale(-1, 1); -} diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index af2fd92016..09d0c97a0d 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -22,7 +22,7 @@ import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed'; import { logger } from 'matrix-js-sdk/src/logger'; import MemberAvatar from "../avatars/MemberAvatar"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { objectHasDiff } from '../../../utils/objects'; +import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes'; interface IProps { call: MatrixCall; @@ -165,39 +165,62 @@ export default class VideoFeed extends React.PureComponent { }; render() { - const videoClasses = { - mx_VideoFeed: true, + const { pipMode, primary, feed } = this.props; + + const wrapperClasses = classnames("mx_VideoFeed", { mx_VideoFeed_voice: this.state.videoMuted, - mx_VideoFeed_video: !this.state.videoMuted, - mx_VideoFeed_mirror: ( - this.props.feed.isLocal() && - SettingsStore.getValue('VideoView.flipVideoHorizontally') - ), - }; + }); + const micIconClasses = classnames("mx_VideoFeed_mic", { + mx_VideoFeed_mic_muted: this.state.audioMuted, + mx_VideoFeed_mic_unmuted: !this.state.audioMuted, + }); - const { pipMode, primary } = this.props; + let micIcon; + if ( + feed.purpose !== SDPStreamMetadataPurpose.Screenshare && + !pipMode && + !feed.isLocal() + ) { + micIcon = ( +
    + ); + } + let content; if (this.state.videoMuted) { const member = this.props.feed.getMember(); + let avatarSize; if (pipMode && primary) avatarSize = 76; else if (pipMode && !primary) avatarSize = 16; else if (!pipMode && primary) avatarSize = 160; else; // TBD - return ( -
    - -
    + content =( + ); } else { - return ( -