Merge pull request #417 from MidhunSureshR/member-details

Member Panel - PR 2 - UI
This commit is contained in:
Bruno Windels 2021-08-06 11:18:52 +00:00 committed by GitHub
commit 1862e31396
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 252 additions and 31 deletions

View file

@ -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;

View 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}`;
}
}

View file

@ -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*/ }));

View file

@ -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) {

View file

@ -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");

View file

@ -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;
} }

View file

@ -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;
} }

View 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`)
])
]);
}
}

View file

@ -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.view(new AvatarView(vm, 32)), t.a({ href: vm.detailsUrl },
t.div({ className: "MemberTileView_name" }, (vm) => vm.name), [
]); t.view(new AvatarView(vm, 32)),
t.div({ className: "MemberTileView_name" }, (vm) => vm.name),
])
);
} }
} }

View file

@ -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 {