Merge branch 'develop' into element
						commit
						7115c07c65
					
				| 
						 | 
				
			
			@ -120,6 +120,7 @@
 | 
			
		|||
    "@babel/register": "^7.7.4",
 | 
			
		||||
    "@peculiar/webcrypto": "^1.0.22",
 | 
			
		||||
    "@types/classnames": "^2.2.10",
 | 
			
		||||
    "@types/counterpart": "^0.18.1",
 | 
			
		||||
    "@types/flux": "^3.1.9",
 | 
			
		||||
    "@types/lodash": "^4.14.152",
 | 
			
		||||
    "@types/modernizr": "^3.5.3",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -77,7 +77,7 @@ limitations under the License.
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .mx_RoomTile2_menuButton {
 | 
			
		||||
    .mx_RoomTile2_notificationsButton {
 | 
			
		||||
        margin-left: 4px; // spacing between buttons
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -108,7 +108,8 @@ limitations under the License.
 | 
			
		|||
        width: 20px;
 | 
			
		||||
        min-width: 20px; // yay flex
 | 
			
		||||
        height: 20px;
 | 
			
		||||
        margin: auto 0;
 | 
			
		||||
        margin-top: auto;
 | 
			
		||||
        margin-bottom: auto;
 | 
			
		||||
        position: relative;
 | 
			
		||||
        display: none;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,7 @@ import { IMatrixClientPeg } from "../MatrixClientPeg";
 | 
			
		|||
import ToastStore from "../stores/ToastStore";
 | 
			
		||||
import DeviceListener from "../DeviceListener";
 | 
			
		||||
import { RoomListStore2 } from "../stores/room-list/RoomListStore2";
 | 
			
		||||
import { PlatformPeg } from "../PlatformPeg";
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
    interface Window {
 | 
			
		||||
| 
						 | 
				
			
			@ -33,6 +34,7 @@ declare global {
 | 
			
		|||
        mx_ToastStore: ToastStore;
 | 
			
		||||
        mx_DeviceListener: DeviceListener;
 | 
			
		||||
        mx_RoomListStore2: RoomListStore2;
 | 
			
		||||
        mxPlatformPeg: PlatformPeg;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // workaround for https://github.com/microsoft/TypeScript/issues/30933
 | 
			
		||||
| 
						 | 
				
			
			@ -45,6 +47,10 @@ declare global {
 | 
			
		|||
        hasStorageAccess?: () => Promise<boolean>;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    interface Navigator {
 | 
			
		||||
        userLanguage?: string;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    interface StorageEstimate {
 | 
			
		||||
        usageDetails?: {[key: string]: number};
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -53,6 +53,10 @@ export default abstract class BasePlatform {
 | 
			
		|||
        this.startUpdateCheck = this.startUpdateCheck.bind(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    abstract async getConfig(): Promise<{}>;
 | 
			
		||||
 | 
			
		||||
    abstract getDefaultDeviceDisplayName(): string;
 | 
			
		||||
 | 
			
		||||
    protected onAction = (payload: ActionPayload) => {
 | 
			
		||||
        switch (payload.action) {
 | 
			
		||||
            case 'on_client_not_viable':
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2016 OpenMarket Ltd
 | 
			
		||||
Copyright 2020 The Matrix.org Foundation C.I.C.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
| 
						 | 
				
			
			@ -14,6 +15,8 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import BasePlatform from "./BasePlatform";
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Holds the current Platform object used by the code to do anything
 | 
			
		||||
 * specific to the platform we're running on (eg. web, electron)
 | 
			
		||||
| 
						 | 
				
			
			@ -21,10 +24,8 @@ limitations under the License.
 | 
			
		|||
 * This allows the app layer to set a Platform without necessarily
 | 
			
		||||
 * having to have a MatrixChat object
 | 
			
		||||
 */
 | 
			
		||||
class PlatformPeg {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.platform = null;
 | 
			
		||||
    }
 | 
			
		||||
export class PlatformPeg {
 | 
			
		||||
    platform: BasePlatform = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the current Platform object for the application.
 | 
			
		||||
| 
						 | 
				
			
			@ -39,12 +40,12 @@ class PlatformPeg {
 | 
			
		|||
     * application.
 | 
			
		||||
     * This should be an instance of a class extending BasePlatform.
 | 
			
		||||
     */
 | 
			
		||||
    set(plaf) {
 | 
			
		||||
    set(plaf: BasePlatform) {
 | 
			
		||||
        this.platform = plaf;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (!global.mxPlatformPeg) {
 | 
			
		||||
    global.mxPlatformPeg = new PlatformPeg();
 | 
			
		||||
if (!window.mxPlatformPeg) {
 | 
			
		||||
    window.mxPlatformPeg = new PlatformPeg();
 | 
			
		||||
}
 | 
			
		||||
export default global.mxPlatformPeg;
 | 
			
		||||
export default window.mxPlatformPeg;
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +32,7 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
 | 
			
		|||
import SettingsStore from "../../settings/SettingsStore";
 | 
			
		||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore2";
 | 
			
		||||
import {Key} from "../../Keyboard";
 | 
			
		||||
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
 | 
			
		||||
 | 
			
		||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
 | 
			
		||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
 | 
			
		||||
| 
						 | 
				
			
			@ -124,6 +125,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
 | 
			
		|||
            if (slRect.top + headerHeight > bottom && !gotBottom) {
 | 
			
		||||
                header.classList.add("mx_RoomSublist2_headerContainer_sticky");
 | 
			
		||||
                header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
 | 
			
		||||
                header.style.width = `${headerStickyWidth}px`;
 | 
			
		||||
                header.style.removeProperty("top");
 | 
			
		||||
                gotBottom = true;
 | 
			
		||||
            } else if ((slRect.top - (headerHeight / 3)) < top) {
 | 
			
		||||
| 
						 | 
				
			
			@ -140,6 +142,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
 | 
			
		|||
                header.classList.remove("mx_RoomSublist2_headerContainer_sticky");
 | 
			
		||||
                header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
 | 
			
		||||
                header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
 | 
			
		||||
                header.style.removeProperty("width");
 | 
			
		||||
                header.style.removeProperty("top");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -221,11 +224,14 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
 | 
			
		|||
 | 
			
		||||
    private renderHeader(): React.ReactNode {
 | 
			
		||||
        let breadcrumbs;
 | 
			
		||||
        if (this.state.showBreadcrumbs) {
 | 
			
		||||
        if (this.state.showBreadcrumbs && !this.props.isMinimized) {
 | 
			
		||||
            breadcrumbs = (
 | 
			
		||||
                <div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer mx_AutoHideScrollbar">
 | 
			
		||||
                    {this.props.isMinimized ? null : <RoomBreadcrumbs2 />}
 | 
			
		||||
                </div>
 | 
			
		||||
                <IndicatorScrollbar
 | 
			
		||||
                    className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer mx_AutoHideScrollbar"
 | 
			
		||||
                    verticalScrollsHorizontally={true}
 | 
			
		||||
                >
 | 
			
		||||
                    <RoomBreadcrumbs2 />
 | 
			
		||||
                </IndicatorScrollbar>
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -270,6 +276,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
 | 
			
		|||
            onFocus={this.onFocus}
 | 
			
		||||
            onBlur={this.onBlur}
 | 
			
		||||
            isMinimized={this.props.isMinimized}
 | 
			
		||||
            onResize={this.onResize}
 | 
			
		||||
        />;
 | 
			
		||||
 | 
			
		||||
        // TODO: Conference handling / calls: https://github.com/vector-im/riot-web/issues/14177
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,7 @@ limitations under the License.
 | 
			
		|||
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import {_t} from '../../../languageHandler.js';
 | 
			
		||||
import {_t} from '../../../languageHandler';
 | 
			
		||||
import Field from "./Field";
 | 
			
		||||
import AccessibleButton from "./AccessibleButton";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -51,6 +51,7 @@ interface IProps {
 | 
			
		|||
    onKeyDown: (ev: React.KeyboardEvent) => void;
 | 
			
		||||
    onFocus: (ev: React.FocusEvent) => void;
 | 
			
		||||
    onBlur: (ev: React.FocusEvent) => void;
 | 
			
		||||
    onResize: () => void;
 | 
			
		||||
    resizeNotifier: ResizeNotifier;
 | 
			
		||||
    collapsed: boolean;
 | 
			
		||||
    searchFilter: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -183,14 +184,16 @@ export default class RoomList2 extends React.Component<IProps, IState> {
 | 
			
		|||
            layoutMap.set(tagId, new ListLayout(tagId));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.setState({sublists: newLists, layouts: layoutMap});
 | 
			
		||||
        this.setState({sublists: newLists, layouts: layoutMap}, () => {
 | 
			
		||||
            this.props.onResize();
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private renderCommunityInvites(): React.ReactElement[] {
 | 
			
		||||
        // TODO: Put community invites in a more sensible place (not in the room list)
 | 
			
		||||
        return MatrixClientPeg.get().getGroups().filter(g => {
 | 
			
		||||
           if (g.myMembership !== 'invite') return false;
 | 
			
		||||
           return !this.searchFilter || this.searchFilter.matches(g.name);
 | 
			
		||||
           return !this.searchFilter || this.searchFilter.matches(g.name || "");
 | 
			
		||||
        }).map(g => {
 | 
			
		||||
            const avatar = (
 | 
			
		||||
                <GroupAvatar
 | 
			
		||||
| 
						 | 
				
			
			@ -256,6 +259,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
 | 
			
		|||
                    isInvite={aesthetics.isInvite}
 | 
			
		||||
                    layout={this.state.layouts.get(orderedTagId)}
 | 
			
		||||
                    isMinimized={this.props.isMinimized}
 | 
			
		||||
                    onResize={this.props.onResize}
 | 
			
		||||
                    extraBadTilesThatShouldntExist={extraTiles}
 | 
			
		||||
                />
 | 
			
		||||
            );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -66,6 +66,7 @@ interface IProps {
 | 
			
		|||
    layout: ListLayout;
 | 
			
		||||
    isMinimized: boolean;
 | 
			
		||||
    tagId: TagID;
 | 
			
		||||
    onResize: () => void;
 | 
			
		||||
 | 
			
		||||
    // TODO: Don't use this. It's for community invites, and community invites shouldn't be here.
 | 
			
		||||
    // You should feel bad if you use this.
 | 
			
		||||
| 
						 | 
				
			
			@ -228,6 +229,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
 | 
			
		|||
    private toggleCollapsed = () => {
 | 
			
		||||
        this.props.layout.isCollapsed = !this.props.layout.isCollapsed;
 | 
			
		||||
        this.forceUpdate(); // because the layout doesn't trigger an update
 | 
			
		||||
        setImmediate(() => this.props.onResize()); // needs to happen when the DOM is updated
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -440,8 +440,8 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
 | 
			
		|||
                            {roomAvatar}
 | 
			
		||||
                            {nameContainer}
 | 
			
		||||
                            {badge}
 | 
			
		||||
                            {this.renderNotificationsMenu(isActive)}
 | 
			
		||||
                            {this.renderGeneralMenu()}
 | 
			
		||||
                            {this.renderNotificationsMenu(isActive)}
 | 
			
		||||
                        </AccessibleButton>
 | 
			
		||||
                    }
 | 
			
		||||
                </RovingTabIndexWrapper>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import React, {ReactChild} from "react";
 | 
			
		||||
import React, {ReactNode} from "react";
 | 
			
		||||
 | 
			
		||||
import FormButton from "../elements/FormButton";
 | 
			
		||||
import {XOR} from "../../../@types/common";
 | 
			
		||||
 | 
			
		||||
export interface IProps {
 | 
			
		||||
    description: ReactChild;
 | 
			
		||||
    description: ReactNode;
 | 
			
		||||
    acceptLabel: string;
 | 
			
		||||
 | 
			
		||||
    onAccept();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,8 @@ limitations under the License.
 | 
			
		|||
*/
 | 
			
		||||
 | 
			
		||||
import { createContext } from "react";
 | 
			
		||||
import { MatrixClient } from "matrix-js-sdk/src/client";
 | 
			
		||||
 | 
			
		||||
const MatrixClientContext = createContext(undefined);
 | 
			
		||||
const MatrixClientContext = createContext<MatrixClient>(undefined);
 | 
			
		||||
MatrixClientContext.displayName = "MatrixClientContext";
 | 
			
		||||
export default MatrixClientContext;
 | 
			
		||||
| 
						 | 
				
			
			@ -15,6 +15,9 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
import {MatrixClient} from "matrix-js-sdk/src/client";
 | 
			
		||||
import {Room} from "matrix-js-sdk/src/models/room";
 | 
			
		||||
 | 
			
		||||
import {MatrixClientPeg} from './MatrixClientPeg';
 | 
			
		||||
import Modal from './Modal';
 | 
			
		||||
import * as sdk from './index';
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +29,56 @@ import {getAddressType} from "./UserAddress";
 | 
			
		|||
 | 
			
		||||
const E2EE_WK_KEY = "im.vector.riot.e2ee";
 | 
			
		||||
 | 
			
		||||
// TODO move these interfaces over to js-sdk once it has been typescripted enough to accept them
 | 
			
		||||
enum Visibility {
 | 
			
		||||
    Public = "public",
 | 
			
		||||
    Private = "private",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum Preset {
 | 
			
		||||
    PrivateChat = "private_chat",
 | 
			
		||||
    TrustedPrivateChat = "trusted_private_chat",
 | 
			
		||||
    PublicChat = "public_chat",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Invite3PID {
 | 
			
		||||
    id_server: string;
 | 
			
		||||
    id_access_token?: string; // this gets injected by the js-sdk
 | 
			
		||||
    medium: string;
 | 
			
		||||
    address: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IStateEvent {
 | 
			
		||||
    type: string;
 | 
			
		||||
    state_key?: string; // defaults to an empty string
 | 
			
		||||
    content: object;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface ICreateOpts {
 | 
			
		||||
    visibility?: Visibility;
 | 
			
		||||
    room_alias_name?: string;
 | 
			
		||||
    name?: string;
 | 
			
		||||
    topic?: string;
 | 
			
		||||
    invite?: string[];
 | 
			
		||||
    invite_3pid?: Invite3PID[];
 | 
			
		||||
    room_version?: string;
 | 
			
		||||
    creation_content?: object;
 | 
			
		||||
    initial_state?: IStateEvent[];
 | 
			
		||||
    preset?: Preset;
 | 
			
		||||
    is_direct?: boolean;
 | 
			
		||||
    power_level_content_override?: object;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IOpts {
 | 
			
		||||
    dmUserId?: string;
 | 
			
		||||
    createOpts?: ICreateOpts;
 | 
			
		||||
    spinner?: boolean;
 | 
			
		||||
    guestAccess?: boolean;
 | 
			
		||||
    encryption?: boolean;
 | 
			
		||||
    inlineErrors?: boolean;
 | 
			
		||||
    andView?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Create a new room, and switch to it.
 | 
			
		||||
 *
 | 
			
		||||
| 
						 | 
				
			
			@ -40,11 +93,12 @@ const E2EE_WK_KEY = "im.vector.riot.e2ee";
 | 
			
		|||
 *     Default: False
 | 
			
		||||
 * @param {bool=} opts.inlineErrors True to raise errors off the promise instead of resolving to null.
 | 
			
		||||
 *     Default: False
 | 
			
		||||
 * @param {bool=} opts.andView True to dispatch an action to view the room once it has been created.
 | 
			
		||||
 *
 | 
			
		||||
 * @returns {Promise} which resolves to the room id, or null if the
 | 
			
		||||
 * action was aborted or failed.
 | 
			
		||||
 */
 | 
			
		||||
export default function createRoom(opts) {
 | 
			
		||||
export default function createRoom(opts: IOpts): Promise<string | null> {
 | 
			
		||||
    opts = opts || {};
 | 
			
		||||
    if (opts.spinner === undefined) opts.spinner = true;
 | 
			
		||||
    if (opts.guestAccess === undefined) opts.guestAccess = true;
 | 
			
		||||
| 
						 | 
				
			
			@ -59,12 +113,12 @@ export default function createRoom(opts) {
 | 
			
		|||
        return Promise.resolve(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const defaultPreset = opts.dmUserId ? 'trusted_private_chat' : 'private_chat';
 | 
			
		||||
    const defaultPreset = opts.dmUserId ? Preset.TrustedPrivateChat : Preset.PrivateChat;
 | 
			
		||||
 | 
			
		||||
    // set some defaults for the creation
 | 
			
		||||
    const createOpts = opts.createOpts || {};
 | 
			
		||||
    createOpts.preset = createOpts.preset || defaultPreset;
 | 
			
		||||
    createOpts.visibility = createOpts.visibility || 'private';
 | 
			
		||||
    createOpts.visibility = createOpts.visibility || Visibility.Private;
 | 
			
		||||
    if (opts.dmUserId && createOpts.invite === undefined) {
 | 
			
		||||
        switch (getAddressType(opts.dmUserId)) {
 | 
			
		||||
            case 'mx-user-id':
 | 
			
		||||
| 
						 | 
				
			
			@ -166,7 +220,7 @@ export default function createRoom(opts) {
 | 
			
		|||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function findDMForUser(client, userId) {
 | 
			
		||||
export function findDMForUser(client: MatrixClient, userId: string): Room {
 | 
			
		||||
    const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
 | 
			
		||||
    const rooms = roomIds.map(id => client.getRoom(id));
 | 
			
		||||
    const suitableDMRooms = rooms.filter(r => {
 | 
			
		||||
| 
						 | 
				
			
			@ -189,7 +243,7 @@ export function findDMForUser(client, userId) {
 | 
			
		|||
 * NOTE: this assumes you've just created the room and there's not been an opportunity
 | 
			
		||||
 * for other code to run, so we shouldn't miss RoomState.newMember when it comes by.
 | 
			
		||||
 */
 | 
			
		||||
export async function _waitForMember(client, roomId, userId, opts = { timeout: 1500 }) {
 | 
			
		||||
export async function _waitForMember(client: MatrixClient, roomId: string, userId: string, opts = { timeout: 1500 }) {
 | 
			
		||||
    const { timeout } = opts;
 | 
			
		||||
    let handler;
 | 
			
		||||
    return new Promise((resolve) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -212,7 +266,7 @@ export async function _waitForMember(client, roomId, userId, opts = { timeout: 1
 | 
			
		|||
 * Ensure that for every user in a room, there is at least one device that we
 | 
			
		||||
 * can encrypt to.
 | 
			
		||||
 */
 | 
			
		||||
export async function canEncryptToAllUsers(client, userIds) {
 | 
			
		||||
export async function canEncryptToAllUsers(client: MatrixClient, userIds: string[]) {
 | 
			
		||||
    const usersDeviceMap = await client.downloadKeys(userIds);
 | 
			
		||||
    // { "@user:host": { "DEVICE": {...}, ... }, ... }
 | 
			
		||||
    return Object.values(usersDeviceMap).every((userDevices) =>
 | 
			
		||||
| 
						 | 
				
			
			@ -221,7 +275,7 @@ export async function canEncryptToAllUsers(client, userIds) {
 | 
			
		|||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function ensureDMExists(client, userId) {
 | 
			
		||||
export async function ensureDMExists(client: MatrixClient, userId: string): Promise<string> {
 | 
			
		||||
    const existingDMRoom = findDMForUser(client, userId);
 | 
			
		||||
    let roomId;
 | 
			
		||||
    if (existingDMRoom) {
 | 
			
		||||
| 
						 | 
				
			
			@ -15,7 +15,8 @@ limitations under the License.
 | 
			
		|||
*/
 | 
			
		||||
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { _t } from './languageHandler.js';
 | 
			
		||||
 | 
			
		||||
import { _t } from './languageHandler';
 | 
			
		||||
 | 
			
		||||
export const GroupMemberType = PropTypes.shape({
 | 
			
		||||
    userId: PropTypes.string.isRequired,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2017 MTRNord and Cooperative EITA
 | 
			
		||||
Copyright 2017 Vector Creations Ltd.
 | 
			
		||||
Copyright 2019 The Matrix.org Foundation C.I.C.
 | 
			
		||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
 | 
			
		||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
| 
						 | 
				
			
			@ -20,10 +20,11 @@ limitations under the License.
 | 
			
		|||
import request from 'browser-request';
 | 
			
		||||
import counterpart from 'counterpart';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
 | 
			
		||||
import PlatformPeg from "./PlatformPeg";
 | 
			
		||||
 | 
			
		||||
// $webapp is a webpack resolve alias pointing to the output directory, see webpack config
 | 
			
		||||
// @ts-ignore - $webapp is a webpack resolve alias pointing to the output directory, see webpack config
 | 
			
		||||
import webpackLangJsonUrl from "$webapp/i18n/languages.json";
 | 
			
		||||
 | 
			
		||||
const i18nFolder = 'i18n/';
 | 
			
		||||
| 
						 | 
				
			
			@ -37,27 +38,31 @@ counterpart.setSeparator('|');
 | 
			
		|||
// Fall back to English
 | 
			
		||||
counterpart.setFallbackLocale('en');
 | 
			
		||||
 | 
			
		||||
interface ITranslatableError extends Error {
 | 
			
		||||
    translatedMessage: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Helper function to create an error which has an English message
 | 
			
		||||
 * with a translatedMessage property for use by the consumer.
 | 
			
		||||
 * @param {string} message Message to translate.
 | 
			
		||||
 * @returns {Error} The constructed error.
 | 
			
		||||
 */
 | 
			
		||||
export function newTranslatableError(message) {
 | 
			
		||||
    const error = new Error(message);
 | 
			
		||||
export function newTranslatableError(message: string) {
 | 
			
		||||
    const error = new Error(message) as ITranslatableError;
 | 
			
		||||
    error.translatedMessage = _t(message);
 | 
			
		||||
    return error;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Function which only purpose is to mark that a string is translatable
 | 
			
		||||
// Does not actually do anything. It's helpful for automatic extraction of translatable strings
 | 
			
		||||
export function _td(s) {
 | 
			
		||||
export function _td(s: string): string {
 | 
			
		||||
    return s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Wrapper for counterpart's translation function so that it handles nulls and undefineds properly
 | 
			
		||||
// Takes the same arguments as counterpart.translate()
 | 
			
		||||
function safeCounterpartTranslate(text, options) {
 | 
			
		||||
function safeCounterpartTranslate(text: string, options?: object) {
 | 
			
		||||
    // Horrible hack to avoid https://github.com/vector-im/riot-web/issues/4191
 | 
			
		||||
    // The interpolation library that counterpart uses does not support undefined/null
 | 
			
		||||
    // values and instead will throw an error. This is a problem since everywhere else
 | 
			
		||||
| 
						 | 
				
			
			@ -89,6 +94,13 @@ function safeCounterpartTranslate(text, options) {
 | 
			
		|||
    return translated;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IVariables {
 | 
			
		||||
    count?: number;
 | 
			
		||||
    [key: string]: number | string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Tags = Record<string, (sub: string) => React.ReactNode>;
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Translates text and optionally also replaces XML-ish elements in the text with e.g. React components
 | 
			
		||||
 * @param {string} text The untranslated text, e.g "click <a>here</a> now to %(foo)s".
 | 
			
		||||
| 
						 | 
				
			
			@ -105,7 +117,9 @@ function safeCounterpartTranslate(text, options) {
 | 
			
		|||
 *
 | 
			
		||||
 * @return a React <span> component if any non-strings were used in substitutions, otherwise a string
 | 
			
		||||
 */
 | 
			
		||||
export function _t(text, variables, tags) {
 | 
			
		||||
export function _t(text: string, variables?: IVariables): string;
 | 
			
		||||
export function _t(text: string, variables: IVariables, tags: Tags): React.ReactNode;
 | 
			
		||||
export function _t(text: string, variables?: IVariables, tags?: Tags): string | React.ReactNode {
 | 
			
		||||
    // Don't do substitutions in counterpart. We handle it ourselves so we can replace with React components
 | 
			
		||||
    // However, still pass the variables to counterpart so that it can choose the correct plural if count is given
 | 
			
		||||
    // It is enough to pass the count variable, but in the future counterpart might make use of other information too
 | 
			
		||||
| 
						 | 
				
			
			@ -141,23 +155,25 @@ export function _t(text, variables, tags) {
 | 
			
		|||
 *
 | 
			
		||||
 * @return a React <span> component if any non-strings were used in substitutions, otherwise a string
 | 
			
		||||
 */
 | 
			
		||||
export function substitute(text, variables, tags) {
 | 
			
		||||
    let result = text;
 | 
			
		||||
export function substitute(text: string, variables?: IVariables): string;
 | 
			
		||||
export function substitute(text: string, variables: IVariables, tags: Tags): string;
 | 
			
		||||
export function substitute(text: string, variables?: IVariables, tags?: Tags): string | React.ReactNode {
 | 
			
		||||
    let result: React.ReactNode | string = text;
 | 
			
		||||
 | 
			
		||||
    if (variables !== undefined) {
 | 
			
		||||
        const regexpMapping = {};
 | 
			
		||||
        const regexpMapping: IVariables = {};
 | 
			
		||||
        for (const variable in variables) {
 | 
			
		||||
            regexpMapping[`%\\(${variable}\\)s`] = variables[variable];
 | 
			
		||||
        }
 | 
			
		||||
        result = replaceByRegexes(result, regexpMapping);
 | 
			
		||||
        result = replaceByRegexes(result as string, regexpMapping);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (tags !== undefined) {
 | 
			
		||||
        const regexpMapping = {};
 | 
			
		||||
        const regexpMapping: Tags = {};
 | 
			
		||||
        for (const tag in tags) {
 | 
			
		||||
            regexpMapping[`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`] = tags[tag];
 | 
			
		||||
        }
 | 
			
		||||
        result = replaceByRegexes(result, regexpMapping);
 | 
			
		||||
        result = replaceByRegexes(result as string, regexpMapping);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
| 
						 | 
				
			
			@ -172,7 +188,9 @@ export function substitute(text, variables, tags) {
 | 
			
		|||
 *
 | 
			
		||||
 * @return a React <span> component if any non-strings were used in substitutions, otherwise a string
 | 
			
		||||
 */
 | 
			
		||||
export function replaceByRegexes(text, mapping) {
 | 
			
		||||
export function replaceByRegexes(text: string, mapping: IVariables): string;
 | 
			
		||||
export function replaceByRegexes(text: string, mapping: Tags): React.ReactNode;
 | 
			
		||||
export function replaceByRegexes(text: string, mapping: IVariables | Tags): string | React.ReactNode {
 | 
			
		||||
    // We initially store our output as an array of strings and objects (e.g. React components).
 | 
			
		||||
    // This will then be converted to a string or a <span> at the end
 | 
			
		||||
    const output = [text];
 | 
			
		||||
| 
						 | 
				
			
			@ -189,7 +207,7 @@ export function replaceByRegexes(text, mapping) {
 | 
			
		|||
        // and everything after the match. Insert all three into the output. We need to do this because we can insert objects.
 | 
			
		||||
        // Otherwise there would be no need for the splitting and we could do simple replacement.
 | 
			
		||||
        let matchFoundSomewhere = false; // If we don't find a match anywhere we want to log it
 | 
			
		||||
        for (const outputIndex in output) {
 | 
			
		||||
        for (let outputIndex = 0; outputIndex < output.length; outputIndex++) {
 | 
			
		||||
            const inputText = output[outputIndex];
 | 
			
		||||
            if (typeof inputText !== 'string') { // We might have inserted objects earlier, don't try to replace them
 | 
			
		||||
                continue;
 | 
			
		||||
| 
						 | 
				
			
			@ -216,7 +234,7 @@ export function replaceByRegexes(text, mapping) {
 | 
			
		|||
                let replaced;
 | 
			
		||||
                // If substitution is a function, call it
 | 
			
		||||
                if (mapping[regexpString] instanceof Function) {
 | 
			
		||||
                    replaced = mapping[regexpString].apply(null, capturedGroups);
 | 
			
		||||
                    replaced = (mapping as Tags)[regexpString].apply(null, capturedGroups);
 | 
			
		||||
                } else {
 | 
			
		||||
                    replaced = mapping[regexpString];
 | 
			
		||||
                }
 | 
			
		||||
| 
						 | 
				
			
			@ -277,11 +295,11 @@ export function replaceByRegexes(text, mapping) {
 | 
			
		|||
// Allow overriding the text displayed when no translation exists
 | 
			
		||||
// Currently only used in unit tests to avoid having to load
 | 
			
		||||
// the translations in riot-web
 | 
			
		||||
export function setMissingEntryGenerator(f) {
 | 
			
		||||
export function setMissingEntryGenerator(f: (value: string) => void) {
 | 
			
		||||
    counterpart.setMissingEntryGenerator(f);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function setLanguage(preferredLangs) {
 | 
			
		||||
export function setLanguage(preferredLangs: string | string[]) {
 | 
			
		||||
    if (!Array.isArray(preferredLangs)) {
 | 
			
		||||
        preferredLangs = [preferredLangs];
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -358,8 +376,8 @@ export function getLanguageFromBrowser() {
 | 
			
		|||
 * @param {string} language The input language string
 | 
			
		||||
 * @return {string[]} List of normalised languages
 | 
			
		||||
 */
 | 
			
		||||
export function getNormalizedLanguageKeys(language) {
 | 
			
		||||
    const languageKeys = [];
 | 
			
		||||
export function getNormalizedLanguageKeys(language: string) {
 | 
			
		||||
    const languageKeys: string[] = [];
 | 
			
		||||
    const normalizedLanguage = normalizeLanguageKey(language);
 | 
			
		||||
    const languageParts = normalizedLanguage.split('-');
 | 
			
		||||
    if (languageParts.length === 2 && languageParts[0] === languageParts[1]) {
 | 
			
		||||
| 
						 | 
				
			
			@ -380,7 +398,7 @@ export function getNormalizedLanguageKeys(language) {
 | 
			
		|||
 * @param {string} language The language string to be normalized
 | 
			
		||||
 * @returns {string} The normalized language string
 | 
			
		||||
 */
 | 
			
		||||
export function normalizeLanguageKey(language) {
 | 
			
		||||
export function normalizeLanguageKey(language: string) {
 | 
			
		||||
    return language.toLowerCase().replace("_", "-");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -396,7 +414,7 @@ export function getCurrentLanguage() {
 | 
			
		|||
 * @param {string[]} langs List of language codes to pick from
 | 
			
		||||
 * @returns {string} The most appropriate language code from langs
 | 
			
		||||
 */
 | 
			
		||||
export function pickBestLanguage(langs) {
 | 
			
		||||
export function pickBestLanguage(langs: string[]): string {
 | 
			
		||||
    const currentLang = getCurrentLanguage();
 | 
			
		||||
    const normalisedLangs = langs.map(normalizeLanguageKey);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -408,13 +426,13 @@ export function pickBestLanguage(langs) {
 | 
			
		|||
 | 
			
		||||
    {
 | 
			
		||||
        // Failing that, a different dialect of the same language
 | 
			
		||||
        const closeLangIndex = normalisedLangs.find((l) => l.substr(0, 2) === currentLang.substr(0, 2));
 | 
			
		||||
        const closeLangIndex = normalisedLangs.findIndex((l) => l.substr(0, 2) === currentLang.substr(0, 2));
 | 
			
		||||
        if (closeLangIndex > -1) return langs[closeLangIndex];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
        // Neither of those? Try an english variant.
 | 
			
		||||
        const enIndex = normalisedLangs.find((l) => l.startsWith('en'));
 | 
			
		||||
        const enIndex = normalisedLangs.findIndex((l) => l.startsWith('en'));
 | 
			
		||||
        if (enIndex > -1) return langs[enIndex];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -422,7 +440,7 @@ export function pickBestLanguage(langs) {
 | 
			
		|||
    return langs[0];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getLangsJson() {
 | 
			
		||||
function getLangsJson(): Promise<object> {
 | 
			
		||||
    return new Promise(async (resolve, reject) => {
 | 
			
		||||
        let url;
 | 
			
		||||
        if (typeof(webpackLangJsonUrl) === 'string') { // in Jest this 'url' isn't a URL, so just fall through
 | 
			
		||||
| 
						 | 
				
			
			@ -443,7 +461,7 @@ function getLangsJson() {
 | 
			
		|||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function weblateToCounterpart(inTrs) {
 | 
			
		||||
function weblateToCounterpart(inTrs: object): object {
 | 
			
		||||
    const outTrs = {};
 | 
			
		||||
 | 
			
		||||
    for (const key of Object.keys(inTrs)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -463,7 +481,7 @@ function weblateToCounterpart(inTrs) {
 | 
			
		|||
    return outTrs;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getLanguage(langPath) {
 | 
			
		||||
function getLanguage(langPath: string): object {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
        request(
 | 
			
		||||
            { method: "GET", url: langPath },
 | 
			
		||||
| 
						 | 
				
			
			@ -15,7 +15,7 @@ limitations under the License.
 | 
			
		|||
*/
 | 
			
		||||
 | 
			
		||||
// Returns a promise which resolves with a given value after the given number of ms
 | 
			
		||||
export function sleep<T>(ms: number, value: T): Promise<T> {
 | 
			
		||||
export function sleep<T>(ms: number, value?: T): Promise<T> {
 | 
			
		||||
    return new Promise((resolve => { setTimeout(resolve, ms, value); }));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1257,6 +1257,11 @@
 | 
			
		|||
  resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.10.tgz#cc658ca319b6355399efc1f5b9e818f1a24bf999"
 | 
			
		||||
  integrity sha512-1UzDldn9GfYYEsWWnn/P4wkTlkZDH7lDb0wBMGbtIQc9zXEQq7FlKBdZUn6OBqD8sKZZ2RQO2mAjGpXiDGoRmQ==
 | 
			
		||||
 | 
			
		||||
"@types/counterpart@^0.18.1":
 | 
			
		||||
  version "0.18.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.1.tgz#b1b784d9e54d9879f0a8cb12f2caedab65430fe8"
 | 
			
		||||
  integrity sha512-PRuFlBBkvdDOtxlIASzTmkEFar+S66Ek48NVVTWMUjtJAdn5vyMSN8y6IZIoIymGpR36q2nZbIYazBWyFxL+IQ==
 | 
			
		||||
 | 
			
		||||
"@types/fbemitter@*":
 | 
			
		||||
  version "2.0.32"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/fbemitter/-/fbemitter-2.0.32.tgz#8ed204da0f54e9c8eaec31b1eec91e25132d082c"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue