Skip to content

Commit

Permalink
Merge pull request #417 from MidhunSureshR/member-details
Browse files Browse the repository at this point in the history
Member Panel - PR 2 - UI
  • Loading branch information
bwindels committed Aug 6, 2021
2 parents 73884cf + 7e8d76a commit 1862e31
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 31 deletions.
19 changes: 12 additions & 7 deletions src/domain/navigation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function allowsChild(parent, child) {
case "room":
return type === "lightbox" || type === "right-panel";
case "right-panel":
return type === "details"|| type === "members";
return type === "details"|| type === "members" || type === "member";
default:
return false;
}
Expand Down Expand Up @@ -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(segment));
array.push(new Segment(segment, value));
}

export function addPanelIfNeeded(navigation, path) {
Expand Down Expand Up @@ -132,10 +132,11 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) {
segments.push(roomsSegmentWithRoom(rooms, roomId, currentNavPath));
}
segments.push(new Segment("room", roomId));
if (currentNavPath.get("details")?.value) {
pushRightPanelSegment(segments, "details");
} else if (currentNavPath.get("members")?.value) {
pushRightPanelSegment(segments, "members");
// Add right-panel segments from previous path
const previousSegments = currentNavPath.segments;
const i = previousSegments.findIndex(s => s.type === "right-panel");
if (i !== -1) {
segments.push(...previousSegments.slice(i));
}
} else if (type === "last-session") {
let sessionSegment = currentNavPath.get("session");
Expand All @@ -147,6 +148,10 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) {
}
} else if (type === "details" || type === "members") {
pushRightPanelSegment(segments, type);
} else if (type === "member") {
const userId = iterator.next().value;
if (!userId) { break; }
pushRightPanelSegment(segments, type, userId);
} else {
// might be undefined, which will be turned into true by Segment
const value = iterator.next().value;
Expand Down
82 changes: 82 additions & 0 deletions src/domain/session/rightpanel/MemberDetailsViewModel.js
Original file line number Diff line number Diff line change
@@ -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}`;
}
}
1 change: 0 additions & 1 deletion src/domain/session/rightpanel/MemberListViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export class MemberListViewModel extends ViewModel {
constructor(options) {
super(options);
const list = options.members;
this.track(() => list.release());

const powerLevelsObservable = options.powerLevelsObservable;
this.track(powerLevelsObservable.subscribe(() => { /*resort based on new power levels here*/ }));
Expand Down
4 changes: 4 additions & 0 deletions src/domain/session/rightpanel/MemberTileViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export class MemberTileViewModel extends ViewModel {
return this._nameChanged;
}

get detailsUrl() {
return `${this.urlCreator.urlUntilSegment("room")}/member/${this._member.userId}`;
}

_updatePreviousName(newName) {
const currentName = this._member.name;
if (currentName !== newName) {
Expand Down
44 changes: 36 additions & 8 deletions src/domain/session/rightpanel/RightPanelViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,42 +17,70 @@ limitations under the License.
import {ViewModel} from "../../ViewModel.js";
import {RoomDetailsViewModel} from "./RoomDetailsViewModel.js";
import {MemberListViewModel} from "./MemberListViewModel.js";
import {MemberDetailsViewModel} from "./MemberDetailsViewModel.js";

export class RightPanelViewModel extends ViewModel {
constructor(options) {
super(options);
this._room = options.room;
this._members = null;
this._setupNavigation();
}

get activeViewModel() { return this._activeViewModel; }

async _getMemberArguments() {
const members = await this._room.loadMemberList();
async _getMemberListArguments() {
if (!this._members) {
this._members = await this._room.loadMemberList();
this.track(() => this._members.release());
}
const room = this._room;
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() {
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 updater = this._setupUpdater(segment, viewmodel, argCreator);
this.track(observable.subscribe(() => updater()));
const updater = this._setupUpdater(segment, viewmodel, argCreator, failCallback);
this.track(observable.subscribe(updater));
}

_setupUpdater(segment, viewmodel, argCreator) {
_setupUpdater(segment, viewmodel, argCreator, failCallback) {
const updater = async (skipDispose = false) => {
if (!skipDispose) {
this._activeViewModel = this.disposeTracked(this._activeViewModel);
}
const enable = !!this.navigation.path.get(segment)?.value;
if (enable) {
const args = await argCreator();
if (!args && failCallback) {
failCallback();
return;
}
this._activeViewModel = this.track(new viewmodel(this.childOptions(args)));
}
this.emitChange("activeViewModel");
Expand Down
8 changes: 4 additions & 4 deletions src/platform/web/ui/css/right-panel.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
flex-direction: column;
}

.RoomDetailsView {
.RoomDetailsView, .MemberDetailsView {
flex-direction: column;
flex: 1;
}
Expand All @@ -15,11 +15,11 @@
display: flex;
}

.RoomDetailsView_name h2 {
.RoomDetailsView_name h2, .MemberDetailsView_name h2 {
text-align: center;
}

.RoomDetailsView_label, .RoomDetailsView_row, .RoomDetailsView, .EncryptionIconView {
.RoomDetailsView_label, .RoomDetailsView_row, .RoomDetailsView, .MemberDetailsView, .EncryptionIconView {
display: flex;
align-items: center;
}
Expand All @@ -45,7 +45,7 @@
visibility: hidden;
}

.MemberTileView {
.MemberTileView a {
display: flex;
align-items: center;
}
40 changes: 38 additions & 2 deletions src/platform/web/ui/css/themes/element/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -807,7 +807,7 @@ button.link {
padding-top: 0;
}

.RoomDetailsView_id {
.RoomDetailsView_id, .MemberDetailsView_id {
color: #737D8C;
font-size: 12px;
}
Expand All @@ -817,7 +817,7 @@ button.link {
width: 100%;
}

.RoomDetailsView_name h2 {
.RoomDetailsView_name h2, .MemberDetailsView_name h2 {
margin-bottom: 4px;
font-size: 1.8rem;
}
Expand Down Expand Up @@ -916,6 +916,11 @@ button.RoomDetailsView_row::after {

.MemberTileView {
margin-bottom: 8px;
list-style: none;
}

.MemberTileView a {
text-decoration: none;
}

.MemberTileView .avatar {
Expand All @@ -929,6 +934,37 @@ button.RoomDetailsView_row::after {
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 {
overflow-y: auto;
}
53 changes: 53 additions & 0 deletions src/platform/web/ui/session/rightpanel/MemberDetailsView.js
Original file line number Diff line number Diff line change
@@ -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`)
])
]);
}
}
11 changes: 7 additions & 4 deletions src/platform/web/ui/session/rightpanel/MemberTileView.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ import {AvatarView} from "../../AvatarView.js";

export class MemberTileView extends TemplateView {
render(t, vm) {
return t.li({ className: "MemberTileView" }, [
t.view(new AvatarView(vm, 32)),
t.div({ className: "MemberTileView_name" }, (vm) => vm.name),
]);
return t.li({ className: "MemberTileView" },
t.a({ href: vm.detailsUrl },
[
t.view(new AvatarView(vm, 32)),
t.div({ className: "MemberTileView_name" }, (vm) => vm.name),
])
);
}
}

0 comments on commit 1862e31

Please sign in to comment.