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

Member Panel - PR 2 - UI #417

Merged
merged 36 commits into from
Aug 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
05f05bb
Add method to get member from storage
MidhunSureshR Jul 19, 2021
3aeb0c4
Indicate if no member found
MidhunSureshR Jul 21, 2021
6cfb4cf
Support state events
MidhunSureshR Aug 2, 2021
bb68b4d
Fetch member data from from state event if needed
MidhunSureshR Aug 2, 2021
e363d32
Return null on error
MidhunSureshR Aug 2, 2021
c9a4c39
Return null
MidhunSureshR Aug 2, 2021
7e48108
Return null
MidhunSureshR Aug 5, 2021
fdd4caa
Create vm and view
MidhunSureshR Jul 19, 2021
e4bb205
Create navigation and add to right panel
MidhunSureshR Jul 19, 2021
acdba8f
Add license headers
MidhunSureshR Jul 20, 2021
7ef7c41
Add basic UI and styling
MidhunSureshR Jul 20, 2021
c2b9c3f
Add isEncrypted to vm
MidhunSureshR Jul 20, 2021
c9224c1
Improve code and css
MidhunSureshR Jul 20, 2021
fe6551f
Use powerlevel
MidhunSureshR Jul 21, 2021
f9b1068
Add links
MidhunSureshR Jul 21, 2021
ef17808
Fallback to memberlist if member not available
MidhunSureshR Jul 21, 2021
7530b28
Improve segment adding logic
MidhunSureshR Jul 22, 2021
7dacbdb
Show back button
MidhunSureshR Jul 22, 2021
65b1a19
Add link to open member details
MidhunSureshR Jul 22, 2021
4784bcc
Make the tiles link to details
MidhunSureshR Jul 22, 2021
448bf3c
Style tile
MidhunSureshR Jul 22, 2021
16887c3
Better formatting
MidhunSureshR Jul 22, 2021
d056a9f
Keep memberlist until rightpanel is closed
MidhunSureshR Jul 22, 2021
0ff0850
Remove dm user option
MidhunSureshR Jul 23, 2021
28a0a08
Copy over all segments after right-panel
MidhunSureshR Aug 3, 2021
a16c3aa
Create getter
MidhunSureshR Aug 3, 2021
862e856
Describe what changed in emitChange()
MidhunSureshR Aug 3, 2021
d3ea26a
Use i8n in role
MidhunSureshR Aug 3, 2021
fea4143
Use texttransform
MidhunSureshR Aug 3, 2021
49be4e6
Remove object
MidhunSureshR Aug 3, 2021
974d9bf
Don't cache powerlevel value
Aug 6, 2021
86ddfc1
Only need to emit
MidhunSureshR Aug 6, 2021
dbd2057
Remove unused prop
MidhunSureshR Aug 6, 2021
3ff5520
No need for lambda here
Aug 6, 2021
bc8e623
Merge suggestion from Github
MidhunSureshR Aug 6, 2021
7e8d76a
Refactor map into switch-case
MidhunSureshR Aug 6, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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));
bwindels marked this conversation as resolved.
Show resolved Hide resolved
}
} 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 @@ -7,7 +7,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 @@ -30,6 +30,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
@@ -1,42 +1,70 @@
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
4 changes: 4 additions & 0 deletions src/matrix/net/HomeServerApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ export class HomeServerApi {
{}, {}, options);
}

state(roomId, eventType, stateKey, options = null) {
return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, null, options);
}

passwordLogin(username, password, initialDeviceDisplayName, options = null) {
return this._unauthedRequest("POST", this._url("/login"), null, {
"type": "m.login.password",
Expand Down
29 changes: 28 additions & 1 deletion src/matrix/room/BaseRoom.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {RelationWriter} from "./timeline/persistence/RelationWriter.js";
import {Timeline} from "./timeline/Timeline.js";
import {FragmentIdComparer} from "./timeline/FragmentIdComparer.js";
import {WrappedError} from "../error.js"
import {fetchOrLoadMembers} from "./members/load.js";
import {fetchOrLoadMembers, fetchOrLoadMember} from "./members/load.js";
import {MemberList} from "./members/MemberList.js";
import {Heroes} from "./members/Heroes.js";
import {EventEntry} from "./timeline/entries/EventEntry.js";
Expand Down Expand Up @@ -54,6 +54,7 @@ export class BaseRoom extends EventEmitter {
this._observedEvents = null;
this._powerLevels = null;
this._powerLevelLoading = null;
this._observedMembers = null;
}

async _eventIdsToEntries(eventIds, txn) {
Expand Down Expand Up @@ -214,6 +215,32 @@ export class BaseRoom extends EventEmitter {
}
}

async observeMember(userId) {
if (!this._observedMembers) {
this._observedMembers = new Map();
}
const mapMember = this._observedMembers.get(userId);
if (mapMember) {
// Hit, we're already observing this member
return mapMember;
}
// Miss, load from storage/hs and set in map
const member = await fetchOrLoadMember({
summary: this._summary,
roomId: this._roomId,
userId,
storage: this._storage,
hsApi: this._hsApi
}, this._platform.logger);
if (!member) {
return null;
}
const observableMember = new RetainedObservableValue(member, () => this._observedMembers.delete(userId));
this._observedMembers.set(userId, observableMember);
return observableMember;
}


/** @public */
async loadMemberList(log = null) {
if (this._memberList) {
Expand Down
12 changes: 12 additions & 0 deletions src/matrix/room/Room.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ export class Room extends BaseRoom {
if (this._memberList) {
this._memberList.afterSync(memberChanges);
}
if (this._observedMembers) {
this._updateObservedMembers(memberChanges);
}
if (this._timeline) {
for (const [userId, memberChange] of memberChanges.entries()) {
if (userId === this._user.id) {
Expand Down Expand Up @@ -250,6 +253,15 @@ export class Room extends BaseRoom {
}
}

_updateObservedMembers(memberChanges) {
for (const [userId, memberChange] of memberChanges) {
const observableMember = this._observedMembers.get(userId);
if (observableMember) {
observableMember.set(memberChange.member);
}
}
}

needsAfterSyncCompleted({shouldFlushKeyShares}) {
return shouldFlushKeyShares;
}
Expand Down
46 changes: 46 additions & 0 deletions src/matrix/room/members/load.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,49 @@ export async function fetchOrLoadMembers(options, logger) {
return loadMembers(options);
}
}

export async function fetchOrLoadMember(options, logger) {
const member = await loadMember(options);
const {summary} = options;
if (!summary.data.hasFetchedMembers && !member) {
// We haven't fetched the memberlist yet; so ping the hs to see if this member does exist
return logger.wrapOrRun(options.log, "fetchMember", log => fetchMember(options, log));
}
return member;
}

async function loadMember({roomId, userId, storage}) {
const txn = await storage.readTxn([storage.storeNames.roomMembers,]);
const member = await txn.roomMembers.get(roomId, userId);
return member? new RoomMember(member) : null;
}

async function fetchMember({roomId, userId, hsApi, storage}, log) {
let memberData;
try {
memberData = await hsApi.state(roomId, "m.room.member", userId, { log }).response();
}
catch (error) {
if (error.name === "HomeServerError" && error.errcode === "M_NOT_FOUND") {
return null;
}
throw error;
}
const member = new RoomMember({
roomId,
userId,
membership: memberData.membership,
avatarUrl: memberData.avatar_url,
displayName: memberData.displayname,
});
const txn = await storage.readWriteTxn([storage.storeNames.roomMembers]);
try {
txn.roomMembers.set(member.serialize());
}
catch(e) {
txn.abort();
throw e;
}
await txn.complete();
return member;
}
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;
}