forked from mystiq/hydrogen-web
Merge branch 'bwindels/calls' into thirdroom/dev
This commit is contained in:
commit
190a405e33
17 changed files with 412 additions and 76 deletions
|
@ -17,6 +17,7 @@ module.exports = {
|
||||||
"globals": {
|
"globals": {
|
||||||
"DEFINE_VERSION": "readonly",
|
"DEFINE_VERSION": "readonly",
|
||||||
"DEFINE_GLOBAL_HASH": "readonly",
|
"DEFINE_GLOBAL_HASH": "readonly",
|
||||||
|
"DEFINE_PROJECT_DIR": "readonly",
|
||||||
// only available in sw.js
|
// only available in sw.js
|
||||||
"DEFINE_UNHASHED_PRECACHED_ASSETS": "readonly",
|
"DEFINE_UNHASHED_PRECACHED_ASSETS": "readonly",
|
||||||
"DEFINE_HASHED_PRECACHED_ASSETS": "readonly",
|
"DEFINE_HASHED_PRECACHED_ASSETS": "readonly",
|
||||||
|
|
|
@ -31,7 +31,8 @@ import {
|
||||||
createNavigation,
|
createNavigation,
|
||||||
createRouter,
|
createRouter,
|
||||||
RoomViewModel,
|
RoomViewModel,
|
||||||
TimelineView
|
TimelineView,
|
||||||
|
viewClassForTile
|
||||||
} from "hydrogen-view-sdk";
|
} from "hydrogen-view-sdk";
|
||||||
import downloadSandboxPath from 'hydrogen-view-sdk/download-sandbox.html?url';
|
import downloadSandboxPath from 'hydrogen-view-sdk/download-sandbox.html?url';
|
||||||
import workerPath from 'hydrogen-view-sdk/main.js?url';
|
import workerPath from 'hydrogen-view-sdk/main.js?url';
|
||||||
|
@ -53,7 +54,7 @@ import "hydrogen-view-sdk/theme-element-light.css";
|
||||||
async function main() {
|
async function main() {
|
||||||
const app = document.querySelector<HTMLDivElement>('#app')!
|
const app = document.querySelector<HTMLDivElement>('#app')!
|
||||||
const config = {};
|
const config = {};
|
||||||
const platform = new Platform(app, assetPaths, config, { development: import.meta.env.DEV });
|
const platform = new Platform({container: app, assetPaths, config, options: { development: import.meta.env.DEV }});
|
||||||
const navigation = createNavigation();
|
const navigation = createNavigation();
|
||||||
platform.setNavigation(navigation);
|
platform.setNavigation(navigation);
|
||||||
const urlRouter = createRouter({
|
const urlRouter = createRouter({
|
||||||
|
@ -88,7 +89,7 @@ async function main() {
|
||||||
navigation,
|
navigation,
|
||||||
});
|
});
|
||||||
await vm.load();
|
await vm.load();
|
||||||
const view = new TimelineView(vm.timelineViewModel);
|
const view = new TimelineView(vm.timelineViewModel, viewClassForTile);
|
||||||
app.appendChild(view.mount());
|
app.appendChild(view.mount());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,9 +40,9 @@ export type Options = {
|
||||||
export class ViewModel<O extends Options = Options> extends EventEmitter<{change: never}> {
|
export class ViewModel<O extends Options = Options> extends EventEmitter<{change: never}> {
|
||||||
private disposables?: Disposables;
|
private disposables?: Disposables;
|
||||||
private _isDisposed = false;
|
private _isDisposed = false;
|
||||||
private _options: O;
|
private _options: Readonly<O>;
|
||||||
|
|
||||||
constructor(options: O) {
|
constructor(options: Readonly<O>) {
|
||||||
super();
|
super();
|
||||||
this._options = options;
|
this._options = options;
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ export class ViewModel<O extends Options = Options> extends EventEmitter<{change
|
||||||
return Object.assign({}, this._options, explicitOptions);
|
return Object.assign({}, this._options, explicitOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
get options(): O { return this._options; }
|
get options(): Readonly<O> { return this._options; }
|
||||||
|
|
||||||
// makes it easier to pass through dependencies of a sub-view model
|
// makes it easier to pass through dependencies of a sub-view model
|
||||||
getOption<N extends keyof O>(name: N): O[N] {
|
getOption<N extends keyof O>(name: N): O[N] {
|
||||||
|
@ -115,10 +115,6 @@ export class ViewModel<O extends Options = Options> extends EventEmitter<{change
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOptions(options: O): void {
|
|
||||||
this._options = Object.assign(this._options, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitChange(changedProps: any): void {
|
emitChange(changedProps: any): void {
|
||||||
if (this._options.emitChange) {
|
if (this._options.emitChange) {
|
||||||
this._options.emitChange(changedProps);
|
this._options.emitChange(changedProps);
|
||||||
|
|
|
@ -22,6 +22,7 @@ export class SimpleTile extends ViewModel {
|
||||||
constructor(entry, options) {
|
constructor(entry, options) {
|
||||||
super(options);
|
super(options);
|
||||||
this._entry = entry;
|
this._entry = entry;
|
||||||
|
this._emitUpdate = undefined;
|
||||||
}
|
}
|
||||||
// view model props for all subclasses
|
// view model props for all subclasses
|
||||||
// hmmm, could also do instanceof ... ?
|
// hmmm, could also do instanceof ... ?
|
||||||
|
@ -67,16 +68,20 @@ export class SimpleTile extends ViewModel {
|
||||||
|
|
||||||
// TilesCollection contract below
|
// TilesCollection contract below
|
||||||
setUpdateEmit(emitUpdate) {
|
setUpdateEmit(emitUpdate) {
|
||||||
this.updateOptions({emitChange: paramName => {
|
this._emitUpdate = emitUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** overrides the emitChange in ViewModel to also emit the update over the tiles collection */
|
||||||
|
emitChange(changedProps) {
|
||||||
|
if (this._emitUpdate) {
|
||||||
// it can happen that after some network call
|
// it can happen that after some network call
|
||||||
// we switched away from the room and the response
|
// we switched away from the room and the response
|
||||||
// comes in, triggering an emitChange in a tile that
|
// comes in, triggering an emitChange in a tile that
|
||||||
// has been disposed already (and hence the change
|
// has been disposed already (and hence the change
|
||||||
// callback has been cleared by dispose) We should just ignore this.
|
// callback has been cleared by dispose) We should just ignore this.
|
||||||
if (emitUpdate) {
|
this._emitUpdate(this, changedProps);
|
||||||
emitUpdate(this, paramName);
|
}
|
||||||
}
|
super.emitChange(changedProps);
|
||||||
}});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get upperEntry() {
|
get upperEntry() {
|
||||||
|
|
|
@ -15,12 +15,20 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export {Logger} from "./logging/Logger";
|
export {Logger} from "./logging/Logger";
|
||||||
|
export type {ILogItem} from "./logging/types";
|
||||||
export {IDBLogPersister} from "./logging/IDBLogPersister";
|
export {IDBLogPersister} from "./logging/IDBLogPersister";
|
||||||
export {ConsoleReporter} from "./logging/ConsoleReporter";
|
export {ConsoleReporter} from "./logging/ConsoleReporter";
|
||||||
export {Platform} from "./platform/web/Platform.js";
|
export {Platform} from "./platform/web/Platform.js";
|
||||||
export {Client, LoadStatus} from "./matrix/Client.js";
|
export {Client, LoadStatus} from "./matrix/Client.js";
|
||||||
export {RoomStatus} from "./matrix/room/common";
|
export {RoomStatus} from "./matrix/room/common";
|
||||||
export {CallIntent} from "./matrix/calls/callEventTypes";
|
export {CallIntent} from "./matrix/calls/callEventTypes";
|
||||||
|
// export everything needed to observe state events on all rooms using session.observeRoomState
|
||||||
|
export type {RoomStateHandler} from "./matrix/room/state/types";
|
||||||
|
export type {MemberChange} from "./matrix/room/members/RoomMember";
|
||||||
|
export type {Transaction} from "./matrix/storage/idb/Transaction";
|
||||||
|
export type {Room} from "./matrix/room/Room";
|
||||||
|
export type {StateEvent} from "./matrix/storage/types";
|
||||||
|
|
||||||
// export main view & view models
|
// export main view & view models
|
||||||
export {createNavigation, createRouter} from "./domain/navigation/index.js";
|
export {createNavigation, createRouter} from "./domain/navigation/index.js";
|
||||||
export {RootViewModel} from "./domain/RootViewModel.js";
|
export {RootViewModel} from "./domain/RootViewModel.js";
|
||||||
|
|
|
@ -132,14 +132,15 @@ export class Client {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async startRegistration(homeserver, username, password, initialDeviceDisplayName) {
|
async startRegistration(homeserver, username, password, initialDeviceDisplayName, flowSelector) {
|
||||||
const request = this._platform.request;
|
const request = this._platform.request;
|
||||||
const hsApi = new HomeServerApi({homeserver, request});
|
const hsApi = new HomeServerApi({homeserver, request});
|
||||||
const registration = new Registration(hsApi, {
|
const registration = new Registration(hsApi, {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
initialDeviceDisplayName,
|
initialDeviceDisplayName,
|
||||||
});
|
},
|
||||||
|
flowSelector);
|
||||||
return registration;
|
return registration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ import {SecretStorage} from "./ssss/SecretStorage";
|
||||||
import {ObservableValue} from "../observable/value/ObservableValue";
|
import {ObservableValue} from "../observable/value/ObservableValue";
|
||||||
import {RetainedObservableValue} from "../observable/value/RetainedObservableValue";
|
import {RetainedObservableValue} from "../observable/value/RetainedObservableValue";
|
||||||
import {CallHandler} from "./calls/CallHandler";
|
import {CallHandler} from "./calls/CallHandler";
|
||||||
|
import {RoomStateHandlerSet} from "./room/state/RoomStateHandlerSet";
|
||||||
|
|
||||||
const PICKLE_KEY = "DEFAULT_KEY";
|
const PICKLE_KEY = "DEFAULT_KEY";
|
||||||
const PUSHER_KEY = "pusher";
|
const PUSHER_KEY = "pusher";
|
||||||
|
@ -101,6 +102,8 @@ export class Session {
|
||||||
}],
|
}],
|
||||||
forceTURN: false,
|
forceTURN: false,
|
||||||
});
|
});
|
||||||
|
this._roomStateHandler = new RoomStateHandlerSet();
|
||||||
|
this.observeRoomState(this._callHandler);
|
||||||
this._deviceMessageHandler = new DeviceMessageHandler({storage, callHandler: this._callHandler});
|
this._deviceMessageHandler = new DeviceMessageHandler({storage, callHandler: this._callHandler});
|
||||||
this._olm = olm;
|
this._olm = olm;
|
||||||
this._olmUtil = null;
|
this._olmUtil = null;
|
||||||
|
@ -595,7 +598,7 @@ export class Session {
|
||||||
user: this._user,
|
user: this._user,
|
||||||
createRoomEncryption: this._createRoomEncryption,
|
createRoomEncryption: this._createRoomEncryption,
|
||||||
platform: this._platform,
|
platform: this._platform,
|
||||||
callHandler: this._callHandler
|
roomStateHandler: this._roomStateHandler
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -937,6 +940,10 @@ export class Session {
|
||||||
return observable;
|
return observable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observeRoomState(roomStateHandler) {
|
||||||
|
return this._roomStateHandler.subscribe(roomStateHandler);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Creates an empty (summary isn't loaded) the archived room if it isn't
|
Creates an empty (summary isn't loaded) the archived room if it isn't
|
||||||
loaded already, assuming sync will either remove it (when rejoining) or
|
loaded already, assuming sync will either remove it (when rejoining) or
|
||||||
|
|
|
@ -35,6 +35,7 @@ import type {Options as GroupCallOptions} from "./group/GroupCall";
|
||||||
import type {Transaction} from "../storage/idb/Transaction";
|
import type {Transaction} from "../storage/idb/Transaction";
|
||||||
import type {CallEntry} from "../storage/idb/stores/CallStore";
|
import type {CallEntry} from "../storage/idb/stores/CallStore";
|
||||||
import type {Clock} from "../../platform/web/dom/Clock";
|
import type {Clock} from "../../platform/web/dom/Clock";
|
||||||
|
import type {RoomStateHandler} from "../room/state/types";
|
||||||
|
|
||||||
export type Options = Omit<GroupCallOptions, "emitUpdate" | "createTimeout"> & {
|
export type Options = Omit<GroupCallOptions, "emitUpdate" | "createTimeout"> & {
|
||||||
clock: Clock
|
clock: Clock
|
||||||
|
@ -44,7 +45,7 @@ function getRoomMemberKey(roomId: string, userId: string): string {
|
||||||
return JSON.stringify(roomId)+`,`+JSON.stringify(userId);
|
return JSON.stringify(roomId)+`,`+JSON.stringify(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CallHandler {
|
export class CallHandler implements RoomStateHandler {
|
||||||
// group calls by call id
|
// group calls by call id
|
||||||
private readonly _calls: ObservableMap<string, GroupCall> = new ObservableMap<string, GroupCall>();
|
private readonly _calls: ObservableMap<string, GroupCall> = new ObservableMap<string, GroupCall>();
|
||||||
// map of `"roomId","userId"` to set of conf_id's they are in
|
// map of `"roomId","userId"` to set of conf_id's they are in
|
||||||
|
@ -143,18 +144,12 @@ export class CallHandler {
|
||||||
// TODO: check and poll turn server credentials here
|
// TODO: check and poll turn server credentials here
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
handleRoomState(room: Room, events: StateEvent[], txn: Transaction, log: ILogItem) {
|
handleRoomState(room: Room, event: StateEvent, txn: Transaction, log: ILogItem) {
|
||||||
// first update call events
|
if (event.type === EventType.GroupCall) {
|
||||||
for (const event of events) {
|
this.handleCallEvent(event, room.id, txn, log);
|
||||||
if (event.type === EventType.GroupCall) {
|
|
||||||
this.handleCallEvent(event, room.id, txn, log);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// then update members
|
if (event.type === EventType.GroupCallMember) {
|
||||||
for (const event of events) {
|
this.handleCallMemberEvent(event, room.id, log);
|
||||||
if (event.type === EventType.GroupCallMember) {
|
|
||||||
this.handleCallMemberEvent(event, room.id, log);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {reduceStateEvents} from "./RoomSummary.js";
|
import {iterateResponseStateEvents} from "./common";
|
||||||
import {BaseRoom} from "./BaseRoom.js";
|
import {BaseRoom} from "./BaseRoom.js";
|
||||||
import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "./members/RoomMember.js";
|
import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "./members/RoomMember.js";
|
||||||
|
|
||||||
|
@ -173,15 +173,15 @@ export class ArchivedRoom extends BaseRoom {
|
||||||
}
|
}
|
||||||
|
|
||||||
function findKickDetails(roomResponse, ownUserId) {
|
function findKickDetails(roomResponse, ownUserId) {
|
||||||
const kickEvent = reduceStateEvents(roomResponse, (kickEvent, event) => {
|
let kickEvent;
|
||||||
|
iterateResponseStateEvents(roomResponse, event => {
|
||||||
if (event.type === MEMBER_EVENT_TYPE) {
|
if (event.type === MEMBER_EVENT_TYPE) {
|
||||||
// did we get kicked?
|
// did we get kicked?
|
||||||
if (event.state_key === ownUserId && event.sender !== event.state_key) {
|
if (event.state_key === ownUserId && event.sender !== event.state_key) {
|
||||||
kickEvent = event;
|
kickEvent = event;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return kickEvent;
|
});
|
||||||
}, null);
|
|
||||||
if (kickEvent) {
|
if (kickEvent) {
|
||||||
return {
|
return {
|
||||||
// this is different from the room membership in the sync section, which can only be leave
|
// this is different from the room membership in the sync section, which can only be leave
|
||||||
|
|
|
@ -31,6 +31,8 @@ import {ensureLogItem} from "../../logging/utils";
|
||||||
import {PowerLevels} from "./PowerLevels.js";
|
import {PowerLevels} from "./PowerLevels.js";
|
||||||
import {RetainedObservableValue} from "../../observable/value/RetainedObservableValue";
|
import {RetainedObservableValue} from "../../observable/value/RetainedObservableValue";
|
||||||
import {TimelineReader} from "./timeline/persistence/TimelineReader";
|
import {TimelineReader} from "./timeline/persistence/TimelineReader";
|
||||||
|
import {ObservedStateTypeMap} from "./state/ObservedStateTypeMap";
|
||||||
|
import {ObservedStateKeyValue} from "./state/ObservedStateKeyValue";
|
||||||
|
|
||||||
const EVENT_ENCRYPTED_TYPE = "m.room.encrypted";
|
const EVENT_ENCRYPTED_TYPE = "m.room.encrypted";
|
||||||
|
|
||||||
|
@ -53,11 +55,35 @@ export class BaseRoom extends EventEmitter {
|
||||||
this._getSyncToken = getSyncToken;
|
this._getSyncToken = getSyncToken;
|
||||||
this._platform = platform;
|
this._platform = platform;
|
||||||
this._observedEvents = null;
|
this._observedEvents = null;
|
||||||
|
this._roomStateObservers = new Set();
|
||||||
this._powerLevels = null;
|
this._powerLevels = null;
|
||||||
this._powerLevelLoading = null;
|
this._powerLevelLoading = null;
|
||||||
this._observedMembers = null;
|
this._observedMembers = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async observeStateType(type, txn = undefined) {
|
||||||
|
const map = new ObservedStateTypeMap(type);
|
||||||
|
await this._addStateObserver(map, txn);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
async observeStateTypeAndKey(type, stateKey, txn = undefined) {
|
||||||
|
const value = new ObservedStateKeyValue(type, stateKey);
|
||||||
|
await this._addStateObserver(value, txn);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _addStateObserver(stateObserver, txn) {
|
||||||
|
if (!txn) {
|
||||||
|
txn = await this._storage.readTxn([this._storage.storeNames.roomState]);
|
||||||
|
}
|
||||||
|
await stateObserver.load(this.id, txn);
|
||||||
|
this._roomStateObservers.add(stateObserver);
|
||||||
|
stateObserver.setRemoveCallback(() => {
|
||||||
|
this._roomStateObservers.delete(stateObserver);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async _eventIdsToEntries(eventIds, txn) {
|
async _eventIdsToEntries(eventIds, txn) {
|
||||||
const retryEntries = [];
|
const retryEntries = [];
|
||||||
await Promise.all(eventIds.map(async eventId => {
|
await Promise.all(eventIds.map(async eventId => {
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {WrappedError} from "../error.js"
|
||||||
import {Heroes} from "./members/Heroes.js";
|
import {Heroes} from "./members/Heroes.js";
|
||||||
import {AttachmentUpload} from "./AttachmentUpload.js";
|
import {AttachmentUpload} from "./AttachmentUpload.js";
|
||||||
import {DecryptionSource} from "../e2ee/common.js";
|
import {DecryptionSource} from "../e2ee/common.js";
|
||||||
|
import {iterateResponseStateEvents} from "./common.js";
|
||||||
import {PowerLevels, EVENT_TYPE as POWERLEVELS_EVENT_TYPE } from "./PowerLevels.js";
|
import {PowerLevels, EVENT_TYPE as POWERLEVELS_EVENT_TYPE } from "./PowerLevels.js";
|
||||||
|
|
||||||
const EVENT_ENCRYPTED_TYPE = "m.room.encrypted";
|
const EVENT_ENCRYPTED_TYPE = "m.room.encrypted";
|
||||||
|
@ -30,7 +31,7 @@ const EVENT_ENCRYPTED_TYPE = "m.room.encrypted";
|
||||||
export class Room extends BaseRoom {
|
export class Room extends BaseRoom {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
this._callHandler = options.callHandler;
|
this._roomStateHandler = options.roomStateHandler;
|
||||||
// TODO: pass pendingEvents to start like pendingOperations?
|
// TODO: pass pendingEvents to start like pendingOperations?
|
||||||
const {pendingEvents} = options;
|
const {pendingEvents} = options;
|
||||||
const relationWriter = new RelationWriter({
|
const relationWriter = new RelationWriter({
|
||||||
|
@ -179,8 +180,9 @@ export class Room extends BaseRoom {
|
||||||
removedPendingEvents = await this._sendQueue.removeRemoteEchos(roomResponse.timeline.events, txn, log);
|
removedPendingEvents = await this._sendQueue.removeRemoteEchos(roomResponse.timeline.events, txn, log);
|
||||||
}
|
}
|
||||||
const powerLevelsEvent = this._getPowerLevelsEvent(roomResponse);
|
const powerLevelsEvent = this._getPowerLevelsEvent(roomResponse);
|
||||||
this._updateCallHandler(roomResponse, txn, log);
|
this._runRoomStateHandlers(roomResponse, txn, log);
|
||||||
return {
|
return {
|
||||||
|
roomResponse,
|
||||||
summaryChanges,
|
summaryChanges,
|
||||||
roomEncryption,
|
roomEncryption,
|
||||||
newEntries,
|
newEntries,
|
||||||
|
@ -203,7 +205,7 @@ export class Room extends BaseRoom {
|
||||||
const {
|
const {
|
||||||
summaryChanges, newEntries, updatedEntries, newLiveKey,
|
summaryChanges, newEntries, updatedEntries, newLiveKey,
|
||||||
removedPendingEvents, memberChanges, powerLevelsEvent,
|
removedPendingEvents, memberChanges, powerLevelsEvent,
|
||||||
heroChanges, roomEncryption
|
heroChanges, roomEncryption, roomResponse
|
||||||
} = changes;
|
} = changes;
|
||||||
log.set("id", this.id);
|
log.set("id", this.id);
|
||||||
this._syncWriter.afterSync(newLiveKey);
|
this._syncWriter.afterSync(newLiveKey);
|
||||||
|
@ -217,9 +219,7 @@ export class Room extends BaseRoom {
|
||||||
if (this._memberList) {
|
if (this._memberList) {
|
||||||
this._memberList.afterSync(memberChanges);
|
this._memberList.afterSync(memberChanges);
|
||||||
}
|
}
|
||||||
if (this._callHandler) {
|
this._roomStateHandler.updateRoomMembers(this, memberChanges);
|
||||||
this._callHandler.updateRoomMembers(this, memberChanges);
|
|
||||||
}
|
|
||||||
if (this._observedMembers) {
|
if (this._observedMembers) {
|
||||||
this._updateObservedMembers(memberChanges);
|
this._updateObservedMembers(memberChanges);
|
||||||
}
|
}
|
||||||
|
@ -265,6 +265,7 @@ export class Room extends BaseRoom {
|
||||||
if (removedPendingEvents) {
|
if (removedPendingEvents) {
|
||||||
this._sendQueue.emitRemovals(removedPendingEvents);
|
this._sendQueue.emitRemovals(removedPendingEvents);
|
||||||
}
|
}
|
||||||
|
this._emitSyncRoomState(roomResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateObservedMembers(memberChanges) {
|
_updateObservedMembers(memberChanges) {
|
||||||
|
@ -277,8 +278,13 @@ export class Room extends BaseRoom {
|
||||||
}
|
}
|
||||||
|
|
||||||
_getPowerLevelsEvent(roomResponse) {
|
_getPowerLevelsEvent(roomResponse) {
|
||||||
const isPowerlevelEvent = event => event.state_key === "" && event.type === POWERLEVELS_EVENT_TYPE;
|
let powerLevelEvent;
|
||||||
const powerLevelEvent = roomResponse.timeline?.events.find(isPowerlevelEvent) ?? roomResponse.state?.events.find(isPowerlevelEvent);
|
iterateResponseStateEvents(roomResponse, event => {
|
||||||
|
if(event.state_key === "" && event.type === POWERLEVELS_EVENT_TYPE) {
|
||||||
|
powerLevelEvent = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
return powerLevelEvent;
|
return powerLevelEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,20 +453,20 @@ export class Room extends BaseRoom {
|
||||||
return this._sendQueue.pendingEvents;
|
return this._sendQueue.pendingEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateCallHandler(roomResponse, txn, log) {
|
/** global room state handlers, run during write sync step */
|
||||||
if (this._callHandler) {
|
_runRoomStateHandlers(roomResponse, txn, log) {
|
||||||
const stateEvents = roomResponse.state?.events;
|
iterateResponseStateEvents(roomResponse, event => {
|
||||||
if (stateEvents?.length) {
|
this._roomStateHandler.handleRoomState(this, event, txn, log);
|
||||||
this._callHandler.handleRoomState(this, stateEvents, txn, log);
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** local room state observers, run during after sync step */
|
||||||
|
_emitSyncRoomState(roomResponse) {
|
||||||
|
iterateResponseStateEvents(roomResponse, event => {
|
||||||
|
for (const handler of this._roomStateObservers) {
|
||||||
|
handler.handleStateEvent(event);
|
||||||
}
|
}
|
||||||
let timelineEvents = roomResponse.timeline?.events;
|
});
|
||||||
if (timelineEvents) {
|
|
||||||
const timelineStateEvents = timelineEvents.filter(e => typeof e.state_key === "string");
|
|
||||||
if (timelineEvents.length !== 0) {
|
|
||||||
this._callHandler.handleRoomState(this, timelineStateEvents, txn, log);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @package */
|
/** @package */
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MEGOLM_ALGORITHM} from "../e2ee/common.js";
|
import {MEGOLM_ALGORITHM} from "../e2ee/common.js";
|
||||||
|
import {iterateResponseStateEvents} from "./common";
|
||||||
|
|
||||||
function applyTimelineEntries(data, timelineEntries, isInitialSync, canMarkUnread, ownUserId) {
|
function applyTimelineEntries(data, timelineEntries, isInitialSync, canMarkUnread, ownUserId) {
|
||||||
if (timelineEntries.length) {
|
if (timelineEntries.length) {
|
||||||
|
@ -27,25 +27,6 @@ function applyTimelineEntries(data, timelineEntries, isInitialSync, canMarkUnrea
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reduceStateEvents(roomResponse, callback, value) {
|
|
||||||
const stateEvents = roomResponse?.state?.events;
|
|
||||||
// state comes before timeline
|
|
||||||
if (Array.isArray(stateEvents)) {
|
|
||||||
value = stateEvents.reduce(callback, value);
|
|
||||||
}
|
|
||||||
const timelineEvents = roomResponse?.timeline?.events;
|
|
||||||
// and after that state events in the timeline
|
|
||||||
if (Array.isArray(timelineEvents)) {
|
|
||||||
value = timelineEvents.reduce((data, event) => {
|
|
||||||
if (typeof event.state_key === "string") {
|
|
||||||
value = callback(value, event);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}, value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applySyncResponse(data, roomResponse, membership, ownUserId) {
|
function applySyncResponse(data, roomResponse, membership, ownUserId) {
|
||||||
if (roomResponse.summary) {
|
if (roomResponse.summary) {
|
||||||
data = updateSummary(data, roomResponse.summary);
|
data = updateSummary(data, roomResponse.summary);
|
||||||
|
@ -60,7 +41,9 @@ function applySyncResponse(data, roomResponse, membership, ownUserId) {
|
||||||
// process state events in state and in timeline.
|
// process state events in state and in timeline.
|
||||||
// non-state events are handled by applyTimelineEntries
|
// non-state events are handled by applyTimelineEntries
|
||||||
// so decryption is handled properly
|
// so decryption is handled properly
|
||||||
data = reduceStateEvents(roomResponse, (data, event) => processStateEvent(data, event, ownUserId), data);
|
iterateResponseStateEvents(roomResponse, event => {
|
||||||
|
data = processStateEvent(data, event, ownUserId);
|
||||||
|
});
|
||||||
const unreadNotifications = roomResponse.unread_notifications;
|
const unreadNotifications = roomResponse.unread_notifications;
|
||||||
if (unreadNotifications) {
|
if (unreadNotifications) {
|
||||||
data = processNotificationCounts(data, unreadNotifications);
|
data = processNotificationCounts(data, unreadNotifications);
|
||||||
|
|
|
@ -14,6 +14,12 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type {Room} from "./Room";
|
||||||
|
import type {StateEvent, TimelineEvent} from "../storage/types";
|
||||||
|
import type {Transaction} from "../storage/idb/Transaction";
|
||||||
|
import type {ILogItem} from "../../logging/types";
|
||||||
|
import type {MemberChange} from "./members/RoomMember";
|
||||||
|
|
||||||
export function getPrevContentFromStateEvent(event) {
|
export function getPrevContentFromStateEvent(event) {
|
||||||
// where to look for prev_content is a bit of a mess,
|
// where to look for prev_content is a bit of a mess,
|
||||||
// see https://matrix.to/#/!NasysSDfxKxZBzJJoE:matrix.org/$DvrAbZJiILkOmOIuRsNoHmh2v7UO5CWp_rYhlGk34fQ?via=matrix.org&via=pixie.town&via=amorgan.xyz
|
// see https://matrix.to/#/!NasysSDfxKxZBzJJoE:matrix.org/$DvrAbZJiILkOmOIuRsNoHmh2v7UO5CWp_rYhlGk34fQ?via=matrix.org&via=pixie.town&via=amorgan.xyz
|
||||||
|
@ -40,3 +46,72 @@ export enum RoomType {
|
||||||
Private,
|
Private,
|
||||||
Public
|
Public
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RoomResponse = {
|
||||||
|
state?: {
|
||||||
|
events?: Array<StateEvent>
|
||||||
|
},
|
||||||
|
timeline?: {
|
||||||
|
events?: Array<StateEvent>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** iterates over any state events in a sync room response, in the order that they should be applied (from older to younger events) */
|
||||||
|
export function iterateResponseStateEvents(roomResponse: RoomResponse, callback: (StateEvent) => void) {
|
||||||
|
// first iterate over state events, they precede the timeline
|
||||||
|
const stateEvents = roomResponse.state?.events;
|
||||||
|
if (stateEvents) {
|
||||||
|
for (let i = 0; i < stateEvents.length; i++) {
|
||||||
|
callback(stateEvents[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// now see if there are any state events within the timeline
|
||||||
|
let timelineEvents = roomResponse.timeline?.events;
|
||||||
|
if (timelineEvents) {
|
||||||
|
for (let i = 0; i < timelineEvents.length; i++) {
|
||||||
|
const event = timelineEvents[i];
|
||||||
|
if (typeof event.state_key === "string") {
|
||||||
|
callback(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tests() {
|
||||||
|
return {
|
||||||
|
"test iterateResponseStateEvents with both state and timeline sections": assert => {
|
||||||
|
const roomResponse = {
|
||||||
|
state: {
|
||||||
|
events: [
|
||||||
|
{type: "m.room.member", state_key: "1"},
|
||||||
|
{type: "m.room.member", state_key: "2", content: {a: 1}},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
timeline: {
|
||||||
|
events: [
|
||||||
|
{type: "m.room.message"},
|
||||||
|
{type: "m.room.member", state_key: "3"},
|
||||||
|
{type: "m.room.message"},
|
||||||
|
{type: "m.room.member", state_key: "2", content: {a: 2}},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} as unknown as RoomResponse;
|
||||||
|
const expectedStateKeys = ["1", "2", "3", "2"];
|
||||||
|
const expectedAForMember2 = [1, 2];
|
||||||
|
iterateResponseStateEvents(roomResponse, event => {
|
||||||
|
assert.strictEqual(event.type, "m.room.member");
|
||||||
|
assert.strictEqual(expectedStateKeys.shift(), event.state_key);
|
||||||
|
if (event.state_key === "2") {
|
||||||
|
assert.strictEqual(expectedAForMember2.shift(), event.content.a);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert.strictEqual(expectedStateKeys.length, 0);
|
||||||
|
assert.strictEqual(expectedAForMember2.length, 0);
|
||||||
|
},
|
||||||
|
"test iterateResponseStateEvents with empty response": assert => {
|
||||||
|
iterateResponseStateEvents({}, () => {
|
||||||
|
assert.fail("no events expected");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
104
src/matrix/room/state/ObservedStateKeyValue.ts
Normal file
104
src/matrix/room/state/ObservedStateKeyValue.ts
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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 type {StateObserver} from "./types";
|
||||||
|
import type {StateEvent} from "../../storage/types";
|
||||||
|
import type {Transaction} from "../../storage/idb/Transaction";
|
||||||
|
import {BaseObservableValue} from "../../../observable/value/BaseObservableValue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable value for a state event with a given type and state key.
|
||||||
|
* Unsubscribes when last subscription is removed */
|
||||||
|
export class ObservedStateKeyValue extends BaseObservableValue<StateEvent | undefined> implements StateObserver {
|
||||||
|
private event?: StateEvent;
|
||||||
|
private removeCallback?: () => void;
|
||||||
|
|
||||||
|
constructor(private readonly type: string, private readonly stateKey: string) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
/** @internal */
|
||||||
|
async load(roomId: string, txn: Transaction): Promise<void> {
|
||||||
|
this.event = (await txn.roomState.get(roomId, this.type, this.stateKey))?.event;
|
||||||
|
}
|
||||||
|
/** @internal */
|
||||||
|
handleStateEvent(event: StateEvent) {
|
||||||
|
if (event.type === this.type && event.state_key === this.stateKey) {
|
||||||
|
this.event = event;
|
||||||
|
this.emit(this.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(): StateEvent | undefined {
|
||||||
|
return this.event;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRemoveCallback(callback: () => void) {
|
||||||
|
this.removeCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnsubscribeLast() {
|
||||||
|
this.removeCallback?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import {createMockStorage} from "../../../mocks/Storage";
|
||||||
|
|
||||||
|
export async function tests() {
|
||||||
|
return {
|
||||||
|
"test load and update": async assert => {
|
||||||
|
const storage = await createMockStorage();
|
||||||
|
const writeTxn = await storage.readWriteTxn([storage.storeNames.roomState]);
|
||||||
|
writeTxn.roomState.set("!abc", {
|
||||||
|
event_id: "$abc",
|
||||||
|
type: "m.room.member",
|
||||||
|
state_key: "@alice",
|
||||||
|
sender: "@alice",
|
||||||
|
origin_server_ts: 5,
|
||||||
|
content: {}
|
||||||
|
});
|
||||||
|
await writeTxn.complete();
|
||||||
|
const txn = await storage.readTxn([storage.storeNames.roomState]);
|
||||||
|
const value = new ObservedStateKeyValue("m.room.member", "@alice");
|
||||||
|
await value.load("!abc", txn);
|
||||||
|
const updates: Array<StateEvent | undefined> = [];
|
||||||
|
assert.strictEqual(value.get()?.origin_server_ts, 5);
|
||||||
|
const unsubscribe = value.subscribe(value => updates.push(value));
|
||||||
|
value.handleStateEvent({
|
||||||
|
event_id: "$abc",
|
||||||
|
type: "m.room.member",
|
||||||
|
state_key: "@bob",
|
||||||
|
sender: "@alice",
|
||||||
|
origin_server_ts: 10,
|
||||||
|
content: {}
|
||||||
|
});
|
||||||
|
assert.strictEqual(updates.length, 0);
|
||||||
|
value.handleStateEvent({
|
||||||
|
event_id: "$abc",
|
||||||
|
type: "m.room.member",
|
||||||
|
state_key: "@alice",
|
||||||
|
sender: "@alice",
|
||||||
|
origin_server_ts: 10,
|
||||||
|
content: {}
|
||||||
|
});
|
||||||
|
assert.strictEqual(updates.length, 1);
|
||||||
|
assert.strictEqual(updates[0]?.origin_server_ts, 10);
|
||||||
|
let removed = false;
|
||||||
|
value.setRemoveCallback(() => removed = true);
|
||||||
|
unsubscribe();
|
||||||
|
assert(removed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
src/matrix/room/state/ObservedStateTypeMap.ts
Normal file
53
src/matrix/room/state/ObservedStateTypeMap.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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 type {StateObserver} from "./types";
|
||||||
|
import type {StateEvent} from "../../storage/types";
|
||||||
|
import type {Transaction} from "../../storage/idb/Transaction";
|
||||||
|
import {ObservableMap} from "../../../observable/map/ObservableMap";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable map for a given type with state keys as map keys.
|
||||||
|
* Unsubscribes when last subscription is removed */
|
||||||
|
export class ObservedStateTypeMap extends ObservableMap<string, StateEvent> implements StateObserver {
|
||||||
|
private removeCallback?: () => void;
|
||||||
|
|
||||||
|
constructor(private readonly type: string) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
/** @internal */
|
||||||
|
async load(roomId: string, txn: Transaction): Promise<void> {
|
||||||
|
const events = await txn.roomState.getAllForType(roomId, this.type);
|
||||||
|
for (let i = 0; i < events.length; ++i) {
|
||||||
|
const {event} = events[i];
|
||||||
|
this.add(event.state_key, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** @internal */
|
||||||
|
handleStateEvent(event: StateEvent) {
|
||||||
|
if (event.type === this.type) {
|
||||||
|
this.set(event.state_key, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRemoveCallback(callback: () => void) {
|
||||||
|
this.removeCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnsubscribeLast() {
|
||||||
|
this.removeCallback?.();
|
||||||
|
}
|
||||||
|
}
|
37
src/matrix/room/state/RoomStateHandlerSet.ts
Normal file
37
src/matrix/room/state/RoomStateHandlerSet.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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 type {ILogItem} from "../../../logging/types";
|
||||||
|
import type {StateEvent} from "../../storage/types";
|
||||||
|
import type {Transaction} from "../../storage/idb/Transaction";
|
||||||
|
import type {Room} from "../Room";
|
||||||
|
import type {MemberChange} from "../members/RoomMember";
|
||||||
|
import type {RoomStateHandler} from "./types";
|
||||||
|
import {BaseObservable} from "../../../observable/BaseObservable";
|
||||||
|
|
||||||
|
/** keeps track of all handlers registered with Session.observeRoomState */
|
||||||
|
export class RoomStateHandlerSet extends BaseObservable<RoomStateHandler> implements RoomStateHandler {
|
||||||
|
handleRoomState(room: Room, stateEvent: StateEvent, txn: Transaction, log: ILogItem) {
|
||||||
|
for(let h of this._handlers) {
|
||||||
|
h.handleRoomState(room, stateEvent, txn, log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateRoomMembers(room: Room, memberChanges: Map<string, MemberChange>) {
|
||||||
|
for(let h of this._handlers) {
|
||||||
|
h.updateRoomMembers(room, memberChanges);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
src/matrix/room/state/types.ts
Normal file
38
src/matrix/room/state/types.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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 type {Room} from "../Room";
|
||||||
|
import type {StateEvent} from "../../storage/types";
|
||||||
|
import type {Transaction} from "../../storage/idb/Transaction";
|
||||||
|
import type {ILogItem} from "../../../logging/types";
|
||||||
|
import type {MemberChange} from "../members/RoomMember";
|
||||||
|
|
||||||
|
/** used for Session.observeRoomState, which observes in all room, but without loading from storage
|
||||||
|
* It receives the sync write transaction, so other stores can be updated as part of the same transaction. */
|
||||||
|
export interface RoomStateHandler {
|
||||||
|
handleRoomState(room: Room, stateEvent: StateEvent, syncWriteTxn: Transaction, log: ILogItem);
|
||||||
|
updateRoomMembers(room: Room, memberChanges: Map<string, MemberChange>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* used for Room.observeStateType and Room.observeStateTypeAndKey
|
||||||
|
* @internal
|
||||||
|
* */
|
||||||
|
export interface StateObserver {
|
||||||
|
handleStateEvent(event: StateEvent);
|
||||||
|
load(roomId: string, txn: Transaction): Promise<void>;
|
||||||
|
setRemoveCallback(callback: () => void);
|
||||||
|
}
|
Loading…
Reference in a new issue