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

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("/login"));
}
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