Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Add spinner in UserMenu to list pending long running actions #6085

Merged
merged 6 commits into from
May 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/CountlyAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import SdkConfig from './SdkConfig';
import {MatrixClientPeg} from "./MatrixClientPeg";
import {sleep} from "./utils/promise";
import RoomViewStore from "./stores/RoomViewStore";
import { Action } from "./dispatcher/actions";

// polyfill textencoder if necessary
import * as TextEncodingUtf8 from 'text-encoding-utf-8';
Expand Down Expand Up @@ -265,7 +266,7 @@ interface ICreateRoomEvent extends IEvent {
}

interface IJoinRoomEvent extends IEvent {
key: "join_room";
key: Action.JoinRoom;
dur: number; // how long it took to join (until remote echo)
segmentation: {
room_id: string; // hashed
Expand Down Expand Up @@ -858,7 +859,7 @@ export default class CountlyAnalytics {
}

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,
});
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/structures/RoomView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1114,7 +1114,8 @@ export default class RoomView extends React.Component<IProps, IState> {
Promise.resolve().then(() => {
const signUrl = this.props.threepidInvite?.signUrl;
dis.dispatch({
action: 'join_room',
action: Action.JoinRoom,
roomId: this.getRoomId(),
opts: { inviteSignUrl: signUrl },
_type: "unknown", // TODO: instrumentation
});
Expand Down
58 changes: 51 additions & 7 deletions src/components/structures/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
import RoomName from "../views/elements/RoomName";
import {replaceableComponent} from "../../utils/replaceableComponent";

import InlineSpinner from "../views/elements/InlineSpinner";
import TooltipButton from "../views/elements/TooltipButton";
interface IProps {
isMinimized: boolean;
}
Expand All @@ -68,6 +69,7 @@ interface IState {
contextMenuPosition: PartialDOMRect;
isDarkTheme: boolean;
selectedSpace?: Room;
pendingRoomJoin: string[]
}

@replaceableComponent("structures.UserMenu")
Expand All @@ -84,6 +86,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.state = {
contextMenuPosition: null,
isDarkTheme: this.isUserOnDarkTheme(),
pendingRoomJoin: [],
};

OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
Expand Down Expand Up @@ -147,15 +150,48 @@ export default class UserMenu extends React.Component<IProps, IState> {
};

private onAction = (ev: ActionPayload) => {
if (ev.action !== Action.ToggleUserMenu) return; // not interested

if (this.state.contextMenuPosition) {
this.setState({contextMenuPosition: null});
} else {
if (this.buttonRef.current) this.buttonRef.current.click();
switch (ev.action) {
case Action.ToggleUserMenu:
if (this.state.contextMenuPosition) {
this.setState({contextMenuPosition: null});
} else {
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) => {
ev.preventDefault();
ev.stopPropagation();
Expand Down Expand Up @@ -617,6 +653,14 @@ export default class UserMenu extends React.Component<IProps, IState> {
/>
</span>
{name}
{this.hasPendingActions && (
<InlineSpinner>
<TooltipButton helpText={_t(
"Currently joining %(count)s rooms",
{ count: this.state.pendingRoomJoin.length },
)} />
</InlineSpinner>
)}
{dnd}
{buttons}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,29 @@ import React from "react";
import {_t} from "../../../languageHandler";
import {replaceableComponent} from "../../../utils/replaceableComponent";

interface IProps {
w?: number;
h?: number;
children?: React.ReactNode;
}

@replaceableComponent("views.elements.InlineSpinner")
export default class InlineSpinner extends React.Component {
render() {
const w = this.props.w || 16;
const h = this.props.h || 16;
export default class InlineSpinner extends React.PureComponent<IProps> {
static defaultProps = {
w: 16,
h: 16,
}

render() {
return (
<div className="mx_InlineSpinner">
<div
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...")}
></div>
>
{this.props.children}
</div>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,30 @@ import React from 'react';
import * as sdk from '../../../index';
import {replaceableComponent} from "../../../utils/replaceableComponent";

interface IProps {
helpText: string;
}

interface IState {
hover: boolean;
}

@replaceableComponent("views.elements.TooltipButton")
export default class TooltipButton extends React.Component {
state = {
hover: false,
};
export default class TooltipButton extends React.Component<IProps, IState> {
constructor(props) {
super(props);
this.state = {
hover: false,
};
}

onMouseOver = () => {
private onMouseOver = () => {
this.setState({
hover: true,
});
};

onMouseLeave = () => {
private onMouseLeave = () => {
this.setState({
hover: false,
});
Expand Down
4 changes: 3 additions & 1 deletion src/createRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { isJoinedOrNearlyJoined } from "./utils/membership";
import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
import SpaceStore from "./stores/SpaceStore";
import { makeSpaceParentEvent } from "./utils/space";
import { Action } from "./dispatcher/actions"

// we define a number of interfaces which take their names from the js-sdk
/* eslint-disable camelcase */
Expand Down Expand Up @@ -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)
dis.dispatch({
action: 'join_room_error',
action: Action.JoinRoomError,
roomId,
});
console.error("Failed to create room " + roomId + " " + err);
let description = _t("Server may be unavailable, overloaded, or you hit a bug.");
Expand Down
15 changes: 15 additions & 0 deletions src/dispatcher/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,19 @@ export enum Action {
* Fired when an upload is cancelled by the user. Should be used with UploadCanceledPayload.
*/
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",
}
2 changes: 2 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2753,6 +2753,8 @@
"Switch theme": "Switch theme",
"User menu": "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",
"Decrypted event source": "Decrypted event source",
"Original event source": "Original event source",
Expand Down
89 changes: 49 additions & 40 deletions src/stores/RoomViewStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ limitations under the License.
*/

import React from "react";
import {Store} from 'flux/utils';
import {MatrixError} from "matrix-js-sdk/src/http-api";
import { Store } from 'flux/utils';
import { MatrixError } from "matrix-js-sdk/src/http-api";

import dis from '../dispatcher/dispatcher';
import {MatrixClientPeg} from '../MatrixClientPeg';
import { MatrixClientPeg } from '../MatrixClientPeg';
import * as sdk from '../index';
import Modal from '../Modal';
import { _t } from '../languageHandler';
import { getCachedRoomIDForAlias, storeRoomAliasInCache } from '../RoomAliasCache';
import {ActionPayload} from "../dispatcher/payloads";
import {retry} from "../utils/promise";
import { ActionPayload } from "../dispatcher/payloads";
import { Action } from "../dispatcher/actions";
import { retry } from "../utils/promise";
import CountlyAnalytics from "../CountlyAnalytics";

const NUM_JOIN_RETRY = 5;
Expand Down Expand Up @@ -136,13 +137,13 @@ class RoomViewStore extends Store<ActionPayload> {
break;
// join_room:
// - opts: options for joinRoom
case 'join_room':
case Action.JoinRoom:
this.joinRoom(payload);
break;
case 'join_room_error':
case Action.JoinRoomError:
this.joinRoomError(payload);
break;
case 'join_room_ready':
case Action.JoinRoomReady:
this.setState({ shouldPeek: false });
break;
case 'on_client_not_viable':
Expand Down Expand Up @@ -217,7 +218,11 @@ class RoomViewStore extends Store<ActionPayload> {
this.setState(newState);

if (payload.auto_join) {
this.joinRoom(payload);
dis.dispatch({
...payload,
action: Action.JoinRoom,
roomId: payload.room_id,
});
}
} else if (payload.room_alias) {
// Try the room alias to room ID navigation cache first to avoid
Expand Down Expand Up @@ -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
// have come down the sync stream yet, and that's the point at which we'd consider the user joined to the
// room.
dis.dispatch({ action: 'join_room_ready' });
dis.dispatch({
action: Action.JoinRoomReady,
roomId: this.state.roomId,
});
} catch (err) {
dis.dispatch({
action: 'join_room_error',
action: Action.JoinRoomError,
roomId: this.state.roomId,
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,
});
}
}

Expand All @@ -351,6 +331,35 @@ class RoomViewStore extends Store<ActionPayload> {
joining: false,
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() {
Expand Down