Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into fix-final-10
This commit is contained in:
commit
49d99c6dc7
43 changed files with 672 additions and 536 deletions
98
CHANGELOG.md
98
CHANGELOG.md
|
@ -1,3 +1,101 @@
|
||||||
|
Changes in [3.6.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.6.0) (2020-10-12)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.6.0-rc.1...v3.6.0)
|
||||||
|
|
||||||
|
* Upgrade JS SDK to 8.5.0
|
||||||
|
* [Release] Fix templating for v1 jitsi widgets
|
||||||
|
[\#5306](https://github.com/matrix-org/matrix-react-sdk/pull/5306)
|
||||||
|
* [Release] Use new preparing event for widget communications
|
||||||
|
[\#5304](https://github.com/matrix-org/matrix-react-sdk/pull/5304)
|
||||||
|
|
||||||
|
Changes in [3.6.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.6.0-rc.1) (2020-10-07)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.5.0...v3.6.0-rc.1)
|
||||||
|
|
||||||
|
* Upgrade JS SDK to 8.5.0-rc.1
|
||||||
|
* Update from Weblate
|
||||||
|
[\#5297](https://github.com/matrix-org/matrix-react-sdk/pull/5297)
|
||||||
|
* Fix edited replies being wrongly treated as big emoji
|
||||||
|
[\#5295](https://github.com/matrix-org/matrix-react-sdk/pull/5295)
|
||||||
|
* Fix StopGapWidget infinitely recursing
|
||||||
|
[\#5294](https://github.com/matrix-org/matrix-react-sdk/pull/5294)
|
||||||
|
* Fix editing and redactions not updating the Reply Thread
|
||||||
|
[\#5281](https://github.com/matrix-org/matrix-react-sdk/pull/5281)
|
||||||
|
* Hide Jump to Read Receipt button for users who have not yet sent an RR
|
||||||
|
[\#5282](https://github.com/matrix-org/matrix-react-sdk/pull/5282)
|
||||||
|
* fix img tags not always being rendered correctly
|
||||||
|
[\#5279](https://github.com/matrix-org/matrix-react-sdk/pull/5279)
|
||||||
|
* Hopefully fix righhtpanel crash
|
||||||
|
[\#5293](https://github.com/matrix-org/matrix-react-sdk/pull/5293)
|
||||||
|
* Fix naive pinning limit and app tile widgetMessaging NPE
|
||||||
|
[\#5283](https://github.com/matrix-org/matrix-react-sdk/pull/5283)
|
||||||
|
* Show server errors from saving profile settings
|
||||||
|
[\#5272](https://github.com/matrix-org/matrix-react-sdk/pull/5272)
|
||||||
|
* Update copy for `redact` permission
|
||||||
|
[\#5273](https://github.com/matrix-org/matrix-react-sdk/pull/5273)
|
||||||
|
* Remove width limit on widgets
|
||||||
|
[\#5265](https://github.com/matrix-org/matrix-react-sdk/pull/5265)
|
||||||
|
* Fix call container avatar initial centering
|
||||||
|
[\#5280](https://github.com/matrix-org/matrix-react-sdk/pull/5280)
|
||||||
|
* Fix right panel for peeking rooms
|
||||||
|
[\#5268](https://github.com/matrix-org/matrix-react-sdk/pull/5268)
|
||||||
|
* Add support for dehydrated devices
|
||||||
|
[\#5239](https://github.com/matrix-org/matrix-react-sdk/pull/5239)
|
||||||
|
* Use Own Profile Store for the Profile Settings
|
||||||
|
[\#5277](https://github.com/matrix-org/matrix-react-sdk/pull/5277)
|
||||||
|
* null-guard defaultAvatarUrlForString
|
||||||
|
[\#5270](https://github.com/matrix-org/matrix-react-sdk/pull/5270)
|
||||||
|
* Choose first result on enter in the emoji picker
|
||||||
|
[\#5257](https://github.com/matrix-org/matrix-react-sdk/pull/5257)
|
||||||
|
* Fix room directory clipping links in the room's topic
|
||||||
|
[\#5276](https://github.com/matrix-org/matrix-react-sdk/pull/5276)
|
||||||
|
* Decorate failed e2ee downgrade attempts better
|
||||||
|
[\#5278](https://github.com/matrix-org/matrix-react-sdk/pull/5278)
|
||||||
|
* MELS use latest avatar rather than the first avatar
|
||||||
|
[\#5262](https://github.com/matrix-org/matrix-react-sdk/pull/5262)
|
||||||
|
* Fix Encryption Panel close button clashing with Base Card
|
||||||
|
[\#5261](https://github.com/matrix-org/matrix-react-sdk/pull/5261)
|
||||||
|
* Wrap canEncryptToAllUsers in a try/catch to handle server errors
|
||||||
|
[\#5275](https://github.com/matrix-org/matrix-react-sdk/pull/5275)
|
||||||
|
* Fix conditional on communities prototype room creation dialog
|
||||||
|
[\#5274](https://github.com/matrix-org/matrix-react-sdk/pull/5274)
|
||||||
|
* Fix ensureDmExists for encryption detection
|
||||||
|
[\#5271](https://github.com/matrix-org/matrix-react-sdk/pull/5271)
|
||||||
|
* Switch to using the Widget API SDK for widget messaging
|
||||||
|
[\#5171](https://github.com/matrix-org/matrix-react-sdk/pull/5171)
|
||||||
|
* Ensure package links exist when releasing
|
||||||
|
[\#5269](https://github.com/matrix-org/matrix-react-sdk/pull/5269)
|
||||||
|
* Fix the call preview when not in same room as the call
|
||||||
|
[\#5267](https://github.com/matrix-org/matrix-react-sdk/pull/5267)
|
||||||
|
* Make the hangup button do things for conference calls
|
||||||
|
[\#5223](https://github.com/matrix-org/matrix-react-sdk/pull/5223)
|
||||||
|
* Render Jitsi widget state events in a more obvious way
|
||||||
|
[\#5222](https://github.com/matrix-org/matrix-react-sdk/pull/5222)
|
||||||
|
* Make the PIP Jitsi look and feel like the 1:1 PIP
|
||||||
|
[\#5226](https://github.com/matrix-org/matrix-react-sdk/pull/5226)
|
||||||
|
* Trim range when formatting so that it excludes leading/trailing spaces
|
||||||
|
[\#5263](https://github.com/matrix-org/matrix-react-sdk/pull/5263)
|
||||||
|
* Fix button label on the Set Password Dialog
|
||||||
|
[\#5264](https://github.com/matrix-org/matrix-react-sdk/pull/5264)
|
||||||
|
* fix link to classic yarn's `yarn link`
|
||||||
|
[\#5259](https://github.com/matrix-org/matrix-react-sdk/pull/5259)
|
||||||
|
* Fix index mismatch between username colors styles and custom theming
|
||||||
|
[\#5256](https://github.com/matrix-org/matrix-react-sdk/pull/5256)
|
||||||
|
* Disable autocompletion on security key input during login
|
||||||
|
[\#5258](https://github.com/matrix-org/matrix-react-sdk/pull/5258)
|
||||||
|
* fix uninitialised state and eventlistener leak in RoomUpgradeWarningBar
|
||||||
|
[\#5255](https://github.com/matrix-org/matrix-react-sdk/pull/5255)
|
||||||
|
* Only set title when it changes
|
||||||
|
[\#5254](https://github.com/matrix-org/matrix-react-sdk/pull/5254)
|
||||||
|
* Convert CallHandler to typescript
|
||||||
|
[\#5248](https://github.com/matrix-org/matrix-react-sdk/pull/5248)
|
||||||
|
* Retry loading i18n language if it fails
|
||||||
|
[\#5209](https://github.com/matrix-org/matrix-react-sdk/pull/5209)
|
||||||
|
* Rework profile area for user and room settings to be more clear
|
||||||
|
[\#5243](https://github.com/matrix-org/matrix-react-sdk/pull/5243)
|
||||||
|
* Validation improve pattern for derived data
|
||||||
|
[\#5241](https://github.com/matrix-org/matrix-react-sdk/pull/5241)
|
||||||
|
|
||||||
Changes in [3.5.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.5.0) (2020-09-28)
|
Changes in [3.5.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.5.0) (2020-09-28)
|
||||||
===================================================================================================
|
===================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.5.0-rc.1...v3.5.0)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.5.0-rc.1...v3.5.0)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.5.0",
|
"version": "3.6.0",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
@ -50,10 +50,6 @@ $MiniAppTileHeight: 200px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppsDrawer_hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppsContainer {
|
.mx_AppsContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -78,15 +74,6 @@ $MiniAppTileHeight: 200px;
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SetAppURLDialog_input {
|
|
||||||
border-radius: 3px;
|
|
||||||
border: 1px solid $input-border-color;
|
|
||||||
padding: 9px;
|
|
||||||
color: $primary-hairline-color;
|
|
||||||
background-color: $primary-bg-color;
|
|
||||||
font-size: $font-15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppTile {
|
.mx_AppTile {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
border: 5px solid $widget-menu-bar-bg-color;
|
border: 5px solid $widget-menu-bar-bg-color;
|
||||||
|
@ -242,72 +229,6 @@ $MiniAppTileHeight: 200px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileMenuBarWidgetPadding {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppIconTile {
|
|
||||||
background-color: $lightbox-bg-color;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0);
|
|
||||||
width: 200px;
|
|
||||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
|
|
||||||
transition: 0.3s;
|
|
||||||
border-radius: 3px;
|
|
||||||
margin: 5px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppIconTile.mx_AppIconTile_active {
|
|
||||||
color: $accent-color;
|
|
||||||
border-color: $accent-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppIconTile:hover {
|
|
||||||
border: 1px solid $accent-color;
|
|
||||||
box-shadow: 0 0 10px 5px rgba(200, 200, 200, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppIconTile_content {
|
|
||||||
padding: 2px 16px;
|
|
||||||
height: 60px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppIconTile_content h4 {
|
|
||||||
margin-top: 5px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppIconTile_content p {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
font-size: smaller;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppIconTile_image {
|
|
||||||
padding: 10px;
|
|
||||||
max-width: 100px;
|
|
||||||
max-height: 100px;
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppIconTile_imageContainer {
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 3px 3px 0 0;
|
|
||||||
height: 155px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
form.mx_Custom_Widget_Form div {
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppPermissionWarning {
|
.mx_AppPermissionWarning {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: $widget-menu-bar-bg-color;
|
background-color: $widget-menu-bar-bg-color;
|
||||||
|
|
5
src/@types/global.d.ts
vendored
5
src/@types/global.d.ts
vendored
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first
|
||||||
import * as ModernizrStatic from "modernizr";
|
import * as ModernizrStatic from "modernizr";
|
||||||
import ContentMessages from "../ContentMessages";
|
import ContentMessages from "../ContentMessages";
|
||||||
import { IMatrixClientPeg } from "../MatrixClientPeg";
|
import { IMatrixClientPeg } from "../MatrixClientPeg";
|
||||||
|
@ -31,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 {
|
||||||
|
@ -55,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 {
|
||||||
|
|
|
@ -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;
|
|
@ -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(
|
|
@ -77,13 +77,28 @@ import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||||
import WidgetStore from "./stores/WidgetStore";
|
import WidgetStore from "./stores/WidgetStore";
|
||||||
import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
|
import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
|
||||||
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
|
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
|
||||||
|
import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty } from "matrix-js-sdk/lib/webrtc/call";
|
||||||
|
|
||||||
// until we ts-ify the js-sdk voip code
|
enum AudioID {
|
||||||
type Call = any;
|
Ring = 'ringAudio',
|
||||||
|
Ringback = 'ringbackAudio',
|
||||||
|
CallEnd = 'callendAudio',
|
||||||
|
Busy = 'busyAudio',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlike 'CallType' in js-sdk, this one includes screen sharing
|
||||||
|
// (because a screen sharing call is only a screen sharing call to the caller,
|
||||||
|
// to the callee it's just a video call, at least as far as the current impl
|
||||||
|
// is concerned).
|
||||||
|
export enum PlaceCallType {
|
||||||
|
Voice = 'voice',
|
||||||
|
Video = 'video',
|
||||||
|
ScreenSharing = 'screensharing',
|
||||||
|
}
|
||||||
|
|
||||||
export default class CallHandler {
|
export default class CallHandler {
|
||||||
private calls = new Map<string, Call>();
|
private calls = new Map<string, MatrixCall>();
|
||||||
private audioPromises = new Map<string, Promise<void>>();
|
private audioPromises = new Map<AudioID, Promise<void>>();
|
||||||
|
|
||||||
static sharedInstance() {
|
static sharedInstance() {
|
||||||
if (!window.mxCallHandler) {
|
if (!window.mxCallHandler) {
|
||||||
|
@ -108,20 +123,20 @@ export default class CallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getCallForRoom(roomId: string): Call {
|
getCallForRoom(roomId: string): MatrixCall {
|
||||||
return this.calls.get(roomId) || null;
|
return this.calls.get(roomId) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAnyActiveCall() {
|
getAnyActiveCall() {
|
||||||
for (const call of this.calls.values()) {
|
for (const call of this.calls.values()) {
|
||||||
if (call.state !== "ended") {
|
if (call.state !== CallState.Ended) {
|
||||||
return call;
|
return call;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
play(audioId: string) {
|
play(audioId: AudioID) {
|
||||||
// TODO: Attach an invisible element for this instead
|
// TODO: Attach an invisible element for this instead
|
||||||
// which listens?
|
// which listens?
|
||||||
const audio = document.getElementById(audioId) as HTMLMediaElement;
|
const audio = document.getElementById(audioId) as HTMLMediaElement;
|
||||||
|
@ -150,7 +165,7 @@ export default class CallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pause(audioId: string) {
|
pause(audioId: AudioID) {
|
||||||
// TODO: Attach an invisible element for this instead
|
// TODO: Attach an invisible element for this instead
|
||||||
// which listens?
|
// which listens?
|
||||||
const audio = document.getElementById(audioId) as HTMLMediaElement;
|
const audio = document.getElementById(audioId) as HTMLMediaElement;
|
||||||
|
@ -164,8 +179,18 @@ export default class CallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setCallListeners(call: Call) {
|
private matchesCallForThisRoom(call: MatrixCall) {
|
||||||
call.on("error", (err) => {
|
// We don't allow placing more than one call per room, but that doesn't mean there
|
||||||
|
// can't be more than one, eg. in a glare situation. This checks that the given call
|
||||||
|
// is the call we consider 'the' call for its room.
|
||||||
|
const callForThisRoom = this.getCallForRoom(call.roomId);
|
||||||
|
return callForThisRoom && call.callId === callForThisRoom.callId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setCallListeners(call: MatrixCall) {
|
||||||
|
call.on(CallEvent.Error, (err) => {
|
||||||
|
if (!this.matchesCallForThisRoom(call)) return;
|
||||||
|
|
||||||
console.error("Call error:", err);
|
console.error("Call error:", err);
|
||||||
if (
|
if (
|
||||||
MatrixClientPeg.get().getTurnServers().length === 0 &&
|
MatrixClientPeg.get().getTurnServers().length === 0 &&
|
||||||
|
@ -180,74 +205,79 @@ export default class CallHandler {
|
||||||
description: err.message,
|
description: err.message,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
call.on("hangup", () => {
|
call.on(CallEvent.Hangup, () => {
|
||||||
|
if (!this.matchesCallForThisRoom(call)) return;
|
||||||
|
|
||||||
this.removeCallForRoom(call.roomId);
|
this.removeCallForRoom(call.roomId);
|
||||||
});
|
});
|
||||||
// map web rtc states to dummy UI state
|
call.on(CallEvent.State, (newState: CallState, oldState: CallState) => {
|
||||||
// ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
|
if (!this.matchesCallForThisRoom(call)) return;
|
||||||
call.on("state", (newState, oldState) => {
|
|
||||||
if (newState === "ringing") {
|
this.setCallState(call, newState);
|
||||||
this.setCallState(call, call.roomId, "ringing");
|
|
||||||
this.pause("ringbackAudio");
|
switch (oldState) {
|
||||||
} else if (newState === "invite_sent") {
|
case CallState.Ringing:
|
||||||
this.setCallState(call, call.roomId, "ringback");
|
this.pause(AudioID.Ring);
|
||||||
this.play("ringbackAudio");
|
break;
|
||||||
} else if (newState === "ended" && oldState === "connected") {
|
case CallState.InviteSent:
|
||||||
this.removeCallForRoom(call.roomId);
|
this.pause(AudioID.Ringback);
|
||||||
this.pause("ringbackAudio");
|
break;
|
||||||
this.play("callendAudio");
|
|
||||||
} else if (newState === "ended" && oldState === "invite_sent" &&
|
|
||||||
(call.hangupParty === "remote" ||
|
|
||||||
(call.hangupParty === "local" && call.hangupReason === "invite_timeout")
|
|
||||||
)) {
|
|
||||||
this.setCallState(call, call.roomId, "busy");
|
|
||||||
this.pause("ringbackAudio");
|
|
||||||
this.play("busyAudio");
|
|
||||||
Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
|
|
||||||
title: _t('Call Timeout'),
|
|
||||||
description: _t('The remote side failed to pick up') + '.',
|
|
||||||
});
|
|
||||||
} else if (oldState === "invite_sent") {
|
|
||||||
this.setCallState(call, call.roomId, "stop_ringback");
|
|
||||||
this.pause("ringbackAudio");
|
|
||||||
} else if (oldState === "ringing") {
|
|
||||||
this.setCallState(call, call.roomId, "stop_ringing");
|
|
||||||
this.pause("ringbackAudio");
|
|
||||||
} else if (newState === "connected") {
|
|
||||||
this.setCallState(call, call.roomId, "connected");
|
|
||||||
this.pause("ringbackAudio");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (newState) {
|
||||||
|
case CallState.Ringing:
|
||||||
|
this.play(AudioID.Ring);
|
||||||
|
break;
|
||||||
|
case CallState.InviteSent:
|
||||||
|
this.play(AudioID.Ringback);
|
||||||
|
break;
|
||||||
|
case CallState.Ended:
|
||||||
|
this.removeCallForRoom(call.roomId);
|
||||||
|
if (oldState === CallState.InviteSent && (
|
||||||
|
call.hangupParty === CallParty.Remote ||
|
||||||
|
(call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout)
|
||||||
|
)) {
|
||||||
|
this.play(AudioID.Busy);
|
||||||
|
Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
|
||||||
|
title: _t('Call Timeout'),
|
||||||
|
description: _t('The remote side failed to pick up') + '.',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.play(AudioID.CallEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
call.on(CallEvent.Replaced, (newCall: MatrixCall) => {
|
||||||
|
if (!this.matchesCallForThisRoom(call)) return;
|
||||||
|
|
||||||
|
console.log(`Call ID ${call.callId} is being replaced by call ID ${newCall.callId}`);
|
||||||
|
|
||||||
|
if (call.state === CallState.Ringing) {
|
||||||
|
this.pause(AudioID.Ring);
|
||||||
|
} else if (call.state === CallState.InviteSent) {
|
||||||
|
this.pause(AudioID.Ringback);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.calls.set(newCall.roomId, newCall);
|
||||||
|
this.setCallListeners(newCall);
|
||||||
|
this.setCallState(newCall, newCall.state);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setCallState(call: Call, roomId: string, status: string) {
|
private setCallState(call: MatrixCall, status: CallState) {
|
||||||
console.log(
|
console.log(
|
||||||
`Call state in ${roomId} changed to ${status} (${call ? call.call_state : "-"})`,
|
`Call state in ${call.roomId} changed to ${status}`,
|
||||||
);
|
);
|
||||||
if (call) {
|
|
||||||
this.calls.set(roomId, call);
|
|
||||||
} else {
|
|
||||||
this.calls.delete(roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === "ringing") {
|
|
||||||
this.play("ringAudio");
|
|
||||||
} else if (call && call.call_state === "ringing") {
|
|
||||||
this.pause("ringAudio");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (call) {
|
|
||||||
call.call_state = status;
|
|
||||||
}
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'call_state',
|
action: 'call_state',
|
||||||
room_id: roomId,
|
room_id: call.roomId,
|
||||||
state: status,
|
state: status,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeCallForRoom(roomId: string) {
|
private removeCallForRoom(roomId: string) {
|
||||||
this.setCallState(null, roomId, null);
|
this.calls.delete(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private showICEFallbackPrompt() {
|
private showICEFallbackPrompt() {
|
||||||
|
@ -279,36 +309,39 @@ export default class CallHandler {
|
||||||
}, null, true);
|
}, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onAction = (payload: ActionPayload) => {
|
|
||||||
const placeCall = (newCall) => {
|
|
||||||
this.setCallListeners(newCall);
|
|
||||||
if (payload.type === 'voice') {
|
|
||||||
newCall.placeVoiceCall();
|
|
||||||
} else if (payload.type === 'video') {
|
|
||||||
newCall.placeVideoCall(
|
|
||||||
payload.remote_element,
|
|
||||||
payload.local_element,
|
|
||||||
);
|
|
||||||
} else if (payload.type === 'screensharing') {
|
|
||||||
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
|
|
||||||
if (screenCapErrorString) {
|
|
||||||
this.removeCallForRoom(newCall.roomId);
|
|
||||||
console.log("Can't capture screen: " + screenCapErrorString);
|
|
||||||
Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
|
|
||||||
title: _t('Unable to capture screen'),
|
|
||||||
description: screenCapErrorString,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
newCall.placeScreenSharingCall(
|
|
||||||
payload.remote_element,
|
|
||||||
payload.local_element,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error("Unknown conf call type: %s", payload.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
private placeCall(
|
||||||
|
roomId: string, type: PlaceCallType,
|
||||||
|
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
|
||||||
|
) {
|
||||||
|
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), roomId);
|
||||||
|
this.calls.set(roomId, call);
|
||||||
|
this.setCallListeners(call);
|
||||||
|
if (type === PlaceCallType.Voice) {
|
||||||
|
call.placeVoiceCall();
|
||||||
|
} else if (type === 'video') {
|
||||||
|
call.placeVideoCall(
|
||||||
|
remoteElement,
|
||||||
|
localElement,
|
||||||
|
);
|
||||||
|
} else if (type === PlaceCallType.ScreenSharing) {
|
||||||
|
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
|
||||||
|
if (screenCapErrorString) {
|
||||||
|
this.removeCallForRoom(roomId);
|
||||||
|
console.log("Can't capture screen: " + screenCapErrorString);
|
||||||
|
Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
|
||||||
|
title: _t('Unable to capture screen'),
|
||||||
|
description: screenCapErrorString,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
call.placeScreenSharingCall(remoteElement, localElement);
|
||||||
|
} else {
|
||||||
|
console.error("Unknown conf call type: %s", type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onAction = (payload: ActionPayload) => {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'place_call':
|
case 'place_call':
|
||||||
{
|
{
|
||||||
|
@ -343,8 +376,8 @@ export default class CallHandler {
|
||||||
return;
|
return;
|
||||||
} else if (members.length === 2) {
|
} else if (members.length === 2) {
|
||||||
console.info("Place %s call in %s", payload.type, payload.room_id);
|
console.info("Place %s call in %s", payload.type, payload.room_id);
|
||||||
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
|
|
||||||
placeCall(call);
|
this.placeCall(payload.room_id, payload.type, payload.local_element, payload.remote_element);
|
||||||
} else { // > 2
|
} else { // > 2
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "place_conference_call",
|
action: "place_conference_call",
|
||||||
|
@ -383,24 +416,23 @@ export default class CallHandler {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const call = payload.call;
|
const call = payload.call as MatrixCall;
|
||||||
|
this.calls.set(call.roomId, call)
|
||||||
this.setCallListeners(call);
|
this.setCallListeners(call);
|
||||||
this.setCallState(call, call.roomId, "ringing");
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'hangup':
|
case 'hangup':
|
||||||
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();
|
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':
|
||||||
if (!this.calls.get(payload.room_id)) {
|
if (!this.calls.has(payload.room_id)) {
|
||||||
return; // no call to answer
|
return; // no call to answer
|
||||||
}
|
}
|
||||||
this.calls.get(payload.room_id).answer();
|
this.calls.get(payload.room_id).answer();
|
||||||
this.setCallState(this.calls.get(payload.room_id), payload.room_id, "connected");
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "view_room",
|
action: "view_room",
|
||||||
room_id: payload.room_id,
|
room_id: payload.room_id,
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -17,6 +17,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ICreateClientOpts } from 'matrix-js-sdk/src/matrix';
|
||||||
import {MatrixClient} from 'matrix-js-sdk/src/client';
|
import {MatrixClient} from 'matrix-js-sdk/src/client';
|
||||||
import {MemoryStore} from 'matrix-js-sdk/src/store/memory';
|
import {MemoryStore} from 'matrix-js-sdk/src/store/memory';
|
||||||
import * as utils from 'matrix-js-sdk/src/utils';
|
import * as utils from 'matrix-js-sdk/src/utils';
|
||||||
|
@ -249,8 +250,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
|
||||||
}
|
}
|
||||||
|
|
||||||
private createClient(creds: IMatrixClientCreds): void {
|
private createClient(creds: IMatrixClientCreds): void {
|
||||||
// TODO: Make these opts typesafe with the js-sdk
|
const opts: ICreateClientOpts = {
|
||||||
const opts = {
|
|
||||||
baseUrl: creds.homeserverUrl,
|
baseUrl: creds.homeserverUrl,
|
||||||
idBaseUrl: creds.identityServerUrl,
|
idBaseUrl: creds.identityServerUrl,
|
||||||
accessToken: creds.accessToken,
|
accessToken: creds.accessToken,
|
||||||
|
|
|
@ -132,7 +132,7 @@ export class ModalManager {
|
||||||
public createTrackedDialogAsync<T extends any[]>(
|
public createTrackedDialogAsync<T extends any[]>(
|
||||||
analyticsAction: string,
|
analyticsAction: string,
|
||||||
analyticsInfo: string,
|
analyticsInfo: string,
|
||||||
...rest: Parameters<ModalManager["appendDialogAsync"]>
|
...rest: Parameters<ModalManager["createDialogAsync"]>
|
||||||
) {
|
) {
|
||||||
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
|
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
|
||||||
return this.createDialogAsync<T>(...rest);
|
return this.createDialogAsync<T>(...rest);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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];
|
|
@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ICryptoCallbacks, IDeviceTrustLevel, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix';
|
||||||
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import * as sdk from './index';
|
import * as sdk from './index';
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
|
@ -31,15 +33,18 @@ import SettingsStore from "./settings/SettingsStore";
|
||||||
// during the same single operation. Use `accessSecretStorage` below to scope a
|
// during the same single operation. Use `accessSecretStorage` below to scope a
|
||||||
// single secret storage operation, as it will clear the cached keys once the
|
// single secret storage operation, as it will clear the cached keys once the
|
||||||
// operation ends.
|
// operation ends.
|
||||||
let secretStorageKeys = {};
|
let secretStorageKeys: Record<string, Uint8Array> = {};
|
||||||
let secretStorageKeyInfo = {};
|
let secretStorageKeyInfo: Record<string, ISecretStorageKeyInfo> = {};
|
||||||
let secretStorageBeingAccessed = false;
|
let secretStorageBeingAccessed = false;
|
||||||
|
|
||||||
let nonInteractive = false;
|
let nonInteractive = false;
|
||||||
|
|
||||||
let dehydrationCache = {};
|
let dehydrationCache: {
|
||||||
|
key?: Uint8Array,
|
||||||
|
keyInfo?: ISecretStorageKeyInfo,
|
||||||
|
} = {};
|
||||||
|
|
||||||
function isCachingAllowed() {
|
function isCachingAllowed(): boolean {
|
||||||
return secretStorageBeingAccessed;
|
return secretStorageBeingAccessed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +55,7 @@ function isCachingAllowed() {
|
||||||
*
|
*
|
||||||
* @returns {bool}
|
* @returns {bool}
|
||||||
*/
|
*/
|
||||||
export function isSecretStorageBeingAccessed() {
|
export function isSecretStorageBeingAccessed(): boolean {
|
||||||
return secretStorageBeingAccessed;
|
return secretStorageBeingAccessed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +65,7 @@ export class AccessCancelledError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmToDismiss() {
|
async function confirmToDismiss(): Promise<boolean> {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
const [sure] = await Modal.createDialog(QuestionDialog, {
|
const [sure] = await Modal.createDialog(QuestionDialog, {
|
||||||
title: _t("Cancel entering passphrase?"),
|
title: _t("Cancel entering passphrase?"),
|
||||||
|
@ -72,7 +77,9 @@ async function confirmToDismiss() {
|
||||||
return !sure;
|
return !sure;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeInputToKey(keyInfo) {
|
function makeInputToKey(
|
||||||
|
keyInfo: ISecretStorageKeyInfo,
|
||||||
|
): (keyParams: { passphrase: string, recoveryKey: string }) => Promise<Uint8Array> {
|
||||||
return async ({ passphrase, recoveryKey }) => {
|
return async ({ passphrase, recoveryKey }) => {
|
||||||
if (passphrase) {
|
if (passphrase) {
|
||||||
return deriveKey(
|
return deriveKey(
|
||||||
|
@ -86,7 +93,10 @@ function makeInputToKey(keyInfo) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
|
async function getSecretStorageKey(
|
||||||
|
{ keys: keyInfos }: { keys: Record<string, ISecretStorageKeyInfo> },
|
||||||
|
ssssItemName,
|
||||||
|
): Promise<[string, Uint8Array]> {
|
||||||
const keyInfoEntries = Object.entries(keyInfos);
|
const keyInfoEntries = Object.entries(keyInfos);
|
||||||
if (keyInfoEntries.length > 1) {
|
if (keyInfoEntries.length > 1) {
|
||||||
throw new Error("Multiple storage key requests not implemented");
|
throw new Error("Multiple storage key requests not implemented");
|
||||||
|
@ -100,7 +110,7 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
|
||||||
|
|
||||||
if (dehydrationCache.key) {
|
if (dehydrationCache.key) {
|
||||||
if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationCache.key, keyInfo)) {
|
if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationCache.key, keyInfo)) {
|
||||||
cacheSecretStorageKey(keyId, dehydrationCache.key, keyInfo);
|
cacheSecretStorageKey(keyId, keyInfo, dehydrationCache.key);
|
||||||
return [keyId, dehydrationCache.key];
|
return [keyId, dehydrationCache.key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,12 +149,15 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
|
||||||
const key = await inputToKey(input);
|
const key = await inputToKey(input);
|
||||||
|
|
||||||
// Save to cache to avoid future prompts in the current session
|
// Save to cache to avoid future prompts in the current session
|
||||||
cacheSecretStorageKey(keyId, key, keyInfo);
|
cacheSecretStorageKey(keyId, keyInfo, key);
|
||||||
|
|
||||||
return [keyId, key];
|
return [keyId, key];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDehydrationKey(keyInfo, checkFunc) {
|
export async function getDehydrationKey(
|
||||||
|
keyInfo: ISecretStorageKeyInfo,
|
||||||
|
checkFunc: (Uint8Array) => void,
|
||||||
|
): Promise<Uint8Array> {
|
||||||
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,
|
||||||
|
@ -185,20 +198,24 @@ export async function getDehydrationKey(keyInfo, checkFunc) {
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cacheSecretStorageKey(keyId, key, keyInfo) {
|
function cacheSecretStorageKey(
|
||||||
|
keyId: string,
|
||||||
|
keyInfo: ISecretStorageKeyInfo,
|
||||||
|
key: Uint8Array,
|
||||||
|
): void {
|
||||||
if (isCachingAllowed()) {
|
if (isCachingAllowed()) {
|
||||||
secretStorageKeys[keyId] = key;
|
secretStorageKeys[keyId] = key;
|
||||||
secretStorageKeyInfo[keyId] = keyInfo;
|
secretStorageKeyInfo[keyId] = keyInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSecretRequested = async function({
|
async function onSecretRequested(
|
||||||
user_id: userId,
|
userId: string,
|
||||||
device_id: deviceId,
|
deviceId: string,
|
||||||
request_id: requestId,
|
requestId: string,
|
||||||
name,
|
name: string,
|
||||||
device_trust: deviceTrust,
|
deviceTrust: IDeviceTrustLevel,
|
||||||
}) {
|
): Promise<string> {
|
||||||
console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust);
|
console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust);
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (userId !== client.getUserId()) {
|
if (userId !== client.getUserId()) {
|
||||||
|
@ -233,16 +250,16 @@ const onSecretRequested = async function({
|
||||||
return key && encodeBase64(key);
|
return key && encodeBase64(key);
|
||||||
}
|
}
|
||||||
console.warn("onSecretRequested didn't recognise the secret named ", name);
|
console.warn("onSecretRequested didn't recognise the secret named ", name);
|
||||||
};
|
}
|
||||||
|
|
||||||
export const crossSigningCallbacks = {
|
export const crossSigningCallbacks: ICryptoCallbacks = {
|
||||||
getSecretStorageKey,
|
getSecretStorageKey,
|
||||||
cacheSecretStorageKey,
|
cacheSecretStorageKey,
|
||||||
onSecretRequested,
|
onSecretRequested,
|
||||||
getDehydrationKey,
|
getDehydrationKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function promptForBackupPassphrase() {
|
export async function promptForBackupPassphrase(): Promise<Uint8Array> {
|
||||||
let key;
|
let key;
|
||||||
|
|
||||||
const { finished } = Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {
|
const { finished } = Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {
|
||||||
|
@ -292,7 +309,7 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
|
||||||
/* priority = */ false,
|
/* priority = */ false,
|
||||||
/* static = */ true,
|
/* static = */ true,
|
||||||
/* options = */ {
|
/* options = */ {
|
||||||
onBeforeClose(reason) {
|
onBeforeClose: async (reason) => {
|
||||||
// If Secure Backup is required, you cannot leave the modal.
|
// If Secure Backup is required, you cannot leave the modal.
|
||||||
if (reason === "backgroundClick") {
|
if (reason === "backgroundClick") {
|
||||||
return !isSecureBackupRequired();
|
return !isSecureBackupRequired();
|
||||||
|
@ -329,10 +346,10 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
|
||||||
|
|
||||||
const keyId = Object.keys(secretStorageKeys)[0];
|
const keyId = Object.keys(secretStorageKeys)[0];
|
||||||
if (keyId && SettingsStore.getValue("feature_dehydration")) {
|
if (keyId && SettingsStore.getValue("feature_dehydration")) {
|
||||||
const dehydrationKeyInfo =
|
let dehydrationKeyInfo = {};
|
||||||
secretStorageKeyInfo[keyId] && secretStorageKeyInfo[keyId].passphrase
|
if (secretStorageKeyInfo[keyId] && secretStorageKeyInfo[keyId].passphrase) {
|
||||||
? {passphrase: secretStorageKeyInfo[keyId].passphrase}
|
dehydrationKeyInfo = { passphrase: secretStorageKeyInfo[keyId].passphrase };
|
||||||
: {};
|
}
|
||||||
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 {
|
} else {
|
||||||
|
@ -354,7 +371,9 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: this function name is a bit of a mouthful
|
// FIXME: this function name is a bit of a mouthful
|
||||||
export async function tryToUnlockSecretStorageWithDehydrationKey(client) {
|
export async function tryToUnlockSecretStorageWithDehydrationKey(
|
||||||
|
client: MatrixClient,
|
||||||
|
): Promise<void> {
|
||||||
const key = dehydrationCache.key;
|
const key = dehydrationCache.key;
|
||||||
let restoringBackup = false;
|
let restoringBackup = false;
|
||||||
if (key && await client.isSecretStorageReady()) {
|
if (key && await client.isSecretStorageReady()) {
|
||||||
|
@ -366,10 +385,10 @@ export async function tryToUnlockSecretStorageWithDehydrationKey(client) {
|
||||||
|
|
||||||
// we also need to set a new dehydrated device to replace the
|
// we also need to set a new dehydrated device to replace the
|
||||||
// device we rehydrated
|
// device we rehydrated
|
||||||
const dehydrationKeyInfo =
|
let dehydrationKeyInfo = {};
|
||||||
dehydrationCache.keyInfo && dehydrationCache.keyInfo.passphrase
|
if (dehydrationCache.keyInfo && dehydrationCache.keyInfo.passphrase) {
|
||||||
? {passphrase: dehydrationCache.keyInfo.passphrase}
|
dehydrationKeyInfo = { passphrase: dehydrationCache.keyInfo.passphrase };
|
||||||
: {};
|
}
|
||||||
await client.setDehydrationKey(key, dehydrationKeyInfo, "Backup device");
|
await client.setDehydrationKey(key, dehydrationKeyInfo, "Backup device");
|
||||||
|
|
||||||
// and restore from backup
|
// and restore from backup
|
|
@ -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();
|
|
@ -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,
|
|
@ -218,6 +218,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
vertical: "mx_ResizeHandle_vertical",
|
vertical: "mx_ResizeHandle_vertical",
|
||||||
reverse: "mx_ResizeHandle_reverse",
|
reverse: "mx_ResizeHandle_reverse",
|
||||||
};
|
};
|
||||||
|
let size;
|
||||||
const collapseConfig = {
|
const collapseConfig = {
|
||||||
toggleSize: 260 - 50,
|
toggleSize: 260 - 50,
|
||||||
onCollapsed: (collapsed) => {
|
onCollapsed: (collapsed) => {
|
||||||
|
@ -228,21 +229,19 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
dis.dispatch({action: "show_left_panel"}, true);
|
dis.dispatch({action: "show_left_panel"}, true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onResized: (size) => {
|
onResized: (_size) => {
|
||||||
window.localStorage.setItem("mx_lhs_size", '' + size);
|
size = _size;
|
||||||
this.props.resizeNotifier.notifyLeftHandleResized();
|
this.props.resizeNotifier.notifyLeftHandleResized();
|
||||||
},
|
},
|
||||||
onResizeStart: () => {
|
onResizeStart: () => {
|
||||||
this.props.resizeNotifier.startResizing();
|
this.props.resizeNotifier.startResizing();
|
||||||
},
|
},
|
||||||
onResizeStop: () => {
|
onResizeStop: () => {
|
||||||
|
window.localStorage.setItem("mx_lhs_size", '' + size);
|
||||||
this.props.resizeNotifier.stopResizing();
|
this.props.resizeNotifier.stopResizing();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const resizer = new Resizer(
|
const resizer = new Resizer(this._resizeContainer.current, CollapseDistributor, collapseConfig);
|
||||||
this._resizeContainer.current,
|
|
||||||
CollapseDistributor,
|
|
||||||
collapseConfig);
|
|
||||||
resizer.setClassNames(classNames);
|
resizer.setClassNames(classNames);
|
||||||
return resizer;
|
return resizer;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015-2020 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2017, 2018 New Vector Ltd
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -26,6 +24,7 @@ import Resend from '../../Resend';
|
||||||
import dis from '../../dispatcher/dispatcher';
|
import dis from '../../dispatcher/dispatcher';
|
||||||
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
|
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
|
||||||
import {Action} from "../../dispatcher/actions";
|
import {Action} from "../../dispatcher/actions";
|
||||||
|
import { CallState, CallType } from 'matrix-js-sdk/lib/webrtc/call';
|
||||||
|
|
||||||
const STATUS_BAR_HIDDEN = 0;
|
const STATUS_BAR_HIDDEN = 0;
|
||||||
const STATUS_BAR_EXPANDED = 1;
|
const STATUS_BAR_EXPANDED = 1;
|
||||||
|
@ -46,10 +45,12 @@ export default class RoomStatusBar extends React.Component {
|
||||||
// Used to suggest to the user to invite someone
|
// Used to suggest to the user to invite someone
|
||||||
sentMessageAndIsAlone: PropTypes.bool,
|
sentMessageAndIsAlone: PropTypes.bool,
|
||||||
|
|
||||||
// true if there is an active call in this room (means we show
|
// The active call in the room, if any (means we show the call bar
|
||||||
// the 'Active Call' text in the status bar if there is nothing
|
// along with the status of the call)
|
||||||
// more interesting)
|
callState: PropTypes.string,
|
||||||
hasActiveCall: PropTypes.bool,
|
|
||||||
|
// The type of the call in progress, or null if no call is in progress
|
||||||
|
callType: PropTypes.string,
|
||||||
|
|
||||||
// true if the room is being peeked at. This affects components that shouldn't
|
// true if the room is being peeked at. This affects components that shouldn't
|
||||||
// logically be shown when peeking, such as a prompt to invite people to a room.
|
// logically be shown when peeking, such as a prompt to invite people to a room.
|
||||||
|
@ -121,6 +122,12 @@ export default class RoomStatusBar extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_showCallBar() {
|
||||||
|
return (this.props.callState &&
|
||||||
|
(this.props.callState !== CallState.Ended && this.props.callState !== CallState.Ringing)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_onResendAllClick = () => {
|
_onResendAllClick = () => {
|
||||||
Resend.resendUnsentEvents(this.props.room);
|
Resend.resendUnsentEvents(this.props.room);
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusComposer);
|
||||||
|
@ -153,7 +160,7 @@ export default class RoomStatusBar extends React.Component {
|
||||||
// indicate other sizes.
|
// indicate other sizes.
|
||||||
_getSize() {
|
_getSize() {
|
||||||
if (this._shouldShowConnectionError() ||
|
if (this._shouldShowConnectionError() ||
|
||||||
this.props.hasActiveCall ||
|
this._showCallBar() ||
|
||||||
this.props.sentMessageAndIsAlone
|
this.props.sentMessageAndIsAlone
|
||||||
) {
|
) {
|
||||||
return STATUS_BAR_EXPANDED;
|
return STATUS_BAR_EXPANDED;
|
||||||
|
@ -165,7 +172,7 @@ export default class RoomStatusBar extends React.Component {
|
||||||
|
|
||||||
// return suitable content for the image on the left of the status bar.
|
// return suitable content for the image on the left of the status bar.
|
||||||
_getIndicator() {
|
_getIndicator() {
|
||||||
if (this.props.hasActiveCall) {
|
if (this._showCallBar()) {
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
return (
|
return (
|
||||||
<TintableSvg src={require("../../../res/img/element-icons/room/in-call.svg")} width="23" height="20" />
|
<TintableSvg src={require("../../../res/img/element-icons/room/in-call.svg")} width="23" height="20" />
|
||||||
|
@ -269,6 +276,25 @@ export default class RoomStatusBar extends React.Component {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getCallStatusText() {
|
||||||
|
switch (this.props.callState) {
|
||||||
|
case CallState.CreateOffer:
|
||||||
|
case CallState.InviteSent:
|
||||||
|
return _t('Calling...');
|
||||||
|
case CallState.Connecting:
|
||||||
|
case CallState.CreateAnswer:
|
||||||
|
return _t('Call connecting...');
|
||||||
|
case CallState.Connected:
|
||||||
|
return _t('Active call');
|
||||||
|
case CallState.WaitLocalMedia:
|
||||||
|
if (this.props.callType === CallType.Video) {
|
||||||
|
return _t('Starting camera...');
|
||||||
|
} else {
|
||||||
|
return _t('Starting microphone...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// return suitable content for the main (text) part of the status bar.
|
// return suitable content for the main (text) part of the status bar.
|
||||||
_getContent() {
|
_getContent() {
|
||||||
if (this._shouldShowConnectionError()) {
|
if (this._shouldShowConnectionError()) {
|
||||||
|
@ -291,10 +317,10 @@ export default class RoomStatusBar extends React.Component {
|
||||||
return this._getUnsentMessageContent();
|
return this._getUnsentMessageContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.hasActiveCall) {
|
if (this._showCallBar()) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar_callBar">
|
<div className="mx_RoomStatusBar_callBar">
|
||||||
<b>{ _t('Active call') }</b>
|
<b>{ this._getCallStatusText() }</b>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,7 @@ import RoomHeader from "../views/rooms/RoomHeader";
|
||||||
import TintableSvg from "../views/elements/TintableSvg";
|
import TintableSvg from "../views/elements/TintableSvg";
|
||||||
import {XOR} from "../../@types/common";
|
import {XOR} from "../../@types/common";
|
||||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||||
|
import { CallState, CallType, MatrixCall } from "matrix-js-sdk/lib/webrtc/call";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
let debuglog = function(msg: string) {};
|
let debuglog = function(msg: string) {};
|
||||||
|
@ -141,7 +142,7 @@ export interface IState {
|
||||||
}>;
|
}>;
|
||||||
searchHighlights?: string[];
|
searchHighlights?: string[];
|
||||||
searchInProgress?: boolean;
|
searchInProgress?: boolean;
|
||||||
callState?: string;
|
callState?: CallState;
|
||||||
guestsCanJoin: boolean;
|
guestsCanJoin: boolean;
|
||||||
canPeek: boolean;
|
canPeek: boolean;
|
||||||
showApps: boolean;
|
showApps: boolean;
|
||||||
|
@ -479,7 +480,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const call = this.getCallForRoom();
|
const call = this.getCallForRoom();
|
||||||
const callState = call ? call.call_state : "ended";
|
const callState = call ? call.state : null;
|
||||||
this.setState({
|
this.setState({
|
||||||
callState: callState,
|
callState: callState,
|
||||||
});
|
});
|
||||||
|
@ -712,14 +713,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const call = this.getCallForRoom();
|
const call = this.getCallForRoom();
|
||||||
let callState = "ended";
|
|
||||||
|
|
||||||
if (call) {
|
|
||||||
callState = call.call_state;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
callState: callState,
|
callState: call ? call.state : null,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1605,7 +1601,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
/**
|
/**
|
||||||
* get any current call for this room
|
* get any current call for this room
|
||||||
*/
|
*/
|
||||||
private getCallForRoom() {
|
private getCallForRoom(): MatrixCall {
|
||||||
if (!this.state.room) {
|
if (!this.state.room) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1742,10 +1738,13 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
// We have successfully loaded this room, and are not previewing.
|
// We have successfully loaded this room, and are not previewing.
|
||||||
// Display the "normal" room view.
|
// Display the "normal" room view.
|
||||||
|
|
||||||
const call = this.getCallForRoom();
|
let activeCall = null;
|
||||||
let inCall = false;
|
{
|
||||||
if (call && (this.state.callState !== 'ended' && this.state.callState !== 'ringing')) {
|
// New block because this variable doesn't need to hang around for the rest of the function
|
||||||
inCall = true;
|
const call = this.getCallForRoom();
|
||||||
|
if (call && (this.state.callState !== 'ended' && this.state.callState !== 'ringing')) {
|
||||||
|
activeCall = call;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollheaderClasses = classNames({
|
const scrollheaderClasses = classNames({
|
||||||
|
@ -1764,7 +1763,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
statusBar = <RoomStatusBar
|
statusBar = <RoomStatusBar
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
sentMessageAndIsAlone={this.state.isAlone}
|
sentMessageAndIsAlone={this.state.isAlone}
|
||||||
hasActiveCall={inCall}
|
callState={this.state.callState}
|
||||||
|
callType={activeCall ? activeCall.type : null}
|
||||||
isPeeking={myMembership !== "join"}
|
isPeeking={myMembership !== "join"}
|
||||||
onInviteClick={this.onInviteButtonClick}
|
onInviteClick={this.onInviteButtonClick}
|
||||||
onStopWarningClick={this.onStopAloneWarningClick}
|
onStopWarningClick={this.onStopAloneWarningClick}
|
||||||
|
@ -1853,7 +1853,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
draggingFile={this.state.draggingFile}
|
draggingFile={this.state.draggingFile}
|
||||||
maxHeight={this.state.auxPanelMaxHeight}
|
maxHeight={this.state.auxPanelMaxHeight}
|
||||||
showApps={this.state.showApps}
|
showApps={this.state.showApps}
|
||||||
hideAppsDrawer={false}
|
|
||||||
onResize={this.onResize}
|
onResize={this.onResize}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
>
|
>
|
||||||
|
@ -1890,10 +1889,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inCall) {
|
if (activeCall) {
|
||||||
let zoomButton; let videoMuteButton;
|
let zoomButton; let videoMuteButton;
|
||||||
|
|
||||||
if (call.type === "video") {
|
if (activeCall.type === CallType.Video) {
|
||||||
zoomButton = (
|
zoomButton = (
|
||||||
<div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick} title={_t("Fill screen")}>
|
<div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick} title={_t("Fill screen")}>
|
||||||
<TintableSvg
|
<TintableSvg
|
||||||
|
@ -1908,10 +1907,11 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
videoMuteButton =
|
videoMuteButton =
|
||||||
<div className="mx_RoomView_voipButton" onClick={this.onMuteVideoClick}>
|
<div className="mx_RoomView_voipButton" onClick={this.onMuteVideoClick}>
|
||||||
<TintableSvg
|
<TintableSvg
|
||||||
src={call.isLocalVideoMuted() ?
|
src={activeCall.isLocalVideoMuted() ?
|
||||||
require("../../../res/img/element-icons/call/video-muted.svg") :
|
require("../../../res/img/element-icons/call/video-muted.svg") :
|
||||||
require("../../../res/img/element-icons/call/video-call.svg")}
|
require("../../../res/img/element-icons/call/video-call.svg")}
|
||||||
alt={call.isLocalVideoMuted() ? _t("Click to unmute video") : _t("Click to mute video")}
|
alt={activeCall.isLocalVideoMuted() ? _t("Click to unmute video") :
|
||||||
|
_t("Click to mute video")}
|
||||||
width=""
|
width=""
|
||||||
height="27"
|
height="27"
|
||||||
/>
|
/>
|
||||||
|
@ -1920,10 +1920,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
const voiceMuteButton =
|
const voiceMuteButton =
|
||||||
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
|
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
|
||||||
<TintableSvg
|
<TintableSvg
|
||||||
src={call.isMicrophoneMuted() ?
|
src={activeCall.isMicrophoneMuted() ?
|
||||||
require("../../../res/img/element-icons/call/voice-muted.svg") :
|
require("../../../res/img/element-icons/call/voice-muted.svg") :
|
||||||
require("../../../res/img/element-icons/call/voice-unmuted.svg")}
|
require("../../../res/img/element-icons/call/voice-unmuted.svg")}
|
||||||
alt={call.isMicrophoneMuted() ? _t("Click to unmute audio") : _t("Click to mute audio")}
|
alt={activeCall.isMicrophoneMuted() ? _t("Click to unmute audio") : _t("Click to mute audio")}
|
||||||
width="21"
|
width="21"
|
||||||
height="26"
|
height="26"
|
||||||
/>
|
/>
|
||||||
|
@ -2041,7 +2041,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
|
|
||||||
const mainClasses = classNames("mx_RoomView", {
|
const mainClasses = classNames("mx_RoomView", {
|
||||||
mx_RoomView_inCall: inCall,
|
mx_RoomView_inCall: Boolean(activeCall),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -240,10 +240,14 @@ export default class AppTile extends React.Component {
|
||||||
this.iframe.src = 'about:blank';
|
this.iframe.src = 'about:blank';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||||
|
dis.dispatch({action: 'hangup_conference'});
|
||||||
|
}
|
||||||
|
|
||||||
// Delete the widget from the persisted store for good measure.
|
// Delete the widget from the persisted store for good measure.
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
PersistedElement.destroyElement(this._persistKey);
|
||||||
|
|
||||||
this._sgWidget.stop();
|
this._sgWidget.stop({forceDestroy: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If user has permission to modify widgets, delete the widget,
|
/* If user has permission to modify widgets, delete the widget,
|
||||||
|
@ -387,6 +391,9 @@ export default class AppTile extends React.Component {
|
||||||
if (this.props.show) {
|
if (this.props.show) {
|
||||||
// if we were being shown, end the widget as we're about to be minimized.
|
// if we were being shown, end the widget as we're about to be minimized.
|
||||||
this._endWidgetActions();
|
this._endWidgetActions();
|
||||||
|
} else {
|
||||||
|
// restart the widget actions
|
||||||
|
this._resetWidget(this.props);
|
||||||
}
|
}
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'appsDrawer',
|
action: 'appsDrawer',
|
||||||
|
|
|
@ -58,6 +58,11 @@ export default class PersistentApp extends React.Component {
|
||||||
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
||||||
if (this.state.roomId !== persistentWidgetInRoomId) {
|
if (this.state.roomId !== persistentWidgetInRoomId) {
|
||||||
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
|
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
|
||||||
|
|
||||||
|
// Sanity check the room - the widget may have been destroyed between render cycles, and
|
||||||
|
// thus no room is associated anymore.
|
||||||
|
if (!persistentWidgetInRoom) return null;
|
||||||
|
|
||||||
// get the widget data
|
// get the widget data
|
||||||
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
|
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
|
||||||
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();
|
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();
|
||||||
|
|
|
@ -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']);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -40,12 +40,10 @@ export default class AppsDrawer extends React.Component {
|
||||||
room: PropTypes.object.isRequired,
|
room: PropTypes.object.isRequired,
|
||||||
resizeNotifier: PropTypes.instanceOf(ResizeNotifier).isRequired,
|
resizeNotifier: PropTypes.instanceOf(ResizeNotifier).isRequired,
|
||||||
showApps: PropTypes.bool, // Should apps be rendered
|
showApps: PropTypes.bool, // Should apps be rendered
|
||||||
hide: PropTypes.bool, // If rendered, should apps drawer be visible
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
showApps: true,
|
showApps: true,
|
||||||
hide: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -173,7 +171,6 @@ export default class AppsDrawer extends React.Component {
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
"mx_AppsDrawer": true,
|
"mx_AppsDrawer": true,
|
||||||
"mx_AppsDrawer_hidden": this.props.hide,
|
|
||||||
"mx_AppsDrawer_fullWidth": apps.length < 2,
|
"mx_AppsDrawer_fullWidth": apps.length < 2,
|
||||||
"mx_AppsDrawer_minimised": !this.props.showApps,
|
"mx_AppsDrawer_minimised": !this.props.showApps,
|
||||||
});
|
});
|
||||||
|
|
|
@ -37,7 +37,6 @@ export default class AuxPanel extends React.Component {
|
||||||
room: PropTypes.object.isRequired,
|
room: PropTypes.object.isRequired,
|
||||||
userId: PropTypes.string.isRequired,
|
userId: PropTypes.string.isRequired,
|
||||||
showApps: PropTypes.bool, // Render apps
|
showApps: PropTypes.bool, // Render apps
|
||||||
hideAppsDrawer: PropTypes.bool, // Do not display apps drawer and content (may still be rendered)
|
|
||||||
|
|
||||||
// set to true to show the file drop target
|
// set to true to show the file drop target
|
||||||
draggingFile: PropTypes.bool,
|
draggingFile: PropTypes.bool,
|
||||||
|
@ -54,7 +53,6 @@ export default class AuxPanel extends React.Component {
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
showApps: true,
|
showApps: true,
|
||||||
hideAppsDrawer: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -170,7 +168,6 @@ export default class AuxPanel extends React.Component {
|
||||||
userId={this.props.userId}
|
userId={this.props.userId}
|
||||||
maxHeight={this.props.maxHeight}
|
maxHeight={this.props.maxHeight}
|
||||||
showApps={this.props.showApps}
|
showApps={this.props.showApps}
|
||||||
hide={this.props.hideAppsDrawer}
|
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -657,8 +657,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;
|
||||||
|
|
|
@ -37,6 +37,7 @@ import WidgetStore from "../../../stores/WidgetStore";
|
||||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
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";
|
||||||
|
|
||||||
function ComposerAvatar(props) {
|
function ComposerAvatar(props) {
|
||||||
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
||||||
|
@ -53,7 +54,7 @@ function CallButton(props) {
|
||||||
const onVoiceCallClick = (ev) => {
|
const onVoiceCallClick = (ev) => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'place_call',
|
action: 'place_call',
|
||||||
type: "voice",
|
type: PlaceCallType.Voice,
|
||||||
room_id: props.roomId,
|
room_id: props.roomId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -73,7 +74,7 @@ function VideoCallButton(props) {
|
||||||
const onCallClick = (ev) => {
|
const onCallClick = (ev) => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'place_call',
|
action: 'place_call',
|
||||||
type: ev.shiftKey ? "screensharing" : "video",
|
type: ev.shiftKey ? PlaceCallType.ScreenSharing : PlaceCallType.Video,
|
||||||
room_id: props.roomId,
|
room_id: props.roomId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -24,13 +24,14 @@ import dis from '../../../dispatcher/dispatcher';
|
||||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||||
import PersistentApp from "../elements/PersistentApp";
|
import PersistentApp from "../elements/PersistentApp";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import { CallState, MatrixCall } from 'matrix-js-sdk/lib/webrtc/call';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
activeCall: any;
|
activeCall: MatrixCall;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CallPreview extends React.Component<IProps, IState> {
|
export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
|
@ -84,7 +85,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
if (call) {
|
if (call) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: call.groupRoomId || call.roomId,
|
room_id: call.roomId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -93,7 +94,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
const callForRoom = CallHandler.sharedInstance().getCallForRoom(this.state.roomId);
|
const callForRoom = CallHandler.sharedInstance().getCallForRoom(this.state.roomId);
|
||||||
const showCall = (
|
const showCall = (
|
||||||
this.state.activeCall &&
|
this.state.activeCall &&
|
||||||
this.state.activeCall.call_state === 'connected' &&
|
this.state.activeCall.state === CallState.Connected &&
|
||||||
!callForRoom
|
!callForRoom
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import VideoView from "./VideoView";
|
import VideoView from "./VideoView";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import PulsedAvatar from '../avatars/PulsedAvatar';
|
import PulsedAvatar from '../avatars/PulsedAvatar';
|
||||||
|
import { CallState, MatrixCall } from 'matrix-js-sdk/lib/webrtc/call';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// js-sdk room object. If set, we will only show calls for the given
|
// js-sdk room object. If set, we will only show calls for the given
|
||||||
|
@ -87,7 +88,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private showCall() {
|
private showCall() {
|
||||||
let call;
|
let call: MatrixCall;
|
||||||
|
|
||||||
if (this.props.room) {
|
if (this.props.room) {
|
||||||
const roomId = this.props.room.roomId;
|
const roomId = this.props.room.roomId;
|
||||||
|
@ -120,7 +121,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
|
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (call && call.type === "video" && call.call_state !== "ended" && call.call_state !== "ringing") {
|
if (call && call.type === "video" && call.state !== CallState.Ended && call.state !== CallState.Ringing) {
|
||||||
this.getVideoView().getLocalVideoElement().style.display = "block";
|
this.getVideoView().getLocalVideoElement().style.display = "block";
|
||||||
this.getVideoView().getRemoteVideoElement().style.display = "block";
|
this.getVideoView().getRemoteVideoElement().style.display = "block";
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import CallHandler from '../../../CallHandler';
|
||||||
import PulsedAvatar from '../avatars/PulsedAvatar';
|
import PulsedAvatar from '../avatars/PulsedAvatar';
|
||||||
import RoomAvatar from '../avatars/RoomAvatar';
|
import RoomAvatar from '../avatars/RoomAvatar';
|
||||||
import FormButton from '../elements/FormButton';
|
import FormButton from '../elements/FormButton';
|
||||||
|
import { CallState } from 'matrix-js-sdk/lib/webrtc/call';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
}
|
}
|
||||||
|
@ -53,7 +54,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'call_state': {
|
case 'call_state': {
|
||||||
const call = CallHandler.sharedInstance().getCallForRoom(payload.room_id);
|
const call = CallHandler.sharedInstance().getCallForRoom(payload.room_id);
|
||||||
if (call && call.call_state === 'ringing') {
|
if (call && call.state === CallState.Ringing) {
|
||||||
this.setState({
|
this.setState({
|
||||||
incomingCall: call,
|
incomingCall: call,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -2096,6 +2096,10 @@
|
||||||
"%(count)s of your messages have not been sent.|one": "Your message was not sent.",
|
"%(count)s of your messages have not been sent.|one": "Your message was not sent.",
|
||||||
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|other": "<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.",
|
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|other": "<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.",
|
||||||
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Resend message</resendText> or <cancelText>cancel message</cancelText> now.",
|
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Resend message</resendText> or <cancelText>cancel message</cancelText> now.",
|
||||||
|
"Calling...": "Calling...",
|
||||||
|
"Call connecting...": "Call connecting...",
|
||||||
|
"Starting camera...": "Starting camera...",
|
||||||
|
"Starting microphone...": "Starting microphone...",
|
||||||
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
||||||
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
||||||
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
||||||
|
|
|
@ -105,10 +105,13 @@ export default abstract class BaseEventIndexManager {
|
||||||
/**
|
/**
|
||||||
* Initialize the event index for the given user.
|
* Initialize the event index for the given user.
|
||||||
*
|
*
|
||||||
|
* @param {string} userId The event that should be added to the index.
|
||||||
|
* @param {string} deviceId The profile of the event sender at the
|
||||||
|
*
|
||||||
* @return {Promise} A promise that will resolve when the event index is
|
* @return {Promise} A promise that will resolve when the event index is
|
||||||
* initialized.
|
* initialized.
|
||||||
*/
|
*/
|
||||||
async initEventIndex(): Promise<void> {
|
async initEventIndex(userId: string, deviceId: string): Promise<void> {
|
||||||
throw new Error("Unimplemented");
|
throw new Error("Unimplemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ limitations under the License.
|
||||||
|
|
||||||
import PlatformPeg from "../PlatformPeg";
|
import PlatformPeg from "../PlatformPeg";
|
||||||
import EventIndex from "../indexing/EventIndex";
|
import EventIndex from "../indexing/EventIndex";
|
||||||
|
import {MatrixClientPeg} from "../MatrixClientPeg";
|
||||||
import SettingsStore from '../settings/SettingsStore';
|
import SettingsStore from '../settings/SettingsStore';
|
||||||
import {SettingLevel} from "../settings/SettingLevel";
|
import {SettingLevel} from "../settings/SettingLevel";
|
||||||
|
|
||||||
|
@ -70,9 +71,13 @@ class EventIndexPeg {
|
||||||
async initEventIndex() {
|
async initEventIndex() {
|
||||||
const index = new EventIndex();
|
const index = new EventIndex();
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
const userId = client.getUserId();
|
||||||
|
const deviceId = client.getDeviceId();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await indexManager.initEventIndex();
|
await indexManager.initEventIndex(userId, deviceId);
|
||||||
|
|
||||||
const userVersion = await indexManager.getUserVersion();
|
const userVersion = await indexManager.getUserVersion();
|
||||||
const eventIndexIsEmpty = await indexManager.isEventIndexEmpty();
|
const eventIndexIsEmpty = await indexManager.isEventIndexEmpty();
|
||||||
|
@ -83,7 +88,7 @@ class EventIndexPeg {
|
||||||
await indexManager.closeEventIndex();
|
await indexManager.closeEventIndex();
|
||||||
await this.deleteEventIndex();
|
await this.deleteEventIndex();
|
||||||
|
|
||||||
await indexManager.initEventIndex();
|
await indexManager.initEventIndex(userId, deviceId);
|
||||||
await indexManager.setUserVersion(INDEX_VERSION);
|
await indexManager.setUserVersion(INDEX_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
|
@ -74,6 +74,16 @@ class ElementWidget extends Widget {
|
||||||
return super.templateUrl;
|
return super.templateUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get popoutTemplateUrl(): string {
|
||||||
|
if (WidgetType.JITSI.matches(this.type)) {
|
||||||
|
return WidgetUtils.getLocalJitsiWrapperUrl({
|
||||||
|
forLocalRender: false, // The only important difference between this and templateUrl()
|
||||||
|
auth: super.rawData?.auth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.templateUrl; // use this instead of super to ensure we get appropriate templating
|
||||||
|
}
|
||||||
|
|
||||||
public get rawData(): IWidgetData {
|
public get rawData(): IWidgetData {
|
||||||
let conferenceId = super.rawData['conferenceId'];
|
let conferenceId = super.rawData['conferenceId'];
|
||||||
if (conferenceId === undefined) {
|
if (conferenceId === undefined) {
|
||||||
|
@ -94,8 +104,8 @@ class ElementWidget extends Widget {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCompleteUrl(params: ITemplateParams): string {
|
public getCompleteUrl(params: ITemplateParams, asPopout=false): string {
|
||||||
return runTemplate(this.templateUrl, {
|
return runTemplate(asPopout ? this.popoutTemplateUrl : this.templateUrl, {
|
||||||
// we need to supply a whole widget to the template, but don't have
|
// we need to supply a whole widget to the template, but don't have
|
||||||
// easy access to the definition the superclass is using, so be sad
|
// easy access to the definition the superclass is using, so be sad
|
||||||
// and gutwrench it.
|
// and gutwrench it.
|
||||||
|
@ -109,7 +119,7 @@ class ElementWidget extends Widget {
|
||||||
|
|
||||||
export class StopGapWidget extends EventEmitter {
|
export class StopGapWidget extends EventEmitter {
|
||||||
private messaging: ClientWidgetApi;
|
private messaging: ClientWidgetApi;
|
||||||
private mockWidget: Widget;
|
private mockWidget: ElementWidget;
|
||||||
private scalarToken: string;
|
private scalarToken: string;
|
||||||
|
|
||||||
constructor(private appTileProps: IAppTileProps) {
|
constructor(private appTileProps: IAppTileProps) {
|
||||||
|
@ -133,12 +143,23 @@ export class StopGapWidget extends EventEmitter {
|
||||||
* The URL to use in the iframe
|
* The URL to use in the iframe
|
||||||
*/
|
*/
|
||||||
public get embedUrl(): string {
|
public get embedUrl(): string {
|
||||||
|
return this.runUrlTemplate({asPopout: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to use in the popout
|
||||||
|
*/
|
||||||
|
public get popoutUrl(): string {
|
||||||
|
return this.runUrlTemplate({asPopout: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
private runUrlTemplate(opts = {asPopout: false}): string {
|
||||||
const templated = this.mockWidget.getCompleteUrl({
|
const templated = this.mockWidget.getCompleteUrl({
|
||||||
currentRoomId: RoomViewStore.getRoomId(),
|
currentRoomId: RoomViewStore.getRoomId(),
|
||||||
currentUserId: MatrixClientPeg.get().getUserId(),
|
currentUserId: MatrixClientPeg.get().getUserId(),
|
||||||
userDisplayName: OwnProfileStore.instance.displayName,
|
userDisplayName: OwnProfileStore.instance.displayName,
|
||||||
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
|
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
|
||||||
});
|
}, opts?.asPopout);
|
||||||
|
|
||||||
// Add in some legacy support sprinkles
|
// Add in some legacy support sprinkles
|
||||||
// TODO: Replace these with proper widget params
|
// TODO: Replace these with proper widget params
|
||||||
|
@ -158,19 +179,6 @@ export class StopGapWidget extends EventEmitter {
|
||||||
return parsed.toString().replace(/%24/g, '$');
|
return parsed.toString().replace(/%24/g, '$');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The URL to use in the popout
|
|
||||||
*/
|
|
||||||
public get popoutUrl(): string {
|
|
||||||
if (WidgetType.JITSI.matches(this.mockWidget.type)) {
|
|
||||||
return WidgetUtils.getLocalJitsiWrapperUrl({
|
|
||||||
forLocalRender: false,
|
|
||||||
auth: this.mockWidget.rawData?.auth,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.embedUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get isManagedByManager(): boolean {
|
public get isManagedByManager(): boolean {
|
||||||
return !!this.scalarToken;
|
return !!this.scalarToken;
|
||||||
}
|
}
|
||||||
|
@ -275,8 +283,8 @@ export class StopGapWidget extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public stop() {
|
public stop(opts = {forceDestroy: false}) {
|
||||||
if (ActiveWidgetStore.getPersistentWidgetId() === this.mockWidget.id) {
|
if (!opts?.forceDestroy && ActiveWidgetStore.getPersistentWidgetId() === this.mockWidget.id) {
|
||||||
console.log("Skipping destroy - persistent widget");
|
console.log("Skipping destroy - persistent widget");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -6501,8 +6501,8 @@ mathml-tag-names@^2.0.1:
|
||||||
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
||||||
|
|
||||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
||||||
version "8.4.1"
|
version "8.5.0"
|
||||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/a727da9193e0ccb2fa8d7c3e8e321916f6717190"
|
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d8c4101fdd521e189f4755c6f02a8971b991ef5f"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.11.2"
|
"@babel/runtime" "^7.11.2"
|
||||||
another-json "^0.2.0"
|
another-json "^0.2.0"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue