diff --git a/res/css/_common.scss b/res/css/_common.scss index 3346394edd..666129af34 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -208,12 +208,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { border: 0; } -/* applied to side-panels and messagepanel when in RoomSettings */ -.mx_fadable { - opacity: 1; - transition: opacity 0.2s ease-in-out; -} - // These are magic constants which are excluded from tinting, to let themes // (which only have CSS, unlike skins) tell the app what their non-tinted // colourscheme is by inspecting the stylesheet DOM. diff --git a/res/css/_components.scss b/res/css/_components.scss index 72686d2938..e3f03dabb6 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -26,7 +26,7 @@ @import "./structures/_ScrollPanel.scss"; @import "./structures/_SearchBox.scss"; @import "./structures/_TabbedView.scss"; -@import "./structures/_TagPanel.scss"; +@import "./structures/_GroupFilterPanel.scss"; @import "./structures/_ToastContainer.scss"; @import "./structures/_UploadBar.scss"; @import "./structures/_UserMenu.scss"; diff --git a/res/css/structures/_CustomRoomTagPanel.scss b/res/css/structures/_CustomRoomTagPanel.scss index 3feb2565be..96813cccea 100644 --- a/res/css/structures/_CustomRoomTagPanel.scss +++ b/res/css/structures/_CustomRoomTagPanel.scss @@ -22,7 +22,7 @@ limitations under the License. } .mx_CustomRoomTagPanel { - background-color: $tagpanel-bg-color; + background-color: $groupFilterPanel-bg-color; max-height: 40vh; } diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_GroupFilterPanel.scss similarity index 80% rename from res/css/structures/_TagPanel.scss rename to res/css/structures/_GroupFilterPanel.scss index cdca1f0764..e5a8ef6df2 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_GroupFilterPanel.scss @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_TagPanel { +.mx_GroupFilterPanel { flex: 1; - background-color: $tagpanel-bg-color; + background-color: $groupFilterPanel-bg-color; cursor: pointer; display: flex; @@ -26,49 +26,49 @@ limitations under the License. min-height: 0; } -.mx_TagPanel_items_selected { +.mx_GroupFilterPanel_items_selected { cursor: pointer; } -.mx_TagPanel .mx_TagPanel_divider { +.mx_GroupFilterPanel .mx_GroupFilterPanel_divider { height: 0px; width: 90%; border: none; - border-bottom: 1px solid $tagpanel-divider-color; + border-bottom: 1px solid $groupFilterPanel-divider-color; } -.mx_TagPanel .mx_TagPanel_scroller { +.mx_GroupFilterPanel .mx_GroupFilterPanel_scroller { flex-grow: 1; width: 100%; } -.mx_TagPanel .mx_TagPanel_tagTileContainer { +.mx_GroupFilterPanel .mx_GroupFilterPanel_tagTileContainer { display: flex; flex-direction: column; align-items: center; padding-top: 6px; } -.mx_TagPanel .mx_TagPanel_tagTileContainer > div { +.mx_GroupFilterPanel .mx_GroupFilterPanel_tagTileContainer > div { margin: 6px 0; } -.mx_TagPanel .mx_TagTile { +.mx_GroupFilterPanel .mx_TagTile { // opacity: 0.5; position: relative; } -.mx_TagPanel .mx_TagTile.mx_TagTile_prototype { +.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype { padding: 3px; } -.mx_TagPanel .mx_TagTile:focus, -.mx_TagPanel .mx_TagTile:hover, -.mx_TagPanel .mx_TagTile.mx_TagTile_selected { +.mx_GroupFilterPanel .mx_TagTile:focus, +.mx_GroupFilterPanel .mx_TagTile:hover, +.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected { // opacity: 1; } -.mx_TagPanel .mx_TagTile.mx_TagTile_selected_prototype { +.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected_prototype { background-color: $primary-bg-color; border-radius: 6px; } @@ -108,7 +108,7 @@ limitations under the License. } } -.mx_TagPanel .mx_TagTile_plus { +.mx_GroupFilterPanel .mx_TagTile_plus { margin-bottom: 12px; height: 32px; width: 32px; @@ -132,7 +132,7 @@ limitations under the License. } } -.mx_TagPanel .mx_TagTile.mx_TagTile_selected::before { +.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected::before { content: ''; height: 100%; background-color: $accent-color; @@ -142,7 +142,7 @@ limitations under the License. border-radius: 0 3px 3px 0; } -.mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus { +.mx_GroupFilterPanel .mx_TagTile.mx_AccessibleButton:focus { filter: none; } diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 5112d07c46..885dd77a84 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -14,29 +14,29 @@ See the License for the specific language governing permissions and limitations under the License. */ -$tagPanelWidth: 56px; // only applies in this file, used for calculations +$groupFilterPanelWidth: 56px; // only applies in this file, used for calculations .mx_LeftPanel { background-color: $roomlist-bg-color; min-width: 260px; max-width: 50%; - // Create a row-based flexbox for the TagPanel and the room list + // Create a row-based flexbox for the GroupFilterPanel and the room list display: flex; - .mx_LeftPanel_tagPanelContainer { + .mx_LeftPanel_GroupFilterPanelContainer { flex-grow: 0; flex-shrink: 0; - flex-basis: $tagPanelWidth; + flex-basis: $groupFilterPanelWidth; height: 100%; - // Create another flexbox so the TagPanel fills the container + // Create another flexbox so the GroupFilterPanel fills the container display: flex; - // TagPanel handles its own CSS + // GroupFilterPanel handles its own CSS } - &:not(.mx_LeftPanel_hasTagPanel) { + &:not(.mx_LeftPanel_hasGroupFilterPanel) { .mx_LeftPanel_roomListContainer { width: 100%; } @@ -45,7 +45,7 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations // Note: The 'room list' in this context is actually everything that isn't the tag // panel, such as the menu options, breadcrumbs, filtering, etc .mx_LeftPanel_roomListContainer { - width: calc(100% - $tagPanelWidth); + width: calc(100% - $groupFilterPanelWidth); background-color: $roomlist-bg-color; // Create another flexbox (this time a column) for the room list components @@ -169,10 +169,10 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations min-width: unset; // We have to forcefully set the width to override the resizer's style attribute. - &.mx_LeftPanel_hasTagPanel { - width: calc(68px + $tagPanelWidth) !important; + &.mx_LeftPanel_hasGroupFilterPanel { + width: calc(68px + $groupFilterPanelWidth) !important; } - &:not(.mx_LeftPanel_hasTagPanel) { + &:not(.mx_LeftPanel_hasGroupFilterPanel) { width: 68px !important; } diff --git a/res/css/views/rooms/_MemberInfo.scss b/res/css/views/rooms/_MemberInfo.scss index fb082843f1..182c280217 100644 --- a/res/css/views/rooms/_MemberInfo.scss +++ b/res/css/views/rooms/_MemberInfo.scss @@ -70,7 +70,7 @@ limitations under the License. } .mx_MemberInfo_avatar { - background: $tagpanel-bg-color; + background: $groupFilterPanel-bg-color; margin-bottom: 16px; } diff --git a/res/css/views/settings/_AvatarSetting.scss b/res/css/views/settings/_AvatarSetting.scss index 4a1f57a00e..a350605ab1 100644 --- a/res/css/views/settings/_AvatarSetting.scss +++ b/res/css/views/settings/_AvatarSetting.scss @@ -85,6 +85,7 @@ limitations under the License. .mx_AvatarSetting_avatarPlaceholder { display: block; height: 90px; + width: inherit; border-radius: 90px; cursor: pointer; } diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 331b5f4692..6e0c9acdfe 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -39,7 +39,7 @@ $info-plinth-fg-color: #888; $preview-bar-bg-color: $header-panel-bg-color; -$tagpanel-bg-color: rgba(38, 39, 43, 0.82); +$groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82); $inverted-bg-color: $base-color; // used by AddressSelector @@ -98,7 +98,7 @@ $roomheader-color: $text-primary-color; $roomheader-bg-color: $bg-color; $roomheader-addroom-bg-color: rgba(92, 100, 112, 0.3); $roomheader-addroom-fg-color: $text-primary-color; -$tagpanel-button-color: $header-panel-text-primary-color; +$groupFilterPanel-button-color: $header-panel-text-primary-color; $groupheader-button-color: $header-panel-text-primary-color; $rightpanel-button-color: $header-panel-text-primary-color; $icon-button-color: #8E99A4; @@ -118,7 +118,7 @@ $roomlist-bg-color: rgba(33, 38, 44, 0.90); $roomlist-header-color: $tertiary-fg-color; $roomsublist-divider-color: $primary-fg-color; -$tagpanel-divider-color: $roomlist-header-color; +$groupFilterPanel-divider-color: $roomlist-header-color; $roomtile-preview-color: $secondary-fg-color; $roomtile-default-badge-bg-color: #61708b; @@ -187,7 +187,7 @@ $reaction-row-button-selected-border-color: $accent-color; $kbd-border-color: #000000; -$tooltip-timeline-bg-color: $tagpanel-bg-color; +$tooltip-timeline-bg-color: $groupFilterPanel-bg-color; $tooltip-timeline-fg-color: #ffffff; $interactive-tooltip-bg-color: $base-color; @@ -202,7 +202,7 @@ $appearance-tab-border-color: $room-highlight-color; // blur amounts for left left panel (only for element theme, used in _mods.scss) $roomlist-background-blur-amount: 60px; -$tagpanel-background-blur-amount: 30px; +$groupFilterPanel-background-blur-amount: 30px; $composer-shadow-color: rgba(0, 0, 0, 0.28); diff --git a/res/themes/dark/css/dark.scss b/res/themes/dark/css/dark.scss index 6d9dc7352c..f9695018e4 100644 --- a/res/themes/dark/css/dark.scss +++ b/res/themes/dark/css/dark.scss @@ -3,7 +3,7 @@ @import "../../light/css/_fonts.scss"; @import "../../light/css/_light.scss"; // important this goes before _mods, -// as $tagpanel-background-blur-amount and +// as $groupFilterPanel-background-blur-amount and // $roomlist-background-blur-amount // are overridden in _dark.scss @import "_dark.scss"; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 14ce264bc0..efde7b3747 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -37,8 +37,8 @@ $info-plinth-fg-color: #888; $preview-bar-bg-color: $header-panel-bg-color; -$tagpanel-bg-color: $base-color; -$inverted-bg-color: $tagpanel-bg-color; +$groupFilterPanel-bg-color: $base-color; +$inverted-bg-color: $groupFilterPanel-bg-color; // used by AddressSelector $selected-color: $room-highlight-color; @@ -95,7 +95,7 @@ $topleftmenu-color: $text-primary-color; $roomheader-color: $text-primary-color; $roomheader-addroom-bg-color: #3c4556; // $search-placeholder-color at 0.5 opacity $roomheader-addroom-fg-color: $text-primary-color; -$tagpanel-button-color: $header-panel-text-primary-color; +$groupFilterPanel-button-color: $header-panel-text-primary-color; $groupheader-button-color: $header-panel-text-primary-color; $rightpanel-button-color: $header-panel-text-primary-color; $icon-button-color: $header-panel-text-primary-color; @@ -115,7 +115,7 @@ $roomlist-bg-color: $header-panel-bg-color; $roomsublist-divider-color: $primary-fg-color; -$tagpanel-divider-color: $roomlist-header-color; +$groupFilterPanel-divider-color: $roomlist-header-color; $roomtile-preview-color: #9e9e9e; $roomtile-default-badge-bg-color: #61708b; @@ -182,7 +182,7 @@ $reaction-row-button-selected-border-color: $accent-color; $kbd-border-color: #000000; -$tooltip-timeline-bg-color: $tagpanel-bg-color; +$tooltip-timeline-bg-color: $groupFilterPanel-bg-color; $tooltip-timeline-fg-color: #ffffff; $interactive-tooltip-bg-color: $base-color; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index b030fb7423..f77226cbca 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -67,8 +67,8 @@ $preview-bar-bg-color: #f7f7f7; $secondary-accent-color: #f2f5f8; $tertiary-accent-color: #d3efe1; -$tagpanel-bg-color: #27303a; -$inverted-bg-color: $tagpanel-bg-color; +$groupFilterPanel-bg-color: #27303a; +$inverted-bg-color: $groupFilterPanel-bg-color; // used by RoomDirectory permissions $plinth-bg-color: $secondary-accent-color; @@ -162,7 +162,7 @@ $roomheader-color: #45474a; $roomheader-bg-color: $primary-bg-color; $roomheader-addroom-bg-color: #91a1c0; $roomheader-addroom-fg-color: $accent-fg-color; -$tagpanel-button-color: #91a1c0; +$groupFilterPanel-button-color: #91a1c0; $groupheader-button-color: #91a1c0; $rightpanel-button-color: #91a1c0; $icon-button-color: #91a1c0; @@ -182,7 +182,7 @@ $roomlist-bg-color: $header-panel-bg-color; $roomlist-header-color: $primary-fg-color; $roomsublist-divider-color: $primary-fg-color; -$tagpanel-divider-color: $roomlist-header-color; +$groupFilterPanel-divider-color: $roomlist-header-color; $roomtile-preview-color: #9e9e9e; $roomtile-default-badge-bg-color: #61708b; @@ -305,7 +305,7 @@ $reaction-row-button-selected-border-color: $accent-color; $kbd-border-color: $reaction-row-button-border-color; -$tooltip-timeline-bg-color: $tagpanel-bg-color; +$tooltip-timeline-bg-color: $groupFilterPanel-bg-color; $tooltip-timeline-fg-color: #ffffff; $interactive-tooltip-bg-color: #27303a; diff --git a/res/themes/light-custom/css/_custom.scss b/res/themes/light-custom/css/_custom.scss index 6bb46e8a67..1b9254d100 100644 --- a/res/themes/light-custom/css/_custom.scss +++ b/res/themes/light-custom/css/_custom.scss @@ -49,7 +49,7 @@ $roomtile-selected-bg-color: var(--roomlist-highlights-color); // // --sidebar-color $interactive-tooltip-bg-color: var(--sidebar-color); -$tagpanel-bg-color: var(--sidebar-color); +$groupFilterPanel-bg-color: var(--sidebar-color); $tooltip-timeline-bg-color: var(--sidebar-color); $dialog-backdrop-color: var(--sidebar-color-50pct); $roomlist-button-bg-color: var(--sidebar-color-15pct); diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 140783212d..d137373bd5 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -62,7 +62,7 @@ $preview-bar-bg-color: #f7f7f7; $secondary-accent-color: #f2f5f8; $tertiary-accent-color: #d3efe1; -$tagpanel-bg-color: rgba(232, 232, 232, 0.77); +$groupFilterPanel-bg-color: rgba(232, 232, 232, 0.77); // used by RoomDirectory permissions $plinth-bg-color: $secondary-accent-color; @@ -156,7 +156,7 @@ $roomheader-color: #45474a; $roomheader-bg-color: $primary-bg-color; $roomheader-addroom-bg-color: rgba(92, 100, 112, 0.2); $roomheader-addroom-fg-color: #5c6470; -$tagpanel-button-color: #91A1C0; +$groupFilterPanel-button-color: #91A1C0; $groupheader-button-color: #91A1C0; $rightpanel-button-color: #91A1C0; $icon-button-color: #C1C6CD; @@ -176,7 +176,7 @@ $roomlist-bg-color: rgba(245, 245, 245, 0.90); $roomlist-header-color: $tertiary-fg-color; $roomsublist-divider-color: $primary-fg-color; -$tagpanel-divider-color: $roomlist-header-color; +$groupFilterPanel-divider-color: $roomlist-header-color; $roomtile-preview-color: $secondary-fg-color; $roomtile-default-badge-bg-color: #61708b; @@ -320,7 +320,7 @@ $appearance-tab-border-color: $input-darker-bg-color; // blur amounts for left left panel (only for element theme, used in _mods.scss) $roomlist-background-blur-amount: 40px; -$tagpanel-background-blur-amount: 20px; +$groupFilterPanel-background-blur-amount: 20px; $composer-shadow-color: rgba(0, 0, 0, 0.04); diff --git a/res/themes/light/css/_mods.scss b/res/themes/light/css/_mods.scss index 9a59acba8e..30aaeedf8f 100644 --- a/res/themes/light/css/_mods.scss +++ b/res/themes/light/css/_mods.scss @@ -6,14 +6,14 @@ @supports (backdrop-filter: none) { .mx_LeftPanel { - background-image: var(--avatar-url); + background-image: var(--avatar-url, unset); background-repeat: no-repeat; background-size: cover; background-position: left top; } - .mx_TagPanel { - backdrop-filter: blur($tagpanel-background-blur-amount); + .mx_GroupFilterPanel { + backdrop-filter: blur($groupFilterPanel-background-blur-amount); } .mx_LeftPanel .mx_LeftPanel_roomListContainer { diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 93be0fafc0..ed28a5c479 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -32,6 +32,8 @@ import type {Renderer} from "react-dom"; import RightPanelStore from "../stores/RightPanelStore"; import WidgetStore from "../stores/WidgetStore"; import CallHandler from "../CallHandler"; +import {Analytics} from "../Analytics"; +import UserActivity from "../UserActivity"; declare global { interface Window { @@ -56,6 +58,8 @@ declare global { mxRightPanelStore: RightPanelStore; mxWidgetStore: WidgetStore; mxCallHandler: CallHandler; + mxAnalytics: Analytics; + mxUserActivity: UserActivity; } interface Document { diff --git a/src/Analytics.js b/src/Analytics.tsx similarity index 74% rename from src/Analytics.js rename to src/Analytics.tsx index 135cc2eb7a..212bfd3757 100644 --- a/src/Analytics.js +++ b/src/Analytics.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; -import { getCurrentLanguage, _t, _td } from './languageHandler'; +import {getCurrentLanguage, _t, _td, IVariables} from './languageHandler'; import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; import Modal from './Modal'; @@ -27,7 +27,7 @@ const hashRegex = /#\/(groups?|room|user|settings|register|login|forgot_password const hashVarRegex = /#\/(group|room|user)\/.*$/; // Remove all but the first item in the hash path. Redact unexpected hashes. -function getRedactedHash(hash) { +function getRedactedHash(hash: string): string { // Don't leak URLs we aren't expecting - they could contain tokens/PII const match = hashRegex.exec(hash); if (!match) { @@ -44,7 +44,7 @@ function getRedactedHash(hash) { // Return the current origin, path and hash separated with a `/`. This does // not include query parameters. -function getRedactedUrl() { +function getRedactedUrl(): string { const { origin, hash } = window.location; let { pathname } = window.location; @@ -56,7 +56,25 @@ function getRedactedUrl() { return origin + pathname + getRedactedHash(hash); } -const customVariables = { +interface IData { + /* eslint-disable camelcase */ + gt_ms?: string; + e_c?: string; + e_a?: string; + e_n?: string; + e_v?: string; + ping?: string; + /* eslint-enable camelcase */ +} + +interface IVariable { + id: number; + expl: string; // explanation + example: string; // example value + getTextVariables?(): IVariables; // object to pass as 2nd argument to `_t` +} + +const customVariables: Record = { // The Matomo installation at https://matomo.riot.im is currently configured // with a limit of 10 custom variables. 'App Platform': { @@ -120,7 +138,7 @@ const customVariables = { }, }; -function whitelistRedact(whitelist, str) { +function whitelistRedact(whitelist: string[], str: string): string { if (whitelist.includes(str)) return str; return ''; } @@ -130,7 +148,7 @@ const CREATION_TS_KEY = "mx_Riot_Analytics_cts"; const VISIT_COUNT_KEY = "mx_Riot_Analytics_vc"; const LAST_VISIT_TS_KEY = "mx_Riot_Analytics_lvts"; -function getUid() { +function getUid(): string { try { let data = localStorage && localStorage.getItem(UID_KEY); if (!data && localStorage) { @@ -145,32 +163,36 @@ function getUid() { const HEARTBEAT_INTERVAL = 30 * 1000; // seconds -class Analytics { +export class Analytics { + private baseUrl: URL = null; + private siteId: string = null; + private visitVariables: Record = {}; // {[id: number]: [name: string, value: string]} + private firstPage = true; + private heartbeatIntervalID: number = null; + + private readonly creationTs: string; + private readonly lastVisitTs: string; + private readonly visitCount: string; + constructor() { - this.baseUrl = null; - this.siteId = null; - this.visitVariables = {}; - - this.firstPage = true; - this._heartbeatIntervalID = null; - this.creationTs = localStorage && localStorage.getItem(CREATION_TS_KEY); if (!this.creationTs && localStorage) { - localStorage.setItem(CREATION_TS_KEY, this.creationTs = new Date().getTime()); + localStorage.setItem(CREATION_TS_KEY, this.creationTs = String(new Date().getTime())); } this.lastVisitTs = localStorage && localStorage.getItem(LAST_VISIT_TS_KEY); - this.visitCount = localStorage && localStorage.getItem(VISIT_COUNT_KEY) || 0; + this.visitCount = localStorage && localStorage.getItem(VISIT_COUNT_KEY) || "0"; + this.visitCount = String(parseInt(this.visitCount, 10) + 1); // increment if (localStorage) { - localStorage.setItem(VISIT_COUNT_KEY, parseInt(this.visitCount, 10) + 1); + localStorage.setItem(VISIT_COUNT_KEY, this.visitCount); } } - get disabled() { + public get disabled() { return !this.baseUrl; } - canEnable() { + public canEnable() { const config = SdkConfig.get(); return navigator.doNotTrack !== "1" && config && config.piwik && config.piwik.url && config.piwik.siteId; } @@ -179,67 +201,67 @@ class Analytics { * Enable Analytics if initialized but disabled * otherwise try and initalize, no-op if piwik config missing */ - async enable() { + public async enable() { if (!this.disabled) return; if (!this.canEnable()) return; const config = SdkConfig.get(); this.baseUrl = new URL("piwik.php", config.piwik.url); // set constants - this.baseUrl.searchParams.set("rec", 1); // rec is required for tracking + this.baseUrl.searchParams.set("rec", "1"); // rec is required for tracking this.baseUrl.searchParams.set("idsite", config.piwik.siteId); // rec is required for tracking - this.baseUrl.searchParams.set("apiv", 1); // API version to use - this.baseUrl.searchParams.set("send_image", 0); // we want a 204, not a tiny GIF + this.baseUrl.searchParams.set("apiv", "1"); // API version to use + this.baseUrl.searchParams.set("send_image", "0"); // we want a 204, not a tiny GIF // set user parameters this.baseUrl.searchParams.set("_id", getUid()); // uuid this.baseUrl.searchParams.set("_idts", this.creationTs); // first ts - this.baseUrl.searchParams.set("_idvc", parseInt(this.visitCount, 10)+ 1); // visit count + this.baseUrl.searchParams.set("_idvc", this.visitCount); // visit count if (this.lastVisitTs) { this.baseUrl.searchParams.set("_viewts", this.lastVisitTs); // last visit ts } const platform = PlatformPeg.get(); - this._setVisitVariable('App Platform', platform.getHumanReadableName()); + this.setVisitVariable('App Platform', platform.getHumanReadableName()); try { - this._setVisitVariable('App Version', await platform.getAppVersion()); + this.setVisitVariable('App Version', await platform.getAppVersion()); } catch (e) { - this._setVisitVariable('App Version', 'unknown'); + this.setVisitVariable('App Version', 'unknown'); } - this._setVisitVariable('Chosen Language', getCurrentLanguage()); + this.setVisitVariable('Chosen Language', getCurrentLanguage()); const hostname = window.location.hostname; if (hostname === 'riot.im') { - this._setVisitVariable('Instance', window.location.pathname); + this.setVisitVariable('Instance', window.location.pathname); } else if (hostname.endsWith('.element.io')) { - this._setVisitVariable('Instance', hostname.replace('.element.io', '')); + this.setVisitVariable('Instance', hostname.replace('.element.io', '')); } let installedPWA = "unknown"; try { // Known to work at least for desktop Chrome - installedPWA = window.matchMedia('(display-mode: standalone)').matches; + installedPWA = String(window.matchMedia('(display-mode: standalone)').matches); } catch (e) { } - this._setVisitVariable('Installed PWA', installedPWA); + this.setVisitVariable('Installed PWA', installedPWA); let touchInput = "unknown"; try { // MDN claims broad support across browsers - touchInput = window.matchMedia('(pointer: coarse)').matches; + touchInput = String(window.matchMedia('(pointer: coarse)').matches); } catch (e) { } - this._setVisitVariable('Touch Input', touchInput); + this.setVisitVariable('Touch Input', touchInput); // start heartbeat - this._heartbeatIntervalID = window.setInterval(this.ping.bind(this), HEARTBEAT_INTERVAL); + this.heartbeatIntervalID = window.setInterval(this.ping.bind(this), HEARTBEAT_INTERVAL); } /** * Disable Analytics, stop the heartbeat and clear identifiers from localStorage */ - disable() { + public disable() { if (this.disabled) return; this.trackEvent('Analytics', 'opt-out'); - window.clearInterval(this._heartbeatIntervalID); + window.clearInterval(this.heartbeatIntervalID); this.baseUrl = null; this.visitVariables = {}; localStorage.removeItem(UID_KEY); @@ -248,7 +270,7 @@ class Analytics { localStorage.removeItem(LAST_VISIT_TS_KEY); } - async _track(data) { + private async _track(data: IData) { if (this.disabled) return; const now = new Date(); @@ -264,13 +286,13 @@ class Analytics { s: now.getSeconds(), }; - const url = new URL(this.baseUrl); + const url = new URL(this.baseUrl.toString()); // copy for (const key in params) { url.searchParams.set(key, params[key]); } try { - await window.fetch(url, { + await window.fetch(url.toString(), { method: "GET", mode: "no-cors", cache: "no-cache", @@ -281,14 +303,14 @@ class Analytics { } } - ping() { + public ping() { this._track({ - ping: 1, + ping: "1", }); - localStorage.setItem(LAST_VISIT_TS_KEY, new Date().getTime()); // update last visit ts + localStorage.setItem(LAST_VISIT_TS_KEY, String(new Date().getTime())); // update last visit ts } - trackPageChange(generationTimeMs) { + public trackPageChange(generationTimeMs?: number) { if (this.disabled) return; if (this.firstPage) { // De-duplicate first page @@ -303,11 +325,11 @@ class Analytics { } this._track({ - gt_ms: generationTimeMs, + gt_ms: String(generationTimeMs), }); } - trackEvent(category, action, name, value) { + public trackEvent(category: string, action: string, name?: string, value?: string) { if (this.disabled) return; this._track({ e_c: category, @@ -317,12 +339,12 @@ class Analytics { }); } - _setVisitVariable(key, value) { + private setVisitVariable(key: keyof typeof customVariables, value: string) { if (this.disabled) return; this.visitVariables[customVariables[key].id] = [key, value]; } - setLoggedIn(isGuest, homeserverUrl, identityServerUrl) { + public setLoggedIn(isGuest: boolean, homeserverUrl: string) { if (this.disabled) return; const config = SdkConfig.get(); @@ -330,16 +352,16 @@ class Analytics { const whitelistedHSUrls = config.piwik.whitelistedHSUrls || []; - this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In'); - this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl)); + this.setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In'); + this.setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl)); } - setBreadcrumbs(state) { + public setBreadcrumbs(state: boolean) { if (this.disabled) return; - this._setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled'); + this.setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled'); } - showDetailsModal = () => { + public showDetailsModal = () => { let rows = []; if (!this.disabled) { rows = Object.values(this.visitVariables); @@ -360,7 +382,7 @@ class Analytics { 'e.g. ', {}, { - CurrentPageURL: getRedactedUrl(), + CurrentPageURL: getRedactedUrl, }, ), }, @@ -401,7 +423,7 @@ class Analytics { }; } -if (!global.mxAnalytics) { - global.mxAnalytics = new Analytics(); +if (!window.mxAnalytics) { + window.mxAnalytics = new Analytics(); } -export default global.mxAnalytics; +export default window.mxAnalytics; diff --git a/src/Avatar.js b/src/Avatar.ts similarity index 85% rename from src/Avatar.js rename to src/Avatar.ts index 1c1182b98d..60bdfdcf75 100644 --- a/src/Avatar.js +++ b/src/Avatar.ts @@ -14,14 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; +import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; +import {RoomMember} from "matrix-js-sdk/src/models/room-member"; +import {User} from "matrix-js-sdk/src/models/user"; +import {Room} from "matrix-js-sdk/src/models/room"; + import {MatrixClientPeg} from './MatrixClientPeg'; import DMRoomMap from './utils/DMRoomMap'; -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; + +export type ResizeMethod = "crop" | "scale"; // Not to be used for BaseAvatar urls as that has similar default avatar fallback already -export function avatarUrlForMember(member, width, height, resizeMethod) { - let url; +export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) { + let url: string; if (member && member.getAvatarUrl) { url = member.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), @@ -41,7 +46,7 @@ export function avatarUrlForMember(member, width, height, resizeMethod) { return url; } -export function avatarUrlForUser(user, width, height, resizeMethod) { +export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) { const url = getHttpUriForMxc( MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl, Math.floor(width * window.devicePixelRatio), @@ -54,14 +59,14 @@ export function avatarUrlForUser(user, width, height, resizeMethod) { return url; } -function isValidHexColor(color) { +function isValidHexColor(color: string): boolean { return typeof color === "string" && - (color.length === 7 || color.lengh === 9) && + (color.length === 7 || color.length === 9) && color.charAt(0) === "#" && !color.substr(1).split("").some(c => isNaN(parseInt(c, 16))); } -function urlForColor(color) { +function urlForColor(color: string): string { const size = 40; const canvas = document.createElement("canvas"); canvas.width = size; @@ -79,9 +84,9 @@ function urlForColor(color) { // XXX: Ideally we'd clear this cache when the theme changes // but since this function is at global scope, it's a bit // hard to install a listener here, even if there were a clear event to listen to -const colorToDataURLCache = new Map(); +const colorToDataURLCache = new Map(); -export function defaultAvatarUrlForString(s) { +export function defaultAvatarUrlForString(s: string): string { if (!s) return ""; // XXX: should never happen but empirically does by evidence of a rageshake const defaultColors = ['#0DBD8B', '#368bd6', '#ac3ba8']; let total = 0; @@ -113,7 +118,7 @@ export function defaultAvatarUrlForString(s) { * @param {string} name * @return {string} the first letter */ -export function getInitialLetter(name) { +export function getInitialLetter(name: string): string { if (!name) { // XXX: We should find out what causes the name to sometimes be falsy. console.trace("`name` argument to `getInitialLetter` not supplied"); @@ -146,7 +151,7 @@ export function getInitialLetter(name) { return firstChar.toUpperCase(); } -export function avatarUrlForRoom(room, width, height, resizeMethod) { +export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) { if (!room) return null; // null-guard const explicitRoomAvatar = room.getAvatarUrl( diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 6b66a614d2..d1ebb15c26 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -179,8 +179,18 @@ export default class CallHandler { } } + private matchesCallForThisRoom(call: MatrixCall) { + // We don't allow placing more than one call per room, but that doesn't mean there + // can't be more than one, eg. in a glare situation. This checks that the given call + // is the call we consider 'the' call for its room. + const callForThisRoom = this.getCallForRoom(call.roomId); + return callForThisRoom && call.callId === callForThisRoom.callId; + } + private setCallListeners(call: MatrixCall) { call.on(CallEvent.Error, (err) => { + if (!this.matchesCallForThisRoom(call)) return; + console.error("Call error:", err); if ( MatrixClientPeg.get().getTurnServers().length === 0 && @@ -196,9 +206,13 @@ export default class CallHandler { }); }); call.on(CallEvent.Hangup, () => { + if (!this.matchesCallForThisRoom(call)) return; + this.removeCallForRoom(call.roomId); }); call.on(CallEvent.State, (newState: CallState, oldState: CallState) => { + if (!this.matchesCallForThisRoom(call)) return; + this.setCallState(call, newState); switch (oldState) { @@ -224,15 +238,45 @@ export default class CallHandler { (call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout) )) { this.play(AudioID.Busy); - Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, { - title: _t('Call Timeout'), - description: _t('The remote side failed to pick up') + '.', + let title; + let description; + if (call.hangupReason === CallErrorCode.UserHangup) { + title = _t("Call Declined"); + description = _t("The other party declined the call."); + } else if (call.hangupReason === CallErrorCode.InviteTimeout) { + title = _t("Call Failed"); + // XXX: full stop appended as some relic here, but these + // strings need proper input from design anyway, so let's + // not change this string until we have a proper one. + description = _t('The remote side failed to pick up') + '.'; + } else { + title = _t("Call Failed"); + description = _t("The call could not be established"); + } + + Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { + title, description, }); } else { this.play(AudioID.CallEnd); } } }); + call.on(CallEvent.Replaced, (newCall: MatrixCall) => { + if (!this.matchesCallForThisRoom(call)) return; + + console.log(`Call ID ${call.callId} is being replaced by call ID ${newCall.callId}`); + + if (call.state === CallState.Ringing) { + this.pause(AudioID.Ring); + } else if (call.state === CallState.InviteSent) { + this.pause(AudioID.Ringback); + } + + this.calls.set(newCall.roomId, newCall); + this.setCallListeners(newCall); + this.setCallState(newCall, newCall.state); + }); } private setCallState(call: MatrixCall, status: CallState) { @@ -393,10 +437,15 @@ export default class CallHandler { } break; case 'hangup': + case 'reject': if (!this.calls.get(payload.room_id)) { return; // no call to hangup } - this.calls.get(payload.room_id).hangup(CallErrorCode.UserHangup, false) + if (payload.action === 'reject') { + this.calls.get(payload.room_id).reject(); + } else { + this.calls.get(payload.room_id).hangup(CallErrorCode.UserHangup, false); + } this.removeCallForRoom(payload.room_id); break; case 'answer': diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index eb8fff0eb1..cba8671143 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -17,7 +17,6 @@ limitations under the License. */ import React from "react"; -import extend from './extend'; import dis from './dispatcher/dispatcher'; import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClient} from "matrix-js-sdk/src/client"; @@ -497,7 +496,7 @@ export default class ContentMessages { if (file.type.indexOf('image/') === 0) { content.msgtype = 'm.image'; infoForImageFile(matrixClient, roomId, file).then((imageInfo) => { - extend(content.info, imageInfo); + Object.assign(content.info, imageInfo); resolve(); }, (e) => { console.error(e); @@ -510,7 +509,7 @@ export default class ContentMessages { } else if (file.type.indexOf('video/') === 0) { content.msgtype = 'm.video'; infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => { - extend(content.info, videoInfo); + Object.assign(content.info, videoInfo); resolve(); }, (e) => { content.msgtype = 'm.file'; diff --git a/src/DateUtils.js b/src/DateUtils.ts similarity index 85% rename from src/DateUtils.js rename to src/DateUtils.ts index 108697238c..9b1edf0775 100644 --- a/src/DateUtils.js +++ b/src/DateUtils.ts @@ -17,7 +17,7 @@ limitations under the License. import { _t } from './languageHandler'; -function getDaysArray() { +function getDaysArray(): string[] { return [ _t('Sun'), _t('Mon'), @@ -29,7 +29,7 @@ function getDaysArray() { ]; } -function getMonthsArray() { +function getMonthsArray(): string[] { return [ _t('Jan'), _t('Feb'), @@ -46,11 +46,11 @@ function getMonthsArray() { ]; } -function pad(n) { +function pad(n: number): string { return (n < 10 ? '0' : '') + n; } -function twelveHourTime(date, showSeconds=false) { +function twelveHourTime(date: Date, showSeconds = false): string { let hours = date.getHours() % 12; const minutes = pad(date.getMinutes()); const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM'); @@ -62,7 +62,7 @@ function twelveHourTime(date, showSeconds=false) { return `${hours}:${minutes}${ampm}`; } -export function formatDate(date, showTwelveHour=false) { +export function formatDate(date: Date, showTwelveHour = false): string { const now = new Date(); const days = getDaysArray(); const months = getMonthsArray(); @@ -86,7 +86,7 @@ export function formatDate(date, showTwelveHour=false) { return formatFullDate(date, showTwelveHour); } -export function formatFullDateNoTime(date) { +export function formatFullDateNoTime(date: Date): string { const days = getDaysArray(); const months = getMonthsArray(); return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s', { @@ -97,7 +97,7 @@ export function formatFullDateNoTime(date) { }); } -export function formatFullDate(date, showTwelveHour=false) { +export function formatFullDate(date: Date, showTwelveHour = false): string { const days = getDaysArray(); const months = getMonthsArray(); return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', { @@ -109,14 +109,14 @@ export function formatFullDate(date, showTwelveHour=false) { }); } -export function formatFullTime(date, showTwelveHour=false) { +export function formatFullTime(date: Date, showTwelveHour = false): string { if (showTwelveHour) { return twelveHourTime(date, true); } return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds()); } -export function formatTime(date, showTwelveHour=false) { +export function formatTime(date: Date, showTwelveHour = false): string { if (showTwelveHour) { return twelveHourTime(date); } @@ -124,7 +124,7 @@ export function formatTime(date, showTwelveHour=false) { } const MILLIS_IN_DAY = 86400000; -export function wantsDateSeparator(prevEventDate, nextEventDate) { +export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean { if (!nextEventDate || !prevEventDate) { return false; } diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index f2cd1bce9e..6293de063d 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -23,6 +23,7 @@ import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { MatrixClient } from "matrix-js-sdk/src/client"; import {IMatrixClientCreds, MatrixClientPeg} from './MatrixClientPeg'; +import SecurityCustomisations from "./customisations/Security"; import EventIndexPeg from './indexing/EventIndexPeg'; import createMatrixClient from './utils/createMatrixClient'; import Analytics from './Analytics'; @@ -567,6 +568,8 @@ function persistCredentialsToLocalStorage(credentials: IMatrixClientCreds): void localStorage.setItem("mx_device_id", credentials.deviceId); } + SecurityCustomisations.persistCredentials?.(credentials); + console.log(`Session persisted for ${credentials.userId}`); } diff --git a/src/Login.ts b/src/Login.ts index 38d78feab6..ae4aa226ed 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -22,6 +22,7 @@ limitations under the License. import Matrix from "matrix-js-sdk"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { IMatrixClientCreds } from "./MatrixClientPeg"; +import SecurityCustomisations from "./customisations/Security"; interface ILoginOptions { defaultDeviceDisplayName?: string; @@ -222,11 +223,15 @@ export async function sendLoginRequest( } } - return { + const creds: IMatrixClientCreds = { homeserverUrl: hsUrl, identityServerUrl: isUrl, userId: data.user_id, deviceId: data.device_id, accessToken: data.access_token, }; + + SecurityCustomisations.examineLoginResponse?.(data, creds); + + return creds; } diff --git a/src/Notifier.ts b/src/Notifier.ts index 2643de1abc..1899896f9b 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -218,7 +218,7 @@ export const Notifier = { // calculated value. It is determined based upon whether or not the master rule is enabled // and other flags. Setting it here would cause a circular reference. - Analytics.trackEvent('Notifier', 'Set Enabled', enable); + Analytics.trackEvent('Notifier', 'Set Enabled', String(enable)); // make sure that we persist the current setting audio_enabled setting // before changing anything @@ -287,7 +287,7 @@ export const Notifier = { setPromptHidden: function(hidden: boolean, persistent = true) { this.toolbarHidden = hidden; - Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden); + Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', String(hidden)); hideNotificationsToast(); diff --git a/src/Presence.js b/src/Presence.ts similarity index 65% rename from src/Presence.js rename to src/Presence.ts index 42bca35f96..660bb0ac94 100644 --- a/src/Presence.js +++ b/src/Presence.ts @@ -19,30 +19,34 @@ limitations under the License. import {MatrixClientPeg} from "./MatrixClientPeg"; import dis from "./dispatcher/dispatcher"; import Timer from './utils/Timer'; +import {ActionPayload} from "./dispatcher/payloads"; - // Time in ms after that a user is considered as unavailable/away +// Time in ms after that a user is considered as unavailable/away const UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins -const PRESENCE_STATES = ["online", "offline", "unavailable"]; + +enum State { + Online = "online", + Offline = "offline", + Unavailable = "unavailable", +} class Presence { - constructor() { - this._activitySignal = null; - this._unavailableTimer = null; - this._onAction = this._onAction.bind(this); - this._dispatcherRef = null; - } + private unavailableTimer: Timer = null; + private dispatcherRef: string = null; + private state: State = null; + /** * Start listening the user activity to evaluate his presence state. * Any state change will be sent to the homeserver. */ - async start() { - this._unavailableTimer = new Timer(UNAVAILABLE_TIME_MS); + public async start() { + this.unavailableTimer = new Timer(UNAVAILABLE_TIME_MS); // the user_activity_start action starts the timer - this._dispatcherRef = dis.register(this._onAction); - while (this._unavailableTimer) { + this.dispatcherRef = dis.register(this.onAction); + while (this.unavailableTimer) { try { - await this._unavailableTimer.finished(); - this.setState("unavailable"); + await this.unavailableTimer.finished(); + this.setState(State.Unavailable); } catch (e) { /* aborted, stop got called */ } } } @@ -50,14 +54,14 @@ class Presence { /** * Stop tracking user activity */ - stop() { - if (this._dispatcherRef) { - dis.unregister(this._dispatcherRef); - this._dispatcherRef = null; + public stop() { + if (this.dispatcherRef) { + dis.unregister(this.dispatcherRef); + this.dispatcherRef = null; } - if (this._unavailableTimer) { - this._unavailableTimer.abort(); - this._unavailableTimer = null; + if (this.unavailableTimer) { + this.unavailableTimer.abort(); + this.unavailableTimer = null; } } @@ -65,14 +69,14 @@ class Presence { * Get the current presence state. * @returns {string} the presence state (see PRESENCE enum) */ - getState() { + public getState() { return this.state; } - _onAction(payload) { + private onAction = (payload: ActionPayload) => { if (payload.action === 'user_activity') { - this.setState("online"); - this._unavailableTimer.restart(); + this.setState(State.Online); + this.unavailableTimer.restart(); } } @@ -81,13 +85,11 @@ class Presence { * If the state has changed, the homeserver will be notified. * @param {string} newState the new presence state (see PRESENCE enum) */ - async setState(newState) { + private async setState(newState: State) { if (newState === this.state) { return; } - if (PRESENCE_STATES.indexOf(newState) === -1) { - throw new Error("Bad presence state: " + newState); - } + const oldState = this.state; this.state = newState; diff --git a/src/Roles.js b/src/Roles.ts similarity index 87% rename from src/Roles.js rename to src/Roles.ts index 7cc3c880d7..b4be97fdce 100644 --- a/src/Roles.js +++ b/src/Roles.ts @@ -13,9 +13,10 @@ 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 { _t } from './languageHandler'; -export function levelRoleMap(usersDefault) { +export function levelRoleMap(usersDefault: number) { return { undefined: _t('Default'), 0: _t('Restricted'), @@ -25,7 +26,7 @@ export function levelRoleMap(usersDefault) { }; } -export function textualPowerLevel(level, usersDefault) { +export function textualPowerLevel(level: number, usersDefault: number): string { const LEVEL_ROLE_MAP = levelRoleMap(usersDefault); if (LEVEL_ROLE_MAP[level]) { return LEVEL_ROLE_MAP[level]; diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 4d277692df..220320470a 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -22,11 +22,12 @@ import {MatrixClientPeg} from './MatrixClientPeg'; import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; import { _t } from './languageHandler'; -import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; +import { encodeBase64 } from "matrix-js-sdk/src/crypto/olmlib"; import { isSecureBackupRequired } from './utils/WellKnownUtils'; import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog'; import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog'; import SettingsStore from "./settings/SettingsStore"; +import SecurityCustomisations from "./customisations/Security"; // This stores the secret storage private keys in memory for the JS SDK. This is // only meant to act as a cache to avoid prompting the user multiple times @@ -115,6 +116,13 @@ async function getSecretStorageKey( } } + const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.(); + if (keyFromCustomisations) { + console.log("Using key from security customisations (secret storage)") + cacheSecretStorageKey(keyId, keyInfo, keyFromCustomisations); + return [keyId, keyFromCustomisations]; + } + if (nonInteractive) { throw new Error("Could not unlock non-interactively"); } @@ -158,6 +166,12 @@ export async function getDehydrationKey( keyInfo: ISecretStorageKeyInfo, checkFunc: (Uint8Array) => void, ): Promise { + const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.(); + if (keyFromCustomisations) { + console.log("Using key from security customisations (dehydration)") + return keyFromCustomisations; + } + const inputToKey = makeInputToKey(keyInfo); const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", AccessSecretStorageDialog, @@ -352,14 +366,19 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f } console.log("Setting dehydration key"); await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo, "Backup device"); + } else if (!keyId) { + console.warn("Not setting dehydration key: no SSSS key found"); } else { - console.log("Not setting dehydration key: no SSSS key found"); + console.log("Not setting dehydration key: feature disabled"); } } // `return await` needed here to ensure `finally` block runs after the // inner operation completes. return await func(); + } catch (e) { + SecurityCustomisations.catchAccessSecretStorageError?.(e); + console.error(e); } finally { // Clear secret storage key cache now that work is complete secretStorageBeingAccessed = false; diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 34d40bf1fd..d86d88a697 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -198,59 +198,30 @@ function textForRelatedGroupsEvent(ev) { function textForServerACLEvent(ev) { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const prevContent = ev.getPrevContent(); - const changes = []; const current = ev.getContent(); const prev = { deny: Array.isArray(prevContent.deny) ? prevContent.deny : [], allow: Array.isArray(prevContent.allow) ? prevContent.allow : [], allow_ip_literals: !(prevContent.allow_ip_literals === false), }; + let text = ""; if (prev.deny.length === 0 && prev.allow.length === 0) { - text = `${senderDisplayName} set server ACLs for this room: `; + text = _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName}); } else { - text = `${senderDisplayName} changed the server ACLs for this room: `; + text = _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName}); } if (!Array.isArray(current.allow)) { current.allow = []; } - /* If we know for sure everyone is banned, don't bother showing the diff view */ + + // If we know for sure everyone is banned, mark the room as obliterated if (current.allow.length === 0) { - return text + "๐ŸŽ‰ All servers are banned from participating! This room can no longer be used."; + return text + " " + _t("๐ŸŽ‰ All servers are banned from participating! This room can no longer be used."); } - if (!Array.isArray(current.deny)) { - current.deny = []; - } - - const bannedServers = current.deny.filter((srv) => typeof(srv) === 'string' && !prev.deny.includes(srv)); - const unbannedServers = prev.deny.filter((srv) => typeof(srv) === 'string' && !current.deny.includes(srv)); - const allowedServers = current.allow.filter((srv) => typeof(srv) === 'string' && !prev.allow.includes(srv)); - const unallowedServers = prev.allow.filter((srv) => typeof(srv) === 'string' && !current.allow.includes(srv)); - - if (bannedServers.length > 0) { - changes.push(`Servers matching ${bannedServers.join(", ")} are now banned.`); - } - - if (unbannedServers.length > 0) { - changes.push(`Servers matching ${unbannedServers.join(", ")} were removed from the ban list.`); - } - - if (allowedServers.length > 0) { - changes.push(`Servers matching ${allowedServers.join(", ")} are now allowed.`); - } - - if (unallowedServers.length > 0) { - changes.push(`Servers matching ${unallowedServers.join(", ")} were removed from the allowed list.`); - } - - if (prev.allow_ip_literals !== current.allow_ip_literals) { - const allowban = current.allow_ip_literals ? "allowed" : "banned"; - changes.push(`Participating from a server using an IP literal hostname is now ${allowban}.`); - } - - return text + changes.join(" "); + return text; } function textForMessageEvent(ev) { @@ -329,14 +300,27 @@ function textForCallHangupEvent(event) { reason = _t('(not supported by this browser)'); } else if (eventContent.reason) { if (eventContent.reason === "ice_failed") { + // We couldn't establish a connection at all reason = _t('(could not connect media)'); + } else if (eventContent.reason === "ice_timeout") { + // We established a connection but it died + reason = _t('(connection failed)'); + } else if (eventContent.reason === "user_media_failed") { + // The other side couldn't open capture devices + reason = _t("(their device couldn't start the camera / microphone)"); + } else if (eventContent.reason === "unknown_error") { + // An error code the other side doesn't have a way to express + // (as opposed to an error code they gave but we don't know about, + // in which case we show the error code) + reason = _t("(an error occurred)"); } else if (eventContent.reason === "invite_timeout") { reason = _t('(no answer)'); - } else if (eventContent.reason === "user hangup") { + } else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") { // workaround for https://github.com/vector-im/element-web/issues/5178 // it seems Android randomly sets a reason of "user hangup" which is // interpreted as an error code :( // https://github.com/vector-im/riot-android/issues/2623 + // Also the correct hangup code as of VoIP v1 (with underscore) reason = ''; } else { reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason}); @@ -345,6 +329,11 @@ function textForCallHangupEvent(event) { return _t('%(senderName)s ended the call.', {senderName}) + ' ' + reason; } +function textForCallRejectEvent(event) { + const senderName = event.sender ? event.sender.name : _t('Someone'); + return _t('%(senderName)s declined the call.', {senderName}); +} + function textForCallInviteEvent(event) { const senderName = event.sender ? event.sender.name : _t('Someone'); // FIXME: Find a better way to determine this from the event? @@ -574,6 +563,7 @@ const handlers = { 'm.call.invite': textForCallInviteEvent, 'm.call.answer': textForCallAnswerEvent, 'm.call.hangup': textForCallHangupEvent, + 'm.call.reject': textForCallRejectEvent, }; const stateHandlers = { diff --git a/src/UserActivity.js b/src/UserActivity.ts similarity index 61% rename from src/UserActivity.js rename to src/UserActivity.ts index 0174aebaf5..606075ec7c 100644 --- a/src/UserActivity.js +++ b/src/UserActivity.ts @@ -38,26 +38,23 @@ const RECENTLY_ACTIVE_THRESHOLD_MS = 2 * 60 * 1000; * see doc on the userActive* functions for what these mean. */ export default class UserActivity { - constructor(windowObj, documentObj) { - this._window = windowObj; - this._document = documentObj; + private readonly activeNowTimeout: Timer; + private readonly activeRecentlyTimeout: Timer; + private attachedActiveNowTimers: Timer[] = []; + private attachedActiveRecentlyTimers: Timer[] = []; + private lastScreenX = 0; + private lastScreenY = 0; - this._attachedActiveNowTimers = []; - this._attachedActiveRecentlyTimers = []; - this._activeNowTimeout = new Timer(CURRENTLY_ACTIVE_THRESHOLD_MS); - this._activeRecentlyTimeout = new Timer(RECENTLY_ACTIVE_THRESHOLD_MS); - this._onUserActivity = this._onUserActivity.bind(this); - this._onWindowBlurred = this._onWindowBlurred.bind(this); - this._onPageVisibilityChanged = this._onPageVisibilityChanged.bind(this); - this.lastScreenX = 0; - this.lastScreenY = 0; + constructor(private readonly window: Window, private readonly document: Document) { + this.activeNowTimeout = new Timer(CURRENTLY_ACTIVE_THRESHOLD_MS); + this.activeRecentlyTimeout = new Timer(RECENTLY_ACTIVE_THRESHOLD_MS); } static sharedInstance() { - if (global.mxUserActivity === undefined) { - global.mxUserActivity = new UserActivity(window, document); + if (window.mxUserActivity === undefined) { + window.mxUserActivity = new UserActivity(window, document); } - return global.mxUserActivity; + return window.mxUserActivity; } /** @@ -69,8 +66,8 @@ export default class UserActivity { * later on when the user does become active. * @param {Timer} timer the timer to use */ - timeWhileActiveNow(timer) { - this._timeWhile(timer, this._attachedActiveNowTimers); + public timeWhileActiveNow(timer: Timer) { + this.timeWhile(timer, this.attachedActiveNowTimers); if (this.userActiveNow()) { timer.start(); } @@ -85,14 +82,14 @@ export default class UserActivity { * later on when the user does become active. * @param {Timer} timer the timer to use */ - timeWhileActiveRecently(timer) { - this._timeWhile(timer, this._attachedActiveRecentlyTimers); + public timeWhileActiveRecently(timer: Timer) { + this.timeWhile(timer, this.attachedActiveRecentlyTimers); if (this.userActiveRecently()) { timer.start(); } } - _timeWhile(timer, attachedTimers) { + private timeWhile(timer: Timer, attachedTimers: Timer[]) { // important this happens first const index = attachedTimers.indexOf(timer); if (index === -1) { @@ -112,36 +109,36 @@ export default class UserActivity { /** * Start listening to user activity */ - start() { - this._document.addEventListener('mousedown', this._onUserActivity); - this._document.addEventListener('mousemove', this._onUserActivity); - this._document.addEventListener('keydown', this._onUserActivity); - this._document.addEventListener("visibilitychange", this._onPageVisibilityChanged); - this._window.addEventListener("blur", this._onWindowBlurred); - this._window.addEventListener("focus", this._onUserActivity); + public start() { + this.document.addEventListener('mousedown', this.onUserActivity); + this.document.addEventListener('mousemove', this.onUserActivity); + this.document.addEventListener('keydown', this.onUserActivity); + this.document.addEventListener("visibilitychange", this.onPageVisibilityChanged); + this.window.addEventListener("blur", this.onWindowBlurred); + this.window.addEventListener("focus", this.onUserActivity); // can't use document.scroll here because that's only the document // itself being scrolled. Need to use addEventListener's useCapture. // also this needs to be the wheel event, not scroll, as scroll is // fired when the view scrolls down for a new message. - this._window.addEventListener('wheel', this._onUserActivity, { - passive: true, capture: true, + this.window.addEventListener('wheel', this.onUserActivity, { + passive: true, + capture: true, }); } /** * Stop tracking user activity */ - stop() { - this._document.removeEventListener('mousedown', this._onUserActivity); - this._document.removeEventListener('mousemove', this._onUserActivity); - this._document.removeEventListener('keydown', this._onUserActivity); - this._window.removeEventListener('wheel', this._onUserActivity, { - passive: true, capture: true, + public stop() { + this.document.removeEventListener('mousedown', this.onUserActivity); + this.document.removeEventListener('mousemove', this.onUserActivity); + this.document.removeEventListener('keydown', this.onUserActivity); + this.window.removeEventListener('wheel', this.onUserActivity, { + capture: true, }); - - this._document.removeEventListener("visibilitychange", this._onPageVisibilityChanged); - this._window.removeEventListener("blur", this._onWindowBlurred); - this._window.removeEventListener("focus", this._onUserActivity); + this.document.removeEventListener("visibilitychange", this.onPageVisibilityChanged); + this.window.removeEventListener("blur", this.onWindowBlurred); + this.window.removeEventListener("focus", this.onUserActivity); } /** @@ -151,8 +148,8 @@ export default class UserActivity { * user's attention at any given moment. * @returns {boolean} true if user is currently 'active' */ - userActiveNow() { - return this._activeNowTimeout.isRunning(); + public userActiveNow() { + return this.activeNowTimeout.isRunning(); } /** @@ -163,27 +160,27 @@ export default class UserActivity { * (or they may have gone to make tea and left the window focused). * @returns {boolean} true if user has been active recently */ - userActiveRecently() { - return this._activeRecentlyTimeout.isRunning(); + public userActiveRecently() { + return this.activeRecentlyTimeout.isRunning(); } - _onPageVisibilityChanged(e) { - if (this._document.visibilityState === "hidden") { - this._activeNowTimeout.abort(); - this._activeRecentlyTimeout.abort(); + private onPageVisibilityChanged = e => { + if (this.document.visibilityState === "hidden") { + this.activeNowTimeout.abort(); + this.activeRecentlyTimeout.abort(); } else { - this._onUserActivity(e); + this.onUserActivity(e); } - } + }; - _onWindowBlurred() { - this._activeNowTimeout.abort(); - this._activeRecentlyTimeout.abort(); - } + private onWindowBlurred = () => { + this.activeNowTimeout.abort(); + this.activeRecentlyTimeout.abort(); + }; - _onUserActivity(event) { + private onUserActivity = (event: MouseEvent) => { // ignore anything if the window isn't focused - if (!this._document.hasFocus()) return; + if (!this.document.hasFocus()) return; if (event.screenX && event.type === "mousemove") { if (event.screenX === this.lastScreenX && event.screenY === this.lastScreenY) { @@ -195,25 +192,25 @@ export default class UserActivity { } dis.dispatch({action: 'user_activity'}); - if (!this._activeNowTimeout.isRunning()) { - this._activeNowTimeout.start(); + if (!this.activeNowTimeout.isRunning()) { + this.activeNowTimeout.start(); dis.dispatch({action: 'user_activity_start'}); - this._runTimersUntilTimeout(this._attachedActiveNowTimers, this._activeNowTimeout); + UserActivity.runTimersUntilTimeout(this.attachedActiveNowTimers, this.activeNowTimeout); } else { - this._activeNowTimeout.restart(); + this.activeNowTimeout.restart(); } - if (!this._activeRecentlyTimeout.isRunning()) { - this._activeRecentlyTimeout.start(); + if (!this.activeRecentlyTimeout.isRunning()) { + this.activeRecentlyTimeout.start(); - this._runTimersUntilTimeout(this._attachedActiveRecentlyTimers, this._activeRecentlyTimeout); + UserActivity.runTimersUntilTimeout(this.attachedActiveRecentlyTimers, this.activeRecentlyTimeout); } else { - this._activeRecentlyTimeout.restart(); + this.activeRecentlyTimeout.restart(); } - } + }; - async _runTimersUntilTimeout(attachedTimers, timeout) { + private static async runTimersUntilTimeout(attachedTimers: Timer[], timeout: Timer) { attachedTimers.forEach((t) => t.start()); try { await timeout.finished(); diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.ts similarity index 72% rename from src/WhoIsTyping.js rename to src/WhoIsTyping.ts index d11cddf487..a8ca425ea8 100644 --- a/src/WhoIsTyping.js +++ b/src/WhoIsTyping.ts @@ -14,19 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {Room} from "matrix-js-sdk/src/models/room"; +import {RoomMember} from "matrix-js-sdk/src/models/room-member"; + import {MatrixClientPeg} from "./MatrixClientPeg"; import { _t } from './languageHandler'; -export function usersTypingApartFromMeAndIgnored(room) { - return usersTyping( - room, [MatrixClientPeg.get().credentials.userId].concat(MatrixClientPeg.get().getIgnoredUsers()), - ); +export function usersTypingApartFromMeAndIgnored(room: Room): RoomMember[] { + return usersTyping(room, [MatrixClientPeg.get().getUserId()].concat(MatrixClientPeg.get().getIgnoredUsers())); } -export function usersTypingApartFromMe(room) { - return usersTyping( - room, [MatrixClientPeg.get().credentials.userId], - ); +export function usersTypingApartFromMe(room: Room): RoomMember[] { + return usersTyping(room, [MatrixClientPeg.get().getUserId()]); } /** @@ -34,15 +33,11 @@ export function usersTypingApartFromMe(room) { * to exclude, return a list of user objects who are typing. * @param {Room} room: room object to get users from. * @param {string[]} exclude: list of user mxids to exclude. - * @returns {string[]} list of user objects who are typing. + * @returns {RoomMember[]} list of user objects who are typing. */ -export function usersTyping(room, exclude) { +export function usersTyping(room: Room, exclude: string[] = []): RoomMember[] { const whoIsTyping = []; - if (exclude === undefined) { - exclude = []; - } - const memberKeys = Object.keys(room.currentState.members); for (let i = 0; i < memberKeys.length; ++i) { const userId = memberKeys[i]; @@ -57,20 +52,21 @@ export function usersTyping(room, exclude) { return whoIsTyping; } -export function whoIsTypingString(whoIsTyping, limit) { +export function whoIsTypingString(whoIsTyping: RoomMember[], limit: number): string { let othersCount = 0; if (whoIsTyping.length > limit) { othersCount = whoIsTyping.length - limit + 1; } + if (whoIsTyping.length === 0) { return ''; } else if (whoIsTyping.length === 1) { return _t('%(displayName)s is typing โ€ฆ', {displayName: whoIsTyping[0].name}); } - const names = whoIsTyping.map(function(m) { - return m.name; - }); - if (othersCount>=1) { + + const names = whoIsTyping.map(m => m.name); + + if (othersCount >= 1) { return _t('%(names)s and %(count)s others are typing โ€ฆ', { names: names.slice(0, limit - 1).join(', '), count: othersCount, diff --git a/src/actions/TagOrderActions.ts b/src/actions/TagOrderActions.ts index c203172874..021cd11b55 100644 --- a/src/actions/TagOrderActions.ts +++ b/src/actions/TagOrderActions.ts @@ -17,14 +17,14 @@ limitations under the License. import Analytics from '../Analytics'; import { asyncAction } from './actionCreators'; -import TagOrderStore from '../stores/TagOrderStore'; +import GroupFilterOrderStore from '../stores/GroupFilterOrderStore'; import { AsyncActionPayload } from "../dispatcher/payloads"; import { MatrixClient } from "matrix-js-sdk/src/client"; export default class TagOrderActions { /** * Creates an action thunk that will do an asynchronous request to - * move a tag in TagOrderStore to destinationIx. + * move a tag in GroupFilterOrderStore to destinationIx. * * @param {MatrixClient} matrixClient the matrix client to set the * account data on. @@ -36,8 +36,8 @@ export default class TagOrderActions { */ public static moveTag(matrixClient: MatrixClient, tag: string, destinationIx: number): AsyncActionPayload { // Only commit tags if the state is ready, i.e. not null - let tags = TagOrderStore.getOrderedTags(); - let removedTags = TagOrderStore.getRemovedTagsAccountData() || []; + let tags = GroupFilterOrderStore.getOrderedTags(); + let removedTags = GroupFilterOrderStore.getRemovedTagsAccountData() || []; if (!tags) { return; } @@ -47,7 +47,7 @@ export default class TagOrderActions { removedTags = removedTags.filter((t) => t !== tag); - const storeId = TagOrderStore.getStoreId(); + const storeId = GroupFilterOrderStore.getStoreId(); return asyncAction('TagOrderActions.moveTag', () => { Analytics.trackEvent('TagOrderActions', 'commitTagOrdering'); @@ -83,8 +83,8 @@ export default class TagOrderActions { */ public static removeTag(matrixClient: MatrixClient, tag: string): AsyncActionPayload { // Don't change tags, just removedTags - const tags = TagOrderStore.getOrderedTags(); - const removedTags = TagOrderStore.getRemovedTagsAccountData() || []; + const tags = GroupFilterOrderStore.getOrderedTags(); + const removedTags = GroupFilterOrderStore.getRemovedTagsAccountData() || []; if (removedTags.includes(tag)) { // Return a thunk that doesn't do anything, we don't even need @@ -94,7 +94,7 @@ export default class TagOrderActions { removedTags.push(tag); - const storeId = TagOrderStore.getStoreId(); + const storeId = GroupFilterOrderStore.getStoreId(); return asyncAction('TagOrderActions.removeTag', () => { Analytics.trackEvent('TagOrderActions', 'removeTag'); diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js index 00aad2a0ce..6819742388 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js @@ -32,6 +32,7 @@ import DialogButtons from "../../../../components/views/elements/DialogButtons"; import InlineSpinner from "../../../../components/views/elements/InlineSpinner"; import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog"; import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils'; +import SecurityCustomisations from "../../../../customisations/Security"; const PHASE_LOADING = 0; const PHASE_LOADERROR = 1; @@ -99,7 +100,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this._passphraseField = createRef(); - this._fetchBackupInfo(); + MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange); + if (this.state.accountPassword) { // If we have an account password in memory, let's simplify and // assume it means password auth is also supported for device @@ -110,13 +112,27 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this._queryKeyUploadAuth(); } - MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange); + this._getInitialPhase(); } componentWillUnmount() { MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange); } + _getInitialPhase() { + const keyFromCustomisations = SecurityCustomisations.createSecretStorageKey?.(); + if (keyFromCustomisations) { + console.log("Created key via customisations, jumping to bootstrap step"); + this._recoveryKey = { + privateKey: keyFromCustomisations, + }; + this._bootstrapSecretStorage(); + return; + } + + this._fetchBackupInfo(); + } + async _fetchBackupInfo() { try { const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); diff --git a/src/components/structures/TagPanel.js b/src/components/structures/GroupFilterPanel.js similarity index 86% rename from src/components/structures/TagPanel.js rename to src/components/structures/GroupFilterPanel.js index 135b2a1c5c..96aa1ba728 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/GroupFilterPanel.js @@ -16,7 +16,7 @@ limitations under the License. */ import React from 'react'; -import TagOrderStore from '../../stores/TagOrderStore'; +import GroupFilterOrderStore from '../../stores/GroupFilterOrderStore'; import GroupActions from '../../actions/GroupActions'; @@ -31,7 +31,7 @@ import AutoHideScrollbar from "./AutoHideScrollbar"; import SettingsStore from "../../settings/SettingsStore"; import UserTagTile from "../views/elements/UserTagTile"; -class TagPanel extends React.Component { +class GroupFilterPanel extends React.Component { static contextType = MatrixClientContext; state = { @@ -44,13 +44,13 @@ class TagPanel extends React.Component { this.context.on("Group.myMembership", this._onGroupMyMembership); this.context.on("sync", this._onClientSync); - this._tagOrderStoreToken = TagOrderStore.addListener(() => { + this._groupFilterOrderStoreToken = GroupFilterOrderStore.addListener(() => { if (this.unmounted) { return; } this.setState({ - orderedTags: TagOrderStore.getOrderedTags() || [], - selectedTags: TagOrderStore.getSelectedTags(), + orderedTags: GroupFilterOrderStore.getOrderedTags() || [], + selectedTags: GroupFilterOrderStore.getSelectedTags(), }); }); // This could be done by anything with a matrix client @@ -61,8 +61,8 @@ class TagPanel extends React.Component { this.unmounted = true; this.context.removeListener("Group.myMembership", this._onGroupMyMembership); this.context.removeListener("sync", this._onClientSync); - if (this._tagOrderStoreToken) { - this._tagOrderStoreToken.remove(); + if (this._groupFilterOrderStoreToken) { + this._groupFilterOrderStoreToken.remove(); } } @@ -98,7 +98,7 @@ class TagPanel extends React.Component { return (
-
+
); } @@ -117,8 +117,8 @@ class TagPanel extends React.Component { }); const itemsSelected = this.state.selectedTags.length > 0; - const classes = classNames('mx_TagPanel', { - mx_TagPanel_items_selected: itemsSelected, + const classes = classNames('mx_GroupFilterPanel', { + mx_GroupFilterPanel_items_selected: itemsSelected, }); let createButton = ( @@ -141,7 +141,7 @@ class TagPanel extends React.Component { return
{ (provided, snapshot) => (
{ this.renderGlobalIcon() } @@ -168,4 +168,4 @@ class TagPanel extends React.Component {
; } } -export default TagPanel; +export default GroupFilterPanel; diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 5dadba983a..482b9f6da2 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -620,7 +620,7 @@ export default class GroupView extends React.Component { profileForm: newProfileForm, // Indicate that FlairStore needs to be poked to show this change - // in TagTile (TagPanel), Flair and GroupTile (MyGroups). + // in TagTile (GroupFilterPanel), Flair and GroupTile (MyGroups). avatarChanged: true, }); }).catch((e) => { @@ -649,7 +649,6 @@ export default class GroupView extends React.Component { editing: false, summary: null, }); - dis.dispatch({action: 'panel_disable'}); this._initGroupStore(this.props.groupId); if (this.state.avatarChanged) { @@ -870,10 +869,7 @@ export default class GroupView extends React.Component { { _t('Add rooms to this community') }
) :
; - const roomDetailListClassName = classnames({ - "mx_fadable": true, - "mx_fadable_faded": this.state.editing, - }); + return

@@ -884,9 +880,7 @@ export default class GroupView extends React.Component {

{ this.state.groupRoomsLoading ? : - + }
; } diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 090a64904c..262d12a700 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -16,7 +16,7 @@ limitations under the License. import * as React from "react"; import { createRef } from "react"; -import TagPanel from "./TagPanel"; +import GroupFilterPanel from "./GroupFilterPanel"; import CustomRoomTagPanel from "./CustomRoomTagPanel"; import classNames from "classnames"; import dis from "../../dispatcher/dispatcher"; @@ -46,7 +46,7 @@ interface IProps { interface IState { showBreadcrumbs: boolean; - showTagPanel: boolean; + showGroupFilterPanel: boolean; } // List of CSS classes which should be included in keyboard navigation within the room list @@ -60,7 +60,7 @@ const cssClasses = [ export default class LeftPanel extends React.Component { private listContainerRef: React.RefObject = createRef(); - private tagPanelWatcherRef: string; + private groupFilterPanelWatcherRef: string; private bgImageWatcherRef: string; private focusedElement = null; private isDoingStickyHeaders = false; @@ -70,7 +70,7 @@ export default class LeftPanel extends React.Component { this.state = { showBreadcrumbs: BreadcrumbsStore.instance.visible, - showTagPanel: SettingsStore.getValue('TagPanel.enableTagPanel'), + showGroupFilterPanel: SettingsStore.getValue('TagPanel.enableTagPanel'), }; BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); @@ -78,8 +78,8 @@ export default class LeftPanel extends React.Component { OwnProfileStore.instance.on(UPDATE_EVENT, this.onBackgroundImageUpdate); this.bgImageWatcherRef = SettingsStore.watchSetting( "RoomList.backgroundImage", null, this.onBackgroundImageUpdate); - this.tagPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => { - this.setState({showTagPanel: SettingsStore.getValue("TagPanel.enableTagPanel")}); + this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => { + this.setState({showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel")}); }); // We watch the middle panel because we don't actually get resized, the middle panel does. @@ -88,7 +88,7 @@ export default class LeftPanel extends React.Component { } public componentWillUnmount() { - SettingsStore.unwatchSetting(this.tagPanelWatcherRef); + SettingsStore.unwatchSetting(this.groupFilterPanelWatcherRef); SettingsStore.unwatchSetting(this.bgImageWatcherRef); BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); @@ -119,8 +119,11 @@ export default class LeftPanel extends React.Component { if (settingBgMxc) { avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(settingBgMxc, avatarSize, avatarSize); } + const avatarUrlProp = `url(${avatarUrl})`; - if (document.body.style.getPropertyValue("--avatar-url") !== avatarUrlProp) { + if (!avatarUrl) { + document.body.style.removeProperty("--avatar-url"); + } else if (document.body.style.getPropertyValue("--avatar-url") !== avatarUrlProp) { document.body.style.setProperty("--avatar-url", avatarUrlProp); } }; @@ -375,9 +378,9 @@ export default class LeftPanel extends React.Component { } public render(): React.ReactNode { - const tagPanel = !this.state.showTagPanel ? null : ( -
- + const groupFilterPanel = !this.state.showGroupFilterPanel ? null : ( +
+ {SettingsStore.getValue("feature_custom_tags") ? : null}
); @@ -394,7 +397,7 @@ export default class LeftPanel extends React.Component { const containerClasses = classNames({ "mx_LeftPanel": true, - "mx_LeftPanel_hasTagPanel": !!tagPanel, + "mx_LeftPanel_hasGroupFilterPanel": !!groupFilterPanel, "mx_LeftPanel_minimized": this.props.isMinimized, }); @@ -405,7 +408,7 @@ export default class LeftPanel extends React.Component { return (
- {tagPanel} + {groupFilterPanel}