mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2025-01-10 20:17:32 +01:00
Merge pull request #59 from vector-im/bwindels/memberlist
Add avatars and display names to the timeline
This commit is contained in:
commit
2241add672
@ -2,7 +2,10 @@
|
|||||||
## Olm
|
## Olm
|
||||||
- implement MemberList as ObservableMap
|
- implement MemberList as ObservableMap
|
||||||
- make sure we have all members (as we're using lazy loading members), and store these somehow
|
- make sure we have all members (as we're using lazy loading members), and store these somehow
|
||||||
|
- keep in mind that the server might not support lazy loading? E.g. we should store in a memberlist all the membership events passed by sync, perhaps with a flag if we already attempted to fetch all. We could also check if the server announces lazy loading support in the version response (I think r0.6.0).
|
||||||
- do we need to update /members on every limited sync response or did we find a way around this?
|
- do we need to update /members on every limited sync response or did we find a way around this?
|
||||||
|
- I don't think we need to ... we get all state events that were sent during the gap in `room.state`
|
||||||
|
- I tested this with riot and synapse, and indeed, we get membership events from the gap on a limited sync. This could be clearer in the spec though.
|
||||||
- fields:
|
- fields:
|
||||||
- user id
|
- user id
|
||||||
- room id
|
- room id
|
||||||
@ -118,7 +121,8 @@ we'll need to pass an implementation of EventSender or something to SendQueue th
|
|||||||
- use AES-CTR from https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto
|
- use AES-CTR from https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- libolm api docs (also for js api) would be great
|
- libolm api docs (also for js api) would be great. Found something that could work:
|
||||||
|
https://gitlab.matrix.org/matrix-org/olm/-/blob/master/javascript/index.d.ts
|
||||||
|
|
||||||
|
|
||||||
## OO Design
|
## OO Design
|
||||||
|
@ -1,3 +1,11 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
## Member list
|
||||||
|
|
||||||
|
- support migrations in StorageFactory
|
||||||
|
- migrate all stores from key to key_path
|
||||||
|
- how to deal with members coming from backfill? do we even need to store them?
|
||||||
|
|
||||||
# How to store members?
|
# How to store members?
|
||||||
|
|
||||||
All of this is assuming we'll use lazy loading of members.
|
All of this is assuming we'll use lazy loading of members.
|
||||||
|
7
doc/impl-thoughts/timeline-member.md
Normal file
7
doc/impl-thoughts/timeline-member.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
## Get member for timeline event
|
||||||
|
|
||||||
|
so when writing sync, we persist the display name and avatar
|
||||||
|
|
||||||
|
the server might or might not support lazy loading
|
||||||
|
|
||||||
|
if it is a room we just joined
|
1
lib/olm.js
Symbolic link
1
lib/olm.js
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../node_modules/olm/olm.js
|
1
lib/olm.wasm
Symbolic link
1
lib/olm.wasm
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../node_modules/olm/olm.wasm
|
58
prototypes/olmtest.html
Normal file
58
prototypes/olmtest.html
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style type="text/css">
|
||||||
|
pre {
|
||||||
|
font-family: monospace;
|
||||||
|
display: block;
|
||||||
|
white-space: pre;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="text/javascript" src="../lib/olm.js"></script>
|
||||||
|
<script type="module">
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const Olm = window.Olm;
|
||||||
|
await Olm.init({
|
||||||
|
locateFile: () => "../lib/olm.wasm",
|
||||||
|
});
|
||||||
|
const alice = new Olm.Account();
|
||||||
|
alice.create();
|
||||||
|
console.log("alice", alice.identity_keys());
|
||||||
|
|
||||||
|
const bob = new Olm.Account();
|
||||||
|
bob.unpickle("secret", "EWfA87or4GgQ+wqVkyuFiW9gUk3FI6QSXgp8E2dS5RFLvXgy4oFvxwQ1gVnbMkdJz2Hy9ex9UmJ/ZyuRU0aRt0IwXpw/SUNq4IQeVJ7J/miXW7rV4Ep+4RSEf945KbDrokDCS2CoL5PIfv/NYyey32gA0hMi8wWIfIlOxFBV4SBJYSC+Qd54VjprwCg0Sn9vjQouKVrM/+5jzsv9+JK5OpWW0Vrb3qrXwyAOEAQ4WlOQcqZHAyPQIw");
|
||||||
|
console.log("bob", bob.identity_keys());
|
||||||
|
// generate OTK on receiver side
|
||||||
|
bob.generate_one_time_keys(1);
|
||||||
|
const bobOneTimeKeys = JSON.parse(bob.one_time_keys());
|
||||||
|
const bobOneTimeKey = Object.values(bobOneTimeKeys.curve25519)[0];
|
||||||
|
// encrypt
|
||||||
|
const aliceSession = new Olm.Session();
|
||||||
|
aliceSession.create_outbound(
|
||||||
|
alice,
|
||||||
|
JSON.parse(bob.identity_keys()).curve25519,
|
||||||
|
bobOneTimeKey,
|
||||||
|
);
|
||||||
|
const message = aliceSession.encrypt("hello secret world");
|
||||||
|
console.log("message", message);
|
||||||
|
// decrypt
|
||||||
|
const bobSession = new Olm.Session();
|
||||||
|
bobSession.create_inbound(bob, message.body);
|
||||||
|
const plaintext = bobSession.decrypt(message.type, message.body);
|
||||||
|
console.log("plaintext", plaintext);
|
||||||
|
// remove Bob's OTK as it was used to start an olm session
|
||||||
|
console.log("bob OTK before removing", bob.one_time_keys());
|
||||||
|
bob.remove_one_time_keys(bobSession);
|
||||||
|
console.log("bob OTK after removing", bob.one_time_keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -67,6 +67,7 @@ export class SessionLoadViewModel extends ViewModel {
|
|||||||
this._error = err;
|
this._error = err;
|
||||||
} finally {
|
} finally {
|
||||||
this._loading = false;
|
this._loading = false;
|
||||||
|
// loadLabel in case of sc.loadError also gets updated through this
|
||||||
this.emitChange("loading");
|
this.emitChange("loading");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -184,6 +184,7 @@ export class SessionPickerViewModel extends ViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async import(json) {
|
async import(json) {
|
||||||
|
try {
|
||||||
const data = JSON.parse(json);
|
const data = JSON.parse(json);
|
||||||
const {sessionInfo} = data;
|
const {sessionInfo} = data;
|
||||||
sessionInfo.comment = `Imported on ${new Date().toLocaleString()} from id ${sessionInfo.id}.`;
|
sessionInfo.comment = `Imported on ${new Date().toLocaleString()} from id ${sessionInfo.id}.`;
|
||||||
@ -191,6 +192,10 @@ export class SessionPickerViewModel extends ViewModel {
|
|||||||
await this._storageFactory.import(sessionInfo.id, data.stores);
|
await this._storageFactory.import(sessionInfo.id, data.stores);
|
||||||
await this._sessionInfoStorage.add(sessionInfo);
|
await this._sessionInfoStorage.add(sessionInfo);
|
||||||
this._sessions.set(new SessionItemViewModel(sessionInfo, this));
|
this._sessions.set(new SessionItemViewModel(sessionInfo, this));
|
||||||
|
} catch (err) {
|
||||||
|
alert(err.message);
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id) {
|
async delete(id) {
|
||||||
|
@ -36,6 +36,9 @@ export class GapTile extends SimpleTile {
|
|||||||
console.error(`timeline.fillGap(): ${err.message}:\n${err.stack}`);
|
console.error(`timeline.fillGap(): ${err.message}:\n${err.stack}`);
|
||||||
this._error = err;
|
this._error = err;
|
||||||
this.emitChange("error");
|
this.emitChange("error");
|
||||||
|
// rethrow so caller of this method
|
||||||
|
// knows not to keep calling this for now
|
||||||
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
this._loading = false;
|
this._loading = false;
|
||||||
this.emitChange("isLoading");
|
this.emitChange("isLoading");
|
||||||
|
@ -20,15 +20,10 @@ const MAX_HEIGHT = 300;
|
|||||||
const MAX_WIDTH = 400;
|
const MAX_WIDTH = 400;
|
||||||
|
|
||||||
export class ImageTile extends MessageTile {
|
export class ImageTile extends MessageTile {
|
||||||
constructor(options, room) {
|
|
||||||
super(options);
|
|
||||||
this._room = room;
|
|
||||||
}
|
|
||||||
|
|
||||||
get thumbnailUrl() {
|
get thumbnailUrl() {
|
||||||
const mxcUrl = this._getContent()?.url;
|
const mxcUrl = this._getContent()?.url;
|
||||||
if (typeof mxcUrl === "string") {
|
if (typeof mxcUrl === "string") {
|
||||||
return this._room.mxcUrlThumbnail(mxcUrl, this.thumbnailWidth, this.thumbnailHeight, "scale");
|
return this._mediaRepository.mxcUrlThumbnail(mxcUrl, this.thumbnailWidth, this.thumbnailHeight, "scale");
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -36,7 +31,7 @@ export class ImageTile extends MessageTile {
|
|||||||
get url() {
|
get url() {
|
||||||
const mxcUrl = this._getContent()?.url;
|
const mxcUrl = this._getContent()?.url;
|
||||||
if (typeof mxcUrl === "string") {
|
if (typeof mxcUrl === "string") {
|
||||||
return this._room.mxcUrl(mxcUrl);
|
return this._mediaRepository.mxcUrl(mxcUrl);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -15,11 +15,12 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {SimpleTile} from "./SimpleTile.js";
|
import {SimpleTile} from "./SimpleTile.js";
|
||||||
import {getIdentifierColorNumber} from "../../../../avatar.js";
|
import {getIdentifierColorNumber, avatarInitials} from "../../../../avatar.js";
|
||||||
|
|
||||||
export class MessageTile extends SimpleTile {
|
export class MessageTile extends SimpleTile {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
this._mediaRepository = options.mediaRepository;
|
||||||
this._clock = options.clock;
|
this._clock = options.clock;
|
||||||
this._isOwn = this._entry.sender === options.ownUserId;
|
this._isOwn = this._entry.sender === options.ownUserId;
|
||||||
this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
|
this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
|
||||||
@ -31,13 +32,24 @@ export class MessageTile extends SimpleTile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get sender() {
|
get sender() {
|
||||||
return this._entry.sender;
|
return this._entry.displayName || this._entry.sender;
|
||||||
}
|
}
|
||||||
|
|
||||||
get senderColorNumber() {
|
get avatarColorNumber() {
|
||||||
return getIdentifierColorNumber(this._entry.sender);
|
return getIdentifierColorNumber(this._entry.sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get avatarUrl() {
|
||||||
|
if (this._entry.avatarUrl) {
|
||||||
|
return this._mediaRepository.mxcUrlThumbnail(this._entry.avatarUrl, 30, 30, "crop");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get avatarLetter() {
|
||||||
|
return avatarInitials(this.sender);
|
||||||
|
}
|
||||||
|
|
||||||
get date() {
|
get date() {
|
||||||
return this._date && this._date.toLocaleDateString({}, {month: "numeric", day: "numeric"});
|
return this._date && this._date.toLocaleDateString({}, {month: "numeric", day: "numeric"});
|
||||||
}
|
}
|
||||||
|
@ -23,35 +23,36 @@ export class RoomMemberTile extends SimpleTile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get announcement() {
|
get announcement() {
|
||||||
const {sender, content, prevContent, stateKey} = this._entry;
|
const {sender, content, prevContent} = this._entry;
|
||||||
|
const name = this._entry.displayName || sender;
|
||||||
const membership = content && content.membership;
|
const membership = content && content.membership;
|
||||||
const prevMembership = prevContent && prevContent.membership;
|
const prevMembership = prevContent && prevContent.membership;
|
||||||
|
|
||||||
if (prevMembership === "join" && membership === "join") {
|
if (prevMembership === "join" && membership === "join") {
|
||||||
if (content.avatar_url !== prevContent.avatar_url) {
|
if (content.avatar_url !== prevContent.avatar_url) {
|
||||||
return `${stateKey} changed their avatar`;
|
return `${name} changed their avatar`;
|
||||||
} else if (content.displayname !== prevContent.displayname) {
|
} else if (content.displayname !== prevContent.displayname) {
|
||||||
return `${stateKey} changed their name to ${content.displayname}`;
|
return `${name} changed their name to ${content.displayname}`;
|
||||||
}
|
}
|
||||||
} else if (membership === "join") {
|
} else if (membership === "join") {
|
||||||
return `${stateKey} joined the room`;
|
return `${name} joined the room`;
|
||||||
} else if (membership === "invite") {
|
} else if (membership === "invite") {
|
||||||
return `${stateKey} was invited to the room by ${sender}`;
|
return `${name} was invited to the room by ${sender}`;
|
||||||
} else if (prevMembership === "invite") {
|
} else if (prevMembership === "invite") {
|
||||||
if (membership === "join") {
|
if (membership === "join") {
|
||||||
return `${stateKey} accepted the invitation to join the room`;
|
return `${name} accepted the invitation to join the room`;
|
||||||
} else if (membership === "leave") {
|
} else if (membership === "leave") {
|
||||||
return `${stateKey} declined the invitation to join the room`;
|
return `${name} declined the invitation to join the room`;
|
||||||
}
|
}
|
||||||
} else if (membership === "leave") {
|
} else if (membership === "leave") {
|
||||||
if (stateKey === sender) {
|
if (name === sender) {
|
||||||
return `${stateKey} left the room`;
|
return `${name} left the room`;
|
||||||
} else {
|
} else {
|
||||||
const reason = content.reason;
|
const reason = content.reason;
|
||||||
return `${stateKey} was kicked from the room by ${sender}${reason ? `: ${reason}` : ""}`;
|
return `${name} was kicked from the room by ${sender}${reason ? `: ${reason}` : ""}`;
|
||||||
}
|
}
|
||||||
} else if (membership === "ban") {
|
} else if (membership === "ban") {
|
||||||
return `${stateKey} was banned from the room by ${sender}`;
|
return `${name} was banned from the room by ${sender}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${sender} membership changed to ${content.membership}`;
|
return `${sender} membership changed to ${content.membership}`;
|
||||||
|
@ -24,6 +24,6 @@ export class RoomNameTile extends SimpleTile {
|
|||||||
|
|
||||||
get announcement() {
|
get announcement() {
|
||||||
const content = this._entry.content;
|
const content = this._entry.content;
|
||||||
return `${this._entry.sender} named the room "${content.name}"`
|
return `${this._entry.displayName || this._entry.sender} named the room "${content.name}"`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,8 @@ import {EncryptedEventTile} from "./tiles/EncryptedEventTile.js";
|
|||||||
|
|
||||||
export function tilesCreator({room, ownUserId, clock}) {
|
export function tilesCreator({room, ownUserId, clock}) {
|
||||||
return function tilesCreator(entry, emitUpdate) {
|
return function tilesCreator(entry, emitUpdate) {
|
||||||
const options = {entry, emitUpdate, ownUserId, clock};
|
const options = {entry, emitUpdate, ownUserId, clock,
|
||||||
|
mediaRepository: room.mediaRepository};
|
||||||
if (entry.isGap) {
|
if (entry.isGap) {
|
||||||
return new GapTile(options, room);
|
return new GapTile(options, room);
|
||||||
} else if (entry.eventType) {
|
} else if (entry.eventType) {
|
||||||
@ -38,7 +39,7 @@ export function tilesCreator({room, ownUserId, clock}) {
|
|||||||
case "m.emote":
|
case "m.emote":
|
||||||
return new TextTile(options);
|
return new TextTile(options);
|
||||||
case "m.image":
|
case "m.image":
|
||||||
return new ImageTile(options, room);
|
return new ImageTile(options);
|
||||||
case "m.location":
|
case "m.location":
|
||||||
return new LocationTile(options);
|
return new LocationTile(options);
|
||||||
default:
|
default:
|
||||||
|
19
src/matrix/ServerFeatures.js
Normal file
19
src/matrix/ServerFeatures.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const R0_5_0 = "r0.5.0";
|
||||||
|
|
||||||
|
export class ServerFeatures {
|
||||||
|
constructor(versionResponse) {
|
||||||
|
this._versionResponse = versionResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
_supportsVersion(version) {
|
||||||
|
if (!this._versionResponse) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const {versions} = this._versionResponse;
|
||||||
|
return Array.isArray(versions) && versions.includes(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
get lazyLoadMembers() {
|
||||||
|
return this._supportsVersion(R0_5_0);
|
||||||
|
}
|
||||||
|
}
|
@ -110,7 +110,7 @@ export class SessionContainer {
|
|||||||
this._status.set(LoadStatus.LoginFailed);
|
this._status.set(LoadStatus.LoginFailed);
|
||||||
} else if (err instanceof ConnectionError) {
|
} else if (err instanceof ConnectionError) {
|
||||||
this._loginFailure = LoginFailure.Connection;
|
this._loginFailure = LoginFailure.Connection;
|
||||||
this._status.set(LoadStatus.LoginFailure);
|
this._status.set(LoadStatus.LoginFailed);
|
||||||
} else {
|
} else {
|
||||||
this._status.set(LoadStatus.Error);
|
this._status.set(LoadStatus.Error);
|
||||||
}
|
}
|
||||||
@ -191,9 +191,14 @@ export class SessionContainer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// only transition into Ready once the first sync has succeeded
|
// only transition into Ready once the first sync has succeeded
|
||||||
this._waitForFirstSyncHandle = this._sync.status.waitFor(s => s === SyncStatus.Syncing);
|
this._waitForFirstSyncHandle = this._sync.status.waitFor(s => s === SyncStatus.Syncing || s === SyncStatus.Stopped);
|
||||||
try {
|
try {
|
||||||
await this._waitForFirstSyncHandle.promise;
|
await this._waitForFirstSyncHandle.promise;
|
||||||
|
if (this._sync.status.get() === SyncStatus.Stopped) {
|
||||||
|
if (this._sync.error) {
|
||||||
|
throw this._sync.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// if dispose is called from stop, bail out
|
// if dispose is called from stop, bail out
|
||||||
if (err instanceof AbortError) {
|
if (err instanceof AbortError) {
|
||||||
|
@ -119,6 +119,7 @@ export class Sync {
|
|||||||
storeNames.session,
|
storeNames.session,
|
||||||
storeNames.roomSummary,
|
storeNames.roomSummary,
|
||||||
storeNames.roomState,
|
storeNames.roomState,
|
||||||
|
storeNames.roomMembers,
|
||||||
storeNames.timelineEvents,
|
storeNames.timelineEvents,
|
||||||
storeNames.timelineFragments,
|
storeNames.timelineFragments,
|
||||||
storeNames.pendingEvents,
|
storeNames.pendingEvents,
|
||||||
@ -148,6 +149,7 @@ export class Sync {
|
|||||||
}
|
}
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.warn("aborting syncTxn because of error");
|
console.warn("aborting syncTxn because of error");
|
||||||
|
console.error(err);
|
||||||
// avoid corrupting state by only
|
// avoid corrupting state by only
|
||||||
// storing the sync up till the point
|
// storing the sync up till the point
|
||||||
// the exception occurred
|
// the exception occurred
|
||||||
|
@ -45,22 +45,7 @@ class RequestWrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HomeServerApi {
|
function encodeQueryParams(queryParams) {
|
||||||
constructor({homeServer, accessToken, request, createTimeout, reconnector}) {
|
|
||||||
// store these both in a closure somehow so it's harder to get at in case of XSS?
|
|
||||||
// one could change the homeserver as well so the token gets sent there, so both must be protected from read/write
|
|
||||||
this._homeserver = homeServer;
|
|
||||||
this._accessToken = accessToken;
|
|
||||||
this._requestFn = request;
|
|
||||||
this._createTimeout = createTimeout;
|
|
||||||
this._reconnector = reconnector;
|
|
||||||
}
|
|
||||||
|
|
||||||
_url(csPath) {
|
|
||||||
return `${this._homeserver}/_matrix/client/r0${csPath}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
_encodeQueryParams(queryParams) {
|
|
||||||
return Object.entries(queryParams || {})
|
return Object.entries(queryParams || {})
|
||||||
.filter(([, value]) => value !== undefined)
|
.filter(([, value]) => value !== undefined)
|
||||||
.map(([name, value]) => {
|
.map(([name, value]) => {
|
||||||
@ -70,10 +55,26 @@ export class HomeServerApi {
|
|||||||
return `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
return `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
||||||
})
|
})
|
||||||
.join("&");
|
.join("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HomeServerApi {
|
||||||
|
constructor({homeServer, accessToken, request, createTimeout, reconnector}) {
|
||||||
|
// store these both in a closure somehow so it's harder to get at in case of XSS?
|
||||||
|
// one could change the homeserver as well so the token gets sent there, so both must be protected from read/write
|
||||||
|
this._homeserver = homeServer;
|
||||||
|
this._accessToken = accessToken;
|
||||||
|
this._requestFn = request;
|
||||||
|
this._createTimeout = createTimeout;
|
||||||
|
this._reconnector = reconnector;
|
||||||
|
this._mediaRepository = new MediaRepository(homeServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
_url(csPath) {
|
||||||
|
return `${this._homeserver}/_matrix/client/r0${csPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
_request(method, url, queryParams, body, options) {
|
_request(method, url, queryParams, body, options) {
|
||||||
const queryString = this._encodeQueryParams(queryParams);
|
const queryString = encodeQueryParams(queryParams);
|
||||||
url = `${url}?${queryString}`;
|
url = `${url}?${queryString}`;
|
||||||
let bodyString;
|
let bodyString;
|
||||||
const headers = new Map();
|
const headers = new Map();
|
||||||
@ -126,6 +127,11 @@ export class HomeServerApi {
|
|||||||
return this._get(`/rooms/${encodeURIComponent(roomId)}/messages`, params, null, options);
|
return this._get(`/rooms/${encodeURIComponent(roomId)}/messages`, params, null, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// params is at, membership and not_membership
|
||||||
|
members(roomId, params, options = null) {
|
||||||
|
return this._get(`/rooms/${encodeURIComponent(roomId)}/members`, params, null, options);
|
||||||
|
}
|
||||||
|
|
||||||
send(roomId, eventType, txnId, content, options = null) {
|
send(roomId, eventType, txnId, content, options = null) {
|
||||||
return this._put(`/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`, {}, content, options);
|
return this._put(`/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`, {}, content, options);
|
||||||
}
|
}
|
||||||
@ -149,13 +155,14 @@ export class HomeServerApi {
|
|||||||
return this._request("GET", `${this._homeserver}/_matrix/client/versions`, null, null, options);
|
return this._request("GET", `${this._homeserver}/_matrix/client/versions`, null, null, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
_parseMxcUrl(url) {
|
get mediaRepository() {
|
||||||
const prefix = "mxc://";
|
return this._mediaRepository;
|
||||||
if (url.startsWith(prefix)) {
|
|
||||||
return url.substr(prefix.length).split("/", 2);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MediaRepository {
|
||||||
|
constructor(homeserver) {
|
||||||
|
this._homeserver = homeserver;
|
||||||
}
|
}
|
||||||
|
|
||||||
mxcUrlThumbnail(url, width, height, method) {
|
mxcUrlThumbnail(url, width, height, method) {
|
||||||
@ -163,7 +170,7 @@ export class HomeServerApi {
|
|||||||
if (parts) {
|
if (parts) {
|
||||||
const [serverName, mediaId] = parts;
|
const [serverName, mediaId] = parts;
|
||||||
const httpUrl = `${this._homeserver}/_matrix/media/r0/thumbnail/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`;
|
const httpUrl = `${this._homeserver}/_matrix/media/r0/thumbnail/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`;
|
||||||
return httpUrl + "?" + this._encodeQueryParams({width, height, method});
|
return httpUrl + "?" + encodeQueryParams({width, height, method});
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -177,6 +184,15 @@ export class HomeServerApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_parseMxcUrl(url) {
|
||||||
|
const prefix = "mxc://";
|
||||||
|
if (url.startsWith(prefix)) {
|
||||||
|
return url.substr(prefix.length).split("/", 2);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tests() {
|
export function tests() {
|
||||||
|
@ -22,6 +22,8 @@ import {Timeline} from "./timeline/Timeline.js";
|
|||||||
import {FragmentIdComparer} from "./timeline/FragmentIdComparer.js";
|
import {FragmentIdComparer} from "./timeline/FragmentIdComparer.js";
|
||||||
import {SendQueue} from "./sending/SendQueue.js";
|
import {SendQueue} from "./sending/SendQueue.js";
|
||||||
import {WrappedError} from "../error.js"
|
import {WrappedError} from "../error.js"
|
||||||
|
import {fetchOrLoadMembers} from "./members/load.js";
|
||||||
|
import {MemberList} from "./members/MemberList.js";
|
||||||
|
|
||||||
export class Room extends EventEmitter {
|
export class Room extends EventEmitter {
|
||||||
constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user}) {
|
constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user}) {
|
||||||
@ -36,22 +38,35 @@ export class Room extends EventEmitter {
|
|||||||
this._sendQueue = new SendQueue({roomId, storage, sendScheduler, pendingEvents});
|
this._sendQueue = new SendQueue({roomId, storage, sendScheduler, pendingEvents});
|
||||||
this._timeline = null;
|
this._timeline = null;
|
||||||
this._user = user;
|
this._user = user;
|
||||||
|
this._changedMembersDuringSync = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @package */
|
||||||
async writeSync(roomResponse, membership, txn) {
|
async writeSync(roomResponse, membership, txn) {
|
||||||
const summaryChanges = this._summary.writeSync(roomResponse, membership, txn);
|
const summaryChanges = this._summary.writeSync(roomResponse, membership, txn);
|
||||||
const {entries, newLiveKey} = await this._syncWriter.writeSync(roomResponse, txn);
|
const {entries, newLiveKey, changedMembers} = await this._syncWriter.writeSync(roomResponse, txn);
|
||||||
let removedPendingEvents;
|
let removedPendingEvents;
|
||||||
if (roomResponse.timeline && roomResponse.timeline.events) {
|
if (roomResponse.timeline && roomResponse.timeline.events) {
|
||||||
removedPendingEvents = this._sendQueue.removeRemoteEchos(roomResponse.timeline.events, txn);
|
removedPendingEvents = this._sendQueue.removeRemoteEchos(roomResponse.timeline.events, txn);
|
||||||
}
|
}
|
||||||
return {summaryChanges, newTimelineEntries: entries, newLiveKey, removedPendingEvents};
|
return {summaryChanges, newTimelineEntries: entries, newLiveKey, removedPendingEvents, changedMembers};
|
||||||
}
|
}
|
||||||
|
|
||||||
afterSync({summaryChanges, newTimelineEntries, newLiveKey, removedPendingEvents}) {
|
/** @package */
|
||||||
|
afterSync({summaryChanges, newTimelineEntries, newLiveKey, removedPendingEvents, changedMembers}) {
|
||||||
this._syncWriter.afterSync(newLiveKey);
|
this._syncWriter.afterSync(newLiveKey);
|
||||||
|
if (changedMembers.length) {
|
||||||
|
if (this._changedMembersDuringSync) {
|
||||||
|
for (const member of changedMembers) {
|
||||||
|
this._changedMembersDuringSync.set(member.userId, member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this._memberList) {
|
||||||
|
this._memberList.afterSync(changedMembers);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (summaryChanges) {
|
if (summaryChanges) {
|
||||||
this._summary.afterSync(summaryChanges);
|
this._summary.applyChanges(summaryChanges);
|
||||||
this.emit("change");
|
this.emit("change");
|
||||||
this._emitCollectionChange(this);
|
this._emitCollectionChange(this);
|
||||||
}
|
}
|
||||||
@ -63,10 +78,12 @@ export class Room extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @package */
|
||||||
resumeSending() {
|
resumeSending() {
|
||||||
this._sendQueue.resumeSending();
|
this._sendQueue.resumeSending();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @package */
|
||||||
load(summary, txn) {
|
load(summary, txn) {
|
||||||
try {
|
try {
|
||||||
this._summary.load(summary);
|
this._summary.load(summary);
|
||||||
@ -76,13 +93,36 @@ export class Room extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
sendEvent(eventType, content) {
|
sendEvent(eventType, content) {
|
||||||
return this._sendQueue.enqueueEvent(eventType, content);
|
return this._sendQueue.enqueueEvent(eventType, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
async loadMemberList() {
|
||||||
|
if (this._memberList) {
|
||||||
|
this._memberList.retain();
|
||||||
|
return this._memberList;
|
||||||
|
} else {
|
||||||
|
const members = await fetchOrLoadMembers({
|
||||||
|
summary: this._summary,
|
||||||
|
roomId: this._roomId,
|
||||||
|
hsApi: this._hsApi,
|
||||||
|
storage: this._storage,
|
||||||
|
// to handle race between /members and /sync
|
||||||
|
setChangedMembersMap: map => this._changedMembersDuringSync = map,
|
||||||
|
});
|
||||||
|
this._memberList = new MemberList({
|
||||||
|
members,
|
||||||
|
closeCallback: () => { this._memberList = null; }
|
||||||
|
});
|
||||||
|
return this._memberList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
async fillGap(fragmentEntry, amount) {
|
async fillGap(fragmentEntry, amount) {
|
||||||
|
// TODO move some/all of this out of Room
|
||||||
if (fragmentEntry.edgeReached) {
|
if (fragmentEntry.edgeReached) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -90,7 +130,10 @@ export class Room extends EventEmitter {
|
|||||||
from: fragmentEntry.token,
|
from: fragmentEntry.token,
|
||||||
dir: fragmentEntry.direction.asApiString(),
|
dir: fragmentEntry.direction.asApiString(),
|
||||||
limit: amount,
|
limit: amount,
|
||||||
filter: {lazy_load_members: true}
|
filter: {
|
||||||
|
lazy_load_members: true,
|
||||||
|
include_redundant_members: true,
|
||||||
|
}
|
||||||
}).response();
|
}).response();
|
||||||
|
|
||||||
const txn = await this._storage.readWriteTxn([
|
const txn = await this._storage.readWriteTxn([
|
||||||
@ -127,14 +170,17 @@ export class Room extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
get name() {
|
get name() {
|
||||||
return this._summary.name;
|
return this._summary.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
get id() {
|
get id() {
|
||||||
return this._roomId;
|
return this._roomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
async openTimeline() {
|
async openTimeline() {
|
||||||
if (this._timeline) {
|
if (this._timeline) {
|
||||||
throw new Error("not dealing with load race here for now");
|
throw new Error("not dealing with load race here for now");
|
||||||
@ -155,12 +201,8 @@ export class Room extends EventEmitter {
|
|||||||
return this._timeline;
|
return this._timeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
mxcUrlThumbnail(url, width, height, method) {
|
get mediaRepository() {
|
||||||
return this._hsApi.mxcUrlThumbnail(url, width, height, method);
|
return this._hsApi.mediaRepository;
|
||||||
}
|
|
||||||
|
|
||||||
mxcUrl(url) {
|
|
||||||
return this._hsApi.mxcUrl(url);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,12 @@ function applySyncResponse(data, roomResponse, membership) {
|
|||||||
data = roomResponse.state.events.reduce(processEvent, data);
|
data = roomResponse.state.events.reduce(processEvent, data);
|
||||||
}
|
}
|
||||||
if (roomResponse.timeline) {
|
if (roomResponse.timeline) {
|
||||||
data = roomResponse.timeline.events.reduce(processEvent, data);
|
const {timeline} = roomResponse;
|
||||||
|
if (timeline.prev_batch) {
|
||||||
|
data = data.cloneIfNeeded();
|
||||||
|
data.lastPaginationToken = timeline.prev_batch;
|
||||||
|
}
|
||||||
|
data = timeline.events.reduce(processEvent, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@ -98,6 +103,8 @@ class SummaryData {
|
|||||||
this.heroes = copy ? copy.heroes : null;
|
this.heroes = copy ? copy.heroes : null;
|
||||||
this.canonicalAlias = copy ? copy.canonicalAlias : null;
|
this.canonicalAlias = copy ? copy.canonicalAlias : null;
|
||||||
this.altAliases = copy ? copy.altAliases : null;
|
this.altAliases = copy ? copy.altAliases : null;
|
||||||
|
this.hasFetchedMembers = copy ? copy.hasFetchedMembers : false;
|
||||||
|
this.lastPaginationToken = copy ? copy.lastPaginationToken : null;
|
||||||
this.cloned = copy ? true : false;
|
this.cloned = copy ? true : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,6 +155,21 @@ export class RoomSummary {
|
|||||||
return this._data.joinCount;
|
return this._data.joinCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasFetchedMembers() {
|
||||||
|
return this._data.hasFetchedMembers;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lastPaginationToken() {
|
||||||
|
return this._data.lastPaginationToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeHasFetchedMembers(value, txn) {
|
||||||
|
const data = new SummaryData(this._data);
|
||||||
|
data.hasFetchedMembers = value;
|
||||||
|
txn.roomSummary.set(data.serialize());
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
writeSync(roomResponse, membership, txn) {
|
writeSync(roomResponse, membership, txn) {
|
||||||
// clear cloned flag, so cloneIfNeeded makes a copy and
|
// clear cloned flag, so cloneIfNeeded makes a copy and
|
||||||
// this._data is not modified if any field is changed.
|
// this._data is not modified if any field is changed.
|
||||||
@ -165,7 +187,7 @@ export class RoomSummary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
afterSync(data) {
|
applyChanges(data) {
|
||||||
this._data = data;
|
this._data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
49
src/matrix/room/members/MemberList.js
Normal file
49
src/matrix/room/members/MemberList.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 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 {ObservableMap} from "../../../observable/map/ObservableMap.js";
|
||||||
|
|
||||||
|
export class MemberList {
|
||||||
|
constructor({members, closeCallback}) {
|
||||||
|
this._members = new ObservableMap();
|
||||||
|
for (const member of members) {
|
||||||
|
this._members.add(member.userId, member);
|
||||||
|
}
|
||||||
|
this._closeCallback = closeCallback;
|
||||||
|
this._retentionCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterSync(updatedMembers) {
|
||||||
|
for (const member of updatedMembers) {
|
||||||
|
this._members.add(member.userId, member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get members() {
|
||||||
|
return this._members;
|
||||||
|
}
|
||||||
|
|
||||||
|
retain() {
|
||||||
|
this._retentionCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
release() {
|
||||||
|
this._retentionCount -= 1;
|
||||||
|
if (this._retentionCount === 0) {
|
||||||
|
this._closeCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
68
src/matrix/room/members/RoomMember.js
Normal file
68
src/matrix/room/members/RoomMember.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
Copyright 2020 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const EVENT_TYPE = "m.room.member";
|
||||||
|
|
||||||
|
export class RoomMember {
|
||||||
|
constructor(data) {
|
||||||
|
this._data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromMemberEvent(roomId, memberEvent) {
|
||||||
|
const userId = memberEvent && memberEvent.state_key;
|
||||||
|
if (typeof userId !== "string") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return this._fromMemberEventContent(roomId, userId, memberEvent.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromReplacingMemberEvent(roomId, memberEvent) {
|
||||||
|
const userId = memberEvent && memberEvent.state_key;
|
||||||
|
if (typeof userId !== "string") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return this._fromMemberEventContent(roomId, userId, memberEvent.prev_content);
|
||||||
|
}
|
||||||
|
|
||||||
|
static _fromMemberEventContent(roomId, userId, content) {
|
||||||
|
const membership = content?.membership;
|
||||||
|
const avatarUrl = content?.avatar_url;
|
||||||
|
const displayName = content?.displayname;
|
||||||
|
if (typeof membership !== "string") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return new RoomMember({
|
||||||
|
roomId,
|
||||||
|
userId,
|
||||||
|
membership,
|
||||||
|
avatarUrl,
|
||||||
|
displayName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get roomId() {
|
||||||
|
return this._data.roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
get userId() {
|
||||||
|
return this._data.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize() {
|
||||||
|
return this._data;
|
||||||
|
}
|
||||||
|
}
|
90
src/matrix/room/members/load.js
Normal file
90
src/matrix/room/members/load.js
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
Copyright 2020 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 {RoomMember} from "./RoomMember.js";
|
||||||
|
|
||||||
|
async function loadMembers({roomId, storage}) {
|
||||||
|
const txn = await storage.readTxn([
|
||||||
|
storage.storeNames.roomMembers,
|
||||||
|
]);
|
||||||
|
const memberDatas = await txn.roomMembers.getAll(roomId);
|
||||||
|
return memberDatas.map(d => new RoomMember(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchMembers({summary, roomId, hsApi, storage, setChangedMembersMap}) {
|
||||||
|
// if any members are changed by sync while we're fetching members,
|
||||||
|
// they will end up here, so we check not to override them
|
||||||
|
const changedMembersDuringSync = new Map();
|
||||||
|
setChangedMembersMap(changedMembersDuringSync);
|
||||||
|
|
||||||
|
const memberResponse = await hsApi.members(roomId, {at: summary.lastPaginationToken}).response;
|
||||||
|
|
||||||
|
const txn = await storage.readWriteTxn([
|
||||||
|
storage.storeNames.roomSummary,
|
||||||
|
storage.storeNames.roomMembers,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let summaryChanges;
|
||||||
|
let members;
|
||||||
|
|
||||||
|
try {
|
||||||
|
summaryChanges = summary.writeHasFetchedMembers(true, txn);
|
||||||
|
const {roomMembers} = txn;
|
||||||
|
const memberEvents = memberResponse.chunk;
|
||||||
|
if (!Array.isArray(memberEvents)) {
|
||||||
|
throw new Error("malformed");
|
||||||
|
}
|
||||||
|
members = await Promise.all(memberEvents.map(async memberEvent => {
|
||||||
|
const userId = memberEvent?.state_key;
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error("malformed");
|
||||||
|
}
|
||||||
|
// this member was changed during a sync that happened while calling /members
|
||||||
|
// and thus is more recent, so don't overwrite
|
||||||
|
const changedMember = changedMembersDuringSync.get(userId);
|
||||||
|
if (changedMember) {
|
||||||
|
return changedMember;
|
||||||
|
} else {
|
||||||
|
const member = RoomMember.fromMemberEvent(roomId, memberEvent);
|
||||||
|
if (member) {
|
||||||
|
roomMembers.set(member.serialize());
|
||||||
|
}
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
// abort txn on any error
|
||||||
|
txn.abort();
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
// important this gets cleared
|
||||||
|
// or otherwise Room remains in "fetching-members" mode
|
||||||
|
setChangedMembersMap(null);
|
||||||
|
}
|
||||||
|
await txn.complete();
|
||||||
|
summary.applyChanges(summaryChanges);
|
||||||
|
return members;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchOrLoadMembers(options) {
|
||||||
|
const {summary} = options;
|
||||||
|
if (!summary.hasFetchedMembers) {
|
||||||
|
return fetchMembers(options);
|
||||||
|
} else {
|
||||||
|
return loadMembers(options);
|
||||||
|
}
|
||||||
|
}
|
@ -50,6 +50,14 @@ export class EventEntry extends BaseEntry {
|
|||||||
return this._eventEntry.event.sender;
|
return this._eventEntry.event.sender;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get displayName() {
|
||||||
|
return this._eventEntry.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
get avatarUrl() {
|
||||||
|
return this._eventEntry.avatarUrl;
|
||||||
|
}
|
||||||
|
|
||||||
get timestamp() {
|
get timestamp() {
|
||||||
return this._eventEntry.event.origin_server_ts;
|
return this._eventEntry.event.origin_server_ts;
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
import {EventKey} from "../EventKey.js";
|
import {EventKey} from "../EventKey.js";
|
||||||
import {EventEntry} from "../entries/EventEntry.js";
|
import {EventEntry} from "../entries/EventEntry.js";
|
||||||
import {createEventEntry, directionalAppend} from "./common.js";
|
import {createEventEntry, directionalAppend} from "./common.js";
|
||||||
|
import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../members/RoomMember.js";
|
||||||
|
|
||||||
export class GapWriter {
|
export class GapWriter {
|
||||||
constructor({roomId, storage, fragmentIdComparer}) {
|
constructor({roomId, storage, fragmentIdComparer}) {
|
||||||
@ -98,14 +99,20 @@ export class GapWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_storeEvents(events, startKey, direction, txn) {
|
_storeEvents(events, startKey, direction, state, txn) {
|
||||||
const entries = [];
|
const entries = [];
|
||||||
// events is in reverse chronological order for backwards pagination,
|
// events is in reverse chronological order for backwards pagination,
|
||||||
// e.g. order is moving away from the `from` point.
|
// e.g. order is moving away from the `from` point.
|
||||||
let key = startKey;
|
let key = startKey;
|
||||||
for(let event of events) {
|
for (let i = 0; i < events.length; ++i) {
|
||||||
|
const event = events[i];
|
||||||
key = key.nextKeyForDirection(direction);
|
key = key.nextKeyForDirection(direction);
|
||||||
const eventStorageEntry = createEventEntry(key, this._roomId, event);
|
const eventStorageEntry = createEventEntry(key, this._roomId, event);
|
||||||
|
const memberData = this._findMemberData(event.sender, state, events, i, direction);
|
||||||
|
if (memberData) {
|
||||||
|
eventStorageEntry.displayName = memberData?.displayName;
|
||||||
|
eventStorageEntry.avatarUrl = memberData?.avatarUrl;
|
||||||
|
}
|
||||||
txn.timelineEvents.insert(eventStorageEntry);
|
txn.timelineEvents.insert(eventStorageEntry);
|
||||||
const eventEntry = new EventEntry(eventStorageEntry, this._fragmentIdComparer);
|
const eventEntry = new EventEntry(eventStorageEntry, this._fragmentIdComparer);
|
||||||
directionalAppend(entries, eventEntry, direction);
|
directionalAppend(entries, eventEntry, direction);
|
||||||
@ -113,6 +120,35 @@ export class GapWriter {
|
|||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_findMemberData(userId, state, events, index, direction) {
|
||||||
|
function isOurUser(event) {
|
||||||
|
return event.type === MEMBER_EVENT_TYPE && event.state_key === userId;
|
||||||
|
}
|
||||||
|
// older messages are at a higher index in the array when going backwards
|
||||||
|
const inc = direction.isBackward ? 1 : -1;
|
||||||
|
for (let i = index + inc; i >= 0 && i < events.length; i += inc) {
|
||||||
|
const event = events[i];
|
||||||
|
if (isOurUser(event)) {
|
||||||
|
return RoomMember.fromMemberEvent(this._roomId, event)?.serialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// look into newer events, but using prev_content if found.
|
||||||
|
// We do this before looking into `state` because it is not well specified
|
||||||
|
// in the spec whether the events in there represent state before or after `chunk`.
|
||||||
|
// So we look both directions first in chunk to make sure it doesn't matter.
|
||||||
|
for (let i = index; i >= 0 && i < events.length; i -= inc) {
|
||||||
|
const event = events[i];
|
||||||
|
if (isOurUser(event)) {
|
||||||
|
return RoomMember.fromReplacingMemberEvent(this._roomId, event)?.serialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// assuming the member hasn't changed within the chunk, just take it from state if it's there
|
||||||
|
const stateMemberEvent = state.find(isOurUser);
|
||||||
|
if (stateMemberEvent) {
|
||||||
|
return RoomMember.fromMemberEvent(this._roomId, stateMemberEvent)?.serialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async _updateFragments(fragmentEntry, neighbourFragmentEntry, end, entries, txn) {
|
async _updateFragments(fragmentEntry, neighbourFragmentEntry, end, entries, txn) {
|
||||||
const {direction} = fragmentEntry;
|
const {direction} = fragmentEntry;
|
||||||
const changedFragments = [];
|
const changedFragments = [];
|
||||||
@ -158,7 +194,7 @@ export class GapWriter {
|
|||||||
async writeFragmentFill(fragmentEntry, response, txn) {
|
async writeFragmentFill(fragmentEntry, response, txn) {
|
||||||
const {fragmentId, direction} = fragmentEntry;
|
const {fragmentId, direction} = fragmentEntry;
|
||||||
// chunk is in reverse-chronological order when backwards
|
// chunk is in reverse-chronological order when backwards
|
||||||
const {chunk, start, end} = response;
|
const {chunk, start, end, state} = response;
|
||||||
let entries;
|
let entries;
|
||||||
|
|
||||||
if (!Array.isArray(chunk)) {
|
if (!Array.isArray(chunk)) {
|
||||||
@ -195,7 +231,7 @@ export class GapWriter {
|
|||||||
} = await this._findOverlappingEvents(fragmentEntry, chunk, txn);
|
} = await this._findOverlappingEvents(fragmentEntry, chunk, txn);
|
||||||
|
|
||||||
// create entries for all events in chunk, add them to entries
|
// create entries for all events in chunk, add them to entries
|
||||||
entries = this._storeEvents(nonOverlappingEvents, lastKey, direction, txn);
|
entries = this._storeEvents(nonOverlappingEvents, lastKey, direction, state, txn);
|
||||||
const fragments = await this._updateFragments(fragmentEntry, neighbourFragmentEntry, end, entries, txn);
|
const fragments = await this._updateFragments(fragmentEntry, neighbourFragmentEntry, end, entries, txn);
|
||||||
|
|
||||||
return {entries, fragments};
|
return {entries, fragments};
|
||||||
|
@ -18,6 +18,7 @@ import {EventKey} from "../EventKey.js";
|
|||||||
import {EventEntry} from "../entries/EventEntry.js";
|
import {EventEntry} from "../entries/EventEntry.js";
|
||||||
import {FragmentBoundaryEntry} from "../entries/FragmentBoundaryEntry.js";
|
import {FragmentBoundaryEntry} from "../entries/FragmentBoundaryEntry.js";
|
||||||
import {createEventEntry} from "./common.js";
|
import {createEventEntry} from "./common.js";
|
||||||
|
import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../members/RoomMember.js";
|
||||||
|
|
||||||
// Synapse bug? where the m.room.create event appears twice in sync response
|
// Synapse bug? where the m.room.create event appears twice in sync response
|
||||||
// when first syncing the room
|
// when first syncing the room
|
||||||
@ -97,9 +98,87 @@ export class SyncWriter {
|
|||||||
return {oldFragment, newFragment};
|
return {oldFragment, newFragment};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_writeStateEvent(event, txn) {
|
||||||
|
if (event.type === MEMBER_EVENT_TYPE) {
|
||||||
|
const userId = event.state_key;
|
||||||
|
if (userId) {
|
||||||
|
const member = RoomMember.fromMemberEvent(this._roomId, event);
|
||||||
|
if (member) {
|
||||||
|
// as this is sync, we can just replace the member
|
||||||
|
// if it is there already
|
||||||
|
txn.roomMembers.set(member.serialize());
|
||||||
|
}
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
txn.roomState.set(this._roomId, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_writeStateEvents(roomResponse, txn) {
|
||||||
|
const changedMembers = [];
|
||||||
|
// persist state
|
||||||
|
const {state} = roomResponse;
|
||||||
|
if (state.events) {
|
||||||
|
for (const event of state.events) {
|
||||||
|
const member = this._writeStateEvent(event, txn);
|
||||||
|
if (member) {
|
||||||
|
changedMembers.push(member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changedMembers;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _writeTimeline(entries, timeline, currentKey, txn) {
|
||||||
|
const changedMembers = [];
|
||||||
|
if (timeline.events) {
|
||||||
|
const events = deduplicateEvents(timeline.events);
|
||||||
|
for(const event of events) {
|
||||||
|
// store event in timeline
|
||||||
|
currentKey = currentKey.nextKey();
|
||||||
|
const entry = createEventEntry(currentKey, this._roomId, event);
|
||||||
|
let memberData = await this._findMemberData(event.sender, events, txn);
|
||||||
|
if (memberData) {
|
||||||
|
entry.displayName = memberData.displayName;
|
||||||
|
entry.avatarUrl = memberData.avatarUrl;
|
||||||
|
}
|
||||||
|
txn.timelineEvents.insert(entry);
|
||||||
|
entries.push(new EventEntry(entry, this._fragmentIdComparer));
|
||||||
|
|
||||||
|
// process live state events first, so new member info is available
|
||||||
|
if (typeof event.state_key === "string") {
|
||||||
|
const member = this._writeStateEvent(event, txn);
|
||||||
|
if (member) {
|
||||||
|
changedMembers.push(member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {currentKey, changedMembers};
|
||||||
|
}
|
||||||
|
|
||||||
|
async _findMemberData(userId, events, txn) {
|
||||||
|
// TODO: perhaps add a small cache here?
|
||||||
|
const memberData = await txn.roomMembers.get(this._roomId, userId);
|
||||||
|
if (memberData) {
|
||||||
|
return memberData;
|
||||||
|
} else {
|
||||||
|
// sometimes the member event isn't included in state, but rather in the timeline,
|
||||||
|
// even if it is not the first event in the timeline. In this case, go look for the
|
||||||
|
// first occurence
|
||||||
|
const memberEvent = events.find(e => {
|
||||||
|
return e.type === MEMBER_EVENT_TYPE && e.state_key === userId;
|
||||||
|
});
|
||||||
|
if (memberEvent) {
|
||||||
|
return RoomMember.fromMemberEvent(this._roomId, memberEvent)?.serialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async writeSync(roomResponse, txn) {
|
async writeSync(roomResponse, txn) {
|
||||||
const entries = [];
|
const entries = [];
|
||||||
const timeline = roomResponse.timeline;
|
const {timeline} = roomResponse;
|
||||||
let currentKey = this._lastLiveKey;
|
let currentKey = this._lastLiveKey;
|
||||||
if (!currentKey) {
|
if (!currentKey) {
|
||||||
// means we haven't synced this room yet (just joined or did initial sync)
|
// means we haven't synced this room yet (just joined or did initial sync)
|
||||||
@ -117,32 +196,14 @@ export class SyncWriter {
|
|||||||
entries.push(FragmentBoundaryEntry.end(oldFragment, this._fragmentIdComparer));
|
entries.push(FragmentBoundaryEntry.end(oldFragment, this._fragmentIdComparer));
|
||||||
entries.push(FragmentBoundaryEntry.start(newFragment, this._fragmentIdComparer));
|
entries.push(FragmentBoundaryEntry.start(newFragment, this._fragmentIdComparer));
|
||||||
}
|
}
|
||||||
if (timeline.events) {
|
// important this happens before _writeTimeline so
|
||||||
const events = deduplicateEvents(timeline.events);
|
// members are available in the transaction
|
||||||
for(const event of events) {
|
const changedMembers = this._writeStateEvents(roomResponse, txn);
|
||||||
currentKey = currentKey.nextKey();
|
const timelineResult = await this._writeTimeline(entries, timeline, currentKey, txn);
|
||||||
const entry = createEventEntry(currentKey, this._roomId, event);
|
currentKey = timelineResult.currentKey;
|
||||||
txn.timelineEvents.insert(entry);
|
changedMembers.push(...timelineResult.changedMembers);
|
||||||
entries.push(new EventEntry(entry, this._fragmentIdComparer));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// persist state
|
|
||||||
const state = roomResponse.state;
|
|
||||||
if (state.events) {
|
|
||||||
for (const event of state.events) {
|
|
||||||
txn.roomState.setStateEvent(this._roomId, event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// persist live state events in timeline
|
|
||||||
if (timeline.events) {
|
|
||||||
for (const event of timeline.events) {
|
|
||||||
if (typeof event.state_key === "string") {
|
|
||||||
txn.roomState.setStateEvent(this._roomId, event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {entries, newLiveKey: currentKey};
|
return {entries, newLiveKey: currentKey, changedMembers};
|
||||||
}
|
}
|
||||||
|
|
||||||
afterSync(newLiveKey) {
|
afterSync(newLiveKey) {
|
||||||
|
@ -18,6 +18,7 @@ export const STORE_NAMES = Object.freeze([
|
|||||||
"session",
|
"session",
|
||||||
"roomState",
|
"roomState",
|
||||||
"roomSummary",
|
"roomSummary",
|
||||||
|
"roomMembers",
|
||||||
"timelineEvents",
|
"timelineEvents",
|
||||||
"timelineFragments",
|
"timelineFragments",
|
||||||
"pendingEvents",
|
"pendingEvents",
|
||||||
@ -37,7 +38,7 @@ export class StorageError extends Error {
|
|||||||
fullMessage += `(name: ${cause.name}) `;
|
fullMessage += `(name: ${cause.name}) `;
|
||||||
}
|
}
|
||||||
if (typeof cause.code === "number") {
|
if (typeof cause.code === "number") {
|
||||||
fullMessage += `(code: ${cause.name}) `;
|
fullMessage += `(code: ${cause.code}) `;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (value) {
|
if (value) {
|
||||||
|
@ -151,17 +151,31 @@ export class QueryTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_selectLimit(range, amount, direction) {
|
_selectLimit(range, amount, direction) {
|
||||||
return this._selectWhile(range, (results) => {
|
return this._selectUntil(range, (results) => {
|
||||||
return results.length === amount;
|
return results.length === amount;
|
||||||
}, direction);
|
}, direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _selectWhile(range, predicate, direction) {
|
async _selectUntil(range, predicate, direction) {
|
||||||
const cursor = this._openCursor(range, direction);
|
const cursor = this._openCursor(range, direction);
|
||||||
const results = [];
|
const results = [];
|
||||||
await iterateCursor(cursor, (value) => {
|
await iterateCursor(cursor, (value) => {
|
||||||
results.push(value);
|
results.push(value);
|
||||||
return {done: predicate(results)};
|
return {done: predicate(results, value)};
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// allows you to fetch one too much that won't get added when the predicate fails
|
||||||
|
async _selectWhile(range, predicate, direction) {
|
||||||
|
const cursor = this._openCursor(range, direction);
|
||||||
|
const results = [];
|
||||||
|
await iterateCursor(cursor, (value) => {
|
||||||
|
const passesPredicate = predicate(value);
|
||||||
|
if (passesPredicate) {
|
||||||
|
results.push(value);
|
||||||
|
}
|
||||||
|
return {done: !passesPredicate};
|
||||||
});
|
});
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,10 @@ limitations under the License.
|
|||||||
import {Storage} from "./Storage.js";
|
import {Storage} from "./Storage.js";
|
||||||
import { openDatabase, reqAsPromise } from "./utils.js";
|
import { openDatabase, reqAsPromise } from "./utils.js";
|
||||||
import { exportSession, importSession } from "./export.js";
|
import { exportSession, importSession } from "./export.js";
|
||||||
|
import { schema } from "./schema.js";
|
||||||
|
|
||||||
const sessionName = sessionId => `brawl_session_${sessionId}`;
|
const sessionName = sessionId => `brawl_session_${sessionId}`;
|
||||||
const openDatabaseWithSessionId = sessionId => openDatabase(sessionName(sessionId), createStores, 1);
|
const openDatabaseWithSessionId = sessionId => openDatabase(sessionName(sessionId), createStores, schema.length);
|
||||||
|
|
||||||
export class StorageFactory {
|
export class StorageFactory {
|
||||||
async create(sessionId) {
|
async create(sessionId) {
|
||||||
@ -44,26 +45,10 @@ export class StorageFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createStores(db) {
|
async function createStores(db, txn, oldVersion, version) {
|
||||||
db.createObjectStore("session", {keyPath: "key"});
|
const startIdx = oldVersion || 0;
|
||||||
// any way to make keys unique here? (just use put?)
|
|
||||||
db.createObjectStore("roomSummary", {keyPath: "roomId"});
|
|
||||||
|
|
||||||
// need index to find live fragment? prooobably ok without for now
|
for(let i = startIdx; i < version; ++i) {
|
||||||
//key = room_id | fragment_id
|
await schema[i](db, txn);
|
||||||
db.createObjectStore("timelineFragments", {keyPath: "key"});
|
}
|
||||||
//key = room_id | fragment_id | event_index
|
|
||||||
const timelineEvents = db.createObjectStore("timelineEvents", {keyPath: "key"});
|
|
||||||
//eventIdKey = room_id | event_id
|
|
||||||
timelineEvents.createIndex("byEventId", "eventIdKey", {unique: true});
|
|
||||||
//key = room_id | event.type | event.state_key,
|
|
||||||
db.createObjectStore("roomState", {keyPath: "key"});
|
|
||||||
db.createObjectStore("pendingEvents", {keyPath: "key"});
|
|
||||||
|
|
||||||
// const roomMembers = db.createObjectStore("roomMembers", {keyPath: [
|
|
||||||
// "event.room_id",
|
|
||||||
// "event.content.membership",
|
|
||||||
// "event.state_key"
|
|
||||||
// ]});
|
|
||||||
// roomMembers.createIndex("byName", ["room_id", "content.name"]);
|
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import {SessionStore} from "./stores/SessionStore.js";
|
|||||||
import {RoomSummaryStore} from "./stores/RoomSummaryStore.js";
|
import {RoomSummaryStore} from "./stores/RoomSummaryStore.js";
|
||||||
import {TimelineEventStore} from "./stores/TimelineEventStore.js";
|
import {TimelineEventStore} from "./stores/TimelineEventStore.js";
|
||||||
import {RoomStateStore} from "./stores/RoomStateStore.js";
|
import {RoomStateStore} from "./stores/RoomStateStore.js";
|
||||||
|
import {RoomMemberStore} from "./stores/RoomMemberStore.js";
|
||||||
import {TimelineFragmentStore} from "./stores/TimelineFragmentStore.js";
|
import {TimelineFragmentStore} from "./stores/TimelineFragmentStore.js";
|
||||||
import {PendingEventStore} from "./stores/PendingEventStore.js";
|
import {PendingEventStore} from "./stores/PendingEventStore.js";
|
||||||
|
|
||||||
@ -72,6 +73,10 @@ export class Transaction {
|
|||||||
return this._store("roomState", idbStore => new RoomStateStore(idbStore));
|
return this._store("roomState", idbStore => new RoomStateStore(idbStore));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get roomMembers() {
|
||||||
|
return this._store("roomMembers", idbStore => new RoomMemberStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
get pendingEvents() {
|
get pendingEvents() {
|
||||||
return this._store("pendingEvents", idbStore => new PendingEventStore(idbStore));
|
return this._store("pendingEvents", idbStore => new PendingEventStore(idbStore));
|
||||||
}
|
}
|
||||||
|
46
src/matrix/storage/idb/schema.js
Normal file
46
src/matrix/storage/idb/schema.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import {iterateCursor} from "./utils.js";
|
||||||
|
import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js";
|
||||||
|
import {RoomMemberStore} from "./stores/RoomMemberStore.js";
|
||||||
|
|
||||||
|
// FUNCTIONS SHOULD ONLY BE APPENDED!!
|
||||||
|
// the index in the array is the database version
|
||||||
|
export const schema = [
|
||||||
|
createInitialStores,
|
||||||
|
createMemberStore,
|
||||||
|
];
|
||||||
|
// TODO: how to deal with git merge conflicts of this array?
|
||||||
|
|
||||||
|
|
||||||
|
// how do we deal with schema updates vs existing data migration in a way that
|
||||||
|
//v1
|
||||||
|
function createInitialStores(db) {
|
||||||
|
db.createObjectStore("session", {keyPath: "key"});
|
||||||
|
// any way to make keys unique here? (just use put?)
|
||||||
|
db.createObjectStore("roomSummary", {keyPath: "roomId"});
|
||||||
|
|
||||||
|
// need index to find live fragment? prooobably ok without for now
|
||||||
|
//key = room_id | fragment_id
|
||||||
|
db.createObjectStore("timelineFragments", {keyPath: "key"});
|
||||||
|
//key = room_id | fragment_id | event_index
|
||||||
|
const timelineEvents = db.createObjectStore("timelineEvents", {keyPath: "key"});
|
||||||
|
//eventIdKey = room_id | event_id
|
||||||
|
timelineEvents.createIndex("byEventId", "eventIdKey", {unique: true});
|
||||||
|
//key = room_id | event.type | event.state_key,
|
||||||
|
db.createObjectStore("roomState", {keyPath: "key"});
|
||||||
|
db.createObjectStore("pendingEvents", {keyPath: "key"});
|
||||||
|
}
|
||||||
|
//v2
|
||||||
|
async function createMemberStore(db, txn) {
|
||||||
|
const roomMembers = new RoomMemberStore(db.createObjectStore("roomMembers", {keyPath: "key"}));
|
||||||
|
// migrate existing member state events over
|
||||||
|
const roomState = txn.objectStore("roomState");
|
||||||
|
await iterateCursor(roomState.openCursor(), entry => {
|
||||||
|
if (entry.event.type === MEMBER_EVENT_TYPE) {
|
||||||
|
roomState.delete(entry.key);
|
||||||
|
const member = RoomMember.fromMemberEvent(entry.roomId, entry.event);
|
||||||
|
if (member) {
|
||||||
|
roomMembers.set(member.serialize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// no historical members for now
|
|
||||||
class MemberStore {
|
|
||||||
async getMember(roomId, userId) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* async getMemberAtSortKey(roomId, userId, sortKey) {
|
|
||||||
|
|
||||||
} */
|
|
||||||
// multiple members here? does it happen at same sort key?
|
|
||||||
async setMembers(roomId, members) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSortedMembers(roomId, offset, amount) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
43
src/matrix/storage/idb/stores/RoomMemberStore.js
Normal file
43
src/matrix/storage/idb/stores/RoomMemberStore.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
Copyright 2020 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function encodeKey(roomId, userId) {
|
||||||
|
return `${roomId}|${userId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no historical members
|
||||||
|
export class RoomMemberStore {
|
||||||
|
constructor(roomMembersStore) {
|
||||||
|
this._roomMembersStore = roomMembersStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(roomId, userId) {
|
||||||
|
return this._roomMembersStore.get(encodeKey(roomId, userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(member) {
|
||||||
|
member.key = encodeKey(member.roomId, member.userId);
|
||||||
|
return this._roomMembersStore.put(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(roomId) {
|
||||||
|
const range = IDBKeyRange.lowerBound(encodeKey(roomId, ""));
|
||||||
|
return this._roomMembersStore.selectWhile(range, member => {
|
||||||
|
return member.roomId === roomId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -19,15 +19,15 @@ export class RoomStateStore {
|
|||||||
this._roomStateStore = idbStore;
|
this._roomStateStore = idbStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEvents(type) {
|
async getAllForType(type) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEventsForKey(type, stateKey) {
|
async get(type, stateKey) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setStateEvent(roomId, event) {
|
async set(roomId, event) {
|
||||||
const key = `${roomId}|${event.type}|${event.state_key}`;
|
const key = `${roomId}|${event.type}|${event.state_key}`;
|
||||||
const entry = {roomId, event, key};
|
const entry = {roomId, event, key};
|
||||||
return this._roomStateStore.put(entry);
|
return this._roomStateStore.put(entry);
|
||||||
|
@ -41,8 +41,9 @@ export function openDatabase(name, createObjectStore, version) {
|
|||||||
const req = window.indexedDB.open(name, version);
|
const req = window.indexedDB.open(name, version);
|
||||||
req.onupgradeneeded = (ev) => {
|
req.onupgradeneeded = (ev) => {
|
||||||
const db = ev.target.result;
|
const db = ev.target.result;
|
||||||
|
const txn = ev.target.transaction;
|
||||||
const oldVersion = ev.oldVersion;
|
const oldVersion = ev.oldVersion;
|
||||||
createObjectStore(db, oldVersion, version);
|
createObjectStore(db, txn, oldVersion, version);
|
||||||
};
|
};
|
||||||
return reqAsPromise(req);
|
return reqAsPromise(req);
|
||||||
}
|
}
|
||||||
@ -74,7 +75,10 @@ export function iterateCursor(cursorRequest, processValue) {
|
|||||||
resolve(false);
|
resolve(false);
|
||||||
return; // end of results
|
return; // end of results
|
||||||
}
|
}
|
||||||
const {done, jumpTo} = processValue(cursor.value, cursor.key);
|
const result = processValue(cursor.value, cursor.key);
|
||||||
|
const done = result?.done;
|
||||||
|
const jumpTo = result?.jumpTo;
|
||||||
|
|
||||||
if (done) {
|
if (done) {
|
||||||
resolve(true);
|
resolve(true);
|
||||||
} else if(jumpTo) {
|
} else if(jumpTo) {
|
||||||
|
@ -297,15 +297,25 @@ ul.Timeline > li:not(.continuation) {
|
|||||||
margin-top: 7px;
|
margin-top: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.Timeline > li.continuation .sender {
|
ul.Timeline > li.continuation .profile {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.message-container {
|
.message-container {
|
||||||
padding: 1px 10px 0px 10px;
|
padding: 1px 10px 0px 10px;
|
||||||
margin: 5px 10px 0 10px;
|
margin: 5px 10px 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-container .profile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-container .avatar {
|
||||||
|
--avatar-size: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
.TextMessageView.continuation .message-container {
|
.TextMessageView.continuation .message-container {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@ -313,6 +323,7 @@ ul.Timeline > li.continuation .sender {
|
|||||||
|
|
||||||
.message-container .sender {
|
.message-container .sender {
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
|
margin-left: 6px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 1.7rem;
|
line-height: 1.7rem;
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ limitations under the License.
|
|||||||
replace with css aspect-ratio once supported */
|
replace with css aspect-ratio once supported */
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-container img {
|
.message-container img.picture {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -48,8 +48,8 @@ export class TimelineList extends ListView {
|
|||||||
while (predicate()) {
|
while (predicate()) {
|
||||||
// fill, not enough content to fill timeline
|
// fill, not enough content to fill timeline
|
||||||
this._topLoadingPromise = this._viewModel.loadAtTop();
|
this._topLoadingPromise = this._viewModel.loadAtTop();
|
||||||
const startReached = await this._topLoadingPromise;
|
const shouldStop = await this._topLoadingPromise;
|
||||||
if (startReached) {
|
if (shouldStop) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ export class ImageView extends TemplateView {
|
|||||||
// replace with css aspect-ratio once supported
|
// replace with css aspect-ratio once supported
|
||||||
const heightRatioPercent = (vm.thumbnailHeight / vm.thumbnailWidth) * 100;
|
const heightRatioPercent = (vm.thumbnailHeight / vm.thumbnailWidth) * 100;
|
||||||
const image = t.img({
|
const image = t.img({
|
||||||
|
className: "picture",
|
||||||
src: vm.thumbnailUrl,
|
src: vm.thumbnailUrl,
|
||||||
width: vm.thumbnailWidth,
|
width: vm.thumbnailWidth,
|
||||||
height: vm.thumbnailHeight,
|
height: vm.thumbnailHeight,
|
||||||
|
@ -22,8 +22,20 @@ export function renderMessage(t, vm, children) {
|
|||||||
pending: vm.isPending,
|
pending: vm.isPending,
|
||||||
continuation: vm => vm.isContinuation,
|
continuation: vm => vm.isContinuation,
|
||||||
};
|
};
|
||||||
const sender = t.div({className: `sender usercolor${vm.senderColorNumber}`}, vm.sender);
|
|
||||||
children = [sender].concat(children);
|
const hasAvatar = !!vm.avatarUrl;
|
||||||
|
const avatarClasses = {
|
||||||
|
avatar: true,
|
||||||
|
[`usercolor${vm.avatarColorNumber}`]: !hasAvatar,
|
||||||
|
};
|
||||||
|
const avatarContent = hasAvatar ?
|
||||||
|
t.img({src: vm.avatarUrl, width: "30", height: "30", title: vm.sender}) :
|
||||||
|
vm.avatarLetter;
|
||||||
|
const profile = t.div({className: "profile"}, [
|
||||||
|
t.div({className: avatarClasses}, [avatarContent]),
|
||||||
|
t.div({className: `sender usercolor${vm.avatarColorNumber}`}, vm.sender)
|
||||||
|
]);
|
||||||
|
children = [profile].concat(children);
|
||||||
return t.li(
|
return t.li(
|
||||||
{className: classes},
|
{className: classes},
|
||||||
t.div({className: "message-container"}, children)
|
t.div({className: "message-container"}, children)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user