Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Peeking unknown rooms #1037

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
16 changes: 15 additions & 1 deletion src/domain/session/RoomViewModelObservable.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,24 @@ export class RoomViewModelObservable extends ObservableValue {
} else if (status & RoomStatus.Archived) {
return await this._sessionViewModel._createArchivedRoomViewModel(this.id);
} else {
return this._sessionViewModel._createUnknownRoomViewModel(this.id);
return this._sessionViewModel._createUnknownRoomViewModel(this.id, this._isWorldReadablePromise());
}
}

async _isWorldReadablePromise() {
const {session} = this._sessionViewModel._client;
const isWorldReadable = await session.isWorldReadableRoom(this.id);
if (isWorldReadable) {
const vm = await this._sessionViewModel._createWorldReadableRoomViewModel(this.id);
if (vm) {
this.get()?.dispose();
this.set(vm);
return true;
}
}
return false;
}

dispose() {
if (this._statusSubscription) {
this._statusSubscription = this._statusSubscription();
Expand Down
13 changes: 12 additions & 1 deletion src/domain/session/SessionViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ limitations under the License.
import {LeftPanelViewModel} from "./leftpanel/LeftPanelViewModel.js";
import {RoomViewModel} from "./room/RoomViewModel.js";
import {UnknownRoomViewModel} from "./room/UnknownRoomViewModel.js";
import {WorldReadableRoomViewModel} from "./room/WorldReadableRoomViewModel.js";
import {InviteViewModel} from "./room/InviteViewModel.js";
import {RoomBeingCreatedViewModel} from "./room/RoomBeingCreatedViewModel.js";
import {LightboxViewModel} from "./room/LightboxViewModel.js";
Expand Down Expand Up @@ -231,13 +232,23 @@ export class SessionViewModel extends ViewModel {
return null;
}

_createUnknownRoomViewModel(roomIdOrAlias) {
_createUnknownRoomViewModel(roomIdOrAlias, isWorldReadablePromise) {
return new UnknownRoomViewModel(this.childOptions({
roomIdOrAlias,
session: this._client.session,
isWorldReadablePromise: isWorldReadablePromise
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
isWorldReadablePromise: isWorldReadablePromise
isWorldReadablePromise

}));
}

async _createWorldReadableRoomViewModel(roomIdOrAlias) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this method used anywhere?

const roomVM = new WorldReadableRoomViewModel(this.childOptions({
room: await this._client.session.loadWorldReadableRoom(roomIdOrAlias),
session: this._client.session,
}));
roomVM.load();
return roomVM;
}

async _createArchivedRoomViewModel(roomId) {
const room = await this._client.session.loadArchivedRoom(roomId);
if (room) {
Expand Down
4 changes: 2 additions & 2 deletions src/domain/session/room/RoomViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class RoomViewModel extends ErrorReportViewModel {
this._composerVM = null;
if (room.isArchived) {
this._composerVM = this.track(new ArchivedViewModel(this.childOptions({archivedRoom: room})));
} else {
} else if (!room.isWorldReadable) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see this property (isWorldReadable) added to Room anywhere?

this._recreateComposerOnPowerLevelChange();
}
this._clearUnreadTimout = null;
Expand Down Expand Up @@ -218,7 +218,7 @@ export class RoomViewModel extends ErrorReportViewModel {
}
}
}

_sendMessage(message, replyingTo) {
return this.logAndCatch("RoomViewModel.sendMessage", async log => {
let success = false;
Expand Down
8 changes: 7 additions & 1 deletion src/domain/session/room/UnknownRoomViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,17 @@ import {ViewModel} from "../../ViewModel";
export class UnknownRoomViewModel extends ViewModel {
constructor(options) {
super(options);
const {roomIdOrAlias, session} = options;
const {roomIdOrAlias, session, isWorldReadablePromise} = options;
this._session = session;
this.roomIdOrAlias = roomIdOrAlias;
this._error = null;
this._busy = false;

this.checkingPreviewCapability = true;
isWorldReadablePromise.then(() => {
this.checkingPreviewCapability = false;
this.emitChange('checkingPreviewCapability');
})
}

get error() {
Expand Down
47 changes: 47 additions & 0 deletions src/domain/session/room/WorldReadableRoomViewModel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {RoomViewModel} from "./RoomViewModel";

export class WorldReadableRoomViewModel extends RoomViewModel {
constructor(options) {
options.room.isWorldReadable = true;
super(options);
this._room = options.room;
this._session = options.session;
this._error = null;
this._busy = false;
}

get kind() {
return "preview";
}

get busy() {
return this._busy;
}

async join() {
this._busy = true;
this.emitChange("busy");
try {
const roomId = await this._session.joinRoom(this._room.id);
// navigate to roomId if we were at the alias
// so we're subscribed to the right room status
// and we'll switch to the room view model once
// the join is synced
this.navigation.push("room", roomId);
// keep busy on true while waiting for the join to sync
} catch (err) {
this._error = err;
this._busy = false;
this.emitChange("error");
}
}

dispose() {
super.dispose();

// if joining the room, _busy would be true and in that case don't delete records
if (!this._busy) {
void this._session.deleteWorldReadableRoomData(this._room.id);
}
}
}
141 changes: 141 additions & 0 deletions src/matrix/Session.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ import {SecretStorage} from "./ssss/SecretStorage";
import {ObservableValue, RetainedObservableValue} from "../observable/value";
import {CallHandler} from "./calls/CallHandler";
import {RoomStateHandlerSet} from "./room/state/RoomStateHandlerSet";
import {EventKey} from "./room/timeline/EventKey";
import {createEventEntry} from "./room/timeline/persistence/common";

const PICKLE_KEY = "DEFAULT_KEY";
const PUSHER_KEY = "pusher";
Expand Down Expand Up @@ -655,6 +657,22 @@ export class Session {
return room;
}

/** @internal */
_createWorldReadableRoom(roomId) {
return new Room({
roomId,
getSyncToken: this._getSyncToken,
storage: this._storage,
emitCollectionChange: this._roomUpdateCallback,
hsApi: this._hsApi,
mediaRepository: this._mediaRepository,
pendingEvents: [],
user: this._user,
platform: this._platform,
roomStateHandler: this._roomStateHandler
});
}

get invites() {
return this._invites;
}
Expand Down Expand Up @@ -1031,12 +1049,135 @@ export class Session {
});
}

loadWorldReadableRoom(roomId, log = null) {
return this._platform.logger.wrapOrRun(log, "loadWorldReadableRoom", async log => {
log.set("id", roomId);

const room = this._createWorldReadableRoom(roomId);
let response = await this._fetchWorldReadableRoomEvents(roomId, 100, 'b', null, log);
// Note: response.end to be used in the next call for sync functionality

let summary = await this._prepareWorldReadableRoomSummary(roomId, log);
const txn = await this._storage.readTxn([
this._storage.storeNames.timelineFragments,
this._storage.storeNames.timelineEvents,
this._storage.storeNames.roomMembers,
]);
await room.load(summary, txn, log);

return room;
});
}

async _prepareWorldReadableRoomSummary(roomId, log = null) {
return this._platform.logger.wrapOrRun(log, "prepareWorldReadableRoomSummary", async log => {
log.set("id", roomId);

let summary = {};
const resp = await this._hsApi.currentState(roomId).response();
for ( let i=0; i<resp.length; i++ ) {
if ( resp[i].type === 'm.room.name') {
summary["name"] = resp[i].content.name;
} else if ( resp[i].type === 'm.room.canonical_alias' ) {
summary["canonicalAlias"] = resp[i].content.alias;
} else if ( resp[i].type === 'm.room.avatar' ) {
summary["avatarUrl"] = resp[i].content.url;
}
}
Comment on lines +1077 to +1086
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually use for-of loops whenever possible. Also can you refactor the if-else ladder here into a switch-case?


return summary;
});
}

async _fetchWorldReadableRoomEvents(roomId, limit = 30, dir = 'b', end = null, log = null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a typescript const enum for dir? You can add it in HomeSeverApi.

return this._platform.logger.wrapOrRun(log, "fetchWorldReadableRoomEvents", async log => {
log.set("id", roomId);
let options = {
limit: limit,
dir: 'b',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dir: 'b',
dir

filter: {
lazy_load_members: true,
include_redundant_members: true,
}
}
if (end !== null) {
options['from'] = end;
}

const response = await this._hsApi.messages(roomId, options, {log}).response();
log.set("/messages endpoint response", response);

await this.deleteWorldReadableRoomData(roomId, log);

const txn = await this._storage.readWriteTxn([
this._storage.storeNames.timelineFragments,
this._storage.storeNames.timelineEvents,
]);

// insert fragment and event records for this room
const fragment = {
roomId: roomId,
id: 0,
previousId: null,
nextId: null,
previousToken: response.start,
nextToken: null,
};
txn.timelineFragments.add(fragment);

let eventKey = EventKey.defaultLiveKey;
for (let i = 0; i < response.chunk.length; i++) {
if (i) {
eventKey = eventKey.previousKey();
}
let txn = await this._storage.readWriteTxn([this._storage.storeNames.timelineEvents]);
let eventEntry = createEventEntry(eventKey, roomId, response.chunk[i]);
await txn.timelineEvents.tryInsert(eventEntry, log);
}

return response;
});
}

async deleteWorldReadableRoomData(roomId, log = null) {
return this._platform.logger.wrapOrRun(log, "deleteWorldReadableRoomData", async log => {
log.set("id", roomId);

const txn = await this._storage.readWriteTxn([
this._storage.storeNames.timelineFragments,
this._storage.storeNames.timelineEvents,
]);

// clear old records for this room
txn.timelineFragments.removeAllForRoom(roomId);
txn.timelineEvents.removeAllForRoom(roomId);
});
}

joinRoom(roomIdOrAlias, log = null) {
return this._platform.logger.wrapOrRun(log, "joinRoom", async log => {
const body = await this._hsApi.joinIdOrAlias(roomIdOrAlias, {log}).response();
return body.room_id;
});
}

async isWorldReadableRoom(roomIdOrAlias, log = null) {
return this._platform.logger.wrapOrRun(log, "isWorldReadableRoom", async log => {
try {
let roomId;
if (!roomIdOrAlias.startsWith("!")) {
let response = await this._hsApi.resolveRoomAlias(roomIdOrAlias).response();
roomId = response.room_id;
} else {
roomId = roomIdOrAlias;
}
const body = await this._hsApi.state(roomId, 'm.room.history_visibility', '', {log}).response();
return body.history_visibility === 'world_readable';
} catch {
return false;
}
});
}
}

import {FeatureSet} from "../features";
Expand Down
8 changes: 8 additions & 0 deletions src/matrix/net/HomeServerApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ export class HomeServerApi {
return this._get("/sync", {since, timeout, filter}, undefined, options);
}

resolveRoomAlias(roomAlias: string): IHomeServerRequest {
return this._unauthedRequest( "GET", this._url( `/directory/room/${encodeURIComponent(roomAlias)}`, CS_V3_PREFIX ) );
}

context(roomId: string, eventId: string, limit: number, filter: string): IHomeServerRequest {
return this._get(`/rooms/${encodeURIComponent(roomId)}/context/${encodeURIComponent(eventId)}`, {filter, limit});
}
Expand Down Expand Up @@ -164,6 +168,10 @@ export class HomeServerApi {
return this._put(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, content, options);
}

currentState(roomId: string): IHomeServerRequest {
return this._get(`/rooms/${encodeURIComponent(roomId)}/state`, {}, undefined);
}

getLoginFlows(): IHomeServerRequest {
return this._unauthedRequest("GET", this._url("/#"));
}
Expand Down
28 changes: 27 additions & 1 deletion src/platform/web/ui/css/themes/element/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -975,7 +975,7 @@ button.link {
width: 100%;
}

.DisabledComposerView {
.DisabledComposerView, .WorldReadableRoomComposerView {
padding: 12px;
background-color: var(--background-color-secondary);
}
Expand All @@ -1002,6 +1002,32 @@ button.link {
width: 100%;
}

.UnknownRoomView .checkingPreviewCapability {
display: flex;
flex-direction: row; /* make main axis vertical */
justify-content: center; /* center items vertically, in this case */
align-items: center; /* center items horizontally, in this case */
margin-top: 5px;
}

.UnknownRoomView .checkingPreviewCapability p {
margin-left: 5px;
}

.WorldReadableRoomView .Timeline_message:hover > .Timeline_messageOptions{
display: none;
}
.WorldReadableRoomView .Timeline_messageAvatar {
pointer-events: none; /* Prevent user panel from opening when clicking on avatars in the timeline. */
}
.WorldReadableRoomComposerView h3 {
display: inline-block;
margin: 0;
}
.WorldReadableRoomComposerView .joinRoomButton {
float: right;
}

.LoadingView {
height: 100%;
width: 100%;
Expand Down
3 changes: 3 additions & 0 deletions src/platform/web/ui/session/SessionView.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ limitations under the License.
import {LeftPanelView} from "./leftpanel/LeftPanelView.js";
import {RoomView} from "./room/RoomView.js";
import {UnknownRoomView} from "./room/UnknownRoomView.js";
import {WorldReadableRoomView} from "./room/WorldReadableRoomView.js";
import {RoomBeingCreatedView} from "./room/RoomBeingCreatedView.js";
import {InviteView} from "./room/InviteView.js";
import {LightboxView} from "./room/LightboxView.js";
Expand Down Expand Up @@ -60,6 +61,8 @@ export class SessionView extends TemplateView {
return new RoomView(vm.currentRoomViewModel, viewClassForTile);
} else if (vm.currentRoomViewModel.kind === "roomBeingCreated") {
return new RoomBeingCreatedView(vm.currentRoomViewModel);
} else if (vm.currentRoomViewModel.kind === "preview") {
return new WorldReadableRoomView(vm.currentRoomViewModel);
} else {
return new UnknownRoomView(vm.currentRoomViewModel);
}
Expand Down
Loading