Merge branches 'develop' and 't3chguy/room-list/123' of github.com:matrix-org/matrix-react-sdk into t3chguy/room-list/123
Conflicts: src/components/views/rooms/RoomSublist2.tsxpull/21833/head
commit
69852ecef4
|
@ -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> {
|
|||
const headerStickyWidth = rlRect.width - headerRightMargin;
|
||||
|
||||
let gotBottom = false;
|
||||
let lastTopHeader;
|
||||
for (const sublist of sublists) {
|
||||
const slRect = sublist.getBoundingClientRect();
|
||||
|
||||
|
@ -133,19 +135,25 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
|||
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
|
||||
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
|
||||
header.style.width = `${headerStickyWidth}px`;
|
||||
header.style.top = `unset`;
|
||||
header.style.removeProperty("top");
|
||||
gotBottom = true;
|
||||
} else if (slRect.top < top) {
|
||||
} else if ((slRect.top - (headerHeight / 3)) < top) {
|
||||
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
|
||||
header.classList.add("mx_RoomSublist2_headerContainer_stickyTop");
|
||||
header.style.width = `${headerStickyWidth}px`;
|
||||
header.style.top = `${rlRect.top}px`;
|
||||
if (lastTopHeader) {
|
||||
lastTopHeader.style.display = "none";
|
||||
}
|
||||
// first unset it, if set in last iteration
|
||||
header.style.removeProperty("display");
|
||||
lastTopHeader = header;
|
||||
} else {
|
||||
header.classList.remove("mx_RoomSublist2_headerContainer_sticky");
|
||||
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
|
||||
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
|
||||
header.style.width = `unset`;
|
||||
header.style.top = `unset`;
|
||||
header.style.removeProperty("width");
|
||||
header.style.removeProperty("top");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -223,11 +231,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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -277,6 +288,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;
|
||||
|
@ -63,8 +64,6 @@ interface IState {
|
|||
}
|
||||
|
||||
const TAG_ORDER: TagID[] = [
|
||||
// -- Community Invites Placeholder --
|
||||
|
||||
DefaultTagID.Invite,
|
||||
DefaultTagID.Favourite,
|
||||
DefaultTagID.DM,
|
||||
|
@ -76,7 +75,6 @@ const TAG_ORDER: TagID[] = [
|
|||
DefaultTagID.ServerNotice,
|
||||
DefaultTagID.Archived,
|
||||
];
|
||||
const COMMUNITY_TAGS_BEFORE_TAG = DefaultTagID.Invite;
|
||||
const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority;
|
||||
const ALWAYS_VISIBLE_TAGS: TagID[] = [
|
||||
DefaultTagID.DM,
|
||||
|
@ -183,14 +181,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
|
||||
|
@ -224,17 +224,15 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
|||
const components: React.ReactElement[] = [];
|
||||
|
||||
for (const orderedTagId of TAG_ORDER) {
|
||||
if (COMMUNITY_TAGS_BEFORE_TAG === orderedTagId) {
|
||||
// Populate community invites if we have the chance
|
||||
// TODO: Community invites: https://github.com/vector-im/riot-web/issues/14179
|
||||
}
|
||||
if (CUSTOM_TAGS_BEFORE_TAG === orderedTagId) {
|
||||
// Populate custom tags if needed
|
||||
// TODO: Custom tags: https://github.com/vector-im/riot-web/issues/14091
|
||||
}
|
||||
|
||||
const orderedRooms = this.state.sublists[orderedTagId] || [];
|
||||
if (orderedRooms.length === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) {
|
||||
const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null;
|
||||
const totalTiles = orderedRooms.length + (extraTiles ? extraTiles.length : 0);
|
||||
if (totalTiles === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) {
|
||||
continue; // skip tag - not needed
|
||||
}
|
||||
|
||||
|
@ -242,7 +240,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
|||
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
||||
|
||||
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
|
||||
const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null;
|
||||
components.push(
|
||||
<RoomSublist2
|
||||
key={`sublist-${orderedTagId}`}
|
||||
|
@ -256,6 +253,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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -40,6 +40,7 @@ import NotificationBadge from "./NotificationBadge";
|
|||
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { Key } from "../../../Keyboard";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
|
||||
// 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
|
||||
|
@ -68,6 +69,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.
|
||||
|
@ -233,6 +235,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) => {
|
||||
|
@ -283,10 +286,6 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
|
||||
const tiles: React.ReactElement[] = [];
|
||||
|
||||
if (this.props.extraBadTilesThatShouldntExist) {
|
||||
tiles.push(...this.props.extraBadTilesThatShouldntExist);
|
||||
}
|
||||
|
||||
if (this.props.rooms) {
|
||||
const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles);
|
||||
for (const room of visibleRooms) {
|
||||
|
@ -302,6 +301,10 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.props.extraBadTilesThatShouldntExist) {
|
||||
tiles.push(...this.props.extraBadTilesThatShouldntExist);
|
||||
}
|
||||
|
||||
// We only have to do this because of the extra tiles. We do it conditionally
|
||||
// to avoid spending cycles on slicing. It's generally fine to do this though
|
||||
// as users are unlikely to have more than a handful of tiles when the extra
|
||||
|
@ -314,15 +317,42 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private renderMenu(): React.ReactElement {
|
||||
// TODO: Get a proper invite context menu, or take invites out of the room list.
|
||||
if (this.props.tagId === DefaultTagID.Invite) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let contextMenu = null;
|
||||
if (this.state.contextMenuPosition) {
|
||||
const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
|
||||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
||||
|
||||
// Invites don't get some nonsense options, so only add them if we have to.
|
||||
let otherSections = null;
|
||||
if (this.props.tagId !== DefaultTagID.Invite) {
|
||||
otherSections = (
|
||||
<React.Fragment>
|
||||
<hr />
|
||||
<div>
|
||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
|
||||
<StyledMenuItemCheckbox
|
||||
onClose={this.onCloseMenu}
|
||||
onChange={this.onUnreadFirstChanged}
|
||||
checked={isUnreadFirst}
|
||||
>
|
||||
{_t("Always show first")}
|
||||
</StyledMenuItemCheckbox>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
|
||||
<StyledMenuItemCheckbox
|
||||
onClose={this.onCloseMenu}
|
||||
onChange={this.onMessagePreviewChanged}
|
||||
checked={this.props.layout.showPreviews}
|
||||
>
|
||||
{_t("Message preview")}
|
||||
</StyledMenuItemCheckbox>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
contextMenu = (
|
||||
<ContextMenu
|
||||
chevronFace="none"
|
||||
|
@ -350,28 +380,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
{_t("A-Z")}
|
||||
</StyledMenuItemRadio>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
|
||||
<StyledMenuItemCheckbox
|
||||
onClose={this.onCloseMenu}
|
||||
onChange={this.onUnreadFirstChanged}
|
||||
checked={isUnreadFirst}
|
||||
>
|
||||
{_t("Always show first")}
|
||||
</StyledMenuItemCheckbox>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
|
||||
<StyledMenuItemCheckbox
|
||||
onClose={this.onCloseMenu}
|
||||
onChange={this.onMessagePreviewChanged}
|
||||
checked={this.props.layout.showPreviews}
|
||||
>
|
||||
{_t("Message preview")}
|
||||
</StyledMenuItemCheckbox>
|
||||
</div>
|
||||
{otherSections}
|
||||
</div>
|
||||
</ContextMenu>
|
||||
);
|
||||
|
|
|
@ -499,8 +499,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,
|
||||
|
|
|
@ -1200,13 +1200,13 @@
|
|||
"Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Securely back up your keys to avoid losing them. <a>Learn more.</a>",
|
||||
"Not now": "Not now",
|
||||
"Don't ask me again": "Don't ask me again",
|
||||
"Sort by": "Sort by",
|
||||
"Activity": "Activity",
|
||||
"A-Z": "A-Z",
|
||||
"Unread rooms": "Unread rooms",
|
||||
"Always show first": "Always show first",
|
||||
"Show": "Show",
|
||||
"Message preview": "Message preview",
|
||||
"Sort by": "Sort by",
|
||||
"Activity": "Activity",
|
||||
"A-Z": "A-Z",
|
||||
"List options": "List options",
|
||||
"Jump to first unread room.": "Jump to first unread room.",
|
||||
"Jump to first invite.": "Jump to first invite.",
|
||||
|
|
|
@ -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 },
|
|
@ -85,8 +85,8 @@ export class ListLayout {
|
|||
}
|
||||
|
||||
public get defaultVisibleTiles(): number {
|
||||
// 10 is what "feels right", and mostly subject to design's opinion.
|
||||
return 10 + RESIZER_BOX_FACTOR;
|
||||
// This number is what "feels right", and mostly subject to design's opinion.
|
||||
return 5 + RESIZER_BOX_FACTOR;
|
||||
}
|
||||
|
||||
public setVisibleTilesWithin(diff: number, maxPossible: number) {
|
||||
|
|
|
@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
|||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { EventEmitter } from "events";
|
||||
import { arrayHasDiff, ArrayUtil } from "../../../utils/arrays";
|
||||
import { arrayDiff, arrayHasDiff, ArrayUtil } from "../../../utils/arrays";
|
||||
import { getEnumValues } from "../../../utils/enums";
|
||||
import { DefaultTagID, RoomUpdateCause, TagID } from "../models";
|
||||
import {
|
||||
|
@ -57,6 +57,7 @@ export class Algorithm extends EventEmitter {
|
|||
private _cachedStickyRooms: ITagMap = {}; // a clone of the _cachedRooms, with the sticky room
|
||||
private filteredRooms: ITagMap = {};
|
||||
private _stickyRoom: IStickyRoom = null;
|
||||
private _lastStickyRoom: IStickyRoom = null; // only not-null when changing the sticky room
|
||||
private sortAlgorithms: ITagSortingMap;
|
||||
private listAlgorithms: IListOrderingMap;
|
||||
private algorithms: IOrderingAlgorithmMap;
|
||||
|
@ -162,9 +163,21 @@ export class Algorithm extends EventEmitter {
|
|||
}
|
||||
|
||||
private async updateStickyRoom(val: Room) {
|
||||
try {
|
||||
return await this.doUpdateStickyRoom(val);
|
||||
} finally {
|
||||
this._lastStickyRoom = null; // clear to indicate we're done changing
|
||||
}
|
||||
}
|
||||
|
||||
private async doUpdateStickyRoom(val: Room) {
|
||||
// Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
|
||||
// otherwise we risk duplicating rooms.
|
||||
|
||||
// Set the last sticky room to indicate that we're in a change. The code throughout the
|
||||
// class can safely handle a null room, so this should be safe to do as a backup.
|
||||
this._lastStickyRoom = this._stickyRoom || <IStickyRoom>{};
|
||||
|
||||
// It's possible to have no selected room. In that case, clear the sticky room
|
||||
if (!val) {
|
||||
if (this._stickyRoom) {
|
||||
|
@ -179,7 +192,7 @@ export class Algorithm extends EventEmitter {
|
|||
}
|
||||
|
||||
// When we do have a room though, we expect to be able to find it
|
||||
const tag = this.roomIdsToTags[val.roomId][0];
|
||||
let tag = this.roomIdsToTags[val.roomId][0];
|
||||
if (!tag) throw new Error(`${val.roomId} does not belong to a tag and cannot be sticky`);
|
||||
|
||||
// We specifically do NOT use the ordered rooms set as it contains the sticky room, which
|
||||
|
@ -196,19 +209,41 @@ export class Algorithm extends EventEmitter {
|
|||
// the same thing it no-ops. After we're done calling the algorithm, we'll issue
|
||||
// a new update for ourselves.
|
||||
const lastStickyRoom = this._stickyRoom;
|
||||
this._stickyRoom = null;
|
||||
this._stickyRoom = null; // clear before we update the algorithm
|
||||
this.recalculateStickyRoom();
|
||||
|
||||
// When we do have the room, re-add the old room (if needed) to the algorithm
|
||||
// and remove the sticky room from the algorithm. This is so the underlying
|
||||
// algorithm doesn't try and confuse itself with the sticky room concept.
|
||||
if (lastStickyRoom) {
|
||||
// We don't add the new room if the sticky room isn't changing because that's
|
||||
// an easy way to cause duplication. We have to do room ID checks instead of
|
||||
// referential checks as the references can differ through the lifecycle.
|
||||
if (lastStickyRoom && lastStickyRoom.room && lastStickyRoom.room.roomId !== val.roomId) {
|
||||
// Lie to the algorithm and re-add the room to the algorithm
|
||||
await this.handleRoomUpdate(lastStickyRoom.room, RoomUpdateCause.NewRoom);
|
||||
}
|
||||
// Lie to the algorithm and remove the room from it's field of view
|
||||
await this.handleRoomUpdate(val, RoomUpdateCause.RoomRemoved);
|
||||
|
||||
// Check for tag & position changes while we're here. We also check the room to ensure
|
||||
// it is still the same room.
|
||||
if (this._stickyRoom) {
|
||||
if (this._stickyRoom.room !== val) {
|
||||
// Check the room IDs just in case
|
||||
if (this._stickyRoom.room.roomId === val.roomId) {
|
||||
console.warn("Sticky room changed references");
|
||||
} else {
|
||||
throw new Error("Sticky room changed while the sticky room was changing");
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(`Sticky room changed tag & position from ${tag} / ${position} `
|
||||
+ `to ${this._stickyRoom.tag} / ${this._stickyRoom.position}`);
|
||||
|
||||
tag = this._stickyRoom.tag;
|
||||
position = this._stickyRoom.position;
|
||||
}
|
||||
|
||||
// Now that we're done lying to the algorithm, we need to update our position
|
||||
// marker only if the user is moving further down the same list. If they're switching
|
||||
// lists, or moving upwards, the position marker will splice in just fine but if
|
||||
|
@ -560,7 +595,7 @@ export class Algorithm extends EventEmitter {
|
|||
/**
|
||||
* Updates the roomsToTags map
|
||||
*/
|
||||
protected updateTagsFromCache() {
|
||||
private updateTagsFromCache() {
|
||||
const newMap = {};
|
||||
|
||||
const tags = Object.keys(this.cachedRooms);
|
||||
|
@ -607,21 +642,94 @@ export class Algorithm extends EventEmitter {
|
|||
* processing.
|
||||
*/
|
||||
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||
console.log(`Handle room update for ${room.roomId} called with cause ${cause}`);
|
||||
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
|
||||
|
||||
// Note: check the isSticky against the room ID just in case the reference is wrong
|
||||
const isSticky = this._stickyRoom && this._stickyRoom.room && this._stickyRoom.room.roomId === room.roomId;
|
||||
if (cause === RoomUpdateCause.NewRoom) {
|
||||
const isForLastSticky = this._lastStickyRoom && this._lastStickyRoom.room === room;
|
||||
const roomTags = this.roomIdsToTags[room.roomId];
|
||||
if (roomTags && roomTags.length > 0) {
|
||||
const hasTags = roomTags && roomTags.length > 0;
|
||||
|
||||
// Don't change the cause if the last sticky room is being re-added. If we fail to
|
||||
// pass the cause through as NewRoom, we'll fail to lie to the algorithm and thus
|
||||
// lose the room.
|
||||
if (hasTags && !isForLastSticky) {
|
||||
console.warn(`${room.roomId} is reportedly new but is already known - assuming TagChange instead`);
|
||||
cause = RoomUpdateCause.PossibleTagChange;
|
||||
}
|
||||
|
||||
// If we have tags for a room and don't have the room referenced, the room reference
|
||||
// probably changed. We need to swap out the problematic reference.
|
||||
if (hasTags && !this.rooms.includes(room) && !isSticky) {
|
||||
console.warn(`${room.roomId} is missing from room array but is known - trying to find duplicate`);
|
||||
this.rooms = this.rooms.map(r => r.roomId === room.roomId ? room : r);
|
||||
|
||||
// Sanity check
|
||||
if (!this.rooms.includes(room)) {
|
||||
throw new Error(`Failed to replace ${room.roomId} with an updated reference`);
|
||||
}
|
||||
}
|
||||
|
||||
// Like above, update the reference to the sticky room if we need to
|
||||
if (hasTags && isSticky) {
|
||||
// Go directly in and set the sticky room's new reference, being careful not
|
||||
// to trigger a sticky room update ourselves.
|
||||
this._stickyRoom.room = room;
|
||||
}
|
||||
}
|
||||
|
||||
if (cause === RoomUpdateCause.PossibleTagChange) {
|
||||
// TODO: Be smarter and splice rather than regen the planet. https://github.com/vector-im/riot-web/issues/14035
|
||||
// TODO: No-op if no change. https://github.com/vector-im/riot-web/issues/14035
|
||||
await this.setKnownRooms(this.rooms);
|
||||
return true;
|
||||
let didTagChange = false;
|
||||
const oldTags = this.roomIdsToTags[room.roomId] || [];
|
||||
const newTags = this.getTagsForRoom(room);
|
||||
const diff = arrayDiff(oldTags, newTags);
|
||||
if (diff.removed.length > 0 || diff.added.length > 0) {
|
||||
for (const rmTag of diff.removed) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||
console.log(`Removing ${room.roomId} from ${rmTag}`);
|
||||
const algorithm: OrderingAlgorithm = this.algorithms[rmTag];
|
||||
if (!algorithm) throw new Error(`No algorithm for ${rmTag}`);
|
||||
await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
|
||||
}
|
||||
for (const addTag of diff.added) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||
console.log(`Adding ${room.roomId} to ${addTag}`);
|
||||
const algorithm: OrderingAlgorithm = this.algorithms[addTag];
|
||||
if (!algorithm) throw new Error(`No algorithm for ${addTag}`);
|
||||
await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom);
|
||||
}
|
||||
|
||||
// Update the tag map so we don't regen it in a moment
|
||||
this.roomIdsToTags[room.roomId] = newTags;
|
||||
|
||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||
console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`);
|
||||
cause = RoomUpdateCause.Timeline;
|
||||
didTagChange = true;
|
||||
} else {
|
||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||
console.warn(`Received no-op update for ${room.roomId} - changing to Timeline update`);
|
||||
cause = RoomUpdateCause.Timeline;
|
||||
}
|
||||
|
||||
if (didTagChange && isSticky) {
|
||||
// Manually update the tag for the sticky room without triggering a sticky room
|
||||
// update. The update will be handled implicitly by the sticky room handling and
|
||||
// requires no changes on our part, if we're in the middle of a sticky room change.
|
||||
if (this._lastStickyRoom) {
|
||||
this._stickyRoom = {
|
||||
room,
|
||||
tag: this.roomIdsToTags[room.roomId][0],
|
||||
position: 0, // right at the top as it changed tags
|
||||
};
|
||||
} else {
|
||||
// We have to clear the lock as the sticky room change will trigger updates.
|
||||
await this.setStickyRoomAsync(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the update is for a room change which might be the sticky room, prevent it. We
|
||||
|
@ -635,8 +743,9 @@ export class Algorithm extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
if (cause === RoomUpdateCause.NewRoom && !this.roomIdsToTags[room.roomId]) {
|
||||
console.log(`[RoomListDebug] Updating tags for new room ${room.roomId} (${room.name})`);
|
||||
if (!this.roomIdsToTags[room.roomId]) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||
console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`);
|
||||
|
||||
// Get the tags for the room and populate the cache
|
||||
const roomTags = this.getTagsForRoom(room).filter(t => !isNullOrUndefined(this.cachedRooms[t]));
|
||||
|
@ -646,9 +755,15 @@ export class Algorithm extends EventEmitter {
|
|||
if (!roomTags.length) throw new Error(`Tags cannot be determined for ${room.roomId}`);
|
||||
|
||||
this.roomIdsToTags[room.roomId] = roomTags;
|
||||
|
||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||
console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags);
|
||||
}
|
||||
|
||||
let tags = this.roomIdsToTags[room.roomId];
|
||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||
console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`);
|
||||
|
||||
const tags = this.roomIdsToTags[room.roomId];
|
||||
if (!tags) {
|
||||
console.warn(`No tags known for "${room.name}" (${room.roomId})`);
|
||||
return false;
|
||||
|
@ -668,6 +783,8 @@ export class Algorithm extends EventEmitter {
|
|||
changed = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||
console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`);
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import { TagID } from "../../models";
|
|||
import { IAlgorithm } from "./IAlgorithm";
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
import * as Unread from "../../../../Unread";
|
||||
import { EffectiveMembership, getEffectiveMembership } from "../../membership";
|
||||
|
||||
/**
|
||||
* Sorts rooms according to the last event's timestamp in each room that seems
|
||||
|
@ -37,6 +38,8 @@ export class RecentAlgorithm implements IAlgorithm {
|
|||
// actually changed (probably needs to be done higher up?) then we could do an
|
||||
// insertion sort or similar on the limited set of changes.
|
||||
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
|
||||
const tsCache: { [roomId: string]: number } = {};
|
||||
const getLastTs = (r: Room) => {
|
||||
if (tsCache[r.roomId]) {
|
||||
|
@ -50,13 +53,23 @@ export class RecentAlgorithm implements IAlgorithm {
|
|||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
// If the room hasn't been joined yet, it probably won't have a timeline to
|
||||
// parse. We'll still fall back to the timeline if this fails, but chances
|
||||
// are we'll at least have our own membership event to go off of.
|
||||
const effectiveMembership = getEffectiveMembership(r.getMyMembership());
|
||||
if (effectiveMembership !== EffectiveMembership.Join) {
|
||||
const membershipEvent = r.currentState.getStateEvents("m.room.member", myUserId);
|
||||
if (membershipEvent && !Array.isArray(membershipEvent)) {
|
||||
return membershipEvent.getTs();
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = r.timeline.length - 1; i >= 0; --i) {
|
||||
const ev = r.timeline[i];
|
||||
if (!ev.getTs()) continue; // skip events that don't have timestamps (tests only?)
|
||||
|
||||
// TODO: Don't assume we're using the same client as the peg
|
||||
if (ev.getSender() === MatrixClientPeg.get().getUserId()
|
||||
|| Unread.eventTriggersUnreadCount(ev)) {
|
||||
if (ev.getSender() === myUserId || Unread.eventTriggersUnreadCount(ev)) {
|
||||
return ev.getTs();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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