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/263] 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/263] 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/263] 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/263] 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/263] 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/263] 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/263] 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/263] 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/263] 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/263] 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/263] 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/263] 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/263] 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 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 014/263] 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 b2292268bc9a551a1b9ecfe535eafd3b823789d4 Mon Sep 17 00:00:00 2001 From: pvagner Date: Wed, 30 Jun 2021 07:06:10 +0200 Subject: [PATCH 015/263] 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 2a48d3c9bc83e9083c3a9d7366c008f3909c9f6d Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 07:40:39 +0100 Subject: [PATCH 016/263] 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 017/263] 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 018/263] 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 019/263] 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 020/263] 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 021/263] 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 1b2d70d816ce151d408cc257b7e667cc25e920c9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 21 Jul 2021 09:57:10 +0100 Subject: [PATCH 022/263] Prompt user to leave rooms/subspaces in a space when leaving space --- res/css/_components.scss | 3 +- .../dialogs/_AddExistingToSpaceDialog.scss | 48 ++-- res/css/views/dialogs/_LeaveSpaceDialog.scss | 107 +++++++++ .../dialogs/AddExistingToSpaceDialog.tsx | 2 +- .../views/dialogs/LeaveSpaceDialog.tsx | 217 ++++++++++++++++++ .../views/spaces/SpaceSettingsGeneralTab.tsx | 7 +- .../views/spaces/SpaceTreeLevel.tsx | 6 +- src/i18n/strings/en_EN.json | 14 ++ src/stores/SpaceStore.tsx | 2 +- src/utils/space.tsx | 25 ++ 10 files changed, 395 insertions(+), 36 deletions(-) create mode 100644 res/css/views/dialogs/_LeaveSpaceDialog.scss create mode 100644 src/components/views/dialogs/LeaveSpaceDialog.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 7df45b857e..93d6b2b36f 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -86,6 +86,7 @@ @import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_InviteDialog.scss"; @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; +@import "./views/dialogs/_LeaveSpaceDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_ModalWidgetDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss"; @@ -200,8 +201,8 @@ @import "./views/rooms/_E2EIcon.scss"; @import "./views/rooms/_EditMessageComposer.scss"; @import "./views/rooms/_EntityTile.scss"; -@import "./views/rooms/_EventTile.scss"; @import "./views/rooms/_EventBubbleTile.scss"; +@import "./views/rooms/_EventTile.scss"; @import "./views/rooms/_GroupLayout.scss"; @import "./views/rooms/_IRCLayout.scss"; @import "./views/rooms/_JumpToBottomButton.scss"; diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index 2776c477fc..e71c34b800 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -49,30 +49,6 @@ limitations under the License. font-weight: $font-semi-bold; line-height: $font-15px; } - - .mx_AddExistingToSpace_entry { - display: flex; - margin-top: 12px; - - // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling - .mx_DecoratedRoomAvatar { - margin-right: 12px; - } - - .mx_AddExistingToSpace_entry_name { - font-size: $font-15px; - line-height: 30px; - flex-grow: 1; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - margin-right: 12px; - } - - .mx_Checkbox { - align-items: center; - } - } } .mx_AddExistingToSpace_section_spaces { @@ -279,3 +255,27 @@ limitations under the License. display: contents; } } + +.mx_AddExistingToSpace_entry { + display: flex; + margin-top: 12px; + + // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling + .mx_DecoratedRoomAvatar { + margin-right: 12px; + } + + .mx_AddExistingToSpace_entry_name { + font-size: $font-15px; + line-height: 30px; + flex-grow: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin-right: 12px; + } + + .mx_Checkbox { + align-items: center; + } +} diff --git a/res/css/views/dialogs/_LeaveSpaceDialog.scss b/res/css/views/dialogs/_LeaveSpaceDialog.scss new file mode 100644 index 0000000000..bb2585f25f --- /dev/null +++ b/res/css/views/dialogs/_LeaveSpaceDialog.scss @@ -0,0 +1,107 @@ +/* +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. +*/ + +.mx_LeaveSpaceDialog_wrapper { + .mx_Dialog { + display: flex; + flex-direction: column; + padding: 24px 32px; + } +} + +.mx_LeaveSpaceDialog { + width: 440px; + height: 500px; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + + .mx_Dialog_content { + flex-grow: 1; + margin: 0 0 24px; + color: $tertiary-fg-color; + overflow-y: auto; + + .mx_Dropdown_input { + border-radius: 8px; + border-color: $input-border-color; + } + + .mx_Dropdown_option { + font-size: $font-15px; + } + + .mx_SearchBox { + // To match the space around the title + margin: 0 0 15px 0; + flex-grow: 0; + border-radius: 8px; + } + + .mx_LeaveSpaceDialog_noResults { + display: block; + margin-top: 24px; + } + + .mx_LeaveSpaceDialog_section { + margin: 16px 0; + + .mx_Dropdown + span { + display: inline-block; + margin-top: 8px; + } + } + + .mx_LeaveSpaceDialog_section_warning { + position: relative; + border-radius: 8px; + margin: 12px 0; + padding: 12px 8px 12px 42px; + background-color: $header-panel-bg-color; + + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + + &::before { + content: ''; + position: absolute; + left: 10px; + top: calc(50% - 8px); // vertical centering + height: 16px; + width: 16px; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + mask-position: center; + } + } + + > p { + color: $primary-fg-color; + } + } + + .mx_Dialog_buttons { + margin-top: 20px; + + .mx_Dialog_primary { + background-color: $notice-primary-color !important; // override default colour + border-color: $notice-primary-color; + } + } +} diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 5024b98def..baebe44ec3 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -49,7 +49,7 @@ interface IProps extends IDialogProps { onCreateRoomClick(cli: MatrixClient, space: Room): void; } -const Entry = ({ room, checked, onChange }) => { +export const Entry = ({ room, checked, onChange }) => { return
; }; @@ -574,7 +557,6 @@ const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished, createdRoom { createdRooms ? _t("Go to my first room") : _t("Go to my space") }
-
; }; @@ -603,9 +585,8 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {

{ _t("Teammates might not be able to view or join any private rooms you make.") }

-

{ _t("We're working on this as part of the beta, but just want to let you know.") }

+

{ _t("We're working on this, but just want to let you know.") }

-
; }; @@ -728,7 +709,6 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => { value={buttonLabel} />
-
; }; diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 01dc729a83..e86b9c3164 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -35,7 +35,6 @@ import StyledCheckbox from "../elements/StyledCheckbox"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import ProgressBar from "../elements/ProgressBar"; -import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import QueryMatcher from "../../../autocomplete/QueryMatcher"; import TruncatedList from "../elements/TruncatedList"; @@ -354,8 +353,6 @@ const AddExistingToSpaceDialog: React.FC = ({ space, onCreateRoomClick, } /> - - onFinished(false)} /> ; }; diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 5f16684fb8..a5d864b140 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -26,16 +26,14 @@ import createRoom from "../../../createRoom"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { SpaceAvatar } from "./SpaceBasicSettings"; import AccessibleButton from "../elements/AccessibleButton"; -import { BetaPill } from "../beta/BetaCard"; -import defaultDispatcher from "../../../dispatcher/dispatcher"; -import { Action } from "../../../dispatcher/actions"; -import { UserTab } from "../dialogs/UserSettingsDialog"; import Field from "../elements/Field"; import withValidation from "../elements/Validation"; -import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView"; import { Preset } from "matrix-js-sdk/src/@types/partials"; import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests"; import RoomAliasField from "../elements/RoomAliasField"; +import SdkConfig from "../../../SdkConfig"; +import Modal from "../../../Modal"; +import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog"; const SpaceCreateMenuType = ({ title, description, className, onClick }) => { return ( @@ -66,6 +64,26 @@ const nameToAlias = (name: string, domain: string): string => { return `#${localpart}:${domain}`; }; +// XXX: Temporary for the Spaces release only +const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => { + if (!SdkConfig.get().bug_report_endpoint_url) return null; + + return
+
+
+ { _t("Spaces are a new feature.") } + { + if (onClick) onClick(); + Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, { + featureId: "feature_spaces", + }); + }}> + { _t("Give feedback.") } + +
+
; +}; + const SpaceCreateMenu = ({ onFinished }) => { const cli = useContext(MatrixClientContext); const [visibility, setVisibility] = useState(null); @@ -252,13 +270,6 @@ const SpaceCreateMenu = ({ onFinished }) => { managed={false} > - { - onFinished(); - defaultDispatcher.dispatch({ - action: Action.ViewUserSettings, - initialTabId: UserTab.Labs, - }); - }} /> { body } ; diff --git a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx index a43b180752..8ee848a28c 100644 --- a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx +++ b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx @@ -21,7 +21,6 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; -import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView"; import SpaceBasicSettings from "./SpaceBasicSettings"; import { avatarUrlForRoom } from "../../../Avatar"; import { IDialogProps } from "../dialogs/IDialogProps"; @@ -96,8 +95,6 @@ const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProp { error &&
{ error }
} - -
create a new room.": "If you can't find the room you're looking for, ask for an invite or create a new room.", "Create room": "Create room", - "Spaces are a beta feature.": "Spaces are a beta feature.", "Public space": "Public space", "Private space": "Private space", " invites you": " invites you", From 7ccc429beaa1c49d4d94b7f1f68c8b115fd5a53f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 23 Jul 2021 15:47:11 +0100 Subject: [PATCH 052/263] Change spaces beta feedback to not be so beta-centric --- res/css/_components.scss | 2 +- ...css => _GenericFeatureFeedbackDialog.scss} | 4 +- src/components/views/beta/BetaCard.tsx | 2 + .../views/dialogs/BetaFeedbackDialog.tsx | 98 +++++------------ .../dialogs/GenericFeatureFeedbackDialog.tsx | 101 ++++++++++++++++++ .../views/spaces/SpaceCreateMenu.tsx | 14 ++- src/i18n/strings/en_EN.json | 17 +-- src/settings/Settings.tsx | 1 + 8 files changed, 151 insertions(+), 88 deletions(-) rename res/css/views/dialogs/{_BetaFeedbackDialog.scss => _GenericFeatureFeedbackDialog.scss} (90%) create mode 100644 src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index f9e3ab1160..95040ce855 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -67,7 +67,6 @@ @import "./views/dialogs/_AddExistingToSpaceDialog.scss"; @import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_Analytics.scss"; -@import "./views/dialogs/_BetaFeedbackDialog.scss"; @import "./views/dialogs/_BugReportDialog.scss"; @import "./views/dialogs/_ChangelogDialog.scss"; @import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss"; @@ -81,6 +80,7 @@ @import "./views/dialogs/_EditCommunityPrototypeDialog.scss"; @import "./views/dialogs/_FeedbackDialog.scss"; @import "./views/dialogs/_ForwardDialog.scss"; +@import "./views/dialogs/_GenericFeatureFeedbackDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_HostSignupDialog.scss"; @import "./views/dialogs/_IncomingSasDialog.scss"; diff --git a/res/css/views/dialogs/_BetaFeedbackDialog.scss b/res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss similarity index 90% rename from res/css/views/dialogs/_BetaFeedbackDialog.scss rename to res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss index 9f5f6b512e..f83eed9c53 100644 --- a/res/css/views/dialogs/_BetaFeedbackDialog.scss +++ b/res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_BetaFeedbackDialog { - .mx_BetaFeedbackDialog_subheading { +.mx_GenericFeatureFeedbackDialog { + .mx_GenericFeatureFeedbackDialog_subheading { color: $primary-fg-color; font-size: $font-14px; line-height: $font-20px; diff --git a/src/components/views/beta/BetaCard.tsx b/src/components/views/beta/BetaCard.tsx index ec662d831b..c2ba869ab4 100644 --- a/src/components/views/beta/BetaCard.tsx +++ b/src/components/views/beta/BetaCard.tsx @@ -27,6 +27,8 @@ import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog"; import SdkConfig from "../../../SdkConfig"; import SettingsFlag from "../elements/SettingsFlag"; +// XXX: Keep this around for re-use in future Betas + interface IProps { title?: string; featureId: string; diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx index 917004dbc7..788832119f 100644 --- a/src/components/views/dialogs/BetaFeedbackDialog.tsx +++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx @@ -14,22 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useState } from "react"; +import React from "react"; -import QuestionDialog from './QuestionDialog'; import { _t } from '../../../languageHandler'; -import Field from "../elements/Field"; -import SdkConfig from "../../../SdkConfig"; import { IDialogProps } from "./IDialogProps"; import SettingsStore from "../../../settings/SettingsStore"; -import { submitFeedback } from "../../../rageshake/submit-rageshake"; -import StyledCheckbox from "../elements/StyledCheckbox"; -import Modal from "../../../Modal"; -import InfoDialog from "./InfoDialog"; import AccessibleButton from "../elements/AccessibleButton"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { UserTab } from "./UserSettingsDialog"; +import GenericFeatureFeedbackDialog from "./FeedbackDialog"; + +// XXX: Keep this around for re-use in future Betas interface IProps extends IDialogProps { featureId: string; @@ -38,74 +34,28 @@ interface IProps extends IDialogProps { const BetaFeedbackDialog: React.FC = ({ featureId, onFinished }) => { const info = SettingsStore.getBetaInfo(featureId); - const [comment, setComment] = useState(""); - const [canContact, setCanContact] = useState(false); + const extraData = SettingsStore.getBetaInfo(featureId)?.extraSettings.reduce((o, k) => { + o[k] = SettingsStore.getValue(k); + return o; + }, {}); - const sendFeedback = async (ok: boolean) => { - if (!ok) return onFinished(false); - - const extraData = SettingsStore.getBetaInfo(featureId)?.extraSettings.reduce((o, k) => { - o[k] = SettingsStore.getValue(k); - return o; - }, {}); - - submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact, extraData); - onFinished(true); - - Modal.createTrackedDialog("Beta Dialog Sent", featureId, InfoDialog, { - title: _t("Beta feedback"), - description: _t("Thank you for your feedback, we really appreciate it."), - button: _t("Done"), - hasCloseButton: false, - fixedWidth: false, - }); - }; - - return ( -
- { _t(info.feedbackSubheading) } -   - { _t("Your platform and username will be noted to help us use your feedback as much as we can.") } - - { - onFinished(false); - defaultDispatcher.dispatch({ - action: Action.ViewUserSettings, - initialTabId: UserTab.Labs, - }); - }}> - { _t("To leave the beta, visit your settings.") } - -
- - { - setComment(ev.target.value); - }} - autoFocus={true} - /> - - setCanContact((e.target as HTMLInputElement).checked)} - > - { _t("You may contact me if you have any follow up questions") } - - } - button={_t("Send feedback")} - buttonDisabled={!comment} - onFinished={sendFeedback} - />); + subheading={_t(info.feedbackSubheading)} + onFinished={onFinished} + rageshakeLabel={info.feedbackLabel} + rageshakeData={extraData} + > + { + onFinished(false); + defaultDispatcher.dispatch({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Labs, + }); + }}> + { _t("To leave the beta, visit your settings.") } + + ; }; export default BetaFeedbackDialog; diff --git a/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx b/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx new file mode 100644 index 0000000000..d68569b126 --- /dev/null +++ b/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx @@ -0,0 +1,101 @@ +/* +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, { useState } from "react"; + +import QuestionDialog from './QuestionDialog'; +import { _t } from '../../../languageHandler'; +import Field from "../elements/Field"; +import SdkConfig from "../../../SdkConfig"; +import { IDialogProps } from "./IDialogProps"; +import { submitFeedback } from "../../../rageshake/submit-rageshake"; +import StyledCheckbox from "../elements/StyledCheckbox"; +import Modal from "../../../Modal"; +import InfoDialog from "./InfoDialog"; + +interface IProps extends IDialogProps { + title: string; + subheading: string; + rageshakeLabel: string; + rageshakeData?: Record; +} + +const GenericFeatureFeedbackDialog: React.FC = ({ + title, + subheading, + children, + rageshakeLabel, + rageshakeData = {}, + onFinished, +}) => { + const [comment, setComment] = useState(""); + const [canContact, setCanContact] = useState(false); + + const sendFeedback = async (ok: boolean) => { + if (!ok) return onFinished(false); + + submitFeedback(SdkConfig.get().bug_report_endpoint_url, rageshakeLabel, comment, canContact, rageshakeData); + onFinished(true); + + Modal.createTrackedDialog("Feedback Sent", rageshakeLabel, InfoDialog, { + title, + description: _t("Thank you for your feedback, we really appreciate it."), + button: _t("Done"), + hasCloseButton: false, + fixedWidth: false, + }); + }; + + return ( +
+ { subheading } +   + { _t("Your platform and username will be noted to help us use your feedback as much as we can.") } + + { children } +
+ + { + setComment(ev.target.value); + }} + autoFocus={true} + /> + + setCanContact((e.target as HTMLInputElement).checked)} + > + { _t("You may contact me if you have any follow up questions") } + + } + button={_t("Send feedback")} + buttonDisabled={!comment} + onFinished={sendFeedback} + />); +}; + +export default GenericFeatureFeedbackDialog; diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index a5d864b140..279ae1165b 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -33,7 +33,8 @@ import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests"; import RoomAliasField from "../elements/RoomAliasField"; import SdkConfig from "../../../SdkConfig"; import Modal from "../../../Modal"; -import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog"; +import GenericFeatureFeedbackDialog from "../dialogs/GenericFeatureFeedbackDialog"; +import SettingsStore from "../../../settings/SettingsStore"; const SpaceCreateMenuType = ({ title, description, className, onClick }) => { return ( @@ -74,8 +75,15 @@ const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => { { _t("Spaces are a new feature.") } { if (onClick) onClick(); - Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, { - featureId: "feature_spaces", + Modal.createTrackedDialog("Spaces Feedback", "", GenericFeatureFeedbackDialog, { + title: _t("Spaces feedback"), + subheading: _t("Thank you for trying Spaces. Your feedback will help inform the next versions."), + rageshakeLabel: "spaces-feedback", + rageshakeData: { + "feature_spaces.all_rooms": SettingsStore.getValue("feature_spaces.all_rooms"), + "feature_spaces.space_member_dms": SettingsStore.getValue("feature_spaces.space_member_dms"), + "feature_spaces.space_dm_badges": SettingsStore.getValue("feature_spaces.space_dm_badges"), + }, }); }}> { _t("Give feedback.") } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5199346d90..89ea7e3a71 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1009,6 +1009,8 @@ "Description": "Description", "Please enter a name for the space": "Please enter a name for the space", "Spaces are a new feature.": "Spaces are a new feature.", + "Spaces feedback": "Spaces feedback", + "Thank you for trying Spaces. Your feedback will help inform the next versions.": "Thank you for trying Spaces. Your feedback will help inform the next versions.", "Give feedback.": "Give feedback.", "Create a space": "Create a space", "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.", @@ -2134,15 +2136,8 @@ "Invite anyway and never warn me again": "Invite anyway and never warn me again", "Invite anyway": "Invite anyway", "Close dialog": "Close dialog", - "Beta feedback": "Beta feedback", - "Thank you for your feedback, we really appreciate it.": "Thank you for your feedback, we really appreciate it.", - "Done": "Done", "%(featureName)s beta feedback": "%(featureName)s beta feedback", - "Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.", "To leave the beta, visit your settings.": "To leave the beta, visit your settings.", - "Feedback": "Feedback", - "You may contact me if you have any follow up questions": "You may contact me if you have any follow up questions", - "Send feedback": "Send feedback", "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.", "Preparing to send logs": "Preparing to send logs", "Logs sent": "Logs sent", @@ -2279,8 +2274,10 @@ "Comment": "Comment", "There are two ways you can provide feedback and help us improve %(brand)s.": "There are two ways you can provide feedback and help us improve %(brand)s.", "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.", + "Feedback": "Feedback", "Report a bug": "Report a bug", "Please view existing bugs on Github first. No match? Start a new one.": "Please view existing bugs on Github first. No match? Start a new one.", + "Send feedback": "Send feedback", "You don't have permission to do this": "You don't have permission to do this", "Sending": "Sending", "Sent": "Sent", @@ -2288,6 +2285,10 @@ "Forward message": "Forward message", "Message preview": "Message preview", "Search for rooms or people": "Search for rooms or people", + "Thank you for your feedback, we really appreciate it.": "Thank you for your feedback, we really appreciate it.", + "Done": "Done", + "Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.", + "You may contact me if you have any follow up questions": "You may contact me if you have any follow up questions", "Confirm abort of host creation": "Confirm abort of host creation", "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.", "Abort": "Abort", @@ -2825,7 +2826,7 @@ "Me and my teammates": "Me and my teammates", "A private space for you and your teammates": "A private space for you and your teammates", "Teammates might not be able to view or join any private rooms you make.": "Teammates might not be able to view or join any private rooms you make.", - "We're working on this as part of the beta, but just want to let you know.": "We're working on this as part of the beta, but just want to let you know.", + "We're working on this, but just want to let you know.": "We're working on this, but just want to let you know.", "Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s", "Inviting...": "Inviting...", "Invite your teammates": "Invite your teammates", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index f0bdb2e0e5..8fbace9cad 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -124,6 +124,7 @@ export interface ISetting { // not use this for new settings. invertedSettingName?: string; + // XXX: Keep this around for re-use in future Betas betaInfo?: { title: string; // _td caption: string; // _td From 1d1a396c1f4b8aafb4b1f522596ba6947bd82c35 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 23 Jul 2021 15:51:59 +0100 Subject: [PATCH 053/263] get rid of spurious hr --- res/css/structures/_SpaceRoomView.scss | 12 ------------ src/components/structures/SpaceRoomView.tsx | 1 - 2 files changed, 13 deletions(-) diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index b1c09350cf..ca97262742 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -332,12 +332,6 @@ $SpaceRoomViewInnerWidth: 428px; word-wrap: break-word; } - > hr { - border: none; - height: 1px; - background-color: $groupFilterPanel-bg-color; - } - .mx_SearchBox { margin: 0 0 20px; } @@ -514,12 +508,6 @@ $SpaceRoomViewInnerWidth: 428px; margin-top: 18px; margin-bottom: 12px; - > hr { - border: none; - border-top: 1px solid $input-border-color; - margin-bottom: 12px; - } - > div { display: flex; flex-direction: row; diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 699c42c871..1187a16d17 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -428,7 +428,6 @@ const SpaceLanding = ({ space }) => {
) } -
Date: Fri, 23 Jul 2021 15:56:00 +0100 Subject: [PATCH 054/263] remove unused import and revert removing some needed css --- res/css/structures/_SpaceRoomView.scss | 57 ------------------- res/css/views/spaces/_SpaceCreateMenu.scss | 63 +++++++++++++++++++++ src/components/structures/SpaceRoomView.tsx | 1 - 3 files changed, 63 insertions(+), 58 deletions(-) diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index ca97262742..d4b164a03e 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -503,60 +503,3 @@ $SpaceRoomViewInnerWidth: 428px; } } } - -.mx_SpaceFeedbackPrompt { - margin-top: 18px; - margin-bottom: 12px; - - > div { - display: flex; - flex-direction: row; - font-size: $font-15px; - line-height: $font-24px; - - > span { - color: $secondary-fg-color; - position: relative; - padding-left: 32px; - font-size: inherit; - line-height: inherit; - margin-right: auto; - - &::before { - content: ''; - position: absolute; - left: 0; - top: 2px; - height: 20px; - width: 20px; - background-color: $secondary-fg-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); - mask-position: center; - } - } - - .mx_AccessibleButton_kind_link { - color: $accent-color; - position: relative; - padding: 0 0 0 24px; - margin-left: 8px; - font-size: inherit; - line-height: inherit; - - &::before { - content: ''; - position: absolute; - left: 0; - height: 16px; - width: 16px; - background-color: $accent-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-image: url('$(res)/img/element-icons/chat-bubbles.svg'); - mask-position: center; - } - } - } -} diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss index 88b9d8f693..7ed71e454c 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.scss +++ b/res/css/views/spaces/_SpaceCreateMenu.scss @@ -99,3 +99,66 @@ $spacePanelWidth: 71px; } } } + +.mx_SpaceFeedbackPrompt { + margin-top: 18px; + margin-bottom: 12px; + + > hr { + border: none; + border-top: 1px solid $input-border-color; + margin-bottom: 12px; + } + + > div { + display: flex; + flex-direction: row; + font-size: $font-15px; + line-height: $font-24px; + + > span { + color: $secondary-fg-color; + position: relative; + padding-left: 32px; + font-size: inherit; + line-height: inherit; + margin-right: auto; + + &::before { + content: ''; + position: absolute; + left: 0; + top: 2px; + height: 20px; + width: 20px; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + mask-position: center; + } + } + + .mx_AccessibleButton_kind_link { + color: $accent-color; + position: relative; + padding: 0 0 0 24px; + margin-left: 8px; + font-size: inherit; + line-height: inherit; + + &::before { + content: ''; + position: absolute; + left: 0; + height: 16px; + width: 16px; + background-color: $accent-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/chat-bubbles.svg'); + mask-position: center; + } + } + } +} diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 1187a16d17..4e278b49e0 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -64,7 +64,6 @@ import { BetaPill } from "../views/beta/BetaCard"; import { UserTab } from "../views/dialogs/UserSettingsDialog"; import Modal from "../../Modal"; import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog"; -import SdkConfig from "../../SdkConfig"; import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership"; interface IProps { From b89434fcbc52b49fe2e557b7c8798c01ed6e0123 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 23 Jul 2021 16:03:15 +0100 Subject: [PATCH 055/263] avoid abusing spans because you're too lazy to give things a class --- res/css/views/dialogs/_CreateSubspaceDialog.scss | 2 +- src/components/views/dialogs/CreateSubspaceDialog.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/res/css/views/dialogs/_CreateSubspaceDialog.scss b/res/css/views/dialogs/_CreateSubspaceDialog.scss index b898ba6d73..c780da4b9e 100644 --- a/res/css/views/dialogs/_CreateSubspaceDialog.scss +++ b/res/css/views/dialogs/_CreateSubspaceDialog.scss @@ -47,7 +47,7 @@ limitations under the License. display: flex; margin-top: 20px; - > span { + .mx_CreateSubspaceDialog_footer_prompt { flex-grow: 1; font-size: $font-12px; line-height: $font-15px; diff --git a/src/components/views/dialogs/CreateSubspaceDialog.tsx b/src/components/views/dialogs/CreateSubspaceDialog.tsx index 55fa0ee881..9f89490f5f 100644 --- a/src/components/views/dialogs/CreateSubspaceDialog.tsx +++ b/src/components/views/dialogs/CreateSubspaceDialog.tsx @@ -158,7 +158,7 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick
- +
{ _t("Want to add an existing space instead?") }
{ onAddExistingSpaceClick(); @@ -166,7 +166,7 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick }}> { _t("Add existing space") } - +
onFinished(false)}> { _t("Cancel") } From 95f4275807ca3207f179288ae7cbde6352f61167 Mon Sep 17 00:00:00 2001 From: James Salter Date: Fri, 23 Jul 2021 16:47:02 +0100 Subject: [PATCH 056/263] 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 057/263] 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 05a9023bac835b8ca114a78c1eeef0739cc55057 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 23 Jul 2021 18:31:31 +0100 Subject: [PATCH 058/263] delint and tidy code --- .../views/dialogs/BetaFeedbackDialog.tsx | 9 ++---- .../views/spaces/SpaceCreateMenu.tsx | 30 +++++++++++-------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx index 788832119f..98b228082a 100644 --- a/src/components/views/dialogs/BetaFeedbackDialog.tsx +++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx @@ -34,17 +34,14 @@ interface IProps extends IDialogProps { const BetaFeedbackDialog: React.FC = ({ featureId, onFinished }) => { const info = SettingsStore.getBetaInfo(featureId); - const extraData = SettingsStore.getBetaInfo(featureId)?.extraSettings.reduce((o, k) => { - o[k] = SettingsStore.getValue(k); - return o; - }, {}); - return { + return SettingsStore.getValue(k); + }))} > { onFinished(false); diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 279ae1165b..ee01984db0 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -73,19 +73,23 @@ const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
{ _t("Spaces are a new feature.") } - { - if (onClick) onClick(); - Modal.createTrackedDialog("Spaces Feedback", "", GenericFeatureFeedbackDialog, { - title: _t("Spaces feedback"), - subheading: _t("Thank you for trying Spaces. Your feedback will help inform the next versions."), - rageshakeLabel: "spaces-feedback", - rageshakeData: { - "feature_spaces.all_rooms": SettingsStore.getValue("feature_spaces.all_rooms"), - "feature_spaces.space_member_dms": SettingsStore.getValue("feature_spaces.space_member_dms"), - "feature_spaces.space_dm_badges": SettingsStore.getValue("feature_spaces.space_dm_badges"), - }, - }); - }}> + { + if (onClick) onClick(); + Modal.createTrackedDialog("Spaces Feedback", "", GenericFeatureFeedbackDialog, { + title: _t("Spaces feedback"), + subheading: _t("Thank you for trying Spaces. " + + "Your feedback will help inform the next versions."), + rageshakeLabel: "spaces-feedback", + rageshakeData: Object.fromEntries([ + "feature_spaces.all_rooms", + "feature_spaces.space_member_dms", + "feature_spaces.space_dm_badges", + ].map(k => [k, SettingsStore.getValue(k)])), + }); + }} + > { _t("Give feedback.") }
From c8d92943501373c520460e688ba25cc8016dcdfa Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 23 Jul 2021 18:40:22 +0100 Subject: [PATCH 059/263] Discard unused utility now that we use Object.fromEntries --- src/utils/objects.ts | 18 ------------------ test/utils/arrays-test.ts | 3 +-- test/utils/objects-test.ts | 18 ------------------ 3 files changed, 1 insertion(+), 38 deletions(-) diff --git a/src/utils/objects.ts b/src/utils/objects.ts index c2ee6ce100..e3b7b6cf59 100644 --- a/src/utils/objects.ts +++ b/src/utils/objects.ts @@ -141,21 +141,3 @@ export function objectKeyChanges(a: O, b: O): (keyof O)[] { export function objectClone(obj: O): O { return JSON.parse(JSON.stringify(obj)); } - -/** - * Converts a series of entries to an object. - * @param entries The entries to convert. - * @returns The converted object. - */ -// NOTE: Deprecated once we have Object.fromEntries() support. -// @ts-ignore - return type is complaining about non-string keys, but we know better -export function objectFromEntries(entries: Iterable<[K, V]>): {[k: K]: V} { - const obj: { - // @ts-ignore - same as return type - [k: K]: V;} = {}; - for (const e of entries) { - // @ts-ignore - same as return type - obj[e[0]] = e[1]; - } - return obj; -} diff --git a/test/utils/arrays-test.ts b/test/utils/arrays-test.ts index cf9a5f0089..277260bf29 100644 --- a/test/utils/arrays-test.ts +++ b/test/utils/arrays-test.ts @@ -29,7 +29,6 @@ import { ArrayUtil, GroupedArray, } from "../../src/utils/arrays"; -import { objectFromEntries } from "../../src/utils/objects"; function expectSample(i: number, input: number[], expected: number[], smooth = false) { console.log(`Resample case index: ${i}`); // for debugging test failures @@ -336,7 +335,7 @@ describe('arrays', () => { expect(result).toBeDefined(); expect(result.value).toBeDefined(); - const asObject = objectFromEntries(result.value.entries()); + const asObject = Object.fromEntries(result.value.entries()); expect(asObject).toMatchObject(output); }); }); diff --git a/test/utils/objects-test.ts b/test/utils/objects-test.ts index 154fa3604f..b360fbd1d1 100644 --- a/test/utils/objects-test.ts +++ b/test/utils/objects-test.ts @@ -18,7 +18,6 @@ import { objectClone, objectDiff, objectExcluding, - objectFromEntries, objectHasDiff, objectKeyChanges, objectShallowClone, @@ -242,21 +241,4 @@ describe('objects', () => { expect(result.test.third).not.toBe(a.test.third); }); }); - - describe('objectFromEntries', () => { - it('should create an object from an array of entries', () => { - const output = { a: 1, b: 2, c: 3 }; - const result = objectFromEntries(Object.entries(output)); - expect(result).toBeDefined(); - expect(result).toMatchObject(output); - }); - - it('should maintain pointers in values', () => { - const output = { a: {}, b: 2, c: 3 }; - const result = objectFromEntries(Object.entries(output)); - expect(result).toBeDefined(); - expect(result).toMatchObject(output); - expect(result['a']).toBe(output.a); - }); - }); }); From 70d8378a66a5842bdf88506e379434d02ca1dabc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 23 Jul 2021 18:50:25 +0100 Subject: [PATCH 060/263] delint --- src/components/structures/SpaceRoomView.tsx | 13 ++++++++----- .../views/dialogs/BetaFeedbackDialog.tsx | 17 ++++++++++------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 4e278b49e0..7a1d501767 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -395,11 +395,14 @@ const SpaceLanding = ({ space }) => { return
{ _t("Spaces are a new feature.") }  - { - Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, { - featureId: "feature_spaces", - }); - }}> + { + Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, { + featureId: "feature_spaces", + }); + }} + > { _t("Give feedback.") }
diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx index 98b228082a..d4fa618464 100644 --- a/src/components/views/dialogs/BetaFeedbackDialog.tsx +++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx @@ -43,13 +43,16 @@ const BetaFeedbackDialog: React.FC = ({ featureId, onFinished }) => { return SettingsStore.getValue(k); }))} > - { - onFinished(false); - defaultDispatcher.dispatch({ - action: Action.ViewUserSettings, - initialTabId: UserTab.Labs, - }); - }}> + { + onFinished(false); + defaultDispatcher.dispatch({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Labs, + }); + }} + > { _t("To leave the beta, visit your settings.") } ; From 64e3c72f657cba0e380c8b84585fcce03f81abf0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 23 Jul 2021 18:51:27 +0100 Subject: [PATCH 061/263] Fix bad import due to naming clash --- src/components/views/dialogs/BetaFeedbackDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx index d4fa618464..c5fba52b51 100644 --- a/src/components/views/dialogs/BetaFeedbackDialog.tsx +++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx @@ -23,7 +23,7 @@ import AccessibleButton from "../elements/AccessibleButton"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { UserTab } from "./UserSettingsDialog"; -import GenericFeatureFeedbackDialog from "./FeedbackDialog"; +import GenericFeatureFeedbackDialog from "./GenericFeatureFeedbackDialog"; // XXX: Keep this around for re-use in future Betas 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 062/263] 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 063/263] 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 064/263] 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 065/263] 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 066/263] 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 067/263] 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 068/263] 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 069/263] 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 070/263] 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 071/263] 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 072/263] 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 073/263] 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 074/263] 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 075/263] 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 076/263] 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 077/263] 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 774e74374137f68f07e105b437e64c7e7397eda8 Mon Sep 17 00:00:00 2001 From: Germain Date: Mon, 26 Jul 2021 08:16:13 +0100 Subject: [PATCH 078/263] Update res/css/views/rooms/_EventBubbleTile.scss --- res/css/views/rooms/_EventBubbleTile.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index f48468aee5..30985481da 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -231,6 +231,7 @@ limitations under the License. } &.mx_EventTile_bubbleContainer { + .mx_EventTile_line, .mx_EventTile_info { min-width: 100%; } From 257721185503d794a1f95e970c66e4945824639d Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 26 Jul 2021 09:42:17 +0200 Subject: [PATCH 079/263] Make CallEvent tiles the same width all the time --- res/css/views/messages/_CallEvent.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index 54c7df3e0b..41bdb8bf6a 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -23,7 +23,7 @@ limitations under the License. background-color: $dark-panel-bg-color; border-radius: 8px; margin: 10px auto; - max-width: 75%; + width: 75%; box-sizing: border-box; height: 60px; 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 080/263] 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 081/263] 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 082/263] 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 083/263] 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 737aa1c55d1dac22288f6e612f0a337afed719e1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 26 Jul 2021 12:41:52 +0100 Subject: [PATCH 084/263] delint --- .../dialogs/AddExistingToSpaceDialog.tsx | 22 ++++++++++++------- .../views/dialogs/CreateSubspaceDialog.tsx | 11 ++++++---- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index a0c2a09943..7194f3d7e2 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -417,10 +417,13 @@ const AddExistingToSpaceDialog: React.FC = ({ space, onCreateRoomClick, onFinished={onFinished} footerPrompt={<>
{ _t("Want to add a new room instead?") }
- { - onCreateRoomClick(); - onFinished(); - }}> + { + onCreateRoomClick(); + onFinished(); + }} + > { _t("Create a new room") } } @@ -429,10 +432,13 @@ const AddExistingToSpaceDialog: React.FC = ({ space, onCreateRoomClick, spacesRenderer={() => (

{ _t("Spaces") }

- { - onAddSubspaceClick(); - onFinished(); - }}> + { + onAddSubspaceClick(); + onFinished(); + }} + > { _t("Adding spaces has moved.") }
diff --git a/src/components/views/dialogs/CreateSubspaceDialog.tsx b/src/components/views/dialogs/CreateSubspaceDialog.tsx index 9f89490f5f..12fd6a3232 100644 --- a/src/components/views/dialogs/CreateSubspaceDialog.tsx +++ b/src/components/views/dialogs/CreateSubspaceDialog.tsx @@ -160,10 +160,13 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick
{ _t("Want to add an existing space instead?") }
- { - onAddExistingSpaceClick(); - onFinished(); - }}> + { + onAddExistingSpaceClick(); + onFinished(); + }} + > { _t("Add existing space") }
From e2688cd5e0ee5e35da7d046e9a03d9a5e7095ec4 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 26 Jul 2021 16:53:37 +0200 Subject: [PATCH 085/263] Left align unbubbled events --- res/css/views/rooms/_EventBubbleTile.scss | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index b281c16107..95186af761 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -232,7 +232,7 @@ limitations under the License. display: flex; align-items: center; - justify-content: center; + justify-content: start; padding: 5px 0; .mx_EventTile_avatar { @@ -244,6 +244,12 @@ limitations under the License. .mx_EventTile_e2eIcon { margin-left: 9px; } + + .mx_EventTile_line > a { + right: auto; + top: -15px; + left: -68px; + } } & ~ .mx_EventListSummary { 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 086/263] 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 087/263] 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 755007cbee4a91f59a025b59cd1c342443b01da5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 27 Jul 2021 14:39:14 +0100 Subject: [PATCH 088/263] Conclude labs flags and write more tests --- src/settings/Settings.tsx | 16 --- src/stores/SpaceStore.tsx | 27 ++--- .../notifications/SpaceNotificationState.ts | 2 +- test/stores/SpaceStore-setup.ts | 2 - test/stores/SpaceStore-test.ts | 108 ++++++++++++++++-- 5 files changed, 112 insertions(+), 43 deletions(-) diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index f0bdb2e0e5..5aa49df8a1 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -181,8 +181,6 @@ export const SETTINGS: {[setting: string]: ISetting} = { feedbackLabel: "spaces-feedback", extraSettings: [ "feature_spaces.all_rooms", - "feature_spaces.space_member_dms", - "feature_spaces.space_dm_badges", ], }, }, @@ -192,20 +190,6 @@ export const SETTINGS: {[setting: string]: ISetting} = { default: true, controller: new ReloadOnChangeController(), }, - "feature_spaces.space_member_dms": { - displayName: _td("Show people in spaces"), - description: _td("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."), - supportedLevels: LEVELS_FEATURE, - default: true, - controller: new ReloadOnChangeController(), - }, - "feature_spaces.space_dm_badges": { - displayName: _td("Show notification badges for People in Spaces"), - supportedLevels: LEVELS_FEATURE, - default: false, - controller: new ReloadOnChangeController(), - }, "feature_dnd": { isFeature: true, displayName: _td("Show options to enable 'Do not disturb' mode"), diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index a338e71838..d064b01257 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -72,8 +72,6 @@ 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 const spacesEnabled = SettingsStore.getValue("feature_spaces"); const spacesTweakAllRoomsEnabled = SettingsStore.getValue("feature_spaces.all_rooms"); -const spacesTweakSpaceMemberDMsEnabled = SettingsStore.getValue("feature_spaces.space_member_dms"); -const spacesTweakSpaceDMBadgesEnabled = SettingsStore.getValue("feature_spaces.space_dm_badges"); const homeSpaceKey = spacesTweakAllRoomsEnabled ? "ALL_ROOMS" : "HOME_SPACE"; const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || homeSpaceKey}`; @@ -535,15 +533,13 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const roomIds = new Set(childRooms.map(r => r.roomId)); const space = this.matrixClient?.getRoom(spaceId); - if (spacesTweakSpaceMemberDMsEnabled) { - // Add relevant DMs - space?.getMembers().forEach(member => { - if (member.membership !== "join" && member.membership !== "invite") return; - DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => { - roomIds.add(roomId); - }); + // Add relevant DMs + space?.getMembers().forEach(member => { + if (member.membership !== "join" && member.membership !== "invite") return; + DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => { + roomIds.add(roomId); }); - } + }); const newPath = new Set(parentPath).add(spaceId); childSpaces.forEach(childSpace => { @@ -568,14 +564,13 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.spaceFilteredRooms.forEach((roomIds, s) => { // Update NotificationStates this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => { - if (roomIds.has(room.roomId)) { - if (s !== HOME_SPACE && spacesTweakSpaceDMBadgesEnabled) return true; + if (!roomIds.has(room.roomId)) return false; - return !DMRoomMap.shared().getUserIdForRoomId(room.roomId) - || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite); + if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { + return s === HOME_SPACE; } - return false; + return true; })); }); }, 100, { trailing: true, leading: true }); @@ -878,8 +873,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { export default class SpaceStore { public static spacesEnabled = spacesEnabled; public static spacesTweakAllRoomsEnabled = spacesTweakAllRoomsEnabled; - public static spacesTweakSpaceMemberDMsEnabled = spacesTweakSpaceMemberDMsEnabled; - public static spacesTweakSpaceDMBadgesEnabled = spacesTweakSpaceDMBadgesEnabled; private static internalInstance = new SpaceStoreClass(); diff --git a/src/stores/notifications/SpaceNotificationState.ts b/src/stores/notifications/SpaceNotificationState.ts index 4c0a582f3f..f8eb07251b 100644 --- a/src/stores/notifications/SpaceNotificationState.ts +++ b/src/stores/notifications/SpaceNotificationState.ts @@ -23,7 +23,7 @@ import { NOTIFICATION_STATE_UPDATE, NotificationState } from "./NotificationStat import { FetchRoomFn } from "./ListNotificationState"; export class SpaceNotificationState extends NotificationState { - private rooms: Room[] = []; + public rooms: Room[] = []; // exposed only for tests private states: { [spaceId: string]: RoomNotificationState } = {}; constructor(private spaceId: string | symbol, private getRoomFn: FetchRoomFn) { diff --git a/test/stores/SpaceStore-setup.ts b/test/stores/SpaceStore-setup.ts index 67d492255f..b9b865e89a 100644 --- a/test/stores/SpaceStore-setup.ts +++ b/test/stores/SpaceStore-setup.ts @@ -19,5 +19,3 @@ limitations under the License. localStorage.setItem("mx_labs_feature_feature_spaces", "true"); localStorage.setItem("mx_labs_feature_feature_spaces.all_rooms", "true"); -localStorage.setItem("mx_labs_feature_feature_spaces.space_member_dms", "true"); -localStorage.setItem("mx_labs_feature_feature_spaces.space_dm_badges", "false"); diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 8855f4e470..d772a7a658 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -17,6 +17,7 @@ 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 import "../skinned-sdk"; // Must be first for skinning to work @@ -53,18 +54,22 @@ const emitPromise = (e: EventEmitter, k: string | symbol) => new Promise(r => e. const testUserId = "@test:user"; const getUserIdForRoomId = jest.fn(); +const getDMRoomsForUserId = jest.fn(); // @ts-ignore -DMRoomMap.sharedInstance = { getUserIdForRoomId }; +DMRoomMap.sharedInstance = { getUserIdForRoomId, getDMRoomsForUserId }; const fav1 = "!fav1:server"; const fav2 = "!fav2:server"; const fav3 = "!fav3:server"; const dm1 = "!dm1:server"; -const dm1Partner = "@dm1Partner:server"; +const dm1Partner = new RoomMember(dm1, "@dm1Partner:server"); +dm1Partner.membership = "join"; const dm2 = "!dm2:server"; -const dm2Partner = "@dm2Partner:server"; +const dm2Partner = new RoomMember(dm2, "@dm2Partner:server"); +dm2Partner.membership = "join"; const dm3 = "!dm3:server"; -const dm3Partner = "@dm3Partner:server"; +const dm3Partner = new RoomMember(dm3, "@dm3Partner:server"); +dm3Partner.membership = "join"; const orphan1 = "!orphan1:server"; const orphan2 = "!orphan2:server"; const invite1 = "!invite1:server"; @@ -320,11 +325,40 @@ describe("SpaceStore", () => { getUserIdForRoomId.mockImplementation(roomId => { return { - [dm1]: dm1Partner, - [dm2]: dm2Partner, - [dm3]: dm3Partner, + [dm1]: dm1Partner.userId, + [dm2]: dm2Partner.userId, + [dm3]: dm3Partner.userId, }[roomId]; }); + getDMRoomsForUserId.mockImplementation(userId => { + switch (userId) { + case dm1Partner.userId: + return [dm1]; + case dm2Partner.userId: + return [dm2]; + case dm3Partner.userId: + return [dm3]; + default: + return []; + } + }); + + // have dmPartner1 be in space1 with you + const mySpace1Member = new RoomMember(space1, testUserId); + mySpace1Member.membership = "join"; + (rooms.find(r => r.roomId === space1).getMembers as jest.Mock).mockReturnValue([ + mySpace1Member, + dm1Partner, + ]); + // have dmPartner2 be in space2 with you + const mySpace2Member = new RoomMember(space2, testUserId); + mySpace2Member.membership = "join"; + (rooms.find(r => r.roomId === space2).getMembers as jest.Mock).mockReturnValue([ + mySpace2Member, + dm2Partner, + ]); + // dmPartner3 is not in any common spaces with you + await run(); }); @@ -375,6 +409,66 @@ describe("SpaceStore", () => { const space = client.getRoom(space3); expect(store.getSpaceFilteredRoomIds(space).has(invite2)).toBeTruthy(); }); + + it("spaces contain dms which you have with members of that space", () => { + expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(dm1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(dm1)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(dm1)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(dm2)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(dm2)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(dm2)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(dm3)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(dm3)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(dm3)).toBeFalsy(); + }); + + it("dms are only added to Notification States for only the Home Space", () => { + // XXX: All rooms space is forcibly enabled, as part of a future PR test Home space better + // [dm1, dm2, dm3].forEach(d => { + // expect(store.getNotificationState(HOME_SPACE).rooms.map(r => r.roomId).includes(d)).toBeTruthy(); + // }); + [space1, space2, space3].forEach(s => { + [dm1, dm2, dm3].forEach(d => { + expect(store.getNotificationState(s).rooms.map(r => r.roomId).includes(d)).toBeFalsy(); + }); + }); + }); + + it("orphan rooms are added to Notification States for only the Home Space", () => { + // XXX: All rooms space is forcibly enabled, as part of a future PR test Home space better + // [orphan1, orphan2].forEach(d => { + // expect(store.getNotificationState(HOME_SPACE).rooms.map(r => r.roomId).includes(d)).toBeTruthy(); + // }); + [space1, space2, space3].forEach(s => { + [orphan1, orphan2].forEach(d => { + expect(store.getNotificationState(s).rooms.map(r => r.roomId).includes(d)).toBeFalsy(); + }); + }); + }); + + it("favourites are added to Notification States for all spaces containing the room inc Home", () => { + // XXX: All rooms space is forcibly enabled, as part of a future PR test Home space better + // [fav1, fav2, fav3].forEach(d => { + // expect(store.getNotificationState(HOME_SPACE).rooms.map(r => r.roomId).includes(d)).toBeTruthy(); + // }); + expect(store.getNotificationState(space1).rooms.map(r => r.roomId).includes(fav1)).toBeTruthy(); + expect(store.getNotificationState(space1).rooms.map(r => r.roomId).includes(fav2)).toBeFalsy(); + expect(store.getNotificationState(space1).rooms.map(r => r.roomId).includes(fav3)).toBeFalsy(); + expect(store.getNotificationState(space2).rooms.map(r => r.roomId).includes(fav1)).toBeTruthy(); + expect(store.getNotificationState(space2).rooms.map(r => r.roomId).includes(fav2)).toBeTruthy(); + expect(store.getNotificationState(space2).rooms.map(r => r.roomId).includes(fav3)).toBeTruthy(); + expect(store.getNotificationState(space3).rooms.map(r => r.roomId).includes(fav1)).toBeFalsy(); + expect(store.getNotificationState(space3).rooms.map(r => r.roomId).includes(fav2)).toBeFalsy(); + expect(store.getNotificationState(space3).rooms.map(r => r.roomId).includes(fav3)).toBeFalsy(); + }); + + it("other rooms are added to Notification States for all spaces containing the room exc Home", () => { + // XXX: All rooms space is forcibly enabled, as part of a future PR test Home space better + // expect(store.getNotificationState(HOME_SPACE).rooms.map(r => r.roomId).includes(room1)).toBeFalsy(); + expect(store.getNotificationState(space1).rooms.map(r => r.roomId).includes(room1)).toBeTruthy(); + expect(store.getNotificationState(space2).rooms.map(r => r.roomId).includes(room1)).toBeTruthy(); + expect(store.getNotificationState(space3).rooms.map(r => r.roomId).includes(room1)).toBeFalsy(); + }); }); }); From 353c70ad75ff79616c8cb9426b89885e7aad5470 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 27 Jul 2021 14:45:36 +0100 Subject: [PATCH 089/263] i18n --- src/i18n/strings/en_EN.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 102a481f52..de432d6177 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -798,9 +798,6 @@ "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 people in spaces": "Show people in spaces", - "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", From 78eb8ffc261b948be714634bfecca04d35a19359 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 27 Jul 2021 15:51:16 +0100 Subject: [PATCH 090/263] 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 091/263] 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 092/263] 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 093/263] 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 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 094/263] 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 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 095/263] 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 096/263] 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 097/263] 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 098/263] 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 099/263] 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 100/263] 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 1d81bdc6f9a676d075e6ad83b055b4ee43080a86 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 09:37:08 +0100 Subject: [PATCH 101/263] 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 ab7d38717c1fc5046a4d581c4ccb964ec0af2507 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 27 Jul 2021 16:24:05 +0200 Subject: [PATCH 102/263] 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 103/263] 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 104/263] 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 105/263] 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 106/263] 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 107/263] 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 c697079eb42727f84cfb6b9f79d73931710d10b9 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 11:22:40 +0100 Subject: [PATCH 108/263] 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 109/263] 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 110/263] 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 111/263] 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 112/263] 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 113/263] 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 df7ebb2e7ce5894afd349f3a1f8cf930f80f5f90 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:00:37 +0100 Subject: [PATCH 114/263] 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 115/263] 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 116/263] 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 117/263] 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 118/263] 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 119/263] 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 120/263] 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 121/263] 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 9420b81eebdfc2b4edb3d6dcf0f179c4d29e358c Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:22:32 +0100 Subject: [PATCH 122/263] 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 123/263] 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 124/263] 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 125/263] 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 126/263] 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 127/263] 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 128/263] 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 129/263] 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 130/263] 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 131/263] 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 132/263] 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 133/263] 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 134/263] 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 135/263] 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 136/263] 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 137/263] 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 138/263] 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 139/263] 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 140/263] 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 a5606c72de1383069c8846218fd61b5e6164f453 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Jul 2021 17:56:57 +0100 Subject: [PATCH 141/263] Update feedback prompt to match designs --- res/css/structures/_SpaceRoomView.scss | 12 +--- res/css/views/spaces/_SpaceCreateMenu.scss | 70 +++++-------------- src/components/structures/SpaceRoomView.tsx | 17 +---- .../views/spaces/SpaceCreateMenu.tsx | 45 ++++++------ 4 files changed, 42 insertions(+), 102 deletions(-) diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index f8c0a5c262..58a4b426c2 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -340,18 +340,12 @@ $SpaceRoomViewInnerWidth: 428px; flex: 0; } - .mx_SpaceFeedbackPrompt_topRight { + .mx_SpaceFeedbackPrompt { padding: 7px; // 8px - 1px border border: 1px solid $menu-border-color; border-radius: 8px; - color: $secondary-fg-color; - font-size: $font-15px; - line-height: $font-24px; - float: right; - - & + .mx_BaseAvatar { - clear: both; - } + width: max-content; + margin: 0 0 -40px auto; // collapse its own height to not push other components down } .mx_SpaceRoomDirectory_list { diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss index 7ed71e454c..e44e16916f 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.scss +++ b/res/css/views/spaces/_SpaceCreateMenu.scss @@ -104,61 +104,23 @@ $spacePanelWidth: 71px; margin-top: 18px; margin-bottom: 12px; - > hr { - border: none; - border-top: 1px solid $input-border-color; - margin-bottom: 12px; + font-size: $font-15px; + line-height: $font-24px; + + > span { + color: $secondary-fg-color; + position: relative; + font-size: inherit; + line-height: inherit; + margin-right: auto; } - > div { - display: flex; - flex-direction: row; - font-size: $font-15px; - line-height: $font-24px; - - > span { - color: $secondary-fg-color; - position: relative; - padding-left: 32px; - font-size: inherit; - line-height: inherit; - margin-right: auto; - - &::before { - content: ''; - position: absolute; - left: 0; - top: 2px; - height: 20px; - width: 20px; - background-color: $secondary-fg-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); - mask-position: center; - } - } - - .mx_AccessibleButton_kind_link { - color: $accent-color; - position: relative; - padding: 0 0 0 24px; - margin-left: 8px; - font-size: inherit; - line-height: inherit; - - &::before { - content: ''; - position: absolute; - left: 0; - height: 16px; - width: 16px; - background-color: $accent-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-image: url('$(res)/img/element-icons/chat-bubbles.svg'); - mask-position: center; - } - } + .mx_AccessibleButton_kind_link { + color: $accent-color; + position: relative; + padding: 0; + margin-left: 8px; + font-size: inherit; + line-height: inherit; } } diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 7a1d501767..b47eb12c74 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -62,9 +62,8 @@ import IconizedContextMenu, { import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { BetaPill } from "../views/beta/BetaCard"; import { UserTab } from "../views/dialogs/UserSettingsDialog"; -import Modal from "../../Modal"; -import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog"; import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership"; +import { SpaceFeedbackPrompt } from "../views/spaces/SpaceCreateMenu"; interface IProps { space: Room; @@ -393,19 +392,7 @@ const SpaceLanding = ({ space }) => { }; return
    -
    - { _t("Spaces are a new feature.") }  - { - Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, { - featureId: "feature_spaces", - }); - }} - > - { _t("Give feedback.") } - -
    +
    diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index ee01984db0..86999547b7 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -66,33 +66,30 @@ const nameToAlias = (name: string, domain: string): string => { }; // XXX: Temporary for the Spaces release only -const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => { +export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => { if (!SdkConfig.get().bug_report_endpoint_url) return null; return
    -
    -
    - { _t("Spaces are a new feature.") } - { - if (onClick) onClick(); - Modal.createTrackedDialog("Spaces Feedback", "", GenericFeatureFeedbackDialog, { - title: _t("Spaces feedback"), - subheading: _t("Thank you for trying Spaces. " + - "Your feedback will help inform the next versions."), - rageshakeLabel: "spaces-feedback", - rageshakeData: Object.fromEntries([ - "feature_spaces.all_rooms", - "feature_spaces.space_member_dms", - "feature_spaces.space_dm_badges", - ].map(k => [k, SettingsStore.getValue(k)])), - }); - }} - > - { _t("Give feedback.") } - -
    + { _t("Spaces are a new feature.") } + { + if (onClick) onClick(); + Modal.createTrackedDialog("Spaces Feedback", "", GenericFeatureFeedbackDialog, { + title: _t("Spaces feedback"), + subheading: _t("Thank you for trying Spaces. " + + "Your feedback will help inform the next versions."), + rageshakeLabel: "spaces-feedback", + rageshakeData: Object.fromEntries([ + "feature_spaces.all_rooms", + "feature_spaces.space_member_dms", + "feature_spaces.space_dm_badges", + ].map(k => [k, SettingsStore.getValue(k)])), + }); + }} + > + { _t("Give feedback.") } +
    ; }; From ed950875e7d3cf8f38cf8b787f4376c08d34a173 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Jul 2021 19:08:59 +0100 Subject: [PATCH 142/263] Fix space hierarchy not updating when user mutates it --- .../structures/SpaceRoomDirectory.tsx | 20 +++++++--- src/components/structures/SpaceRoomView.tsx | 32 ++++----------- .../dialogs/AddExistingToSpaceDialog.tsx | 2 +- src/dispatcher/actions.ts | 5 +++ src/utils/space.tsx | 40 ++++++++++++++----- 5 files changed, 57 insertions(+), 42 deletions(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 038c1df514..610ac5e103 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -44,11 +44,13 @@ import { getChildOrder } from "../../stores/SpaceStore"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { linkifyElement } from "../../HtmlUtils"; import { getDisplayAliasForAliasSet } from "../../Rooms"; +import { useDispatcher } from "../../hooks/useDispatcher"; +import defaultDispatcher from "../../dispatcher/dispatcher"; +import { Action } from "../../dispatcher/actions"; interface IHierarchyProps { space: Room; initialText?: string; - refreshToken?: any; additionalButtons?: ReactNode; showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void; } @@ -315,18 +317,25 @@ export const HierarchyLevel = ({ ; }; -// mutate argument refreshToken to force a reload -export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [ +export const useSpaceSummary = (space: Room): [ null, ISpaceSummaryRoom[], Map>?, Map>?, Map>?, ] | [Error] => { + // crude temporary refresh token approach until we have pagination and rework the data flow here + const [refreshToken, setRefreshToken] = useState(0); + useDispatcher(defaultDispatcher, (payload => { + if (payload.action === Action.UpdateSpaceHierarchy) { + setRefreshToken(t => t + 1); + } + })); + // TODO pagination return useAsyncMemo(async () => { try { - const data = await cli.getSpaceSummary(space.roomId); + const data = await space.client.getSpaceSummary(space.roomId); const parentChildRelations = new EnhancedMap>(); const childParentRelations = new EnhancedMap>(); @@ -354,7 +363,6 @@ export const SpaceHierarchy: React.FC = ({ space, initialText = "", showRoom, - refreshToken, additionalButtons, children, }) => { @@ -364,7 +372,7 @@ export const SpaceHierarchy: React.FC = ({ const [selected, setSelected] = useState(new Map>()); // Map> - const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken); + const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(space); const roomsMap = useMemo(() => { if (!rooms) return null; diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index cf5cad1651..5829578cd2 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -56,7 +56,6 @@ import { } from "../../utils/space"; import { showRoom, SpaceHierarchy } from "./SpaceRoomDirectory"; import MemberAvatar from "../views/avatars/MemberAvatar"; -import { useStateToggle } from "../../hooks/useStateToggle"; import SpaceStore from "../../stores/SpaceStore"; import FacePile from "../views/elements/FacePile"; import { @@ -318,7 +317,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
    ; }; -const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => { +const SpaceLandingAddButton = ({ space }) => { const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); let contextMenu; @@ -342,36 +341,28 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => { closeMenu(); if (await showCreateNewRoom(space)) { - onNewRoomAdded(); + defaultDispatcher.fire(Action.UpdateSpaceHierarchy); } }} /> { + onClick={(e) => { e.preventDefault(); e.stopPropagation(); closeMenu(); - - const [added] = await showAddExistingRooms(space); - if (added) { - onNewRoomAdded(); - } + showAddExistingRooms(space); }} /> { + onClick={(e) => { e.preventDefault(); e.stopPropagation(); closeMenu(); - - const [added] = await showCreateNewSubspace(space); - if (added) { - onNewRoomAdded(); - } + showCreateNewSubspace(space); }} > @@ -416,11 +407,9 @@ const SpaceLanding = ({ space }) => { const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId); - const [refreshToken, forceUpdate] = useStateToggle(false); - let addRoomButton; if (canAddRooms) { - addRoomButton = ; + addRoomButton = ; } let settingsButton; @@ -470,12 +459,7 @@ const SpaceLanding = ({ space }) => {
    - +
    ; }; diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 7194f3d7e2..9b64af0c80 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -249,7 +249,7 @@ export const AddExistingToSpace: React.FC = ({ let noResults = true; if ((roomsRenderer && rooms.length > 0) || (dmsRenderer && dms.length > 0) || - (!roomsRenderer && !dmsRenderer && spacesRenderer && dms.length > 0) // only count spaces when alone + (!roomsRenderer && !dmsRenderer && spacesRenderer && spaces.length > 0) // only count spaces when alone ) { noResults = false; } diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 5732428201..06cbbba46c 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -193,4 +193,9 @@ export enum Action { * Switches space. Should be used with SwitchSpacePayload. */ SwitchSpace = "switch_space", + + /** + * Signals to the visible space hierarchy that a change has occurred an that it should refresh. + */ + UpdateSpaceHierarchy = "update_space_hierarchy", } diff --git a/src/utils/space.tsx b/src/utils/space.tsx index 1e8a6fbeaf..e705b4eee4 100644 --- a/src/utils/space.tsx +++ b/src/utils/space.tsx @@ -30,6 +30,9 @@ import InfoDialog from "../components/views/dialogs/InfoDialog"; import { showRoomInviteDialog } from "../RoomInvite"; import CreateSubspaceDialog from "../components/views/dialogs/CreateSubspaceDialog"; import AddExistingSubspaceDialog from "../components/views/dialogs/AddExistingSubspaceDialog"; +import defaultDispatcher from "../dispatcher/dispatcher"; +import RoomViewStore from "../stores/RoomViewStore"; +import { Action } from "../dispatcher/actions"; export const shouldShowSpaceSettings = (space: Room) => { const userId = space.client.getUserId(); @@ -56,8 +59,8 @@ export const showSpaceSettings = (space: Room) => { }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }; -export const showAddExistingRooms = async (space: Room): Promise<[boolean]> => { - return Modal.createTrackedDialog( +export const showAddExistingRooms = (space: Room): void => { + Modal.createTrackedDialog( "Space Landing", "Add Existing", AddExistingToSpaceDialog, @@ -65,12 +68,17 @@ export const showAddExistingRooms = async (space: Room): Promise<[boolean]> => { onCreateRoomClick: () => showCreateNewRoom(space), onAddSubspaceClick: () => showAddExistingSubspace(space), space, + onFinished: (added: boolean) => { + if (added && RoomViewStore.getRoomId() === space.roomId) { + defaultDispatcher.fire(Action.UpdateSpaceHierarchy); + } + }, }, "mx_AddExistingToSpaceDialog_wrapper", - ).finished as Promise<[boolean]>; + ); }; -export const showCreateNewRoom = async (space: Room) => { +export const showCreateNewRoom = async (space: Room): Promise => { const modal = Modal.createTrackedDialog<[boolean, IOpts]>( "Space Landing", "Create Room", @@ -87,7 +95,7 @@ export const showCreateNewRoom = async (space: Room) => { return shouldCreate; }; -export const showSpaceInvite = (space: Room, initialText = "") => { +export const showSpaceInvite = (space: Room, initialText = ""): void => { if (space.getJoinRule() === "public") { const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, { title: _t("Invite to %(spaceName)s", { spaceName: space.name }), @@ -105,28 +113,38 @@ export const showSpaceInvite = (space: Room, initialText = "") => { } }; -export const showAddExistingSubspace = async (space: Room): Promise<[boolean]> => { - return Modal.createTrackedDialog( +export const showAddExistingSubspace = (space: Room): void => { + Modal.createTrackedDialog( "Space Landing", "Create Subspace", AddExistingSubspaceDialog, { space, onCreateSubspaceClick: () => showCreateNewSubspace(space), + onFinished: (added: boolean) => { + if (added && RoomViewStore.getRoomId() === space.roomId) { + defaultDispatcher.fire(Action.UpdateSpaceHierarchy); + } + }, }, "mx_AddExistingToSpaceDialog_wrapper", - ).finished as Promise<[boolean]>; + ); }; -export const showCreateNewSubspace = async (space: Room): Promise<[boolean]> => { - return Modal.createTrackedDialog( +export const showCreateNewSubspace = (space: Room): void => { + Modal.createTrackedDialog( "Space Landing", "Create Subspace", CreateSubspaceDialog, { space, onAddExistingSpaceClick: () => showAddExistingSubspace(space), + onFinished: (added: boolean) => { + if (added && RoomViewStore.getRoomId() === space.roomId) { + defaultDispatcher.fire(Action.UpdateSpaceHierarchy); + } + }, }, "mx_CreateSubspaceDialog_wrapper", - ).finished as Promise<[boolean]>; + ); }; From 40493634ae98d517b3416ab0020b883fb2948b3e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Jul 2021 19:23:33 +0100 Subject: [PATCH 143/263] Iterate PR --- .../views/dialogs/_CreateSubspaceDialog.scss | 6 +++++ .../views/dialogs/CreateSubspaceDialog.tsx | 24 +++++++++++++++++++ src/i18n/strings/en_EN.json | 3 +++ 3 files changed, 33 insertions(+) diff --git a/res/css/views/dialogs/_CreateSubspaceDialog.scss b/res/css/views/dialogs/_CreateSubspaceDialog.scss index c780da4b9e..1ec4731ae6 100644 --- a/res/css/views/dialogs/_CreateSubspaceDialog.scss +++ b/res/css/views/dialogs/_CreateSubspaceDialog.scss @@ -39,8 +39,14 @@ limitations under the License. .mx_BetaCard_betaPill { margin-right: 8px; + vertical-align: middle; } } + + .mx_JoinRuleDropdown + p { + color: $muted-fg-color; + font-size: $font-12px; + } } .mx_CreateSubspaceDialog_footer { diff --git a/src/components/views/dialogs/CreateSubspaceDialog.tsx b/src/components/views/dialogs/CreateSubspaceDialog.tsx index 12fd6a3232..6a81bb04df 100644 --- a/src/components/views/dialogs/CreateSubspaceDialog.tsx +++ b/src/components/views/dialogs/CreateSubspaceDialog.tsx @@ -110,6 +110,29 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick } }; + let joinRuleMicrocopy: JSX.Element; + if (joinRule === JoinRule.Restricted) { + joinRuleMicrocopy =

    + { _t( + "Anyone in will be able to find and join.", {}, { + SpaceName: () => { parentSpace.name }, + }, + ) } +

    ; + } else if (joinRule === JoinRule.Public) { + joinRuleMicrocopy =

    + { _t( + "Anyone will be able to find and join this space, not just members of .", {}, { + SpaceName: () => { parentSpace.name }, + }, + ) } +

    ; + } else if (joinRule === JoinRule.Invite) { + joinRuleMicrocopy =

    + { _t("Only people invited will be able to find and join this space.") } +

    ; + } + return = ({ space, onAddExistingSpaceClick value={joinRule} onChange={setJoinRule} /> + { joinRuleMicrocopy }
    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e6b96a132d..ef4537bddf 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2219,6 +2219,9 @@ "Visible to space members": "Visible to space members", "Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.", "Create Room": "Create Room", + "Anyone in will be able to find and join.": "Anyone in will be able to find and join.", + "Anyone will be able to find and join this space, not just members of .": "Anyone will be able to find and join this space, not just members of .", + "Only people invited will be able to find and join this space.": "Only people invited will be able to find and join this space.", "Create a subspace": "Create a subspace", "Add a subspace to a space you manage.": "Add a subspace to a space you manage.", "Subspace visibility": "Subspace visibility", 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 144/263] 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 67b8c0a8113752a13eb251a9f3443c370d10ba50 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Jul 2021 19:33:54 +0100 Subject: [PATCH 145/263] remove unused import --- src/components/structures/SpaceRoomDirectory.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 610ac5e103..d8cc9593f0 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -16,7 +16,6 @@ limitations under the License. import React, { ReactNode, useMemo, useState } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; -import { MatrixClient } from "matrix-js-sdk/src/client"; import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces"; import classNames from "classnames"; 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 146/263] 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 147/263] 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 148/263] 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 78a911759835c6ffa34b1d4e6c12ff50e593c28b Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 29 Jul 2021 11:12:07 +0200 Subject: [PATCH 149/263] Prevent timeline flickering on hover --- res/css/views/rooms/_EventBubbleTile.scss | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 866bc904c2..65e4e58078 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -38,18 +38,22 @@ limitations under the License. padding-top: 0; } + &::before { + content: ''; + position: absolute; + top: -1px; + bottom: -1px; + left: -60px; + right: -60px; + z-index: -1; + border-radius: 4px; + } + &:hover, &.mx_EventTile_selected { + &::before { - content: ''; - position: absolute; - top: -1px; - bottom: -1px; - left: -60px; - right: -60px; - z-index: -1; background: $eventbubble-bg-hover; - border-radius: 4px; } .mx_EventTile_avatar { From 9c4101fa8999ed490011e9424443b875de5b986a Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 29 Jul 2021 11:48:22 +0200 Subject: [PATCH 150/263] Align event list summary read receipts when using message bubbles --- res/css/views/rooms/_EventBubbleTile.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 866bc904c2..8dd4a6e515 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -288,9 +288,9 @@ limitations under the License. } .mx_EventListSummary[data-layout=bubble] { - --maxWidth: 80%; + --maxWidth: 70%; margin-left: calc(var(--avatarSize) + var(--gutterSize)); - margin-right: calc(var(--gutterSize) + var(--avatarSize)); + margin-right: 94px; .mx_EventListSummary_toggle { float: none; margin: 0; From 6c68f779ec13278cdac373bf6d30c9da56db1133 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 29 Jul 2021 11:58:32 +0200 Subject: [PATCH 151/263] Restore pointer cursor for SenderProfile in message bubbles --- res/css/views/rooms/_EventTile.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 206fe843de..5de9a9f9d1 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -59,7 +59,6 @@ $hover-select-border: 4px; font-size: $font-14px; display: inline-block; /* anti-zalgo, with overflow hidden */ overflow: hidden; - cursor: pointer; padding-bottom: 0px; padding-top: 0px; margin: 0px; @@ -322,6 +321,10 @@ $hover-select-border: 4px; // on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter } +.mx_SenderProfile { + cursor: pointer; +} + .mx_EventTile_bubbleContainer { display: grid; grid-template-columns: 1fr 100px; From 7baa3284e3441361920234547993bb661398b385 Mon Sep 17 00:00:00 2001 From: Dariusz Niemczyk Date: Thu, 29 Jul 2021 12:39:32 +0200 Subject: [PATCH 152/263] 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 12:39:32 +0200 Subject: [PATCH 153/263] 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 13:02:07 +0100 Subject: [PATCH 154/263] Update all copy of `subspace` to `space` --- src/components/structures/SpaceRoomView.tsx | 2 +- .../views/dialogs/AddExistingSubspaceDialog.tsx | 2 +- .../views/dialogs/CreateSubspaceDialog.tsx | 10 +++++----- src/components/views/spaces/SpaceTreeLevel.tsx | 2 +- src/i18n/strings/en_EN.json | 14 ++++++-------- 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 5829578cd2..682a0c68db 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -356,7 +356,7 @@ const SpaceLandingAddButton = ({ space }) => { }} /> { e.preventDefault(); diff --git a/src/components/views/dialogs/AddExistingSubspaceDialog.tsx b/src/components/views/dialogs/AddExistingSubspaceDialog.tsx index f0e5e9241e..a6dbf9dd42 100644 --- a/src/components/views/dialogs/AddExistingSubspaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingSubspaceDialog.tsx @@ -54,7 +54,7 @@ const AddExistingSubspaceDialog: React.FC = ({ space, onCreateSubspaceCl footerPrompt={<>
    { _t("Want to add a new space instead?") }
    - { _t("Create a new subspace") } + { _t("Create a new space") } } filterPlaceholder={_t("Search for spaces")} diff --git a/src/components/views/dialogs/CreateSubspaceDialog.tsx b/src/components/views/dialogs/CreateSubspaceDialog.tsx index 6a81bb04df..0d71eb2de3 100644 --- a/src/components/views/dialogs/CreateSubspaceDialog.tsx +++ b/src/components/views/dialogs/CreateSubspaceDialog.tsx @@ -136,7 +136,7 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick return = ({ space, onAddExistingSpaceClick
    - { _t("Add a subspace to a space you manage.") } + { _t("Add a space to a space you manage.") }
    = ({ space, onAddExistingSpaceClick aliasFieldRef={spaceAliasField} > { /> diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ef4537bddf..314f8a469e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1056,7 +1056,7 @@ "Leave space": "Leave space", "Create new room": "Create new room", "Add existing room": "Add existing room", - "Add subspace": "Add subspace", + "Add space": "Add space", "Members": "Members", "Manage & explore rooms": "Manage & explore rooms", "Explore rooms": "Explore rooms", @@ -2113,7 +2113,7 @@ "Matrix rooms": "Matrix rooms", "Add existing space": "Add existing space", "Want to add a new space instead?": "Want to add a new space instead?", - "Create a new subspace": "Create a new subspace", + "Create a new space": "Create a new space", "Search for spaces": "Search for spaces", "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)", @@ -2222,11 +2222,10 @@ "Anyone in will be able to find and join.": "Anyone in will be able to find and join.", "Anyone will be able to find and join this space, not just members of .": "Anyone will be able to find and join this space, not just members of .", "Only people invited will be able to find and join this space.": "Only people invited will be able to find and join this space.", - "Create a subspace": "Create a subspace", - "Add a subspace to a space you manage.": "Add a subspace to a space you manage.", - "Subspace visibility": "Subspace visibility", - "Private subspace (invite only)": "Private subspace (invite only)", - "Public subspace": "Public subspace", + "Add a space to a space you manage.": "Add a space to a space you manage.", + "Space visibility": "Space visibility", + "Private space (invite only)": "Private space (invite only)", + "Public space": "Public space", "Want to add an existing space instead?": "Want to add an existing space instead?", "Adding...": "Adding...", "Sign out": "Sign out", @@ -2815,7 +2814,6 @@ "If you can't find the room you're looking for, ask for an invite or create a new room.": "If you can't find the room you're looking for, ask for an invite or create a new room.", "Create room": "Create room", "Spaces are a beta feature.": "Spaces are a beta feature.", - "Public space": "Public space", "Private space": "Private space", " invites you": " invites you", "To view %(spaceName)s, turn on the Spaces beta": "To view %(spaceName)s, turn on the Spaces beta", 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 155/263] 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 ( -