mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-23 03:25:12 +01:00
Merge pull request #870 from vector-im/implement-room-join-ui
Implement room join UI
This commit is contained in:
commit
664038b946
@ -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";
|
||||
|
63
src/domain/session/JoinRoomViewModel.ts
Normal file
63
src/domain/session/JoinRoomViewModel.ts
Normal file
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
@ -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";
|
||||
@ -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();
|
||||
}
|
||||
@ -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);
|
||||
@ -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() {
|
||||
@ -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");
|
||||
@ -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);
|
||||
|
@ -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) {
|
||||
@ -74,7 +73,13 @@ 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");
|
||||
}
|
||||
|
||||
_setupNavigation() {
|
||||
const roomObservable = this.navigation.observe("room");
|
||||
|
@ -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) {
|
||||
@ -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");
|
||||
}
|
||||
|
42
src/matrix/room/joinRoom.ts
Normal file
42
src/matrix/room/joinRoom.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1180,7 +1180,7 @@ button.RoomDetailsView_row::after {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.CreateRoomView, .RoomBeingCreated_error {
|
||||
.CreateRoomView, .JoinRoomView, .RoomBeingCreated_error {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
@ -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
src/platform/web/ui/session/JoinRoomView.ts
Normal file
63
src/platform/web/ui/session/JoinRoomView.ts
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
@ -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);
|
||||
|
@ -17,6 +17,8 @@ limitations under the License.
|
||||
import {ListView} from "../../general/ListView";
|
||||
import {TemplateView} from "../../general/TemplateView";
|
||||
import {RoomTileView} from "./RoomTileView.js";
|
||||
import {Menu} from "../../general/Menu.js";
|
||||
import {Popup} from "../../general/Popup.js";
|
||||
|
||||
class FilterField extends TemplateView {
|
||||
render(t, options) {
|
||||
@ -51,6 +53,11 @@ class FilterField extends TemplateView {
|
||||
}
|
||||
|
||||
export class LeftPanelView extends TemplateView {
|
||||
constructor(vm) {
|
||||
super(vm);
|
||||
this._createMenuPopup = null;
|
||||
}
|
||||
|
||||
render(t, vm) {
|
||||
const gridButtonLabel = vm => {
|
||||
return vm.gridEnabled ?
|
||||
@ -90,7 +97,11 @@ export class LeftPanelView extends TemplateView {
|
||||
"aria-label": gridButtonLabel
|
||||
}),
|
||||
t.a({className: "button-utility settings", href: vm.settingsUrl, "aria-label": vm.i18n`Settings`, title: vm.i18n`Settings`}),
|
||||
t.a({className: "button-utility create", href: vm.createRoomUrl, "aria-label": vm.i18n`Create room`, title: vm.i18n`Create room`}),
|
||||
t.button({
|
||||
className: "button-utility create",
|
||||
"aria-label": vm.i18n`Create room`,
|
||||
onClick: evt => this._toggleCreateMenu(evt)
|
||||
}),
|
||||
]);
|
||||
|
||||
return t.div({className: "LeftPanel"}, [
|
||||
@ -98,4 +109,18 @@ export class LeftPanelView extends TemplateView {
|
||||
roomList
|
||||
]);
|
||||
}
|
||||
|
||||
_toggleCreateMenu(evt) {
|
||||
if (this._createMenuPopup && this._createMenuPopup.isOpen) {
|
||||
this._createMenuPopup.close();
|
||||
} else {
|
||||
const vm = this.value;
|
||||
const options = [];
|
||||
options.push(Menu.option(vm.i18n`Create Room`, () => vm.showCreateRoomView()));
|
||||
options.push(Menu.option(vm.i18n`Join Room`, () => vm.showJoinRoomView()));
|
||||
this._createMenuPopup = new Popup(new Menu(options));
|
||||
this._createMenuPopup.trackInTemplateView(this);
|
||||
this._createMenuPopup.showRelativeTo(evt.target, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user