forked from mystiq/hydrogen-web
Merge pull request #417 from MidhunSureshR/member-details
Member Panel - PR 2 - UI
This commit is contained in:
commit
1862e31396
10 changed files with 252 additions and 31 deletions
|
@ -39,7 +39,7 @@ function allowsChild(parent, child) {
|
||||||
case "room":
|
case "room":
|
||||||
return type === "lightbox" || type === "right-panel";
|
return type === "lightbox" || type === "right-panel";
|
||||||
case "right-panel":
|
case "right-panel":
|
||||||
return type === "details"|| type === "members";
|
return type === "details"|| type === "members" || type === "member";
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -87,9 +87,9 @@ function roomsSegmentWithRoom(rooms, roomId, path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pushRightPanelSegment(array, segment) {
|
function pushRightPanelSegment(array, segment, value = true) {
|
||||||
array.push(new Segment("right-panel"));
|
array.push(new Segment("right-panel"));
|
||||||
array.push(new Segment(segment));
|
array.push(new Segment(segment, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addPanelIfNeeded(navigation, path) {
|
export function addPanelIfNeeded(navigation, path) {
|
||||||
|
@ -132,10 +132,11 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) {
|
||||||
segments.push(roomsSegmentWithRoom(rooms, roomId, currentNavPath));
|
segments.push(roomsSegmentWithRoom(rooms, roomId, currentNavPath));
|
||||||
}
|
}
|
||||||
segments.push(new Segment("room", roomId));
|
segments.push(new Segment("room", roomId));
|
||||||
if (currentNavPath.get("details")?.value) {
|
// Add right-panel segments from previous path
|
||||||
pushRightPanelSegment(segments, "details");
|
const previousSegments = currentNavPath.segments;
|
||||||
} else if (currentNavPath.get("members")?.value) {
|
const i = previousSegments.findIndex(s => s.type === "right-panel");
|
||||||
pushRightPanelSegment(segments, "members");
|
if (i !== -1) {
|
||||||
|
segments.push(...previousSegments.slice(i));
|
||||||
}
|
}
|
||||||
} else if (type === "last-session") {
|
} else if (type === "last-session") {
|
||||||
let sessionSegment = currentNavPath.get("session");
|
let sessionSegment = currentNavPath.get("session");
|
||||||
|
@ -147,6 +148,10 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) {
|
||||||
}
|
}
|
||||||
} else if (type === "details" || type === "members") {
|
} else if (type === "details" || type === "members") {
|
||||||
pushRightPanelSegment(segments, type);
|
pushRightPanelSegment(segments, type);
|
||||||
|
} else if (type === "member") {
|
||||||
|
const userId = iterator.next().value;
|
||||||
|
if (!userId) { break; }
|
||||||
|
pushRightPanelSegment(segments, type, userId);
|
||||||
} else {
|
} else {
|
||||||
// might be undefined, which will be turned into true by Segment
|
// might be undefined, which will be turned into true by Segment
|
||||||
const value = iterator.next().value;
|
const value = iterator.next().value;
|
||||||
|
|
82
src/domain/session/rightpanel/MemberDetailsViewModel.js
Normal file
82
src/domain/session/rightpanel/MemberDetailsViewModel.js
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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} from "../../ViewModel.js";
|
||||||
|
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js";
|
||||||
|
|
||||||
|
export class MemberDetailsViewModel extends ViewModel {
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
this._observableMember = options.observableMember;
|
||||||
|
this._mediaRepository = options.mediaRepository;
|
||||||
|
this._member = this._observableMember.get();
|
||||||
|
this._isEncrypted = options.isEncrypted;
|
||||||
|
this._powerLevelsObservable = options.powerLevelsObservable;
|
||||||
|
this.track(this._powerLevelsObservable.subscribe(() => this._onPowerLevelsChange()));
|
||||||
|
this.track(this._observableMember.subscribe( () => this._onMemberChange()));
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() { return this._member.name; }
|
||||||
|
get userId() { return this._member.userId; }
|
||||||
|
|
||||||
|
get type() { return "member-details"; }
|
||||||
|
get shouldShowBackButton() { return true; }
|
||||||
|
get previousSegmentName() { return "members"; }
|
||||||
|
|
||||||
|
get role() {
|
||||||
|
if (this.powerLevel >= 100) { return this.i18n`Admin`; }
|
||||||
|
else if (this.powerLevel >= 50) { return this.i18n`Moderator`; }
|
||||||
|
else if (this.powerLevel === 0) { return this.i18n`Default`; }
|
||||||
|
else { return this.i18n`Custom (${this.powerLevel})`; }
|
||||||
|
}
|
||||||
|
|
||||||
|
_onMemberChange() {
|
||||||
|
this._member = this._observableMember.get();
|
||||||
|
this.emitChange("member");
|
||||||
|
}
|
||||||
|
|
||||||
|
_onPowerLevelsChange() {
|
||||||
|
this.emitChange("role");
|
||||||
|
}
|
||||||
|
|
||||||
|
get avatarLetter() {
|
||||||
|
return avatarInitials(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
get avatarColorNumber() {
|
||||||
|
return getIdentifierColorNumber(this.userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
avatarUrl(size) {
|
||||||
|
return getAvatarHttpUrl(this._member.avatarUrl, size, this.platform, this._mediaRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
get avatarTitle() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isEncrypted() {
|
||||||
|
return this._isEncrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
get powerLevel() {
|
||||||
|
return this._powerLevelsObservable.get()?.getUserLevel(this._member.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
get linkToUser() {
|
||||||
|
return `https://matrix.to/#/${this._member.userId}`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,7 +23,6 @@ export class MemberListViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
const list = options.members;
|
const list = options.members;
|
||||||
this.track(() => list.release());
|
|
||||||
|
|
||||||
const powerLevelsObservable = options.powerLevelsObservable;
|
const powerLevelsObservable = options.powerLevelsObservable;
|
||||||
this.track(powerLevelsObservable.subscribe(() => { /*resort based on new power levels here*/ }));
|
this.track(powerLevelsObservable.subscribe(() => { /*resort based on new power levels here*/ }));
|
||||||
|
|
|
@ -46,6 +46,10 @@ export class MemberTileViewModel extends ViewModel {
|
||||||
return this._nameChanged;
|
return this._nameChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get detailsUrl() {
|
||||||
|
return `${this.urlCreator.urlUntilSegment("room")}/member/${this._member.userId}`;
|
||||||
|
}
|
||||||
|
|
||||||
_updatePreviousName(newName) {
|
_updatePreviousName(newName) {
|
||||||
const currentName = this._member.name;
|
const currentName = this._member.name;
|
||||||
if (currentName !== newName) {
|
if (currentName !== newName) {
|
||||||
|
|
|
@ -17,35 +17,59 @@ limitations under the License.
|
||||||
import {ViewModel} from "../../ViewModel.js";
|
import {ViewModel} from "../../ViewModel.js";
|
||||||
import {RoomDetailsViewModel} from "./RoomDetailsViewModel.js";
|
import {RoomDetailsViewModel} from "./RoomDetailsViewModel.js";
|
||||||
import {MemberListViewModel} from "./MemberListViewModel.js";
|
import {MemberListViewModel} from "./MemberListViewModel.js";
|
||||||
|
import {MemberDetailsViewModel} from "./MemberDetailsViewModel.js";
|
||||||
|
|
||||||
export class RightPanelViewModel extends ViewModel {
|
export class RightPanelViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
this._room = options.room;
|
this._room = options.room;
|
||||||
|
this._members = null;
|
||||||
this._setupNavigation();
|
this._setupNavigation();
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeViewModel() { return this._activeViewModel; }
|
get activeViewModel() { return this._activeViewModel; }
|
||||||
|
|
||||||
async _getMemberArguments() {
|
async _getMemberListArguments() {
|
||||||
const members = await this._room.loadMemberList();
|
if (!this._members) {
|
||||||
|
this._members = await this._room.loadMemberList();
|
||||||
|
this.track(() => this._members.release());
|
||||||
|
}
|
||||||
const room = this._room;
|
const room = this._room;
|
||||||
const powerLevelsObservable = await this._room.observePowerLevels();
|
const powerLevelsObservable = await this._room.observePowerLevels();
|
||||||
return {members, powerLevelsObservable, mediaRepository: room.mediaRepository};
|
return {members: this._members, powerLevelsObservable, mediaRepository: room.mediaRepository};
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getMemberDetailsArguments() {
|
||||||
|
const segment = this.navigation.path.get("member");
|
||||||
|
const userId = segment.value;
|
||||||
|
const observableMember = await this._room.observeMember(userId);
|
||||||
|
if (!observableMember) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const isEncrypted = this._room.isEncrypted;
|
||||||
|
const powerLevelsObservable = await this._room.observePowerLevels();
|
||||||
|
return {observableMember, isEncrypted, powerLevelsObservable, mediaRepository: this._room.mediaRepository};
|
||||||
}
|
}
|
||||||
|
|
||||||
_setupNavigation() {
|
_setupNavigation() {
|
||||||
this._hookUpdaterToSegment("details", RoomDetailsViewModel, () => { return {room: this._room}; });
|
this._hookUpdaterToSegment("details", RoomDetailsViewModel, () => { return {room: this._room}; });
|
||||||
this._hookUpdaterToSegment("members", MemberListViewModel, () => this._getMemberArguments());
|
this._hookUpdaterToSegment("members", MemberListViewModel, () => this._getMemberListArguments());
|
||||||
|
this._hookUpdaterToSegment("member", MemberDetailsViewModel, () => this._getMemberDetailsArguments(),
|
||||||
|
() => {
|
||||||
|
// If we fail to create the member details panel, fallback to memberlist
|
||||||
|
const url = `${this.urlCreator.urlUntilSegment("room")}/members`;
|
||||||
|
this.urlCreator.pushUrl(url);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_hookUpdaterToSegment(segment, viewmodel, argCreator) {
|
_hookUpdaterToSegment(segment, viewmodel, argCreator, failCallback) {
|
||||||
const observable = this.navigation.observe(segment);
|
const observable = this.navigation.observe(segment);
|
||||||
const updater = this._setupUpdater(segment, viewmodel, argCreator);
|
const updater = this._setupUpdater(segment, viewmodel, argCreator, failCallback);
|
||||||
this.track(observable.subscribe(() => updater()));
|
this.track(observable.subscribe(updater));
|
||||||
}
|
}
|
||||||
|
|
||||||
_setupUpdater(segment, viewmodel, argCreator) {
|
_setupUpdater(segment, viewmodel, argCreator, failCallback) {
|
||||||
const updater = async (skipDispose = false) => {
|
const updater = async (skipDispose = false) => {
|
||||||
if (!skipDispose) {
|
if (!skipDispose) {
|
||||||
this._activeViewModel = this.disposeTracked(this._activeViewModel);
|
this._activeViewModel = this.disposeTracked(this._activeViewModel);
|
||||||
|
@ -53,6 +77,10 @@ export class RightPanelViewModel extends ViewModel {
|
||||||
const enable = !!this.navigation.path.get(segment)?.value;
|
const enable = !!this.navigation.path.get(segment)?.value;
|
||||||
if (enable) {
|
if (enable) {
|
||||||
const args = await argCreator();
|
const args = await argCreator();
|
||||||
|
if (!args && failCallback) {
|
||||||
|
failCallback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._activeViewModel = this.track(new viewmodel(this.childOptions(args)));
|
this._activeViewModel = this.track(new viewmodel(this.childOptions(args)));
|
||||||
}
|
}
|
||||||
this.emitChange("activeViewModel");
|
this.emitChange("activeViewModel");
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.RoomDetailsView {
|
.RoomDetailsView, .MemberDetailsView {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
@ -15,11 +15,11 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.RoomDetailsView_name h2 {
|
.RoomDetailsView_name h2, .MemberDetailsView_name h2 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.RoomDetailsView_label, .RoomDetailsView_row, .RoomDetailsView, .EncryptionIconView {
|
.RoomDetailsView_label, .RoomDetailsView_row, .RoomDetailsView, .MemberDetailsView, .EncryptionIconView {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MemberTileView {
|
.MemberTileView a {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -807,7 +807,7 @@ button.link {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.RoomDetailsView_id {
|
.RoomDetailsView_id, .MemberDetailsView_id {
|
||||||
color: #737D8C;
|
color: #737D8C;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
@ -817,7 +817,7 @@ button.link {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.RoomDetailsView_name h2 {
|
.RoomDetailsView_name h2, .MemberDetailsView_name h2 {
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
}
|
}
|
||||||
|
@ -916,6 +916,11 @@ button.RoomDetailsView_row::after {
|
||||||
|
|
||||||
.MemberTileView {
|
.MemberTileView {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MemberTileView a {
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MemberTileView .avatar {
|
.MemberTileView .avatar {
|
||||||
|
@ -929,6 +934,37 @@ button.RoomDetailsView_row::after {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Member details panel */
|
||||||
|
.MemberDetailsView_section {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MemberDetailsView_label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #8d99a5;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MemberDetailsView_value, .MemberDetailsView_options {
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MemberDetailsView_options {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MemberDetailsView_options a{
|
||||||
|
color: #0dbd8b;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.LazyListParent {
|
.LazyListParent {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
53
src/platform/web/ui/session/rightpanel/MemberDetailsView.js
Normal file
53
src/platform/web/ui/session/rightpanel/MemberDetailsView.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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 {AvatarView} from "../../AvatarView.js";
|
||||||
|
import {TemplateView} from "../../general/TemplateView.js";
|
||||||
|
|
||||||
|
export class MemberDetailsView extends TemplateView {
|
||||||
|
render(t, vm) {
|
||||||
|
return t.div({className: "MemberDetailsView"},
|
||||||
|
[ t.view(new AvatarView(vm, 128)),
|
||||||
|
t.div({className: "MemberDetailsView_name"}, t.h2(vm => vm.name)),
|
||||||
|
t.div({className: "MemberDetailsView_id"}, vm.userId),
|
||||||
|
this._createSection(t, vm.i18n`Role`, vm => vm.role),
|
||||||
|
this._createSection(t, vm.i18n`Security`, vm.isEncrypted ?
|
||||||
|
vm.i18n`Messages in this room are end-to-end encrypted.` :
|
||||||
|
vm.i18n`Messages in this room are not end-to-end encrypted.`
|
||||||
|
),
|
||||||
|
this._createOptions(t, vm)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_createSection(t, label, value) {
|
||||||
|
return t.div({ className: "MemberDetailsView_section" },
|
||||||
|
[
|
||||||
|
t.div({className: "MemberDetailsView_label"}, label),
|
||||||
|
t.div({className: "MemberDetailsView_value"}, value)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_createOptions(t, vm) {
|
||||||
|
return t.div({ className: "MemberDetailsView_section" },
|
||||||
|
[
|
||||||
|
t.div({className: "MemberDetailsView_label"}, vm.i18n`Options`),
|
||||||
|
t.div({className: "MemberDetailsView_options"},
|
||||||
|
[
|
||||||
|
t.a({href: vm.linkToUser, target: "_blank", rel: "noopener"}, vm.i18n`Open Link to User`)
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,9 +19,12 @@ import {AvatarView} from "../../AvatarView.js";
|
||||||
|
|
||||||
export class MemberTileView extends TemplateView {
|
export class MemberTileView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
return t.li({ className: "MemberTileView" }, [
|
return t.li({ className: "MemberTileView" },
|
||||||
|
t.a({ href: vm.detailsUrl },
|
||||||
|
[
|
||||||
t.view(new AvatarView(vm, 32)),
|
t.view(new AvatarView(vm, 32)),
|
||||||
t.div({ className: "MemberTileView_name" }, (vm) => vm.name),
|
t.div({ className: "MemberTileView_name" }, (vm) => vm.name),
|
||||||
]);
|
])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,20 +18,31 @@ import {TemplateView} from "../../general/TemplateView.js";
|
||||||
import {RoomDetailsView} from "./RoomDetailsView.js";
|
import {RoomDetailsView} from "./RoomDetailsView.js";
|
||||||
import {MemberListView} from "./MemberListView.js";
|
import {MemberListView} from "./MemberListView.js";
|
||||||
import {LoadingView} from "../../general/LoadingView.js";
|
import {LoadingView} from "../../general/LoadingView.js";
|
||||||
|
import {MemberDetailsView} from "./MemberDetailsView.js";
|
||||||
|
|
||||||
export class RightPanelView extends TemplateView {
|
export class RightPanelView extends TemplateView {
|
||||||
render(t) {
|
render(t) {
|
||||||
const viewFromType = {
|
|
||||||
"room-details": RoomDetailsView,
|
|
||||||
"member-list": MemberListView
|
|
||||||
};
|
|
||||||
return t.div({ className: "RightPanelView" },
|
return t.div({ className: "RightPanelView" },
|
||||||
[
|
[
|
||||||
t.ifView(vm => vm.activeViewModel, vm => new ButtonsView(vm)),
|
t.ifView(vm => vm.activeViewModel, vm => new ButtonsView(vm)),
|
||||||
t.mapView(vm => vm.activeViewModel, vm => vm ? new viewFromType[vm.type](vm) : new LoadingView())
|
t.mapView(vm => vm.activeViewModel, vm => this._viewFromType(vm))
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_viewFromType(vm) {
|
||||||
|
const type = vm?.type;
|
||||||
|
switch (type) {
|
||||||
|
case "room-details":
|
||||||
|
return new RoomDetailsView(vm);
|
||||||
|
case "member-list":
|
||||||
|
return new MemberListView(vm);
|
||||||
|
case "member-details":
|
||||||
|
return new MemberDetailsView(vm);
|
||||||
|
default:
|
||||||
|
return new LoadingView();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ButtonsView extends TemplateView {
|
class ButtonsView extends TemplateView {
|
||||||
|
|
Loading…
Reference in a new issue