Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement room join UI #870

Merged
merged 13 commits into from
Sep 20, 2022
2 changes: 1 addition & 1 deletion src/domain/navigation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function allowsChild(parent: Segment<SegmentType> | undefined, child: Segment<Se
// allowed root segments
return type === "login" || type === "session" || type === "sso" || type === "logout";
case "session":
return type === "room" || type === "rooms" || type === "settings" || type === "create-room";
return type === "room" || type === "rooms" || type === "settings" || type === "create-room" || type === "join-room";
case "rooms":
// downside of the approach: both of these will control which tile is selected
return type === "room" || type === "empty-grid-tile";
Expand Down
63 changes: 63 additions & 0 deletions src/domain/session/JoinRoomViewModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
Copyright 2022 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.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import {ViewModel, Options as BaseOptions} from "../ViewModel";
import {SegmentType} from "../navigation/index";
import type {Session} from "../../matrix/Session.js";
import {joinRoom} from "../../matrix/room/joinRoom";

type Options = BaseOptions & {
session: Session;
};

export class JoinRoomViewModel extends ViewModel<SegmentType, Options> {
private _session: Session;
private _joinInProgress: boolean = false;
private _error: Error | undefined;

constructor(options: Readonly<Options>) {
super(options);
this._session = options.session;
}

async join(roomId: string): Promise<void> {
this._error = undefined;
this._joinInProgress = true;
this.emitChange("joinInProgress");
try {
const id = await joinRoom(roomId, this._session);
this.navigation.push("room", id);
}
catch (e) {
this._error = e;
this._joinInProgress = false;
this.emitChange("error");
}
}

get joinInProgress(): boolean {
return this._joinInProgress;
}

get status(): string | undefined {
if (this._error) {
return this._error.message;
}
else if(this._joinInProgress){
return "Joining room";
}
}
}
30 changes: 29 additions & 1 deletion src/domain/session/SessionViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {SessionStatusViewModel} from "./SessionStatusViewModel.js";
import {RoomGridViewModel} from "./RoomGridViewModel.js";
import {SettingsViewModel} from "./settings/SettingsViewModel.js";
import {CreateRoomViewModel} from "./CreateRoomViewModel.js";
import {JoinRoomViewModel} from "./JoinRoomViewModel";
import {ViewModel} from "../ViewModel";
import {RoomViewModelObservable} from "./RoomViewModelObservable.js";
import {RightPanelViewModel} from "./rightpanel/RightPanelViewModel.js";
Expand All @@ -45,6 +46,7 @@ export class SessionViewModel extends ViewModel {
this._roomViewModelObservable = null;
this._gridViewModel = null;
this._createRoomViewModel = null;
this._joinRoomViewModel = null;
this._setupNavigation();
this._setupForcedLogoutOnAccessTokenInvalidation();
}
Expand Down Expand Up @@ -83,6 +85,12 @@ export class SessionViewModel extends ViewModel {
}));
this._updateCreateRoom(createRoom.get());

const joinRoom = this.navigation.observe("join-room");
this.track(joinRoom.subscribe((joinRoomOpen) => {
this._updateJoinRoom(joinRoomOpen);
}));
this._updateJoinRoom(joinRoom.get());

const lightbox = this.navigation.observe("lightbox");
this.track(lightbox.subscribe(eventId => {
this._updateLightbox(eventId);
Expand Down Expand Up @@ -121,7 +129,13 @@ export class SessionViewModel extends ViewModel {
}

get activeMiddleViewModel() {
return this._roomViewModelObservable?.get() || this._gridViewModel || this._settingsViewModel || this._createRoomViewModel;
return (
this._roomViewModelObservable?.get() ||
this._gridViewModel ||
this._settingsViewModel ||
this._createRoomViewModel ||
this._joinRoomViewModel
);
}

get roomGridViewModel() {
Expand Down Expand Up @@ -152,6 +166,10 @@ export class SessionViewModel extends ViewModel {
return this._createRoomViewModel;
}

get joinRoomViewModel() {
return this._joinRoomViewModel;
}

_updateGrid(roomIds) {
const changed = !(this._gridViewModel && roomIds);
const currentRoomId = this.navigation.path.get("room");
Expand Down Expand Up @@ -286,6 +304,16 @@ export class SessionViewModel extends ViewModel {
this.emitChange("activeMiddleViewModel");
}

_updateJoinRoom(joinRoomOpen) {
if (this._joinRoomViewModel) {
this._joinRoomViewModel = this.disposeTracked(this._joinRoomViewModel);
}
if (joinRoomOpen) {
this._joinRoomViewModel = this.track(new JoinRoomViewModel(this.childOptions({session: this._client.session})));
}
this.emitChange("activeMiddleViewModel");
}

_updateLightbox(eventId) {
if (this._lightboxViewModel) {
this._lightboxViewModel = this.disposeTracked(this._lightboxViewModel);
Expand Down
9 changes: 7 additions & 2 deletions src/domain/session/leftpanel/LeftPanelViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export class LeftPanelViewModel extends ViewModel {
this._setupNavigation();
this._closeUrl = this.urlCreator.urlForSegment("session");
this._settingsUrl = this.urlCreator.urlForSegment("settings");
this._createRoomUrl = this.urlCreator.urlForSegment("create-room");
}

_mapTileViewModels(roomsBeingCreated, invites, rooms) {
Expand Down Expand Up @@ -74,8 +73,14 @@ export class LeftPanelViewModel extends ViewModel {
return this._settingsUrl;
}

get createRoomUrl() { return this._createRoomUrl; }
showCreateRoomView() {
this.navigation.push("create-room");
}

showJoinRoomView() {
this.navigation.push("join-room");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once this gets converted to TS this line will cause it to complain

Argument of type '"join-room"' is not assignable to parameter of type 'keyof SegmentType'.

cc @MidhunSureshR @bwindels

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because I forgot to add join-room here.

}

_setupNavigation() {
const roomObservable = this.navigation.observe("room");
this.track(roomObservable.subscribe(roomId => this._open(roomId)));
Expand Down
19 changes: 4 additions & 15 deletions src/domain/session/room/RoomViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {imageToInfo} from "../common.js";
// TODO: remove fallback so default isn't included in bundle for SDK users that have their custom tileClassForEntry
// this is a breaking SDK change though to make this option mandatory
import {tileClassForEntry as defaultTileClassForEntry} from "./timeline/tiles/index";
import {RoomStatus} from "../../../matrix/room/common";
import {joinRoom} from "../../../matrix/room/joinRoom";

export class RoomViewModel extends ViewModel {
constructor(options) {
Expand Down Expand Up @@ -200,22 +200,11 @@ export class RoomViewModel extends ViewModel {

async _processCommandJoin(roomName) {
try {
const roomId = await this._options.client.session.joinRoom(roomName);
const roomStatusObserver = await this._options.client.session.observeRoomStatus(roomId);
await roomStatusObserver.waitFor(status => status === RoomStatus.Joined);
const session = this._options.client.session;
const roomId = await joinRoom(roomName, session);
this.navigation.push("room", roomId);
} catch (err) {
let exc;
if ((err.statusCode ?? err.status) === 400) {
exc = new Error(`/join : '${roomName}' was not legal room ID or room alias`);
} else if ((err.statusCode ?? err.status) === 404 || (err.statusCode ?? err.status) === 502 || err.message == "Internal Server Error") {
exc = new Error(`/join : room '${roomName}' not found`);
} else if ((err.statusCode ?? err.status) === 403) {
exc = new Error(`/join : you're not invited to join '${roomName}'`);
} else {
exc = err;
}
this._sendError = exc;
this._sendError = err;
this._timelineError = null;
this.emitChange("error");
}
Expand Down
42 changes: 42 additions & 0 deletions src/matrix/room/joinRoom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
Copyright 2022 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.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import type {Session} from "../Session.js";
import {RoomStatus} from "./common";

/**
* Join a room and wait for it to arrive in the next sync
* @param roomId The id of the room to join
* @param session A session instance
*/
export async function joinRoom(roomId: string, session: Session): Promise<string> {
try {
const internalRoomId = await session.joinRoom(roomId);
const roomStatusObservable = await session.observeRoomStatus(internalRoomId);
await roomStatusObservable.waitFor((status: RoomStatus) => status === RoomStatus.Joined);
return internalRoomId;
}
catch (e) {
if ((e.statusCode ?? e.status) === 400) {
throw new Error(`'${roomId}' is not a legal room ID or alias`);
} else if ((e.statusCode ?? e.status) === 404 || (e.statusCode ?? e.status) === 502 || e.message == "Internal Server eor") {
throw new Error(`Room '${roomId}' could not be found`);
} else if ((e.statusCode ?? e.status) === 403) {
throw new Error(`You are not invited to join '${roomId}'`);
} else {
throw e;
}
}
}
13 changes: 12 additions & 1 deletion src/platform/web/ui/css/themes/element/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -1180,7 +1180,7 @@ button.RoomDetailsView_row::after {
gap: 12px;
}

.CreateRoomView, .RoomBeingCreated_error {
.CreateRoomView, .JoinRoomView, .RoomBeingCreated_error {
max-width: 400px;
}

Expand Down Expand Up @@ -1211,3 +1211,14 @@ button.RoomDetailsView_row::after {
background-position: center;
background-size: 36px;
}

.JoinRoomView_status {
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
}

.JoinRoomView_status .spinner {
margin-right: 5px;
}
63 changes: 63 additions & 0 deletions src/platform/web/ui/session/JoinRoomView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
Copyright 2022 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.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import {TemplateView} from "../general/TemplateView";
import type {JoinRoomViewModel} from "../../../../domain/session/JoinRoomViewModel";
import {spinner} from "../common.js";

export class JoinRoomView extends TemplateView<JoinRoomViewModel> {
render(t, vm) {
const input = t.input({
type: "text",
name: "id",
id: "id",
placeholder: vm.i18n`Enter a room id or alias`,
disabled: vm => vm.joinInProgress,
});
return t.main({className: "middle"},
t.div({className: "JoinRoomView centered-column"}, [
t.h2("Join room"),
t.form({className: "JoinRoomView_detailsForm form", onSubmit: evt => this.onSubmit(evt, input.value)}, [
t.div({className: "vertical-layout"}, [
t.div({className: "stretch form-row text"}, [
t.label({for: "id"}, vm.i18n`Room id`),
input,
]),
]),
t.div({className: "button-row"}, [
t.button({
className: "button-action primary",
type: "submit",
disabled: vm => vm.joinInProgress
}, vm.i18n`Join`),
]),
t.map(vm => vm.status, (status, t) => {
return t.div({ className: "JoinRoomView_status" }, [
spinner(t, { hidden: vm => !vm.joinInProgress }),
t.span(status),
]);
})
])
])
);
}

onSubmit(evt, id) {
evt.preventDefault();
this.value.join(id);
}
}

3 changes: 3 additions & 0 deletions src/platform/web/ui/session/SessionView.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {SettingsView} from "./settings/SettingsView.js";
import {CreateRoomView} from "./CreateRoomView.js";
import {RightPanelView} from "./rightpanel/RightPanelView.js";
import {viewClassForTile} from "./room/common";
import {JoinRoomView} from "./JoinRoomView";

export class SessionView extends TemplateView {
render(t, vm) {
Expand All @@ -48,6 +49,8 @@ export class SessionView extends TemplateView {
return new SettingsView(vm.settingsViewModel);
} else if (vm.createRoomViewModel) {
return new CreateRoomView(vm.createRoomViewModel);
} else if (vm.joinRoomViewModel) {
return new JoinRoomView(vm.joinRoomViewModel);
} else if (vm.currentRoomViewModel) {
if (vm.currentRoomViewModel.kind === "invite") {
return new InviteView(vm.currentRoomViewModel);
Expand Down
Loading