Merge pull request #6085 from matrix-org/gsouquet/15451

This commit is contained in:
Germain 2021-05-25 11:58:46 +01:00 committed by GitHub
commit 9525dc3202
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 160 additions and 65 deletions

View file

@ -22,6 +22,7 @@ import SdkConfig from './SdkConfig';
import {MatrixClientPeg} from "./MatrixClientPeg"; import {MatrixClientPeg} from "./MatrixClientPeg";
import {sleep} from "./utils/promise"; import {sleep} from "./utils/promise";
import RoomViewStore from "./stores/RoomViewStore"; import RoomViewStore from "./stores/RoomViewStore";
import { Action } from "./dispatcher/actions";
// polyfill textencoder if necessary // polyfill textencoder if necessary
import * as TextEncodingUtf8 from 'text-encoding-utf-8'; import * as TextEncodingUtf8 from 'text-encoding-utf-8';
@ -265,7 +266,7 @@ interface ICreateRoomEvent extends IEvent {
} }
interface IJoinRoomEvent extends IEvent { interface IJoinRoomEvent extends IEvent {
key: "join_room"; key: Action.JoinRoom;
dur: number; // how long it took to join (until remote echo) dur: number; // how long it took to join (until remote echo)
segmentation: { segmentation: {
room_id: string; // hashed room_id: string; // hashed
@ -858,7 +859,7 @@ export default class CountlyAnalytics {
} }
public trackRoomJoin(startTime: number, roomId: string, type: IJoinRoomEvent["segmentation"]["type"]) { public trackRoomJoin(startTime: number, roomId: string, type: IJoinRoomEvent["segmentation"]["type"]) {
this.track<IJoinRoomEvent>("join_room", { type }, roomId, { this.track<IJoinRoomEvent>(Action.JoinRoom, { type }, roomId, {
dur: CountlyAnalytics.getTimestamp() - startTime, dur: CountlyAnalytics.getTimestamp() - startTime,
}); });
} }

View file

@ -1114,7 +1114,8 @@ export default class RoomView extends React.Component<IProps, IState> {
Promise.resolve().then(() => { Promise.resolve().then(() => {
const signUrl = this.props.threepidInvite?.signUrl; const signUrl = this.props.threepidInvite?.signUrl;
dis.dispatch({ dis.dispatch({
action: 'join_room', action: Action.JoinRoom,
roomId: this.getRoomId(),
opts: { inviteSignUrl: signUrl }, opts: { inviteSignUrl: signUrl },
_type: "unknown", // TODO: instrumentation _type: "unknown", // TODO: instrumentation
}); });

View file

@ -57,7 +57,8 @@ import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore"; import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
import RoomName from "../views/elements/RoomName"; import RoomName from "../views/elements/RoomName";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import InlineSpinner from "../views/elements/InlineSpinner";
import TooltipButton from "../views/elements/TooltipButton";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
} }
@ -68,6 +69,7 @@ interface IState {
contextMenuPosition: PartialDOMRect; contextMenuPosition: PartialDOMRect;
isDarkTheme: boolean; isDarkTheme: boolean;
selectedSpace?: Room; selectedSpace?: Room;
pendingRoomJoin: string[]
} }
@replaceableComponent("structures.UserMenu") @replaceableComponent("structures.UserMenu")
@ -84,6 +86,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.state = { this.state = {
contextMenuPosition: null, contextMenuPosition: null,
isDarkTheme: this.isUserOnDarkTheme(), isDarkTheme: this.isUserOnDarkTheme(),
pendingRoomJoin: [],
}; };
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
@ -147,15 +150,48 @@ export default class UserMenu extends React.Component<IProps, IState> {
}; };
private onAction = (ev: ActionPayload) => { private onAction = (ev: ActionPayload) => {
if (ev.action !== Action.ToggleUserMenu) return; // not interested switch (ev.action) {
case Action.ToggleUserMenu:
if (this.state.contextMenuPosition) { if (this.state.contextMenuPosition) {
this.setState({contextMenuPosition: null}); this.setState({contextMenuPosition: null});
} else { } else {
if (this.buttonRef.current) this.buttonRef.current.click(); if (this.buttonRef.current) this.buttonRef.current.click();
}
break;
case Action.JoinRoom:
this.addPendingJoinRoom(ev.roomId);
break;
case Action.JoinRoomReady:
case Action.JoinRoomError:
this.removePendingJoinRoom(ev.roomId);
break;
} }
}; };
private addPendingJoinRoom(roomId) {
this.setState({
pendingRoomJoin: [
...this.state.pendingRoomJoin,
roomId,
],
});
}
private removePendingJoinRoom(roomId) {
const newPendingRoomJoin = this.state.pendingRoomJoin.filter(pendingJoinRoomId => {
return pendingJoinRoomId !== roomId;
});
if (newPendingRoomJoin.length !== this.state.pendingRoomJoin.length) {
this.setState({
pendingRoomJoin: newPendingRoomJoin,
})
}
}
get hasPendingActions(): boolean {
return this.state.pendingRoomJoin.length > 0;
}
private onOpenMenuClick = (ev: React.MouseEvent) => { private onOpenMenuClick = (ev: React.MouseEvent) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
@ -617,6 +653,14 @@ export default class UserMenu extends React.Component<IProps, IState> {
/> />
</span> </span>
{name} {name}
{this.hasPendingActions && (
<InlineSpinner>
<TooltipButton helpText={_t(
"Currently joining %(count)s rooms",
{ count: this.state.pendingRoomJoin.length },
)} />
</InlineSpinner>
)}
{dnd} {dnd}
{buttons} {buttons}
</div> </div>

View file

@ -18,19 +18,29 @@ import React from "react";
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.InlineSpinner") interface IProps {
export default class InlineSpinner extends React.Component { w?: number;
render() { h?: number;
const w = this.props.w || 16; children?: React.ReactNode;
const h = this.props.h || 16; }
@replaceableComponent("views.elements.InlineSpinner")
export default class InlineSpinner extends React.PureComponent<IProps> {
static defaultProps = {
w: 16,
h: 16,
}
render() {
return ( return (
<div className="mx_InlineSpinner"> <div className="mx_InlineSpinner">
<div <div
className="mx_InlineSpinner_icon mx_Spinner_icon" className="mx_InlineSpinner_icon mx_Spinner_icon"
style={{width: w, height: h}} style={{width: this.props.w, height: this.props.h}}
aria-label={_t("Loading...")} aria-label={_t("Loading...")}
></div> >
{this.props.children}
</div>
</div> </div>
); );
} }

View file

@ -19,19 +19,30 @@ import React from 'react';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.TooltipButton") interface IProps {
export default class TooltipButton extends React.Component { helpText: string;
state = { }
hover: false,
};
onMouseOver = () => { interface IState {
hover: boolean;
}
@replaceableComponent("views.elements.TooltipButton")
export default class TooltipButton extends React.Component<IProps, IState> {
constructor(props) {
super(props);
this.state = {
hover: false,
};
}
private onMouseOver = () => {
this.setState({ this.setState({
hover: true, hover: true,
}); });
}; };
onMouseLeave = () => { private onMouseLeave = () => {
this.setState({ this.setState({
hover: false, hover: false,
}); });

View file

@ -34,6 +34,7 @@ import { isJoinedOrNearlyJoined } from "./utils/membership";
import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler"; import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
import SpaceStore from "./stores/SpaceStore"; import SpaceStore from "./stores/SpaceStore";
import { makeSpaceParentEvent } from "./utils/space"; import { makeSpaceParentEvent } from "./utils/space";
import { Action } from "./dispatcher/actions"
// we define a number of interfaces which take their names from the js-sdk // we define a number of interfaces which take their names from the js-sdk
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@ -243,7 +244,8 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
// We also failed to join the room (this sets joining to false in RoomViewStore) // We also failed to join the room (this sets joining to false in RoomViewStore)
dis.dispatch({ dis.dispatch({
action: 'join_room_error', action: Action.JoinRoomError,
roomId,
}); });
console.error("Failed to create room " + roomId + " " + err); console.error("Failed to create room " + roomId + " " + err);
let description = _t("Server may be unavailable, overloaded, or you hit a bug."); let description = _t("Server may be unavailable, overloaded, or you hit a bug.");

View file

@ -138,4 +138,19 @@ export enum Action {
* Fired when an upload is cancelled by the user. Should be used with UploadCanceledPayload. * Fired when an upload is cancelled by the user. Should be used with UploadCanceledPayload.
*/ */
UploadCanceled = "upload_canceled", UploadCanceled = "upload_canceled",
/**
* Fired when requesting to join a room
*/
JoinRoom = "join_room",
/**
* Fired when successfully joining a room
*/
JoinRoomReady = "join_room_ready",
/**
* Fired when joining a room failed
*/
JoinRoomError = "join_room",
} }

View file

@ -2753,6 +2753,8 @@
"Switch theme": "Switch theme", "Switch theme": "Switch theme",
"User menu": "User menu", "User menu": "User menu",
"Community and user menu": "Community and user menu", "Community and user menu": "Community and user menu",
"Currently joining %(count)s rooms|other": "Currently joining %(count)s rooms",
"Currently joining %(count)s rooms|one": "Currently joining %(count)s room",
"Could not load user profile": "Could not load user profile", "Could not load user profile": "Could not load user profile",
"Decrypted event source": "Decrypted event source", "Decrypted event source": "Decrypted event source",
"Original event source": "Original event source", "Original event source": "Original event source",

View file

@ -17,17 +17,18 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import {Store} from 'flux/utils'; import { Store } from 'flux/utils';
import {MatrixError} from "matrix-js-sdk/src/http-api"; import { MatrixError } from "matrix-js-sdk/src/http-api";
import dis from '../dispatcher/dispatcher'; import dis from '../dispatcher/dispatcher';
import {MatrixClientPeg} from '../MatrixClientPeg'; import { MatrixClientPeg } from '../MatrixClientPeg';
import * as sdk from '../index'; import * as sdk from '../index';
import Modal from '../Modal'; import Modal from '../Modal';
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
import { getCachedRoomIDForAlias, storeRoomAliasInCache } from '../RoomAliasCache'; import { getCachedRoomIDForAlias, storeRoomAliasInCache } from '../RoomAliasCache';
import {ActionPayload} from "../dispatcher/payloads"; import { ActionPayload } from "../dispatcher/payloads";
import {retry} from "../utils/promise"; import { Action } from "../dispatcher/actions";
import { retry } from "../utils/promise";
import CountlyAnalytics from "../CountlyAnalytics"; import CountlyAnalytics from "../CountlyAnalytics";
const NUM_JOIN_RETRY = 5; const NUM_JOIN_RETRY = 5;
@ -136,13 +137,13 @@ class RoomViewStore extends Store<ActionPayload> {
break; break;
// join_room: // join_room:
// - opts: options for joinRoom // - opts: options for joinRoom
case 'join_room': case Action.JoinRoom:
this.joinRoom(payload); this.joinRoom(payload);
break; break;
case 'join_room_error': case Action.JoinRoomError:
this.joinRoomError(payload); this.joinRoomError(payload);
break; break;
case 'join_room_ready': case Action.JoinRoomReady:
this.setState({ shouldPeek: false }); this.setState({ shouldPeek: false });
break; break;
case 'on_client_not_viable': case 'on_client_not_viable':
@ -217,7 +218,11 @@ class RoomViewStore extends Store<ActionPayload> {
this.setState(newState); this.setState(newState);
if (payload.auto_join) { if (payload.auto_join) {
this.joinRoom(payload); dis.dispatch({
...payload,
action: Action.JoinRoom,
roomId: payload.room_id,
});
} }
} else if (payload.room_alias) { } else if (payload.room_alias) {
// Try the room alias to room ID navigation cache first to avoid // Try the room alias to room ID navigation cache first to avoid
@ -298,41 +303,16 @@ class RoomViewStore extends Store<ActionPayload> {
// We do *not* clear the 'joining' flag because the Room object and/or our 'joined' member event may not // We do *not* clear the 'joining' flag because the Room object and/or our 'joined' member event may not
// have come down the sync stream yet, and that's the point at which we'd consider the user joined to the // have come down the sync stream yet, and that's the point at which we'd consider the user joined to the
// room. // room.
dis.dispatch({ action: 'join_room_ready' }); dis.dispatch({
action: Action.JoinRoomReady,
roomId: this.state.roomId,
});
} catch (err) { } catch (err) {
dis.dispatch({ dis.dispatch({
action: 'join_room_error', action: Action.JoinRoomError,
roomId: this.state.roomId,
err: err, err: err,
}); });
let msg = err.message ? err.message : JSON.stringify(err);
console.log("Failed to join room:", msg);
if (err.name === "ConnectionError") {
msg = _t("There was an error joining the room");
} else if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') {
msg = <div>
{_t("Sorry, your homeserver is too old to participate in this room.")}<br />
{_t("Please contact your homeserver administrator.")}
</div>;
} else if (err.httpStatus === 404) {
const invitingUserId = this.getInvitingUserId(this.state.roomId);
// only provide a better error message for invites
if (invitingUserId) {
// if the inviting user is on the same HS, there can only be one cause: they left.
if (invitingUserId.endsWith(`:${MatrixClientPeg.get().getDomain()}`)) {
msg = _t("The person who invited you already left the room.");
} else {
msg = _t("The person who invited you already left the room, or their server is offline.");
}
}
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, {
title: _t("Failed to join room"),
description: msg,
});
} }
} }
@ -351,6 +331,35 @@ class RoomViewStore extends Store<ActionPayload> {
joining: false, joining: false,
joinError: payload.err, joinError: payload.err,
}); });
const err = payload.err;
let msg = err.message ? err.message : JSON.stringify(err);
console.log("Failed to join room:", msg);
if (err.name === "ConnectionError") {
msg = _t("There was an error joining the room");
} else if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') {
msg = <div>
{_t("Sorry, your homeserver is too old to participate in this room.")}<br />
{_t("Please contact your homeserver administrator.")}
</div>;
} else if (err.httpStatus === 404) {
const invitingUserId = this.getInvitingUserId(this.state.roomId);
// only provide a better error message for invites
if (invitingUserId) {
// if the inviting user is on the same HS, there can only be one cause: they left.
if (invitingUserId.endsWith(`:${MatrixClientPeg.get().getDomain()}`)) {
msg = _t("The person who invited you already left the room.");
} else {
msg = _t("The person who invited you already left the room, or their server is offline.");
}
}
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, {
title: _t("Failed to join room"),
description: msg,
});
} }
public reset() { public reset() {