Merge pull request #6085 from matrix-org/gsouquet/15451
This commit is contained in:
commit
9525dc3202
9 changed files with 160 additions and 65 deletions
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -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,
|
||||||
});
|
});
|
|
@ -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.");
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue