Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/feat/widgets

This commit is contained in:
Michael Telatynski 2020-10-19 13:08:17 +01:00
commit 744a4abd1c
68 changed files with 795 additions and 593 deletions

View file

@ -208,12 +208,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
border: 0; 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 // 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 // (which only have CSS, unlike skins) tell the app what their non-tinted
// colourscheme is by inspecting the stylesheet DOM. // colourscheme is by inspecting the stylesheet DOM.

View file

@ -26,7 +26,7 @@
@import "./structures/_ScrollPanel.scss"; @import "./structures/_ScrollPanel.scss";
@import "./structures/_SearchBox.scss"; @import "./structures/_SearchBox.scss";
@import "./structures/_TabbedView.scss"; @import "./structures/_TabbedView.scss";
@import "./structures/_TagPanel.scss"; @import "./structures/_GroupFilterPanel.scss";
@import "./structures/_ToastContainer.scss"; @import "./structures/_ToastContainer.scss";
@import "./structures/_UploadBar.scss"; @import "./structures/_UploadBar.scss";
@import "./structures/_UserMenu.scss"; @import "./structures/_UserMenu.scss";

View file

@ -22,7 +22,7 @@ limitations under the License.
} }
.mx_CustomRoomTagPanel { .mx_CustomRoomTagPanel {
background-color: $tagpanel-bg-color; background-color: $groupFilterPanel-bg-color;
max-height: 40vh; max-height: 40vh;
} }

View file

@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_TagPanel { .mx_GroupFilterPanel {
flex: 1; flex: 1;
background-color: $tagpanel-bg-color; background-color: $groupFilterPanel-bg-color;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@ -26,49 +26,49 @@ limitations under the License.
min-height: 0; min-height: 0;
} }
.mx_TagPanel_items_selected { .mx_GroupFilterPanel_items_selected {
cursor: pointer; cursor: pointer;
} }
.mx_TagPanel .mx_TagPanel_divider { .mx_GroupFilterPanel .mx_GroupFilterPanel_divider {
height: 0px; height: 0px;
width: 90%; width: 90%;
border: none; 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; flex-grow: 1;
width: 100%; width: 100%;
} }
.mx_TagPanel .mx_TagPanel_tagTileContainer { .mx_GroupFilterPanel .mx_GroupFilterPanel_tagTileContainer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding-top: 6px; padding-top: 6px;
} }
.mx_TagPanel .mx_TagPanel_tagTileContainer > div { .mx_GroupFilterPanel .mx_GroupFilterPanel_tagTileContainer > div {
margin: 6px 0; margin: 6px 0;
} }
.mx_TagPanel .mx_TagTile { .mx_GroupFilterPanel .mx_TagTile {
// opacity: 0.5; // opacity: 0.5;
position: relative; position: relative;
} }
.mx_TagPanel .mx_TagTile.mx_TagTile_prototype { .mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype {
padding: 3px; padding: 3px;
} }
.mx_TagPanel .mx_TagTile:focus, .mx_GroupFilterPanel .mx_TagTile:focus,
.mx_TagPanel .mx_TagTile:hover, .mx_GroupFilterPanel .mx_TagTile:hover,
.mx_TagPanel .mx_TagTile.mx_TagTile_selected { .mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected {
// opacity: 1; // opacity: 1;
} }
.mx_TagPanel .mx_TagTile.mx_TagTile_selected_prototype { .mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected_prototype {
background-color: $primary-bg-color; background-color: $primary-bg-color;
border-radius: 6px; border-radius: 6px;
} }
@ -108,7 +108,7 @@ limitations under the License.
} }
} }
.mx_TagPanel .mx_TagTile_plus { .mx_GroupFilterPanel .mx_TagTile_plus {
margin-bottom: 12px; margin-bottom: 12px;
height: 32px; height: 32px;
width: 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: ''; content: '';
height: 100%; height: 100%;
background-color: $accent-color; background-color: $accent-color;
@ -142,7 +142,7 @@ limitations under the License.
border-radius: 0 3px 3px 0; border-radius: 0 3px 3px 0;
} }
.mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus { .mx_GroupFilterPanel .mx_TagTile.mx_AccessibleButton:focus {
filter: none; filter: none;
} }

View file

@ -14,29 +14,29 @@ See the License for the specific language governing permissions and
limitations under the License. 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 { .mx_LeftPanel {
background-color: $roomlist-bg-color; background-color: $roomlist-bg-color;
min-width: 260px; min-width: 260px;
max-width: 50%; 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; display: flex;
.mx_LeftPanel_tagPanelContainer { .mx_LeftPanel_GroupFilterPanelContainer {
flex-grow: 0; flex-grow: 0;
flex-shrink: 0; flex-shrink: 0;
flex-basis: $tagPanelWidth; flex-basis: $groupFilterPanelWidth;
height: 100%; height: 100%;
// Create another flexbox so the TagPanel fills the container // Create another flexbox so the GroupFilterPanel fills the container
display: flex; display: flex;
// TagPanel handles its own CSS // GroupFilterPanel handles its own CSS
} }
&:not(.mx_LeftPanel_hasTagPanel) { &:not(.mx_LeftPanel_hasGroupFilterPanel) {
.mx_LeftPanel_roomListContainer { .mx_LeftPanel_roomListContainer {
width: 100%; 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 // Note: The 'room list' in this context is actually everything that isn't the tag
// panel, such as the menu options, breadcrumbs, filtering, etc // panel, such as the menu options, breadcrumbs, filtering, etc
.mx_LeftPanel_roomListContainer { .mx_LeftPanel_roomListContainer {
width: calc(100% - $tagPanelWidth); width: calc(100% - $groupFilterPanelWidth);
background-color: $roomlist-bg-color; background-color: $roomlist-bg-color;
// Create another flexbox (this time a column) for the room list components // 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; min-width: unset;
// We have to forcefully set the width to override the resizer's style attribute. // We have to forcefully set the width to override the resizer's style attribute.
&.mx_LeftPanel_hasTagPanel { &.mx_LeftPanel_hasGroupFilterPanel {
width: calc(68px + $tagPanelWidth) !important; width: calc(68px + $groupFilterPanelWidth) !important;
} }
&:not(.mx_LeftPanel_hasTagPanel) { &:not(.mx_LeftPanel_hasGroupFilterPanel) {
width: 68px !important; width: 68px !important;
} }

View file

@ -70,7 +70,7 @@ limitations under the License.
} }
.mx_MemberInfo_avatar { .mx_MemberInfo_avatar {
background: $tagpanel-bg-color; background: $groupFilterPanel-bg-color;
margin-bottom: 16px; margin-bottom: 16px;
} }

View file

@ -85,6 +85,7 @@ limitations under the License.
.mx_AvatarSetting_avatarPlaceholder { .mx_AvatarSetting_avatarPlaceholder {
display: block; display: block;
height: 90px; height: 90px;
width: inherit;
border-radius: 90px; border-radius: 90px;
cursor: pointer; cursor: pointer;
} }

View file

@ -39,7 +39,7 @@ $info-plinth-fg-color: #888;
$preview-bar-bg-color: $header-panel-bg-color; $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; $inverted-bg-color: $base-color;
// used by AddressSelector // used by AddressSelector
@ -98,7 +98,7 @@ $roomheader-color: $text-primary-color;
$roomheader-bg-color: $bg-color; $roomheader-bg-color: $bg-color;
$roomheader-addroom-bg-color: rgba(92, 100, 112, 0.3); $roomheader-addroom-bg-color: rgba(92, 100, 112, 0.3);
$roomheader-addroom-fg-color: $text-primary-color; $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; $groupheader-button-color: $header-panel-text-primary-color;
$rightpanel-button-color: $header-panel-text-primary-color; $rightpanel-button-color: $header-panel-text-primary-color;
$icon-button-color: #8E99A4; $icon-button-color: #8E99A4;
@ -118,7 +118,7 @@ $roomlist-bg-color: rgba(33, 38, 44, 0.90);
$roomlist-header-color: $tertiary-fg-color; $roomlist-header-color: $tertiary-fg-color;
$roomsublist-divider-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: $secondary-fg-color; $roomtile-preview-color: $secondary-fg-color;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;
@ -187,7 +187,7 @@ $reaction-row-button-selected-border-color: $accent-color;
$kbd-border-color: #000000; $kbd-border-color: #000000;
$tooltip-timeline-bg-color: $tagpanel-bg-color; $tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
$tooltip-timeline-fg-color: #ffffff; $tooltip-timeline-fg-color: #ffffff;
$interactive-tooltip-bg-color: $base-color; $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) // blur amounts for left left panel (only for element theme, used in _mods.scss)
$roomlist-background-blur-amount: 60px; $roomlist-background-blur-amount: 60px;
$tagpanel-background-blur-amount: 30px; $groupFilterPanel-background-blur-amount: 30px;
$composer-shadow-color: rgba(0, 0, 0, 0.28); $composer-shadow-color: rgba(0, 0, 0, 0.28);

View file

@ -3,7 +3,7 @@
@import "../../light/css/_fonts.scss"; @import "../../light/css/_fonts.scss";
@import "../../light/css/_light.scss"; @import "../../light/css/_light.scss";
// important this goes before _mods, // important this goes before _mods,
// as $tagpanel-background-blur-amount and // as $groupFilterPanel-background-blur-amount and
// $roomlist-background-blur-amount // $roomlist-background-blur-amount
// are overridden in _dark.scss // are overridden in _dark.scss
@import "_dark.scss"; @import "_dark.scss";

View file

@ -37,8 +37,8 @@ $info-plinth-fg-color: #888;
$preview-bar-bg-color: $header-panel-bg-color; $preview-bar-bg-color: $header-panel-bg-color;
$tagpanel-bg-color: $base-color; $groupFilterPanel-bg-color: $base-color;
$inverted-bg-color: $tagpanel-bg-color; $inverted-bg-color: $groupFilterPanel-bg-color;
// used by AddressSelector // used by AddressSelector
$selected-color: $room-highlight-color; $selected-color: $room-highlight-color;
@ -95,7 +95,7 @@ $topleftmenu-color: $text-primary-color;
$roomheader-color: $text-primary-color; $roomheader-color: $text-primary-color;
$roomheader-addroom-bg-color: #3c4556; // $search-placeholder-color at 0.5 opacity $roomheader-addroom-bg-color: #3c4556; // $search-placeholder-color at 0.5 opacity
$roomheader-addroom-fg-color: $text-primary-color; $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; $groupheader-button-color: $header-panel-text-primary-color;
$rightpanel-button-color: $header-panel-text-primary-color; $rightpanel-button-color: $header-panel-text-primary-color;
$icon-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; $roomsublist-divider-color: $primary-fg-color;
$tagpanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
$roomtile-preview-color: #9e9e9e; $roomtile-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;
@ -182,7 +182,7 @@ $reaction-row-button-selected-border-color: $accent-color;
$kbd-border-color: #000000; $kbd-border-color: #000000;
$tooltip-timeline-bg-color: $tagpanel-bg-color; $tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
$tooltip-timeline-fg-color: #ffffff; $tooltip-timeline-fg-color: #ffffff;
$interactive-tooltip-bg-color: $base-color; $interactive-tooltip-bg-color: $base-color;

View file

@ -67,8 +67,8 @@ $preview-bar-bg-color: #f7f7f7;
$secondary-accent-color: #f2f5f8; $secondary-accent-color: #f2f5f8;
$tertiary-accent-color: #d3efe1; $tertiary-accent-color: #d3efe1;
$tagpanel-bg-color: #27303a; $groupFilterPanel-bg-color: #27303a;
$inverted-bg-color: $tagpanel-bg-color; $inverted-bg-color: $groupFilterPanel-bg-color;
// used by RoomDirectory permissions // used by RoomDirectory permissions
$plinth-bg-color: $secondary-accent-color; $plinth-bg-color: $secondary-accent-color;
@ -162,7 +162,7 @@ $roomheader-color: #45474a;
$roomheader-bg-color: $primary-bg-color; $roomheader-bg-color: $primary-bg-color;
$roomheader-addroom-bg-color: #91a1c0; $roomheader-addroom-bg-color: #91a1c0;
$roomheader-addroom-fg-color: $accent-fg-color; $roomheader-addroom-fg-color: $accent-fg-color;
$tagpanel-button-color: #91a1c0; $groupFilterPanel-button-color: #91a1c0;
$groupheader-button-color: #91a1c0; $groupheader-button-color: #91a1c0;
$rightpanel-button-color: #91a1c0; $rightpanel-button-color: #91a1c0;
$icon-button-color: #91a1c0; $icon-button-color: #91a1c0;
@ -182,7 +182,7 @@ $roomlist-bg-color: $header-panel-bg-color;
$roomlist-header-color: $primary-fg-color; $roomlist-header-color: $primary-fg-color;
$roomsublist-divider-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-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b; $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; $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; $tooltip-timeline-fg-color: #ffffff;
$interactive-tooltip-bg-color: #27303a; $interactive-tooltip-bg-color: #27303a;

View file

@ -49,7 +49,7 @@ $roomtile-selected-bg-color: var(--roomlist-highlights-color);
// //
// --sidebar-color // --sidebar-color
$interactive-tooltip-bg-color: var(--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); $tooltip-timeline-bg-color: var(--sidebar-color);
$dialog-backdrop-color: var(--sidebar-color-50pct); $dialog-backdrop-color: var(--sidebar-color-50pct);
$roomlist-button-bg-color: var(--sidebar-color-15pct); $roomlist-button-bg-color: var(--sidebar-color-15pct);

View file

@ -62,7 +62,7 @@ $preview-bar-bg-color: #f7f7f7;
$secondary-accent-color: #f2f5f8; $secondary-accent-color: #f2f5f8;
$tertiary-accent-color: #d3efe1; $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 // used by RoomDirectory permissions
$plinth-bg-color: $secondary-accent-color; $plinth-bg-color: $secondary-accent-color;
@ -156,7 +156,7 @@ $roomheader-color: #45474a;
$roomheader-bg-color: $primary-bg-color; $roomheader-bg-color: $primary-bg-color;
$roomheader-addroom-bg-color: rgba(92, 100, 112, 0.2); $roomheader-addroom-bg-color: rgba(92, 100, 112, 0.2);
$roomheader-addroom-fg-color: #5c6470; $roomheader-addroom-fg-color: #5c6470;
$tagpanel-button-color: #91A1C0; $groupFilterPanel-button-color: #91A1C0;
$groupheader-button-color: #91A1C0; $groupheader-button-color: #91A1C0;
$rightpanel-button-color: #91A1C0; $rightpanel-button-color: #91A1C0;
$icon-button-color: #C1C6CD; $icon-button-color: #C1C6CD;
@ -176,7 +176,7 @@ $roomlist-bg-color: rgba(245, 245, 245, 0.90);
$roomlist-header-color: $tertiary-fg-color; $roomlist-header-color: $tertiary-fg-color;
$roomsublist-divider-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: $secondary-fg-color; $roomtile-preview-color: $secondary-fg-color;
$roomtile-default-badge-bg-color: #61708b; $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) // blur amounts for left left panel (only for element theme, used in _mods.scss)
$roomlist-background-blur-amount: 40px; $roomlist-background-blur-amount: 40px;
$tagpanel-background-blur-amount: 20px; $groupFilterPanel-background-blur-amount: 20px;
$composer-shadow-color: rgba(0, 0, 0, 0.04); $composer-shadow-color: rgba(0, 0, 0, 0.04);

View file

@ -6,14 +6,14 @@
@supports (backdrop-filter: none) { @supports (backdrop-filter: none) {
.mx_LeftPanel { .mx_LeftPanel {
background-image: var(--avatar-url); background-image: var(--avatar-url, unset);
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
background-position: left top; background-position: left top;
} }
.mx_TagPanel { .mx_GroupFilterPanel {
backdrop-filter: blur($tagpanel-background-blur-amount); backdrop-filter: blur($groupFilterPanel-background-blur-amount);
} }
.mx_LeftPanel .mx_LeftPanel_roomListContainer { .mx_LeftPanel .mx_LeftPanel_roomListContainer {

View file

@ -32,6 +32,8 @@ import type {Renderer} from "react-dom";
import RightPanelStore from "../stores/RightPanelStore"; import RightPanelStore from "../stores/RightPanelStore";
import WidgetStore from "../stores/WidgetStore"; import WidgetStore from "../stores/WidgetStore";
import CallHandler from "../CallHandler"; import CallHandler from "../CallHandler";
import {Analytics} from "../Analytics";
import UserActivity from "../UserActivity";
declare global { declare global {
interface Window { interface Window {
@ -56,6 +58,8 @@ declare global {
mxRightPanelStore: RightPanelStore; mxRightPanelStore: RightPanelStore;
mxWidgetStore: WidgetStore; mxWidgetStore: WidgetStore;
mxCallHandler: CallHandler; mxCallHandler: CallHandler;
mxAnalytics: Analytics;
mxUserActivity: UserActivity;
} }
interface Document { interface Document {

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { getCurrentLanguage, _t, _td } from './languageHandler'; import {getCurrentLanguage, _t, _td, IVariables} from './languageHandler';
import PlatformPeg from './PlatformPeg'; import PlatformPeg from './PlatformPeg';
import SdkConfig from './SdkConfig'; import SdkConfig from './SdkConfig';
import Modal from './Modal'; import Modal from './Modal';
@ -27,7 +27,7 @@ const hashRegex = /#\/(groups?|room|user|settings|register|login|forgot_password
const hashVarRegex = /#\/(group|room|user)\/.*$/; const hashVarRegex = /#\/(group|room|user)\/.*$/;
// Remove all but the first item in the hash path. Redact unexpected hashes. // 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 // Don't leak URLs we aren't expecting - they could contain tokens/PII
const match = hashRegex.exec(hash); const match = hashRegex.exec(hash);
if (!match) { if (!match) {
@ -44,7 +44,7 @@ function getRedactedHash(hash) {
// Return the current origin, path and hash separated with a `/`. This does // Return the current origin, path and hash separated with a `/`. This does
// not include query parameters. // not include query parameters.
function getRedactedUrl() { function getRedactedUrl(): string {
const { origin, hash } = window.location; const { origin, hash } = window.location;
let { pathname } = window.location; let { pathname } = window.location;
@ -56,7 +56,25 @@ function getRedactedUrl() {
return origin + pathname + getRedactedHash(hash); 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<string, IVariable> = {
// The Matomo installation at https://matomo.riot.im is currently configured // The Matomo installation at https://matomo.riot.im is currently configured
// with a limit of 10 custom variables. // with a limit of 10 custom variables.
'App Platform': { '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; if (whitelist.includes(str)) return str;
return '<redacted>'; return '<redacted>';
} }
@ -130,7 +148,7 @@ const CREATION_TS_KEY = "mx_Riot_Analytics_cts";
const VISIT_COUNT_KEY = "mx_Riot_Analytics_vc"; const VISIT_COUNT_KEY = "mx_Riot_Analytics_vc";
const LAST_VISIT_TS_KEY = "mx_Riot_Analytics_lvts"; const LAST_VISIT_TS_KEY = "mx_Riot_Analytics_lvts";
function getUid() { function getUid(): string {
try { try {
let data = localStorage && localStorage.getItem(UID_KEY); let data = localStorage && localStorage.getItem(UID_KEY);
if (!data && localStorage) { if (!data && localStorage) {
@ -145,32 +163,36 @@ function getUid() {
const HEARTBEAT_INTERVAL = 30 * 1000; // seconds const HEARTBEAT_INTERVAL = 30 * 1000; // seconds
class Analytics { export class Analytics {
private baseUrl: URL = null;
private siteId: string = null;
private visitVariables: Record<number, [string, string]> = {}; // {[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() { constructor() {
this.baseUrl = null;
this.siteId = null;
this.visitVariables = {};
this.firstPage = true;
this._heartbeatIntervalID = null;
this.creationTs = localStorage && localStorage.getItem(CREATION_TS_KEY); this.creationTs = localStorage && localStorage.getItem(CREATION_TS_KEY);
if (!this.creationTs && localStorage) { 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.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) { 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; return !this.baseUrl;
} }
canEnable() { public canEnable() {
const config = SdkConfig.get(); const config = SdkConfig.get();
return navigator.doNotTrack !== "1" && config && config.piwik && config.piwik.url && config.piwik.siteId; 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 * Enable Analytics if initialized but disabled
* otherwise try and initalize, no-op if piwik config missing * otherwise try and initalize, no-op if piwik config missing
*/ */
async enable() { public async enable() {
if (!this.disabled) return; if (!this.disabled) return;
if (!this.canEnable()) return; if (!this.canEnable()) return;
const config = SdkConfig.get(); const config = SdkConfig.get();
this.baseUrl = new URL("piwik.php", config.piwik.url); this.baseUrl = new URL("piwik.php", config.piwik.url);
// set constants // 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("idsite", config.piwik.siteId); // rec is required for tracking
this.baseUrl.searchParams.set("apiv", 1); // API version to use 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("send_image", "0"); // we want a 204, not a tiny GIF
// set user parameters // set user parameters
this.baseUrl.searchParams.set("_id", getUid()); // uuid this.baseUrl.searchParams.set("_id", getUid()); // uuid
this.baseUrl.searchParams.set("_idts", this.creationTs); // first ts 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) { if (this.lastVisitTs) {
this.baseUrl.searchParams.set("_viewts", this.lastVisitTs); // last visit ts this.baseUrl.searchParams.set("_viewts", this.lastVisitTs); // last visit ts
} }
const platform = PlatformPeg.get(); const platform = PlatformPeg.get();
this._setVisitVariable('App Platform', platform.getHumanReadableName()); this.setVisitVariable('App Platform', platform.getHumanReadableName());
try { try {
this._setVisitVariable('App Version', await platform.getAppVersion()); this.setVisitVariable('App Version', await platform.getAppVersion());
} catch (e) { } 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; const hostname = window.location.hostname;
if (hostname === 'riot.im') { if (hostname === 'riot.im') {
this._setVisitVariable('Instance', window.location.pathname); this.setVisitVariable('Instance', window.location.pathname);
} else if (hostname.endsWith('.element.io')) { } else if (hostname.endsWith('.element.io')) {
this._setVisitVariable('Instance', hostname.replace('.element.io', '')); this.setVisitVariable('Instance', hostname.replace('.element.io', ''));
} }
let installedPWA = "unknown"; let installedPWA = "unknown";
try { try {
// Known to work at least for desktop Chrome // 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) { } } catch (e) { }
this._setVisitVariable('Installed PWA', installedPWA); this.setVisitVariable('Installed PWA', installedPWA);
let touchInput = "unknown"; let touchInput = "unknown";
try { try {
// MDN claims broad support across browsers // MDN claims broad support across browsers
touchInput = window.matchMedia('(pointer: coarse)').matches; touchInput = String(window.matchMedia('(pointer: coarse)').matches);
} catch (e) { } } catch (e) { }
this._setVisitVariable('Touch Input', touchInput); this.setVisitVariable('Touch Input', touchInput);
// start heartbeat // 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 Analytics, stop the heartbeat and clear identifiers from localStorage
*/ */
disable() { public disable() {
if (this.disabled) return; if (this.disabled) return;
this.trackEvent('Analytics', 'opt-out'); this.trackEvent('Analytics', 'opt-out');
window.clearInterval(this._heartbeatIntervalID); window.clearInterval(this.heartbeatIntervalID);
this.baseUrl = null; this.baseUrl = null;
this.visitVariables = {}; this.visitVariables = {};
localStorage.removeItem(UID_KEY); localStorage.removeItem(UID_KEY);
@ -248,7 +270,7 @@ class Analytics {
localStorage.removeItem(LAST_VISIT_TS_KEY); localStorage.removeItem(LAST_VISIT_TS_KEY);
} }
async _track(data) { private async _track(data: IData) {
if (this.disabled) return; if (this.disabled) return;
const now = new Date(); const now = new Date();
@ -264,13 +286,13 @@ class Analytics {
s: now.getSeconds(), s: now.getSeconds(),
}; };
const url = new URL(this.baseUrl); const url = new URL(this.baseUrl.toString()); // copy
for (const key in params) { for (const key in params) {
url.searchParams.set(key, params[key]); url.searchParams.set(key, params[key]);
} }
try { try {
await window.fetch(url, { await window.fetch(url.toString(), {
method: "GET", method: "GET",
mode: "no-cors", mode: "no-cors",
cache: "no-cache", cache: "no-cache",
@ -281,14 +303,14 @@ class Analytics {
} }
} }
ping() { public ping() {
this._track({ 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.disabled) return;
if (this.firstPage) { if (this.firstPage) {
// De-duplicate first page // De-duplicate first page
@ -303,11 +325,11 @@ class Analytics {
} }
this._track({ 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; if (this.disabled) return;
this._track({ this._track({
e_c: category, e_c: category,
@ -317,12 +339,12 @@ class Analytics {
}); });
} }
_setVisitVariable(key, value) { private setVisitVariable(key: keyof typeof customVariables, value: string) {
if (this.disabled) return; if (this.disabled) return;
this.visitVariables[customVariables[key].id] = [key, value]; this.visitVariables[customVariables[key].id] = [key, value];
} }
setLoggedIn(isGuest, homeserverUrl, identityServerUrl) { public setLoggedIn(isGuest: boolean, homeserverUrl: string) {
if (this.disabled) return; if (this.disabled) return;
const config = SdkConfig.get(); const config = SdkConfig.get();
@ -330,16 +352,16 @@ class Analytics {
const whitelistedHSUrls = config.piwik.whitelistedHSUrls || []; const whitelistedHSUrls = config.piwik.whitelistedHSUrls || [];
this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In'); this.setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl)); this.setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
} }
setBreadcrumbs(state) { public setBreadcrumbs(state: boolean) {
if (this.disabled) return; if (this.disabled) return;
this._setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled'); this.setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled');
} }
showDetailsModal = () => { public showDetailsModal = () => {
let rows = []; let rows = [];
if (!this.disabled) { if (!this.disabled) {
rows = Object.values(this.visitVariables); rows = Object.values(this.visitVariables);
@ -360,7 +382,7 @@ class Analytics {
'e.g. <CurrentPageURL>', 'e.g. <CurrentPageURL>',
{}, {},
{ {
CurrentPageURL: getRedactedUrl(), CurrentPageURL: getRedactedUrl,
}, },
), ),
}, },
@ -401,7 +423,7 @@ class Analytics {
}; };
} }
if (!global.mxAnalytics) { if (!window.mxAnalytics) {
global.mxAnalytics = new Analytics(); window.mxAnalytics = new Analytics();
} }
export default global.mxAnalytics; export default window.mxAnalytics;

View file

@ -14,14 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License. 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 {MatrixClientPeg} from './MatrixClientPeg';
import DMRoomMap from './utils/DMRoomMap'; 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 // Not to be used for BaseAvatar urls as that has similar default avatar fallback already
export function avatarUrlForMember(member, width, height, resizeMethod) { export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) {
let url; let url: string;
if (member && member.getAvatarUrl) { if (member && member.getAvatarUrl) {
url = member.getAvatarUrl( url = member.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(), MatrixClientPeg.get().getHomeserverUrl(),
@ -41,7 +46,7 @@ export function avatarUrlForMember(member, width, height, resizeMethod) {
return url; return url;
} }
export function avatarUrlForUser(user, width, height, resizeMethod) { export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) {
const url = getHttpUriForMxc( const url = getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl, MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
Math.floor(width * window.devicePixelRatio), Math.floor(width * window.devicePixelRatio),
@ -54,14 +59,14 @@ export function avatarUrlForUser(user, width, height, resizeMethod) {
return url; return url;
} }
function isValidHexColor(color) { function isValidHexColor(color: string): boolean {
return typeof color === "string" && return typeof color === "string" &&
(color.length === 7 || color.lengh === 9) && (color.length === 7 || color.length === 9) &&
color.charAt(0) === "#" && color.charAt(0) === "#" &&
!color.substr(1).split("").some(c => isNaN(parseInt(c, 16))); !color.substr(1).split("").some(c => isNaN(parseInt(c, 16)));
} }
function urlForColor(color) { function urlForColor(color: string): string {
const size = 40; const size = 40;
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.width = size; canvas.width = size;
@ -79,9 +84,9 @@ function urlForColor(color) {
// XXX: Ideally we'd clear this cache when the theme changes // XXX: Ideally we'd clear this cache when the theme changes
// but since this function is at global scope, it's a bit // 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 // hard to install a listener here, even if there were a clear event to listen to
const colorToDataURLCache = new Map(); const colorToDataURLCache = new Map<string, string>();
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 if (!s) return ""; // XXX: should never happen but empirically does by evidence of a rageshake
const defaultColors = ['#0DBD8B', '#368bd6', '#ac3ba8']; const defaultColors = ['#0DBD8B', '#368bd6', '#ac3ba8'];
let total = 0; let total = 0;
@ -113,7 +118,7 @@ export function defaultAvatarUrlForString(s) {
* @param {string} name * @param {string} name
* @return {string} the first letter * @return {string} the first letter
*/ */
export function getInitialLetter(name) { export function getInitialLetter(name: string): string {
if (!name) { if (!name) {
// XXX: We should find out what causes the name to sometimes be falsy. // XXX: We should find out what causes the name to sometimes be falsy.
console.trace("`name` argument to `getInitialLetter` not supplied"); console.trace("`name` argument to `getInitialLetter` not supplied");
@ -146,7 +151,7 @@ export function getInitialLetter(name) {
return firstChar.toUpperCase(); 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 if (!room) return null; // null-guard
const explicitRoomAvatar = room.getAvatarUrl( const explicitRoomAvatar = room.getAvatarUrl(

View file

@ -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) { private setCallListeners(call: MatrixCall) {
call.on(CallEvent.Error, (err) => { call.on(CallEvent.Error, (err) => {
if (!this.matchesCallForThisRoom(call)) return;
console.error("Call error:", err); console.error("Call error:", err);
if ( if (
MatrixClientPeg.get().getTurnServers().length === 0 && MatrixClientPeg.get().getTurnServers().length === 0 &&
@ -196,9 +206,13 @@ export default class CallHandler {
}); });
}); });
call.on(CallEvent.Hangup, () => { call.on(CallEvent.Hangup, () => {
if (!this.matchesCallForThisRoom(call)) return;
this.removeCallForRoom(call.roomId); this.removeCallForRoom(call.roomId);
}); });
call.on(CallEvent.State, (newState: CallState, oldState: CallState) => { call.on(CallEvent.State, (newState: CallState, oldState: CallState) => {
if (!this.matchesCallForThisRoom(call)) return;
this.setCallState(call, newState); this.setCallState(call, newState);
switch (oldState) { switch (oldState) {
@ -224,15 +238,45 @@ export default class CallHandler {
(call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout) (call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout)
)) { )) {
this.play(AudioID.Busy); this.play(AudioID.Busy);
Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, { let title;
title: _t('Call Timeout'), let description;
description: _t('The remote side failed to pick up') + '.', 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 { } else {
this.play(AudioID.CallEnd); 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) { private setCallState(call: MatrixCall, status: CallState) {
@ -393,10 +437,15 @@ export default class CallHandler {
} }
break; break;
case 'hangup': case 'hangup':
case 'reject':
if (!this.calls.get(payload.room_id)) { if (!this.calls.get(payload.room_id)) {
return; // no call to hangup 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); this.removeCallForRoom(payload.room_id);
break; break;
case 'answer': case 'answer':

View file

@ -17,7 +17,6 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import extend from './extend';
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import {MatrixClient} from "matrix-js-sdk/src/client"; import {MatrixClient} from "matrix-js-sdk/src/client";
@ -497,7 +496,7 @@ export default class ContentMessages {
if (file.type.indexOf('image/') === 0) { if (file.type.indexOf('image/') === 0) {
content.msgtype = 'm.image'; content.msgtype = 'm.image';
infoForImageFile(matrixClient, roomId, file).then((imageInfo) => { infoForImageFile(matrixClient, roomId, file).then((imageInfo) => {
extend(content.info, imageInfo); Object.assign(content.info, imageInfo);
resolve(); resolve();
}, (e) => { }, (e) => {
console.error(e); console.error(e);
@ -510,7 +509,7 @@ export default class ContentMessages {
} else if (file.type.indexOf('video/') === 0) { } else if (file.type.indexOf('video/') === 0) {
content.msgtype = 'm.video'; content.msgtype = 'm.video';
infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => { infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => {
extend(content.info, videoInfo); Object.assign(content.info, videoInfo);
resolve(); resolve();
}, (e) => { }, (e) => {
content.msgtype = 'm.file'; content.msgtype = 'm.file';

View file

@ -17,7 +17,7 @@ limitations under the License.
import { _t } from './languageHandler'; import { _t } from './languageHandler';
function getDaysArray() { function getDaysArray(): string[] {
return [ return [
_t('Sun'), _t('Sun'),
_t('Mon'), _t('Mon'),
@ -29,7 +29,7 @@ function getDaysArray() {
]; ];
} }
function getMonthsArray() { function getMonthsArray(): string[] {
return [ return [
_t('Jan'), _t('Jan'),
_t('Feb'), _t('Feb'),
@ -46,11 +46,11 @@ function getMonthsArray() {
]; ];
} }
function pad(n) { function pad(n: number): string {
return (n < 10 ? '0' : '') + n; return (n < 10 ? '0' : '') + n;
} }
function twelveHourTime(date, showSeconds=false) { function twelveHourTime(date: Date, showSeconds = false): string {
let hours = date.getHours() % 12; let hours = date.getHours() % 12;
const minutes = pad(date.getMinutes()); const minutes = pad(date.getMinutes());
const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM'); const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM');
@ -62,7 +62,7 @@ function twelveHourTime(date, showSeconds=false) {
return `${hours}:${minutes}${ampm}`; return `${hours}:${minutes}${ampm}`;
} }
export function formatDate(date, showTwelveHour=false) { export function formatDate(date: Date, showTwelveHour = false): string {
const now = new Date(); const now = new Date();
const days = getDaysArray(); const days = getDaysArray();
const months = getMonthsArray(); const months = getMonthsArray();
@ -86,7 +86,7 @@ export function formatDate(date, showTwelveHour=false) {
return formatFullDate(date, showTwelveHour); return formatFullDate(date, showTwelveHour);
} }
export function formatFullDateNoTime(date) { export function formatFullDateNoTime(date: Date): string {
const days = getDaysArray(); const days = getDaysArray();
const months = getMonthsArray(); const months = getMonthsArray();
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s', { 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 days = getDaysArray();
const months = getMonthsArray(); const months = getMonthsArray();
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', { 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) { if (showTwelveHour) {
return twelveHourTime(date, true); return twelveHourTime(date, true);
} }
return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds()); 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) { if (showTwelveHour) {
return twelveHourTime(date); return twelveHourTime(date);
} }
@ -124,7 +124,7 @@ export function formatTime(date, showTwelveHour=false) {
} }
const MILLIS_IN_DAY = 86400000; const MILLIS_IN_DAY = 86400000;
export function wantsDateSeparator(prevEventDate, nextEventDate) { export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean {
if (!nextEventDate || !prevEventDate) { if (!nextEventDate || !prevEventDate) {
return false; return false;
} }

View file

@ -23,6 +23,7 @@ import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import {IMatrixClientCreds, MatrixClientPeg} from './MatrixClientPeg'; import {IMatrixClientCreds, MatrixClientPeg} from './MatrixClientPeg';
import SecurityCustomisations from "./customisations/Security";
import EventIndexPeg from './indexing/EventIndexPeg'; import EventIndexPeg from './indexing/EventIndexPeg';
import createMatrixClient from './utils/createMatrixClient'; import createMatrixClient from './utils/createMatrixClient';
import Analytics from './Analytics'; import Analytics from './Analytics';
@ -567,6 +568,8 @@ function persistCredentialsToLocalStorage(credentials: IMatrixClientCreds): void
localStorage.setItem("mx_device_id", credentials.deviceId); localStorage.setItem("mx_device_id", credentials.deviceId);
} }
SecurityCustomisations.persistCredentials?.(credentials);
console.log(`Session persisted for ${credentials.userId}`); console.log(`Session persisted for ${credentials.userId}`);
} }

View file

@ -22,6 +22,7 @@ limitations under the License.
import Matrix from "matrix-js-sdk"; import Matrix from "matrix-js-sdk";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { IMatrixClientCreds } from "./MatrixClientPeg"; import { IMatrixClientCreds } from "./MatrixClientPeg";
import SecurityCustomisations from "./customisations/Security";
interface ILoginOptions { interface ILoginOptions {
defaultDeviceDisplayName?: string; defaultDeviceDisplayName?: string;
@ -222,11 +223,15 @@ export async function sendLoginRequest(
} }
} }
return { const creds: IMatrixClientCreds = {
homeserverUrl: hsUrl, homeserverUrl: hsUrl,
identityServerUrl: isUrl, identityServerUrl: isUrl,
userId: data.user_id, userId: data.user_id,
deviceId: data.device_id, deviceId: data.device_id,
accessToken: data.access_token, accessToken: data.access_token,
}; };
SecurityCustomisations.examineLoginResponse?.(data, creds);
return creds;
} }

View file

@ -218,7 +218,7 @@ export const Notifier = {
// calculated value. It is determined based upon whether or not the master rule is enabled // 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. // 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 // make sure that we persist the current setting audio_enabled setting
// before changing anything // before changing anything
@ -287,7 +287,7 @@ export const Notifier = {
setPromptHidden: function(hidden: boolean, persistent = true) { setPromptHidden: function(hidden: boolean, persistent = true) {
this.toolbarHidden = hidden; this.toolbarHidden = hidden;
Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden); Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', String(hidden));
hideNotificationsToast(); hideNotificationsToast();

View file

@ -19,30 +19,34 @@ limitations under the License.
import {MatrixClientPeg} from "./MatrixClientPeg"; import {MatrixClientPeg} from "./MatrixClientPeg";
import dis from "./dispatcher/dispatcher"; import dis from "./dispatcher/dispatcher";
import Timer from './utils/Timer'; 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 UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
const PRESENCE_STATES = ["online", "offline", "unavailable"];
enum State {
Online = "online",
Offline = "offline",
Unavailable = "unavailable",
}
class Presence { class Presence {
constructor() { private unavailableTimer: Timer = null;
this._activitySignal = null; private dispatcherRef: string = null;
this._unavailableTimer = null; private state: State = null;
this._onAction = this._onAction.bind(this);
this._dispatcherRef = null;
}
/** /**
* Start listening the user activity to evaluate his presence state. * Start listening the user activity to evaluate his presence state.
* Any state change will be sent to the homeserver. * Any state change will be sent to the homeserver.
*/ */
async start() { public async start() {
this._unavailableTimer = new Timer(UNAVAILABLE_TIME_MS); this.unavailableTimer = new Timer(UNAVAILABLE_TIME_MS);
// the user_activity_start action starts the timer // the user_activity_start action starts the timer
this._dispatcherRef = dis.register(this._onAction); this.dispatcherRef = dis.register(this.onAction);
while (this._unavailableTimer) { while (this.unavailableTimer) {
try { try {
await this._unavailableTimer.finished(); await this.unavailableTimer.finished();
this.setState("unavailable"); this.setState(State.Unavailable);
} catch (e) { /* aborted, stop got called */ } } catch (e) { /* aborted, stop got called */ }
} }
} }
@ -50,14 +54,14 @@ class Presence {
/** /**
* Stop tracking user activity * Stop tracking user activity
*/ */
stop() { public stop() {
if (this._dispatcherRef) { if (this.dispatcherRef) {
dis.unregister(this._dispatcherRef); dis.unregister(this.dispatcherRef);
this._dispatcherRef = null; this.dispatcherRef = null;
} }
if (this._unavailableTimer) { if (this.unavailableTimer) {
this._unavailableTimer.abort(); this.unavailableTimer.abort();
this._unavailableTimer = null; this.unavailableTimer = null;
} }
} }
@ -65,14 +69,14 @@ class Presence {
* Get the current presence state. * Get the current presence state.
* @returns {string} the presence state (see PRESENCE enum) * @returns {string} the presence state (see PRESENCE enum)
*/ */
getState() { public getState() {
return this.state; return this.state;
} }
_onAction(payload) { private onAction = (payload: ActionPayload) => {
if (payload.action === 'user_activity') { if (payload.action === 'user_activity') {
this.setState("online"); this.setState(State.Online);
this._unavailableTimer.restart(); this.unavailableTimer.restart();
} }
} }
@ -81,13 +85,11 @@ class Presence {
* If the state has changed, the homeserver will be notified. * If the state has changed, the homeserver will be notified.
* @param {string} newState the new presence state (see PRESENCE enum) * @param {string} newState the new presence state (see PRESENCE enum)
*/ */
async setState(newState) { private async setState(newState: State) {
if (newState === this.state) { if (newState === this.state) {
return; return;
} }
if (PRESENCE_STATES.indexOf(newState) === -1) {
throw new Error("Bad presence state: " + newState);
}
const oldState = this.state; const oldState = this.state;
this.state = newState; this.state = newState;

View file

@ -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 See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { _t } from './languageHandler'; import { _t } from './languageHandler';
export function levelRoleMap(usersDefault) { export function levelRoleMap(usersDefault: number) {
return { return {
undefined: _t('Default'), undefined: _t('Default'),
0: _t('Restricted'), 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); const LEVEL_ROLE_MAP = levelRoleMap(usersDefault);
if (LEVEL_ROLE_MAP[level]) { if (LEVEL_ROLE_MAP[level]) {
return LEVEL_ROLE_MAP[level]; return LEVEL_ROLE_MAP[level];

View file

@ -22,11 +22,12 @@ import {MatrixClientPeg} from './MatrixClientPeg';
import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase';
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
import { _t } from './languageHandler'; 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 { isSecureBackupRequired } from './utils/WellKnownUtils';
import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog'; import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog';
import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog'; import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
import SettingsStore from "./settings/SettingsStore"; 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 // 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 // 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) { if (nonInteractive) {
throw new Error("Could not unlock non-interactively"); throw new Error("Could not unlock non-interactively");
} }
@ -158,6 +166,12 @@ export async function getDehydrationKey(
keyInfo: ISecretStorageKeyInfo, keyInfo: ISecretStorageKeyInfo,
checkFunc: (Uint8Array) => void, checkFunc: (Uint8Array) => void,
): Promise<Uint8Array> { ): Promise<Uint8Array> {
const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
if (keyFromCustomisations) {
console.log("Using key from security customisations (dehydration)")
return keyFromCustomisations;
}
const inputToKey = makeInputToKey(keyInfo); const inputToKey = makeInputToKey(keyInfo);
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
AccessSecretStorageDialog, AccessSecretStorageDialog,
@ -352,14 +366,19 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
} }
console.log("Setting dehydration key"); console.log("Setting dehydration key");
await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo, "Backup device"); await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo, "Backup device");
} else if (!keyId) {
console.warn("Not setting dehydration key: no SSSS key found");
} else { } 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 // `return await` needed here to ensure `finally` block runs after the
// inner operation completes. // inner operation completes.
return await func(); return await func();
} catch (e) {
SecurityCustomisations.catchAccessSecretStorageError?.(e);
console.error(e);
} finally { } finally {
// Clear secret storage key cache now that work is complete // Clear secret storage key cache now that work is complete
secretStorageBeingAccessed = false; secretStorageBeingAccessed = false;

View file

@ -198,59 +198,30 @@ function textForRelatedGroupsEvent(ev) {
function textForServerACLEvent(ev) { function textForServerACLEvent(ev) {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const prevContent = ev.getPrevContent(); const prevContent = ev.getPrevContent();
const changes = [];
const current = ev.getContent(); const current = ev.getContent();
const prev = { const prev = {
deny: Array.isArray(prevContent.deny) ? prevContent.deny : [], deny: Array.isArray(prevContent.deny) ? prevContent.deny : [],
allow: Array.isArray(prevContent.allow) ? prevContent.allow : [], allow: Array.isArray(prevContent.allow) ? prevContent.allow : [],
allow_ip_literals: !(prevContent.allow_ip_literals === false), allow_ip_literals: !(prevContent.allow_ip_literals === false),
}; };
let text = ""; let text = "";
if (prev.deny.length === 0 && prev.allow.length === 0) { 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 { } 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)) { if (!Array.isArray(current.allow)) {
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) { 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)) { return text;
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(" ");
} }
function textForMessageEvent(ev) { function textForMessageEvent(ev) {
@ -329,14 +300,27 @@ function textForCallHangupEvent(event) {
reason = _t('(not supported by this browser)'); reason = _t('(not supported by this browser)');
} else if (eventContent.reason) { } else if (eventContent.reason) {
if (eventContent.reason === "ice_failed") { if (eventContent.reason === "ice_failed") {
// We couldn't establish a connection at all
reason = _t('(could not connect media)'); 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") { } else if (eventContent.reason === "invite_timeout") {
reason = _t('(no answer)'); 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 // workaround for https://github.com/vector-im/element-web/issues/5178
// it seems Android randomly sets a reason of "user hangup" which is // it seems Android randomly sets a reason of "user hangup" which is
// interpreted as an error code :( // interpreted as an error code :(
// https://github.com/vector-im/riot-android/issues/2623 // https://github.com/vector-im/riot-android/issues/2623
// Also the correct hangup code as of VoIP v1 (with underscore)
reason = ''; reason = '';
} else { } else {
reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason}); 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; 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) { function textForCallInviteEvent(event) {
const senderName = event.sender ? event.sender.name : _t('Someone'); const senderName = event.sender ? event.sender.name : _t('Someone');
// FIXME: Find a better way to determine this from the event? // FIXME: Find a better way to determine this from the event?
@ -574,6 +563,7 @@ const handlers = {
'm.call.invite': textForCallInviteEvent, 'm.call.invite': textForCallInviteEvent,
'm.call.answer': textForCallAnswerEvent, 'm.call.answer': textForCallAnswerEvent,
'm.call.hangup': textForCallHangupEvent, 'm.call.hangup': textForCallHangupEvent,
'm.call.reject': textForCallRejectEvent,
}; };
const stateHandlers = { const stateHandlers = {

View file

@ -38,26 +38,23 @@ const RECENTLY_ACTIVE_THRESHOLD_MS = 2 * 60 * 1000;
* see doc on the userActive* functions for what these mean. * see doc on the userActive* functions for what these mean.
*/ */
export default class UserActivity { export default class UserActivity {
constructor(windowObj, documentObj) { private readonly activeNowTimeout: Timer;
this._window = windowObj; private readonly activeRecentlyTimeout: Timer;
this._document = documentObj; private attachedActiveNowTimers: Timer[] = [];
private attachedActiveRecentlyTimers: Timer[] = [];
private lastScreenX = 0;
private lastScreenY = 0;
this._attachedActiveNowTimers = []; constructor(private readonly window: Window, private readonly document: Document) {
this._attachedActiveRecentlyTimers = []; this.activeNowTimeout = new Timer(CURRENTLY_ACTIVE_THRESHOLD_MS);
this._activeNowTimeout = new Timer(CURRENTLY_ACTIVE_THRESHOLD_MS); this.activeRecentlyTimeout = new Timer(RECENTLY_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;
} }
static sharedInstance() { static sharedInstance() {
if (global.mxUserActivity === undefined) { if (window.mxUserActivity === undefined) {
global.mxUserActivity = new UserActivity(window, document); 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. * later on when the user does become active.
* @param {Timer} timer the timer to use * @param {Timer} timer the timer to use
*/ */
timeWhileActiveNow(timer) { public timeWhileActiveNow(timer: Timer) {
this._timeWhile(timer, this._attachedActiveNowTimers); this.timeWhile(timer, this.attachedActiveNowTimers);
if (this.userActiveNow()) { if (this.userActiveNow()) {
timer.start(); timer.start();
} }
@ -85,14 +82,14 @@ export default class UserActivity {
* later on when the user does become active. * later on when the user does become active.
* @param {Timer} timer the timer to use * @param {Timer} timer the timer to use
*/ */
timeWhileActiveRecently(timer) { public timeWhileActiveRecently(timer: Timer) {
this._timeWhile(timer, this._attachedActiveRecentlyTimers); this.timeWhile(timer, this.attachedActiveRecentlyTimers);
if (this.userActiveRecently()) { if (this.userActiveRecently()) {
timer.start(); timer.start();
} }
} }
_timeWhile(timer, attachedTimers) { private timeWhile(timer: Timer, attachedTimers: Timer[]) {
// important this happens first // important this happens first
const index = attachedTimers.indexOf(timer); const index = attachedTimers.indexOf(timer);
if (index === -1) { if (index === -1) {
@ -112,36 +109,36 @@ export default class UserActivity {
/** /**
* Start listening to user activity * Start listening to user activity
*/ */
start() { public start() {
this._document.addEventListener('mousedown', this._onUserActivity); this.document.addEventListener('mousedown', this.onUserActivity);
this._document.addEventListener('mousemove', this._onUserActivity); this.document.addEventListener('mousemove', this.onUserActivity);
this._document.addEventListener('keydown', this._onUserActivity); this.document.addEventListener('keydown', this.onUserActivity);
this._document.addEventListener("visibilitychange", this._onPageVisibilityChanged); this.document.addEventListener("visibilitychange", this.onPageVisibilityChanged);
this._window.addEventListener("blur", this._onWindowBlurred); this.window.addEventListener("blur", this.onWindowBlurred);
this._window.addEventListener("focus", this._onUserActivity); this.window.addEventListener("focus", this.onUserActivity);
// can't use document.scroll here because that's only the document // can't use document.scroll here because that's only the document
// itself being scrolled. Need to use addEventListener's useCapture. // itself being scrolled. Need to use addEventListener's useCapture.
// also this needs to be the wheel event, not scroll, as scroll is // also this needs to be the wheel event, not scroll, as scroll is
// fired when the view scrolls down for a new message. // fired when the view scrolls down for a new message.
this._window.addEventListener('wheel', this._onUserActivity, { this.window.addEventListener('wheel', this.onUserActivity, {
passive: true, capture: true, passive: true,
capture: true,
}); });
} }
/** /**
* Stop tracking user activity * Stop tracking user activity
*/ */
stop() { public stop() {
this._document.removeEventListener('mousedown', this._onUserActivity); this.document.removeEventListener('mousedown', this.onUserActivity);
this._document.removeEventListener('mousemove', this._onUserActivity); this.document.removeEventListener('mousemove', this.onUserActivity);
this._document.removeEventListener('keydown', this._onUserActivity); this.document.removeEventListener('keydown', this.onUserActivity);
this._window.removeEventListener('wheel', this._onUserActivity, { this.window.removeEventListener('wheel', this.onUserActivity, {
passive: true, capture: true, capture: true,
}); });
this.document.removeEventListener("visibilitychange", this.onPageVisibilityChanged);
this._document.removeEventListener("visibilitychange", this._onPageVisibilityChanged); this.window.removeEventListener("blur", this.onWindowBlurred);
this._window.removeEventListener("blur", this._onWindowBlurred); this.window.removeEventListener("focus", this.onUserActivity);
this._window.removeEventListener("focus", this._onUserActivity);
} }
/** /**
@ -151,8 +148,8 @@ export default class UserActivity {
* user's attention at any given moment. * user's attention at any given moment.
* @returns {boolean} true if user is currently 'active' * @returns {boolean} true if user is currently 'active'
*/ */
userActiveNow() { public userActiveNow() {
return this._activeNowTimeout.isRunning(); 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). * (or they may have gone to make tea and left the window focused).
* @returns {boolean} true if user has been active recently * @returns {boolean} true if user has been active recently
*/ */
userActiveRecently() { public userActiveRecently() {
return this._activeRecentlyTimeout.isRunning(); return this.activeRecentlyTimeout.isRunning();
} }
_onPageVisibilityChanged(e) { private onPageVisibilityChanged = e => {
if (this._document.visibilityState === "hidden") { if (this.document.visibilityState === "hidden") {
this._activeNowTimeout.abort(); this.activeNowTimeout.abort();
this._activeRecentlyTimeout.abort(); this.activeRecentlyTimeout.abort();
} else { } else {
this._onUserActivity(e); this.onUserActivity(e);
} }
} };
_onWindowBlurred() { private onWindowBlurred = () => {
this._activeNowTimeout.abort(); this.activeNowTimeout.abort();
this._activeRecentlyTimeout.abort(); this.activeRecentlyTimeout.abort();
} };
_onUserActivity(event) { private onUserActivity = (event: MouseEvent) => {
// ignore anything if the window isn't focused // 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 && event.type === "mousemove") {
if (event.screenX === this.lastScreenX && event.screenY === this.lastScreenY) { if (event.screenX === this.lastScreenX && event.screenY === this.lastScreenY) {
@ -195,25 +192,25 @@ export default class UserActivity {
} }
dis.dispatch({action: 'user_activity'}); dis.dispatch({action: 'user_activity'});
if (!this._activeNowTimeout.isRunning()) { if (!this.activeNowTimeout.isRunning()) {
this._activeNowTimeout.start(); this.activeNowTimeout.start();
dis.dispatch({action: 'user_activity_start'}); dis.dispatch({action: 'user_activity_start'});
this._runTimersUntilTimeout(this._attachedActiveNowTimers, this._activeNowTimeout); UserActivity.runTimersUntilTimeout(this.attachedActiveNowTimers, this.activeNowTimeout);
} else { } else {
this._activeNowTimeout.restart(); this.activeNowTimeout.restart();
} }
if (!this._activeRecentlyTimeout.isRunning()) { if (!this.activeRecentlyTimeout.isRunning()) {
this._activeRecentlyTimeout.start(); this.activeRecentlyTimeout.start();
this._runTimersUntilTimeout(this._attachedActiveRecentlyTimers, this._activeRecentlyTimeout); UserActivity.runTimersUntilTimeout(this.attachedActiveRecentlyTimers, this.activeRecentlyTimeout);
} else { } else {
this._activeRecentlyTimeout.restart(); this.activeRecentlyTimeout.restart();
} }
} };
async _runTimersUntilTimeout(attachedTimers, timeout) { private static async runTimersUntilTimeout(attachedTimers: Timer[], timeout: Timer) {
attachedTimers.forEach((t) => t.start()); attachedTimers.forEach((t) => t.start());
try { try {
await timeout.finished(); await timeout.finished();

View file

@ -14,19 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License. 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 {MatrixClientPeg} from "./MatrixClientPeg";
import { _t } from './languageHandler'; import { _t } from './languageHandler';
export function usersTypingApartFromMeAndIgnored(room) { export function usersTypingApartFromMeAndIgnored(room: Room): RoomMember[] {
return usersTyping( return usersTyping(room, [MatrixClientPeg.get().getUserId()].concat(MatrixClientPeg.get().getIgnoredUsers()));
room, [MatrixClientPeg.get().credentials.userId].concat(MatrixClientPeg.get().getIgnoredUsers()),
);
} }
export function usersTypingApartFromMe(room) { export function usersTypingApartFromMe(room: Room): RoomMember[] {
return usersTyping( return usersTyping(room, [MatrixClientPeg.get().getUserId()]);
room, [MatrixClientPeg.get().credentials.userId],
);
} }
/** /**
@ -34,15 +33,11 @@ export function usersTypingApartFromMe(room) {
* to exclude, return a list of user objects who are typing. * to exclude, return a list of user objects who are typing.
* @param {Room} room: room object to get users from. * @param {Room} room: room object to get users from.
* @param {string[]} exclude: list of user mxids to exclude. * @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 = []; const whoIsTyping = [];
if (exclude === undefined) {
exclude = [];
}
const memberKeys = Object.keys(room.currentState.members); const memberKeys = Object.keys(room.currentState.members);
for (let i = 0; i < memberKeys.length; ++i) { for (let i = 0; i < memberKeys.length; ++i) {
const userId = memberKeys[i]; const userId = memberKeys[i];
@ -57,20 +52,21 @@ export function usersTyping(room, exclude) {
return whoIsTyping; return whoIsTyping;
} }
export function whoIsTypingString(whoIsTyping, limit) { export function whoIsTypingString(whoIsTyping: RoomMember[], limit: number): string {
let othersCount = 0; let othersCount = 0;
if (whoIsTyping.length > limit) { if (whoIsTyping.length > limit) {
othersCount = whoIsTyping.length - limit + 1; othersCount = whoIsTyping.length - limit + 1;
} }
if (whoIsTyping.length === 0) { if (whoIsTyping.length === 0) {
return ''; return '';
} else if (whoIsTyping.length === 1) { } else if (whoIsTyping.length === 1) {
return _t('%(displayName)s is typing …', {displayName: whoIsTyping[0].name}); return _t('%(displayName)s is typing …', {displayName: whoIsTyping[0].name});
} }
const names = whoIsTyping.map(function(m) {
return m.name; const names = whoIsTyping.map(m => m.name);
});
if (othersCount>=1) { if (othersCount >= 1) {
return _t('%(names)s and %(count)s others are typing …', { return _t('%(names)s and %(count)s others are typing …', {
names: names.slice(0, limit - 1).join(', '), names: names.slice(0, limit - 1).join(', '),
count: othersCount, count: othersCount,

View file

@ -17,14 +17,14 @@ limitations under the License.
import Analytics from '../Analytics'; import Analytics from '../Analytics';
import { asyncAction } from './actionCreators'; import { asyncAction } from './actionCreators';
import TagOrderStore from '../stores/TagOrderStore'; import GroupFilterOrderStore from '../stores/GroupFilterOrderStore';
import { AsyncActionPayload } from "../dispatcher/payloads"; import { AsyncActionPayload } from "../dispatcher/payloads";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
export default class TagOrderActions { export default class TagOrderActions {
/** /**
* Creates an action thunk that will do an asynchronous request to * 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 * @param {MatrixClient} matrixClient the matrix client to set the
* account data on. * account data on.
@ -36,8 +36,8 @@ export default class TagOrderActions {
*/ */
public static moveTag(matrixClient: MatrixClient, tag: string, destinationIx: number): AsyncActionPayload { public static moveTag(matrixClient: MatrixClient, tag: string, destinationIx: number): AsyncActionPayload {
// Only commit tags if the state is ready, i.e. not null // Only commit tags if the state is ready, i.e. not null
let tags = TagOrderStore.getOrderedTags(); let tags = GroupFilterOrderStore.getOrderedTags();
let removedTags = TagOrderStore.getRemovedTagsAccountData() || []; let removedTags = GroupFilterOrderStore.getRemovedTagsAccountData() || [];
if (!tags) { if (!tags) {
return; return;
} }
@ -47,7 +47,7 @@ export default class TagOrderActions {
removedTags = removedTags.filter((t) => t !== tag); removedTags = removedTags.filter((t) => t !== tag);
const storeId = TagOrderStore.getStoreId(); const storeId = GroupFilterOrderStore.getStoreId();
return asyncAction('TagOrderActions.moveTag', () => { return asyncAction('TagOrderActions.moveTag', () => {
Analytics.trackEvent('TagOrderActions', 'commitTagOrdering'); Analytics.trackEvent('TagOrderActions', 'commitTagOrdering');
@ -83,8 +83,8 @@ export default class TagOrderActions {
*/ */
public static removeTag(matrixClient: MatrixClient, tag: string): AsyncActionPayload { public static removeTag(matrixClient: MatrixClient, tag: string): AsyncActionPayload {
// Don't change tags, just removedTags // Don't change tags, just removedTags
const tags = TagOrderStore.getOrderedTags(); const tags = GroupFilterOrderStore.getOrderedTags();
const removedTags = TagOrderStore.getRemovedTagsAccountData() || []; const removedTags = GroupFilterOrderStore.getRemovedTagsAccountData() || [];
if (removedTags.includes(tag)) { if (removedTags.includes(tag)) {
// Return a thunk that doesn't do anything, we don't even need // Return a thunk that doesn't do anything, we don't even need
@ -94,7 +94,7 @@ export default class TagOrderActions {
removedTags.push(tag); removedTags.push(tag);
const storeId = TagOrderStore.getStoreId(); const storeId = GroupFilterOrderStore.getStoreId();
return asyncAction('TagOrderActions.removeTag', () => { return asyncAction('TagOrderActions.removeTag', () => {
Analytics.trackEvent('TagOrderActions', 'removeTag'); Analytics.trackEvent('TagOrderActions', 'removeTag');

View file

@ -32,6 +32,7 @@ import DialogButtons from "../../../../components/views/elements/DialogButtons";
import InlineSpinner from "../../../../components/views/elements/InlineSpinner"; import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog"; import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils'; import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils';
import SecurityCustomisations from "../../../../customisations/Security";
const PHASE_LOADING = 0; const PHASE_LOADING = 0;
const PHASE_LOADERROR = 1; const PHASE_LOADERROR = 1;
@ -99,7 +100,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this._passphraseField = createRef(); this._passphraseField = createRef();
this._fetchBackupInfo(); MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
if (this.state.accountPassword) { if (this.state.accountPassword) {
// If we have an account password in memory, let's simplify and // If we have an account password in memory, let's simplify and
// assume it means password auth is also supported for device // assume it means password auth is also supported for device
@ -110,13 +112,27 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this._queryKeyUploadAuth(); this._queryKeyUploadAuth();
} }
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange); this._getInitialPhase();
} }
componentWillUnmount() { componentWillUnmount() {
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange); 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() { async _fetchBackupInfo() {
try { try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();

View file

@ -16,7 +16,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import TagOrderStore from '../../stores/TagOrderStore'; import GroupFilterOrderStore from '../../stores/GroupFilterOrderStore';
import GroupActions from '../../actions/GroupActions'; import GroupActions from '../../actions/GroupActions';
@ -31,7 +31,7 @@ import AutoHideScrollbar from "./AutoHideScrollbar";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import UserTagTile from "../views/elements/UserTagTile"; import UserTagTile from "../views/elements/UserTagTile";
class TagPanel extends React.Component { class GroupFilterPanel extends React.Component {
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
state = { state = {
@ -44,13 +44,13 @@ class TagPanel extends React.Component {
this.context.on("Group.myMembership", this._onGroupMyMembership); this.context.on("Group.myMembership", this._onGroupMyMembership);
this.context.on("sync", this._onClientSync); this.context.on("sync", this._onClientSync);
this._tagOrderStoreToken = TagOrderStore.addListener(() => { this._groupFilterOrderStoreToken = GroupFilterOrderStore.addListener(() => {
if (this.unmounted) { if (this.unmounted) {
return; return;
} }
this.setState({ this.setState({
orderedTags: TagOrderStore.getOrderedTags() || [], orderedTags: GroupFilterOrderStore.getOrderedTags() || [],
selectedTags: TagOrderStore.getSelectedTags(), selectedTags: GroupFilterOrderStore.getSelectedTags(),
}); });
}); });
// This could be done by anything with a matrix client // This could be done by anything with a matrix client
@ -61,8 +61,8 @@ class TagPanel extends React.Component {
this.unmounted = true; this.unmounted = true;
this.context.removeListener("Group.myMembership", this._onGroupMyMembership); this.context.removeListener("Group.myMembership", this._onGroupMyMembership);
this.context.removeListener("sync", this._onClientSync); this.context.removeListener("sync", this._onClientSync);
if (this._tagOrderStoreToken) { if (this._groupFilterOrderStoreToken) {
this._tagOrderStoreToken.remove(); this._groupFilterOrderStoreToken.remove();
} }
} }
@ -98,7 +98,7 @@ class TagPanel extends React.Component {
return ( return (
<div> <div>
<UserTagTile /> <UserTagTile />
<hr className="mx_TagPanel_divider" /> <hr className="mx_GroupFilterPanel_divider" />
</div> </div>
); );
} }
@ -117,8 +117,8 @@ class TagPanel extends React.Component {
}); });
const itemsSelected = this.state.selectedTags.length > 0; const itemsSelected = this.state.selectedTags.length > 0;
const classes = classNames('mx_TagPanel', { const classes = classNames('mx_GroupFilterPanel', {
mx_TagPanel_items_selected: itemsSelected, mx_GroupFilterPanel_items_selected: itemsSelected,
}); });
let createButton = ( let createButton = (
@ -141,7 +141,7 @@ class TagPanel extends React.Component {
return <div className={classes} onClick={this.onClearFilterClick}> return <div className={classes} onClick={this.onClearFilterClick}>
<AutoHideScrollbar <AutoHideScrollbar
className="mx_TagPanel_scroller" className="mx_GroupFilterPanel_scroller"
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273 // XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
// instead of onClick. Otherwise we experience https://github.com/vector-im/element-web/issues/6253 // instead of onClick. Otherwise we experience https://github.com/vector-im/element-web/issues/6253
onMouseDown={this.onMouseDown} onMouseDown={this.onMouseDown}
@ -152,7 +152,7 @@ class TagPanel extends React.Component {
> >
{ (provided, snapshot) => ( { (provided, snapshot) => (
<div <div
className="mx_TagPanel_tagTileContainer" className="mx_GroupFilterPanel_tagTileContainer"
ref={provided.innerRef} ref={provided.innerRef}
> >
{ this.renderGlobalIcon() } { this.renderGlobalIcon() }
@ -168,4 +168,4 @@ class TagPanel extends React.Component {
</div>; </div>;
} }
} }
export default TagPanel; export default GroupFilterPanel;

View file

@ -620,7 +620,7 @@ export default class GroupView extends React.Component {
profileForm: newProfileForm, profileForm: newProfileForm,
// Indicate that FlairStore needs to be poked to show this change // 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, avatarChanged: true,
}); });
}).catch((e) => { }).catch((e) => {
@ -649,7 +649,6 @@ export default class GroupView extends React.Component {
editing: false, editing: false,
summary: null, summary: null,
}); });
dis.dispatch({action: 'panel_disable'});
this._initGroupStore(this.props.groupId); this._initGroupStore(this.props.groupId);
if (this.state.avatarChanged) { if (this.state.avatarChanged) {
@ -870,10 +869,7 @@ export default class GroupView extends React.Component {
{ _t('Add rooms to this community') } { _t('Add rooms to this community') }
</div> </div>
</AccessibleButton>) : <div />; </AccessibleButton>) : <div />;
const roomDetailListClassName = classnames({
"mx_fadable": true,
"mx_fadable_faded": this.state.editing,
});
return <div className="mx_GroupView_rooms"> return <div className="mx_GroupView_rooms">
<div className="mx_GroupView_rooms_header"> <div className="mx_GroupView_rooms_header">
<h3> <h3>
@ -884,9 +880,7 @@ export default class GroupView extends React.Component {
</div> </div>
{ this.state.groupRoomsLoading ? { this.state.groupRoomsLoading ?
<Spinner /> : <Spinner /> :
<RoomDetailList <RoomDetailList rooms={this.state.groupRooms} />
rooms={this.state.groupRooms}
className={roomDetailListClassName} />
} }
</div>; </div>;
} }

View file

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

View file

@ -71,9 +71,6 @@ interface IProps {
viaServers?: string[]; viaServers?: string[];
hideToSRUsers: boolean; hideToSRUsers: boolean;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
middleDisabled: boolean;
leftDisabled: boolean;
rightDisabled: boolean;
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
page_type: string; page_type: string;
autoJoin: boolean; autoJoin: boolean;
@ -100,10 +97,6 @@ interface IUsageLimit {
} }
interface IState { interface IState {
mouseDown?: {
x: number;
y: number;
};
syncErrorData?: { syncErrorData?: {
error: { error: {
data: IUsageLimit; data: IUsageLimit;
@ -151,7 +144,6 @@ class LoggedInView extends React.Component<IProps, IState> {
super(props, context); super(props, context);
this.state = { this.state = {
mouseDown: undefined,
syncErrorData: undefined, syncErrorData: undefined,
// use compact timeline view // use compact timeline view
useCompactLayout: SettingsStore.getValue('useCompactLayout'), useCompactLayout: SettingsStore.getValue('useCompactLayout'),
@ -517,8 +509,8 @@ class LoggedInView extends React.Component<IProps, IState> {
// Could be "GroupTile +groupId:domain" // Could be "GroupTile +groupId:domain"
const draggableId = result.draggableId.split(' ').pop(); const draggableId = result.draggableId.split(' ').pop();
// Dispatch synchronously so that the TagPanel receives an // Dispatch synchronously so that the GroupFilterPanel receives an
// optimistic update from TagOrderStore before the previous // optimistic update from GroupFilterOrderStore before the previous
// state is shown. // state is shown.
dis.dispatch(TagOrderActions.moveTag( dis.dispatch(TagOrderActions.moveTag(
this._matrixClient, this._matrixClient,
@ -549,48 +541,6 @@ class LoggedInView extends React.Component<IProps, IState> {
), true); ), true);
}; };
_onMouseDown = (ev) => {
// When the panels are disabled, clicking on them results in a mouse event
// which bubbles to certain elements in the tree. When this happens, close
// any settings page that is currently open (user/room/group).
if (this.props.leftDisabled && this.props.rightDisabled) {
const targetClasses = new Set(ev.target.className.split(' '));
if (
targetClasses.has('mx_MatrixChat') ||
targetClasses.has('mx_MatrixChat_middlePanel') ||
targetClasses.has('mx_RoomView')
) {
this.setState({
mouseDown: {
x: ev.pageX,
y: ev.pageY,
},
});
}
}
};
_onMouseUp = (ev) => {
if (!this.state.mouseDown) return;
const deltaX = ev.pageX - this.state.mouseDown.x;
const deltaY = ev.pageY - this.state.mouseDown.y;
const distance = Math.sqrt((deltaX * deltaX) + (deltaY + deltaY));
const maxRadius = 5; // People shouldn't be straying too far, hopefully
// Note: we track how far the user moved their mouse to help
// combat against https://github.com/vector-im/element-web/issues/7158
if (distance < maxRadius) {
// This is probably a real click, and not a drag
dis.dispatch({ action: 'close_settings' });
}
// Always clear the mouseDown state to ensure we don't accidentally
// use stale values due to the mouseDown checks.
this.setState({mouseDown: null});
};
render() { render() {
const RoomView = sdk.getComponent('structures.RoomView'); const RoomView = sdk.getComponent('structures.RoomView');
const UserView = sdk.getComponent('structures.UserView'); const UserView = sdk.getComponent('structures.UserView');
@ -610,7 +560,6 @@ class LoggedInView extends React.Component<IProps, IState> {
oobData={this.props.roomOobData} oobData={this.props.roomOobData}
viaServers={this.props.viaServers} viaServers={this.props.viaServers}
key={this.props.currentRoomId || 'roomview'} key={this.props.currentRoomId || 'roomview'}
disabled={this.props.middleDisabled}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
/>; />;
break; break;
@ -658,8 +607,6 @@ class LoggedInView extends React.Component<IProps, IState> {
onKeyDown={this._onReactKeyDown} onKeyDown={this._onReactKeyDown}
className='mx_MatrixChat_wrapper' className='mx_MatrixChat_wrapper'
aria-hidden={this.props.hideToSRUsers} aria-hidden={this.props.hideToSRUsers}
onMouseDown={this._onMouseDown}
onMouseUp={this._onMouseUp}
> >
<ToastContainer /> <ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}> <DragDropContext onDragEnd={this._onDragEnd}>

View file

@ -181,9 +181,6 @@ interface IState {
currentUserId?: string; currentUserId?: string;
// this is persisted as mx_lhs_size, loaded in LoggedInView // this is persisted as mx_lhs_size, loaded in LoggedInView
collapseLhs: boolean; collapseLhs: boolean;
leftDisabled: boolean;
middleDisabled: boolean;
// the right panel's disabled state is tracked in its store.
// Parameters used in the registration dance with the IS // Parameters used in the registration dance with the IS
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
register_client_secret?: string; register_client_secret?: string;
@ -236,8 +233,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.state = { this.state = {
view: Views.LOADING, view: Views.LOADING,
collapseLhs: false, collapseLhs: false,
leftDisabled: false,
middleDisabled: false,
hideToSRUsers: false, hideToSRUsers: false,
@ -710,14 +705,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.state.resizeNotifier.notifyLeftHandleResized(); this.state.resizeNotifier.notifyLeftHandleResized();
}); });
break; break;
case 'panel_disable': {
this.setState({
leftDisabled: payload.leftDisabled || payload.sideDisabled || false,
middleDisabled: payload.middleDisabled || false,
// We don't track the right panel being disabled here - it's tracked in the store.
});
break;
}
case 'on_logged_in': case 'on_logged_in':
if ( if (
!Lifecycle.isSoftLogout() && !Lifecycle.isSoftLogout() &&

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames';
import {Room} from "matrix-js-sdk/src/models/room"; import {Room} from "matrix-js-sdk/src/models/room";
import * as sdk from '../../index'; import * as sdk from '../../index';
@ -304,14 +303,8 @@ export default class RightPanel extends React.Component {
break; break;
} }
const classes = classNames("mx_RightPanel", "mx_fadable", {
"collapsed": this.props.collapsed,
"mx_fadable_faded": this.props.disabled,
"dark-panel": true,
});
return ( return (
<aside className={classes} id="mx_RightPanel"> <aside className="mx_RightPanel dark-panel" id="mx_RightPanel">
{ panel } { panel }
</aside> </aside>
); );

View file

@ -30,7 +30,7 @@ import Analytics from '../../Analytics';
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import {ALL_ROOMS} from "../views/directory/NetworkDropdown"; import {ALL_ROOMS} from "../views/directory/NetworkDropdown";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import TagOrderStore from "../../stores/TagOrderStore"; import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
import GroupStore from "../../stores/GroupStore"; import GroupStore from "../../stores/GroupStore";
import FlairStore from "../../stores/FlairStore"; import FlairStore from "../../stores/FlairStore";
@ -49,7 +49,7 @@ export default class RoomDirectory extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const selectedCommunityId = TagOrderStore.getSelectedTags()[0]; const selectedCommunityId = GroupFilterOrderStore.getSelectedTags()[0];
this.state = { this.state = {
publicRooms: [], publicRooms: [],
loading: true, loading: true,

View file

@ -107,7 +107,6 @@ interface IProps {
viaServers?: string[]; viaServers?: string[];
autoJoin?: boolean; autoJoin?: boolean;
disabled?: boolean;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
// Called with the credentials of a registered user (if they were a ROU that transitioned to PWLU) // Called with the credentials of a registered user (if they were a ROU that transitioned to PWLU)
@ -1897,7 +1896,6 @@ export default class RoomView extends React.Component<IProps, IState> {
<MessageComposer <MessageComposer
room={this.state.room} room={this.state.room}
callState={this.state.callState} callState={this.state.callState}
disabled={this.props.disabled}
showApps={this.state.showApps} showApps={this.state.showApps}
e2eStatus={this.state.e2eStatus} e2eStatus={this.state.e2eStatus}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
@ -2053,10 +2051,6 @@ export default class RoomView extends React.Component<IProps, IState> {
"mx_RoomView_statusArea_expanded": isStatusAreaExpanded, "mx_RoomView_statusArea_expanded": isStatusAreaExpanded,
}); });
const fadableSectionClasses = classNames("mx_RoomView_body", "mx_fadable", {
"mx_fadable_faded": this.props.disabled,
});
const showRightPanel = this.state.room && this.state.showRightPanel; const showRightPanel = this.state.room && this.state.showRightPanel;
const rightPanel = showRightPanel const rightPanel = showRightPanel
? <RightPanel room={this.state.room} resizeNotifier={this.props.resizeNotifier} /> ? <RightPanel room={this.state.room} resizeNotifier={this.props.resizeNotifier} />
@ -2090,7 +2084,7 @@ export default class RoomView extends React.Component<IProps, IState> {
appsShown={this.state.showApps} appsShown={this.state.showApps}
/> />
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}> <MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
<div className={fadableSectionClasses}> <div className="mx_RoomView_body">
{auxPanel} {auxPanel}
<div className={timelineClasses}> <div className={timelineClasses}>
{topUnreadMessagesBar} {topUnreadMessagesBar}

View file

@ -44,7 +44,7 @@ import IconizedContextMenu, {
} from "../views/context_menus/IconizedContextMenu"; } from "../views/context_menus/IconizedContextMenu";
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore"; import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
import * as fbEmitter from "fbemitter"; import * as fbEmitter from "fbemitter";
import TagOrderStore from "../../stores/TagOrderStore"; import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
import { showCommunityInviteDialog } from "../../RoomInvite"; import { showCommunityInviteDialog } from "../../RoomInvite";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
@ -87,7 +87,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
public componentDidMount() { public componentDidMount() {
this.dispatcherRef = defaultDispatcher.register(this.onAction); this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged); this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
this.tagStoreRef = TagOrderStore.addListener(this.onTagStoreUpdate); this.tagStoreRef = GroupFilterOrderStore.addListener(this.onTagStoreUpdate);
} }
public componentWillUnmount() { public componentWillUnmount() {

View file

@ -22,6 +22,7 @@ import ImageView from '../elements/ImageView';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import * as Avatar from '../../../Avatar'; import * as Avatar from '../../../Avatar';
import {ResizeMethod} from "../../../Avatar";
interface IProps { interface IProps {
// Room may be left unset here, but if it is, // Room may be left unset here, but if it is,
@ -32,7 +33,7 @@ interface IProps {
oobData?: any; oobData?: any;
width?: number; width?: number;
height?: number; height?: number;
resizeMethod?: string; resizeMethod?: ResizeMethod;
viewAvatarOnClick?: boolean; viewAvatarOnClick?: boolean;
} }

View file

@ -26,12 +26,12 @@ import * as FormattingUtils from '../../../utils/FormattingUtils';
import FlairStore from '../../../stores/FlairStore'; import FlairStore from '../../../stores/FlairStore';
import GroupStore from '../../../stores/GroupStore'; import GroupStore from '../../../stores/GroupStore';
import TagOrderStore from '../../../stores/TagOrderStore'; import GroupFilterOrderStore from '../../../stores/GroupFilterOrderStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AccessibleButton from "./AccessibleButton"; import AccessibleButton from "./AccessibleButton";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents // A class for a child of GroupFilterPanel (possibly wrapped in a DNDTagTile) that represents
// a thing to click on for the user to filter the visible rooms in the RoomList to: // a thing to click on for the user to filter the visible rooms in the RoomList to:
// - Rooms that are part of the group // - Rooms that are part of the group
// - Direct messages with members of the group // - Direct messages with members of the group
@ -142,7 +142,7 @@ export default class TagTile extends React.Component {
mx_TagTile_selected_prototype: this.props.selected && isPrototype, mx_TagTile_selected_prototype: this.props.selected && isPrototype,
}); });
const badge = TagOrderStore.getGroupBadge(this.props.tag); const badge = GroupFilterOrderStore.getGroupBadge(this.props.tag);
let badgeElement; let badgeElement;
if (badge && !this.state.hover && !this.props.menuDisplayed) { if (badge && !this.state.hover && !this.props.menuDisplayed) {
const badgeClasses = classNames({ const badgeClasses = classNames({

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from "react"; import React from "react";
import defaultDispatcher from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
import * as fbEmitter from "fbemitter"; import * as fbEmitter from "fbemitter";
import TagOrderStore from "../../../stores/TagOrderStore"; import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
import AccessibleTooltipButton from "./AccessibleTooltipButton"; import AccessibleTooltipButton from "./AccessibleTooltipButton";
import classNames from "classnames"; import classNames from "classnames";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
@ -36,12 +36,12 @@ export default class UserTagTile extends React.PureComponent<IProps, IState> {
super(props); super(props);
this.state = { this.state = {
selected: TagOrderStore.getSelectedTags().length === 0, selected: GroupFilterOrderStore.getSelectedTags().length === 0,
}; };
} }
public componentDidMount() { public componentDidMount() {
this.tagStoreRef = TagOrderStore.addListener(this.onTagStoreUpdate); this.tagStoreRef = GroupFilterOrderStore.addListener(this.onTagStoreUpdate);
} }
public componentWillUnmount() { public componentWillUnmount() {
@ -49,7 +49,7 @@ export default class UserTagTile extends React.PureComponent<IProps, IState> {
} }
private onTagStoreUpdate = () => { private onTagStoreUpdate = () => {
const selected = TagOrderStore.getSelectedTags().length === 0; const selected = GroupFilterOrderStore.getSelectedTags().length === 0;
this.setState({selected}); this.setState({selected});
}; };

View file

@ -45,7 +45,7 @@ export default class RoomCreate extends React.Component {
render() { render() {
const predecessor = this.props.mxEvent.getContent()['predecessor']; const predecessor = this.props.mxEvent.getContent()['predecessor'];
if (predecessor === undefined) { if (predecessor === undefined) {
return <div />; // We should never have been instaniated in this case return <div />; // We should never have been instantiated in this case
} }
const prevRoom = MatrixClientPeg.get().getRoom(predecessor['room_id']); const prevRoom = MatrixClientPeg.get().getRoom(predecessor['room_id']);
const permalinkCreator = new RoomPermalinkCreator(prevRoom, predecessor['room_id']); const permalinkCreator = new RoomPermalinkCreator(prevRoom, predecessor['room_id']);

View file

@ -31,7 +31,7 @@ interface IProps {
// The badge to display above the icon // The badge to display above the icon
badge?: React.ReactNode; badge?: React.ReactNode;
// The parameters to track the click event // The parameters to track the click event
analytics: string[]; analytics: Parameters<typeof Analytics.trackEvent>;
// Button name // Button name
name: string; name: string;

View file

@ -801,6 +801,11 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
} = powerLevels; } = powerLevels;
const me = room.getMember(cli.getUserId()); const me = room.getMember(cli.getUserId());
if (!me) {
// we aren't in the room, so return no admin tooling
return <div />;
}
const isMe = me.userId === member.userId; const isMe = me.userId === member.userId;
const canAffectUser = member.powerLevel < me.powerLevel || isMe; const canAffectUser = member.powerLevel < me.powerLevel || isMe;

View file

@ -46,6 +46,7 @@ const eventTileTypes = {
'm.call.invite': 'messages.TextualEvent', 'm.call.invite': 'messages.TextualEvent',
'm.call.answer': 'messages.TextualEvent', 'm.call.answer': 'messages.TextualEvent',
'm.call.hangup': 'messages.TextualEvent', 'm.call.hangup': 'messages.TextualEvent',
'm.call.reject': 'messages.TextualEvent',
}; };
const stateEventTileTypes = { const stateEventTileTypes = {
@ -657,8 +658,7 @@ export default class EventTile extends React.Component {
// source tile when there's no regular tile for an event and also for // source tile when there's no regular tile for an event and also for
// replace relations (which otherwise would display as a confusing // replace relations (which otherwise would display as a confusing
// duplicate of the thing they are replacing). // duplicate of the thing they are replacing).
const useSource = !tileHandler || this.props.mxEvent.isRelation("m.replace"); if (SettingsStore.getValue("showHiddenEventsInTimeline") && !haveTileForEvent(this.props.mxEvent)) {
if (useSource && SettingsStore.getValue("showHiddenEventsInTimeline")) {
tileHandler = "messages.ViewSourceEvent"; tileHandler = "messages.ViewSourceEvent";
// Reuse info message avatar and sender profile styling // Reuse info message avatar and sender profile styling
isInfoMessage = true; isInfoMessage = true;

View file

@ -18,7 +18,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher';
import {Key} from '../../../Keyboard'; import {Key} from '../../../Keyboard';
@ -28,19 +27,10 @@ export default class ForwardMessage extends React.Component {
}; };
componentDidMount() { componentDidMount() {
dis.dispatch({
action: 'panel_disable',
middleDisabled: true,
});
document.addEventListener('keydown', this._onKeyDown); document.addEventListener('keydown', this._onKeyDown);
} }
componentWillUnmount() { componentWillUnmount() {
dis.dispatch({
action: 'panel_disable',
middleDisabled: false,
});
document.removeEventListener('keydown', this._onKeyDown); document.removeEventListener('keydown', this._onKeyDown);
} }

View file

@ -38,6 +38,7 @@ import WidgetUtils from "../../../utils/WidgetUtils";
import {UPDATE_EVENT} from "../../../stores/AsyncStore"; import {UPDATE_EVENT} from "../../../stores/AsyncStore";
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore"; import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
import { PlaceCallType } from "../../../CallHandler"; import { PlaceCallType } from "../../../CallHandler";
import { CallState } from 'matrix-js-sdk/src/webrtc/call';
function ComposerAvatar(props) { function ComposerAvatar(props) {
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
@ -104,8 +105,11 @@ function HangupButton(props) {
if (!call) { if (!call) {
return; return;
} }
const action = call.state === CallState.Ringing ? 'reject' : 'hangup';
dis.dispatch({ dis.dispatch({
action: 'hangup', action,
// hangup the call for this room, which may not be the room in props // hangup the call for this room, which may not be the room in props
// (e.g. conferences which will hangup the 1:1 room instead) // (e.g. conferences which will hangup the 1:1 room instead)
room_id: call.roomId, room_id: call.roomId,

View file

@ -76,7 +76,7 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
}; };
private viewRoom = (room: Room, index: number) => { private viewRoom = (room: Room, index: number) => {
Analytics.trackEvent("Breadcrumbs", "click_node", index); Analytics.trackEvent("Breadcrumbs", "click_node", String(index));
defaultDispatcher.dispatch({action: "view_room", room_id: room.roomId}); defaultDispatcher.dispatch({action: "view_room", room_id: room.roomId});
}; };

View file

@ -42,6 +42,14 @@ export default class IntegrationManager extends React.Component {
loading: false, loading: false,
}; };
constructor(props) {
super(props);
this.state = {
errored: false,
};
}
componentDidMount() { componentDidMount() {
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
document.addEventListener("keydown", this.onKeyDown); document.addEventListener("keydown", this.onKeyDown);
@ -66,6 +74,10 @@ export default class IntegrationManager extends React.Component {
} }
}; };
onError = () => {
this.setState({ errored: true });
};
render() { render() {
if (this.props.loading) { if (this.props.loading) {
const Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
@ -77,7 +89,7 @@ export default class IntegrationManager extends React.Component {
); );
} }
if (!this.props.connected) { if (!this.props.connected || this.state.errored) {
return ( return (
<div className='mx_IntegrationManager_error'> <div className='mx_IntegrationManager_error'>
<h3>{_t("Cannot connect to integration manager")}</h3> <h3>{_t("Cannot connect to integration manager")}</h3>
@ -86,6 +98,6 @@ export default class IntegrationManager extends React.Component {
); );
} }
return <iframe src={this.props.url}></iframe>; return <iframe src={this.props.url} onError={this.onError} />;
} }
} }

View file

@ -78,7 +78,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
private onRejectClick: React.MouseEventHandler = (e) => { private onRejectClick: React.MouseEventHandler = (e) => {
e.stopPropagation(); e.stopPropagation();
dis.dispatch({ dis.dispatch({
action: 'hangup', action: 'reject',
room_id: this.state.incomingCall.roomId, room_id: this.state.incomingCall.roomId,
}); });
}; };

View file

@ -0,0 +1,81 @@
/*
Copyright 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 { IMatrixClientCreds } from "../MatrixClientPeg";
import { Kind as SetupEncryptionKind } from "../toasts/SetupEncryptionToast";
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
function examineLoginResponse(
response: any,
credentials: IMatrixClientCreds,
): void {
// E.g. add additional data to the persisted credentials
}
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
function persistCredentials(
credentials: IMatrixClientCreds,
): void {
// E.g. store any additional credential fields
}
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
function createSecretStorageKey(): Uint8Array {
// E.g. generate or retrieve secret storage key somehow
return null;
}
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
function getSecretStorageKey(): Uint8Array {
// E.g. retrieve secret storage key from some other place
return null;
}
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
function catchAccessSecretStorageError(e: Error): void {
// E.g. notify the user in some way
}
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
function setupEncryptionNeeded(kind: SetupEncryptionKind): boolean {
// E.g. trigger some kind of setup
return false;
}
// This interface summarises all available customisation points and also marks
// them all as optional. This allows customisers to only define and export the
// customisations they need while still maintaining type safety.
export interface ISecurityCustomisations {
examineLoginResponse?: (
response: any,
credentials: IMatrixClientCreds,
) => void;
persistCredentials?: (
credentials: IMatrixClientCreds,
) => void;
createSecretStorageKey?: () => Uint8Array,
getSecretStorageKey?: () => Uint8Array,
catchAccessSecretStorageError?: (
e: Error,
) => void,
setupEncryptionNeeded?: (
kind: SetupEncryptionKind,
) => boolean,
}
// A real customisation module will define and export one or more of the
// customisation points that make up `ISecurityCustomisations`.
export default {} as ISecurityCustomisations;

View file

@ -16,6 +16,6 @@ limitations under the License.
const EMAIL_ADDRESS_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i; const EMAIL_ADDRESS_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
export function looksValid(email) { export function looksValid(email: string): boolean {
return EMAIL_ADDRESS_REGEX.test(email); return EMAIL_ADDRESS_REGEX.test(email);
} }

View file

@ -1,26 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
export default function(dest, src) {
for (const i in src) {
if (src.hasOwnProperty(i)) {
dest[i] = src[i];
}
}
return dest;
}

View file

@ -35,8 +35,10 @@
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.", "Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
"Dismiss": "Dismiss", "Dismiss": "Dismiss",
"Call Failed": "Call Failed", "Call Failed": "Call Failed",
"Call Timeout": "Call Timeout", "Call Declined": "Call Declined",
"The other party declined the call.": "The other party declined the call.",
"The remote side failed to pick up": "The remote side failed to pick up", "The remote side failed to pick up": "The remote side failed to pick up",
"The call could not be established": "The call could not be established",
"Call failed due to misconfigured server": "Call failed due to misconfigured server", "Call failed due to misconfigured server": "Call failed due to misconfigured server",
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.", "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.",
"Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.", "Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.",
@ -242,6 +244,9 @@
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s enabled flair for %(groups)s in this room.", "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s enabled flair for %(groups)s in this room.",
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s disabled flair for %(groups)s in this room.", "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s disabled flair for %(groups)s in this room.",
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.", "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.",
"%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s set the server ACLs for this room.",
"%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s changed the server ACLs for this room.",
"🎉 All servers are banned from participating! This room can no longer be used.": "🎉 All servers are banned from participating! This room can no longer be used.",
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.", "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.",
"%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s set the main address for this room to %(address)s.", "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s set the main address for this room to %(address)s.",
"%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.", "%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.",
@ -256,9 +261,13 @@
"(not supported by this browser)": "(not supported by this browser)", "(not supported by this browser)": "(not supported by this browser)",
"%(senderName)s answered the call.": "%(senderName)s answered the call.", "%(senderName)s answered the call.": "%(senderName)s answered the call.",
"(could not connect media)": "(could not connect media)", "(could not connect media)": "(could not connect media)",
"(connection failed)": "(connection failed)",
"(their device couldn't start the camera / microphone)": "(their device couldn't start the camera / microphone)",
"(an error occurred)": "(an error occurred)",
"(no answer)": "(no answer)", "(no answer)": "(no answer)",
"(unknown failure: %(reason)s)": "(unknown failure: %(reason)s)", "(unknown failure: %(reason)s)": "(unknown failure: %(reason)s)",
"%(senderName)s ended the call.": "%(senderName)s ended the call.", "%(senderName)s ended the call.": "%(senderName)s ended the call.",
"%(senderName)s declined the call.": "%(senderName)s declined the call.",
"%(senderName)s placed a voice call.": "%(senderName)s placed a voice call.", "%(senderName)s placed a voice call.": "%(senderName)s placed a voice call.",
"%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s placed a voice call. (not supported by this browser)", "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s placed a voice call. (not supported by this browser)",
"%(senderName)s placed a video call.": "%(senderName)s placed a video call.", "%(senderName)s placed a video call.": "%(senderName)s placed a video call.",
@ -1986,6 +1995,8 @@
"You must join the room to see its files": "You must join the room to see its files", "You must join the room to see its files": "You must join the room to see its files",
"No files visible in this room": "No files visible in this room", "No files visible in this room": "No files visible in this room",
"Attach files from chat or just drag and drop them anywhere in a room.": "Attach files from chat or just drag and drop them anywhere in a room.", "Attach files from chat or just drag and drop them anywhere in a room.": "Attach files from chat or just drag and drop them anywhere in a room.",
"Communities": "Communities",
"Create community": "Create community",
"<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even use 'img' tags\n</p>\n": "<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even use 'img' tags\n</p>\n", "<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even use 'img' tags\n</p>\n": "<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even use 'img' tags\n</p>\n",
"Add rooms to the community summary": "Add rooms to the community summary", "Add rooms to the community summary": "Add rooms to the community summary",
"Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?", "Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?",
@ -2055,7 +2066,6 @@
"Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!", "Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!",
"To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.", "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.",
"Error whilst fetching joined communities": "Error whilst fetching joined communities", "Error whilst fetching joined communities": "Error whilst fetching joined communities",
"Communities": "Communities",
"Create a new community": "Create a new community", "Create a new community": "Create a new community",
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
"Youre all caught up": "Youre all caught up", "Youre all caught up": "Youre all caught up",
@ -2111,7 +2121,6 @@
"Click to mute video": "Click to mute video", "Click to mute video": "Click to mute video",
"Click to unmute audio": "Click to unmute audio", "Click to unmute audio": "Click to unmute audio",
"Click to mute audio": "Click to mute audio", "Click to mute audio": "Click to mute audio",
"Create community": "Create community",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
"Failed to load timeline position": "Failed to load timeline position", "Failed to load timeline position": "Failed to load timeline position",

View file

@ -96,7 +96,7 @@ function safeCounterpartTranslate(text: string, options?: object) {
return translated; return translated;
} }
interface IVariables { export interface IVariables {
count?: number; count?: number;
[key: string]: number | string; [key: string]: number | string;
} }

View file

@ -14,10 +14,20 @@
limitations under the License. limitations under the License.
*/ */
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
function memberEventDiff(ev) { interface IDiff {
const diff = { isMemberEvent: boolean;
isJoin?: boolean;
isPart?: boolean;
isDisplaynameChange?: boolean;
isAvatarChange?: boolean;
}
function memberEventDiff(ev: MatrixEvent): IDiff {
const diff: IDiff = {
isMemberEvent: ev.getType() === 'm.room.member', isMemberEvent: ev.getType() === 'm.room.member',
}; };
@ -37,7 +47,7 @@ function memberEventDiff(ev) {
return diff; return diff;
} }
export default function shouldHideEvent(ev) { export default function shouldHideEvent(ev: MatrixEvent): boolean {
// Wrap getValue() for readability. Calling the SettingsStore can be // Wrap getValue() for readability. Calling the SettingsStore can be
// fairly resource heavy, so the checks below should avoid hitting it // fairly resource heavy, so the checks below should avoid hitting it
// where possible. // where possible.

View file

@ -23,7 +23,7 @@ import SettingsStore from "../settings/SettingsStore";
import * as utils from "matrix-js-sdk/src/utils"; import * as utils from "matrix-js-sdk/src/utils";
import { UPDATE_EVENT } from "./AsyncStore"; import { UPDATE_EVENT } from "./AsyncStore";
import FlairStore from "./FlairStore"; import FlairStore from "./FlairStore";
import TagOrderStore from "./TagOrderStore"; import GroupFilterOrderStore from "./GroupFilterOrderStore";
import GroupStore from "./GroupStore"; import GroupStore from "./GroupStore";
import dis from "../dispatcher/dispatcher"; import dis from "../dispatcher/dispatcher";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
@ -50,7 +50,7 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
public getSelectedCommunityId(): string { public getSelectedCommunityId(): string {
if (SettingsStore.getValue("feature_communities_v2_prototypes")) { if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
return TagOrderStore.getSelectedTags()[0]; return GroupFilterOrderStore.getSelectedTags()[0];
} }
return null; // no selection as far as this function is concerned return null; // no selection as far as this function is concerned
} }

View file

@ -46,7 +46,7 @@ function commonPrefix(a, b) {
return ""; return "";
} }
/** /**
* A class for storing application state for ordering tags in the TagPanel. * A class for storing application state for ordering tags in the GroupFilterPanel.
*/ */
class CustomRoomTagStore extends EventEmitter { class CustomRoomTagStore extends EventEmitter {
constructor() { constructor() {

View file

@ -33,9 +33,9 @@ const INITIAL_STATE = {
}; };
/** /**
* A class for storing application state for ordering tags in the TagPanel. * A class for storing application state for ordering tags in the GroupFilterPanel.
*/ */
class TagOrderStore extends Store { class GroupFilterOrderStore extends Store {
constructor() { constructor() {
super(dis); super(dis);
@ -268,7 +268,7 @@ class TagOrderStore extends Store {
} }
} }
if (global.singletonTagOrderStore === undefined) { if (global.singletonGroupFilterOrderStore === undefined) {
global.singletonTagOrderStore = new TagOrderStore(); global.singletonGroupFilterOrderStore = new GroupFilterOrderStore();
} }
export default global.singletonTagOrderStore; export default global.singletonGroupFilterOrderStore;

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import { RoomListStoreClass } from "./RoomListStore"; import { RoomListStoreClass } from "./RoomListStore";
import TagOrderStore from "../TagOrderStore"; import GroupFilterOrderStore from "../GroupFilterOrderStore";
import { CommunityFilterCondition } from "./filters/CommunityFilterCondition"; import { CommunityFilterCondition } from "./filters/CommunityFilterCondition";
import { arrayDiff, arrayHasDiff } from "../../utils/arrays"; import { arrayDiff, arrayHasDiff } from "../../utils/arrays";
@ -26,12 +26,12 @@ export class TagWatcher {
private filters = new Map<string, CommunityFilterCondition>(); private filters = new Map<string, CommunityFilterCondition>();
constructor(private store: RoomListStoreClass) { constructor(private store: RoomListStoreClass) {
TagOrderStore.addListener(this.onTagsUpdated); GroupFilterOrderStore.addListener(this.onTagsUpdated);
} }
private onTagsUpdated = () => { private onTagsUpdated = () => {
const lastTags = Array.from(this.filters.keys()); const lastTags = Array.from(this.filters.keys());
const newTags = TagOrderStore.getSelectedTags(); const newTags = GroupFilterOrderStore.getSelectedTags();
if (arrayHasDiff(lastTags, newTags)) { if (arrayHasDiff(lastTags, newTags)) {
// Selected tags changed, do some filtering // Selected tags changed, do some filtering

View file

@ -27,7 +27,13 @@ export class ReactionEventPreview implements IPreview {
const showDms = SettingsStore.getValue("feature_roomlist_preview_reactions_dms"); const showDms = SettingsStore.getValue("feature_roomlist_preview_reactions_dms");
const showAll = SettingsStore.getValue("feature_roomlist_preview_reactions_all"); const showAll = SettingsStore.getValue("feature_roomlist_preview_reactions_all");
if (!showAll && (!showDms || DMRoomMap.shared().getUserIdForRoomId(event.getRoomId()))) return null; // If we're not showing all reactions, see if we're showing DMs instead
if (!showAll) {
// If we're not showing reactions on DMs, or we are and the room isn't a DM, skip
if (!(showDms && DMRoomMap.shared().getUserIdForRoomId(event.getRoomId()))) {
return null;
}
}
const relation = event.getRelation(); const relation = event.getRelation();
if (!relation) return null; // invalid reaction (probably redacted) if (!relation) return null; // invalid reaction (probably redacted)

View file

@ -17,6 +17,8 @@
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { import {
ClientWidgetApi, ClientWidgetApi,
IGetOpenIDActionRequest,
IGetOpenIDActionResponseData,
IStickerActionRequest, IStickerActionRequest,
IStickyActionRequest, IStickyActionRequest,
ITemplateParams, ITemplateParams,
@ -25,8 +27,10 @@ import {
IWidgetApiRequestEmptyData, IWidgetApiRequestEmptyData,
IWidgetData, IWidgetData,
MatrixCapabilities, MatrixCapabilities,
OpenIDRequestState,
runTemplate, runTemplate,
Widget, Widget,
WidgetApiToWidgetAction,
WidgetApiFromWidgetAction, WidgetApiFromWidgetAction,
} from "matrix-widget-api"; } from "matrix-widget-api";
import { StopGapWidgetDriver } from "./StopGapWidgetDriver"; import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
@ -43,6 +47,8 @@ import ActiveWidgetStore from "../ActiveWidgetStore";
import { objectShallowClone } from "../../utils/objects"; import { objectShallowClone } from "../../utils/objects";
import defaultDispatcher from "../../dispatcher/dispatcher"; import defaultDispatcher from "../../dispatcher/dispatcher";
import { ElementWidgetActions } from "./ElementWidgetActions"; import { ElementWidgetActions } from "./ElementWidgetActions";
import Modal from "../../Modal";
import WidgetOpenIDPermissionsDialog from "../../components/views/dialogs/WidgetOpenIDPermissionsDialog";
// TODO: Destroy all of this code // TODO: Destroy all of this code
@ -161,17 +167,20 @@ export class StopGapWidget extends EventEmitter {
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(), userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
}, opts?.asPopout); }, opts?.asPopout);
// Add in some legacy support sprinkles const parsed = new URL(templated);
// Add in some legacy support sprinkles (for non-popout widgets)
// TODO: Replace these with proper widget params // TODO: Replace these with proper widget params
// See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833 // See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
const parsed = new URL(templated); if (!opts?.asPopout) {
parsed.searchParams.set('widgetId', this.mockWidget.id); parsed.searchParams.set('widgetId', this.mockWidget.id);
parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]); parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]);
// Give the widget a scalar token if we're supposed to (more legacy) // Give the widget a scalar token if we're supposed to (more legacy)
// TODO: Stop doing this // TODO: Stop doing this
if (this.scalarToken) { if (this.scalarToken) {
parsed.searchParams.set('scalar_token', this.scalarToken); parsed.searchParams.set('scalar_token', this.scalarToken);
}
} }
// Replace the encoded dollar signs back to dollar signs. They have no special meaning // Replace the encoded dollar signs back to dollar signs. They have no special meaning
@ -187,12 +196,66 @@ export class StopGapWidget extends EventEmitter {
return !!this.messaging; return !!this.messaging;
} }
private get widgetId() {
return this.messaging.widget.id;
}
private onOpenIdReq = async (ev: CustomEvent<IGetOpenIDActionRequest>) => {
if (ev?.detail?.widgetId !== this.widgetId) return;
const rawUrl = this.appTileProps.app.url;
const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, rawUrl, this.appTileProps.userWidget);
const settings = SettingsStore.getValue("widgetOpenIDPermissions");
if (settings.deny && settings.deny.includes(widgetSecurityKey)) {
this.messaging.transport.reply(ev.detail, <IGetOpenIDActionResponseData>{
state: OpenIDRequestState.Blocked,
});
return;
}
if (settings.allow && settings.allow.includes(widgetSecurityKey)) {
const credentials = await MatrixClientPeg.get().getOpenIdToken();
this.messaging.transport.reply(ev.detail, <IGetOpenIDActionResponseData>{
state: OpenIDRequestState.Allowed,
...credentials,
});
return;
}
// Confirm that we received the request
this.messaging.transport.reply(ev.detail, <IGetOpenIDActionResponseData>{
state: OpenIDRequestState.PendingUserConfirmation,
});
// Actually ask for permission to send the user's data
Modal.createTrackedDialog("OpenID widget permissions", '', WidgetOpenIDPermissionsDialog, {
widgetUrl: rawUrl.substr(0, rawUrl.lastIndexOf("?")),
widgetId: this.widgetId,
isUserWidget: this.appTileProps.userWidget,
onFinished: async (confirm) => {
const responseBody: IGetOpenIDActionResponseData = {
state: confirm ? OpenIDRequestState.Allowed : OpenIDRequestState.Blocked,
original_request_id: ev.detail.requestId, // eslint-disable-line camelcase
};
if (confirm) {
const credentials = await MatrixClientPeg.get().getOpenIdToken();
Object.assign(responseBody, credentials);
}
this.messaging.transport.send(WidgetApiToWidgetAction.OpenIDCredentials, responseBody).catch(error => {
console.error("Failed to send OpenID credentials: ", error);
});
},
});
};
public start(iframe: HTMLIFrameElement) { public start(iframe: HTMLIFrameElement) {
if (this.started) return; if (this.started) return;
const driver = new StopGapWidgetDriver( this.appTileProps.whitelistCapabilities || []); const driver = new StopGapWidgetDriver( this.appTileProps.whitelistCapabilities || []);
this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver); this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver);
this.messaging.addEventListener("preparing", () => this.emit("preparing")); this.messaging.addEventListener("preparing", () => this.emit("preparing"));
this.messaging.addEventListener("ready", () => this.emit("ready")); this.messaging.addEventListener("ready", () => this.emit("ready"));
this.messaging.addEventListener(`action:${WidgetApiFromWidgetAction.GetOpenIDCredentials}`, this.onOpenIdReq);
WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging); WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging);
if (!this.appTileProps.userWidget && this.appTileProps.room) { if (!this.appTileProps.userWidget && this.appTileProps.room) {

View file

@ -22,6 +22,7 @@ import SetupEncryptionDialog from "../components/views/dialogs/security/SetupEnc
import { accessSecretStorage } from "../SecurityManager"; import { accessSecretStorage } from "../SecurityManager";
import ToastStore from "../stores/ToastStore"; import ToastStore from "../stores/ToastStore";
import GenericToast from "../components/views/toasts/GenericToast"; import GenericToast from "../components/views/toasts/GenericToast";
import SecurityCustomisations from "../customisations/Security";
const TOAST_KEY = "setupencryption"; const TOAST_KEY = "setupencryption";
@ -78,6 +79,10 @@ const onReject = () => {
}; };
export const showToast = (kind: Kind) => { export const showToast = (kind: Kind) => {
if (SecurityCustomisations.setupEncryptionNeeded?.(kind)) {
return;
}
const onAccept = async () => { const onAccept = async () => {
if (kind === Kind.VERIFY_THIS_SESSION) { if (kind === Kind.VERIFY_THIS_SESSION) {
Modal.createTrackedDialog("Verify session", "Verify session", SetupEncryptionDialog, Modal.createTrackedDialog("Verify session", "Verify session", SetupEncryptionDialog,

View file

@ -1,67 +0,0 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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.
*/
export function hueToRGB(h, s, l) {
const c = s * (1 - Math.abs(2 * l - 1));
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
const m = l - c / 2;
let r = 0;
let g = 0;
let b = 0;
if (0 <= h && h < 60) {
r = c;
g = x;
b = 0;
} else if (60 <= h && h < 120) {
r = x;
g = c;
b = 0;
} else if (120 <= h && h < 180) {
r = 0;
g = c;
b = x;
} else if (180 <= h && h < 240) {
r = 0;
g = x;
b = c;
} else if (240 <= h && h < 300) {
r = x;
g = 0;
b = c;
} else if (300 <= h && h < 360) {
r = c;
g = 0;
b = x;
}
return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
}
export function textToHtmlRainbow(str) {
const frequency = 360 / str.length;
return Array.from(str).map((c, i) => {
const [r, g, b] = hueToRGB(i * frequency, 1.0, 0.5);
return '<font color="#' +
r.toString(16).padStart(2, "0") +
g.toString(16).padStart(2, "0") +
b.toString(16).padStart(2, "0") +
'">' + c + '</font>';
}).join("");
}

88
src/utils/colour.ts Normal file
View file

@ -0,0 +1,88 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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.
*/
export function textToHtmlRainbow(str: string): string {
const frequency = (2 * Math.PI) / str.length;
return Array.from(str)
.map((c, i) => {
if (c === " ") {
return c;
}
const [a, b] = generateAB(i * frequency, 1);
const [red, green, blue] = labToRGB(75, a, b);
return (
'<font color="#' +
red.toString(16).padStart(2, "0") +
green.toString(16).padStart(2, "0") +
blue.toString(16).padStart(2, "0") +
'">' +
c +
"</font>"
);
})
.join("");
}
function generateAB(hue: number, chroma: number): [number, number] {
const a = chroma * 127 * Math.cos(hue);
const b = chroma * 127 * Math.sin(hue);
return [a, b];
}
function labToRGB(l: number, a: number, b: number): [number, number, number] {
// https://en.wikipedia.org/wiki/CIELAB_color_space#Reverse_transformation
// https://en.wikipedia.org/wiki/SRGB#The_forward_transformation_(CIE_XYZ_to_sRGB)
// Convert CIELAB to CIEXYZ (D65)
let y = (l + 16) / 116;
const x = adjustXYZ(y + a / 500) * 0.9505;
const z = adjustXYZ(y - b / 200) * 1.089;
y = adjustXYZ(y);
// Linear transformation from CIEXYZ to RGB
const red = 3.24096994 * x - 1.53738318 * y - 0.49861076 * z;
const green = -0.96924364 * x + 1.8759675 * y + 0.04155506 * z;
const blue = 0.05563008 * x - 0.20397696 * y + 1.05697151 * z;
return [adjustRGB(red), adjustRGB(green), adjustRGB(blue)];
}
function adjustXYZ(v: number): number {
if (v > 0.2069) {
return Math.pow(v, 3);
}
return 0.1284 * v - 0.01771;
}
function gammaCorrection(v: number): number {
// Non-linear transformation to sRGB
if (v <= 0.0031308) {
return 12.92 * v;
}
return 1.055 * Math.pow(v, 1 / 2.4) - 0.055;
}
function adjustRGB(v: number): number {
const corrected = gammaCorrection(v);
// Limits number between 0 and 1
const limited = Math.min(Math.max(corrected, 0), 1);
return Math.round(limited * 255);
}

View file

@ -64,7 +64,7 @@ describe('UserActivity', function() {
it('should not consider user active after activity if no window focus', function() { it('should not consider user active after activity if no window focus', function() {
fakeDocument.hasFocus = jest.fn().mockReturnValue(false); fakeDocument.hasFocus = jest.fn().mockReturnValue(false);
userActivity._onUserActivity({}); userActivity.onUserActivity({});
expect(userActivity.userActiveNow()).toBe(false); expect(userActivity.userActiveNow()).toBe(false);
expect(userActivity.userActiveRecently()).toBe(false); expect(userActivity.userActiveRecently()).toBe(false);
}); });
@ -72,7 +72,7 @@ describe('UserActivity', function() {
it('should consider user active shortly after activity', function() { it('should consider user active shortly after activity', function() {
fakeDocument.hasFocus = jest.fn().mockReturnValue(true); fakeDocument.hasFocus = jest.fn().mockReturnValue(true);
userActivity._onUserActivity({}); userActivity.onUserActivity({});
expect(userActivity.userActiveNow()).toBe(true); expect(userActivity.userActiveNow()).toBe(true);
expect(userActivity.userActiveRecently()).toBe(true); expect(userActivity.userActiveRecently()).toBe(true);
clock.tick(200); clock.tick(200);
@ -83,7 +83,7 @@ describe('UserActivity', function() {
it('should consider user not active after 10s of no activity', function() { it('should consider user not active after 10s of no activity', function() {
fakeDocument.hasFocus = jest.fn().mockReturnValue(true); fakeDocument.hasFocus = jest.fn().mockReturnValue(true);
userActivity._onUserActivity({}); userActivity.onUserActivity({});
clock.tick(10000); clock.tick(10000);
expect(userActivity.userActiveNow()).toBe(false); expect(userActivity.userActiveNow()).toBe(false);
}); });
@ -91,7 +91,7 @@ describe('UserActivity', function() {
it('should consider user passive after 10s of no activity', function() { it('should consider user passive after 10s of no activity', function() {
fakeDocument.hasFocus = jest.fn().mockReturnValue(true); fakeDocument.hasFocus = jest.fn().mockReturnValue(true);
userActivity._onUserActivity({}); userActivity.onUserActivity({});
clock.tick(10000); clock.tick(10000);
expect(userActivity.userActiveRecently()).toBe(true); expect(userActivity.userActiveRecently()).toBe(true);
}); });
@ -99,7 +99,7 @@ describe('UserActivity', function() {
it('should not consider user passive after 10s if window un-focused', function() { it('should not consider user passive after 10s if window un-focused', function() {
fakeDocument.hasFocus = jest.fn().mockReturnValue(true); fakeDocument.hasFocus = jest.fn().mockReturnValue(true);
userActivity._onUserActivity({}); userActivity.onUserActivity({});
clock.tick(10000); clock.tick(10000);
fakeDocument.hasFocus = jest.fn().mockReturnValue(false); fakeDocument.hasFocus = jest.fn().mockReturnValue(false);
@ -111,7 +111,7 @@ describe('UserActivity', function() {
it('should not consider user passive after 3 mins', function() { it('should not consider user passive after 3 mins', function() {
fakeDocument.hasFocus = jest.fn().mockReturnValue(true); fakeDocument.hasFocus = jest.fn().mockReturnValue(true);
userActivity._onUserActivity({}); userActivity.onUserActivity({});
clock.tick(3 * 60 * 1000); clock.tick(3 * 60 * 1000);
expect(userActivity.userActiveRecently()).toBe(false); expect(userActivity.userActiveRecently()).toBe(false);
@ -120,11 +120,11 @@ describe('UserActivity', function() {
it('should extend timer on activity', function() { it('should extend timer on activity', function() {
fakeDocument.hasFocus = jest.fn().mockReturnValue(true); fakeDocument.hasFocus = jest.fn().mockReturnValue(true);
userActivity._onUserActivity({}); userActivity.onUserActivity({});
clock.tick(1 * 60 * 1000); clock.tick(1 * 60 * 1000);
userActivity._onUserActivity({}); userActivity.onUserActivity({});
clock.tick(1 * 60 * 1000); clock.tick(1 * 60 * 1000);
userActivity._onUserActivity({}); userActivity.onUserActivity({});
clock.tick(1 * 60 * 1000); clock.tick(1 * 60 * 1000);
expect(userActivity.userActiveRecently()).toBe(true); expect(userActivity.userActiveRecently()).toBe(true);