From 83d2b58badaee3218821b215f695f46d0a1d91ea Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Feb 2022 18:58:30 +0100 Subject: [PATCH] add avatar support to creating room --- .../RoomBeingCreatedTileViewModel.js | 5 + src/matrix/Session.js | 10 +- src/matrix/room/AttachmentUpload.js | 8 +- src/matrix/room/create.ts | 115 +++++++++++++----- src/platform/types/types.ts | 14 +++ 5 files changed, 111 insertions(+), 41 deletions(-) diff --git a/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js b/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js index 8c629892..d7840bfa 100644 --- a/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js +++ b/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js @@ -50,4 +50,9 @@ export class RoomBeingCreatedTileViewModel extends BaseTileViewModel { get _avatarSource() { return this._roomBeingCreated; } + + avatarUrl(size) { + // allow blob url which doesn't need mxc => http resolution + return this._roomBeingCreated.avatarBlobUrl ?? super.avatarUrl(size); + } } diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 4bb88f4e..b03306ed 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -600,15 +600,16 @@ export class Session { return this._roomsBeingCreated; } - createRoom({type, isEncrypted, explicitName, topic, invites, loadProfiles = true}, log = undefined) { + createRoom(options, log = undefined) { let roomBeingCreated; this._platform.logger.runDetached("create room", async log => { const localId = `local-${Math.floor(this._platform.random() * Number.MAX_SAFE_INTEGER)}`; - roomBeingCreated = new RoomBeingCreated(localId, type, isEncrypted, - explicitName, topic, invites, this._roomsBeingCreatedUpdateCallback, - this._mediaRepository, log); + roomBeingCreated = new RoomBeingCreated( + localId, options, this._roomsBeingCreatedUpdateCallback, + this._mediaRepository, this._platform, log); this._roomsBeingCreated.set(localId, roomBeingCreated); const promises = [roomBeingCreated.create(this._hsApi, log)]; + const loadProfiles = !(options.loadProfiles === false); // default to true if (loadProfiles) { promises.push(roomBeingCreated.loadProfiles(this._hsApi, log)); } @@ -715,6 +716,7 @@ export class Session { .set("roomId", roomBeingCreated.roomId); observableStatus.set(observableStatus.get() | RoomStatus.Replaced); } + roomBeingCreated.dispose(); this._roomsBeingCreated.remove(roomBeingCreated.localId); return; } diff --git a/src/matrix/room/AttachmentUpload.js b/src/matrix/room/AttachmentUpload.js index e2e7e3bf..6cf4c1db 100644 --- a/src/matrix/room/AttachmentUpload.js +++ b/src/matrix/room/AttachmentUpload.js @@ -40,17 +40,15 @@ export class AttachmentUpload { return this._sentBytes; } - /** @public */ abort() { this._uploadRequest?.abort(); } - /** @public */ get localPreview() { return this._unencryptedBlob; } - /** @package */ + /** @internal */ async encrypt() { if (this._encryptionInfo) { throw new Error("already encrypted"); @@ -60,7 +58,7 @@ export class AttachmentUpload { this._encryptionInfo = info; } - /** @package */ + /** @internal */ async upload(hsApi, progressCallback, log) { this._uploadRequest = hsApi.uploadAttachment(this._transferredBlob, this._filename, { uploadProgress: sentBytes => { @@ -73,7 +71,7 @@ export class AttachmentUpload { this._mxcUrl = content_uri; } - /** @package */ + /** @internal */ applyToContent(urlPath, content) { if (!this._mxcUrl) { throw new Error("upload has not finished"); diff --git a/src/matrix/room/create.ts b/src/matrix/room/create.ts index 4bdae474..e3a1ed4d 100644 --- a/src/matrix/room/create.ts +++ b/src/matrix/room/create.ts @@ -18,10 +18,12 @@ import {calculateRoomName} from "./members/Heroes"; import {createRoomEncryptionEvent} from "../e2ee/common"; import {MediaRepository} from "../net/MediaRepository"; import {EventEmitter} from "../../utils/EventEmitter"; +import {AttachmentUpload} from "./AttachmentUpload"; -import type {StateEvent} from "../storage/types"; import type {HomeServerApi} from "../net/HomeServerApi"; import type {ILogItem} from "../../logging/types"; +import type {Platform} from "../../platform/web/Platform"; +import type {IBlobHandle} from "../../platform/types/types"; type CreateRoomPayload = { is_direct?: boolean; @@ -29,7 +31,31 @@ type CreateRoomPayload = { name?: string; topic?: string; invite?: string[]; - initial_state?: StateEvent[] + room_alias_name?: string; + initial_state: {type: string; state_key: string; content: Record}[] +} + +type ImageInfo = { + w: number; + h: number; + mimetype: string; + size: number; +} + +type Avatar = { + info: ImageInfo; + blob: IBlobHandle; + name: string; +} + +type Options = { + type: RoomType; + isEncrypted?: boolean; + name?: string; + topic?: string; + invites?: string[]; + avatar?: Avatar; + alias?: string; } export enum RoomType { @@ -64,54 +90,72 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { private profiles: Profile[] = []; public readonly isEncrypted: boolean; - private _name: string; + private _calculatedName: string; private _error?: Error; constructor( public readonly localId: string, - private readonly type: RoomType, - isEncrypted: boolean | undefined, - private readonly explicitName: string | undefined, - private readonly topic: string | undefined, - private readonly inviteUserIds: string[] | undefined, + private readonly options: Options, private readonly updateCallback: (self: RoomBeingCreated, params: string | undefined) => void, public readonly mediaRepository: MediaRepository, + public readonly platform: Platform, log: ILogItem ) { super(); - this.isEncrypted = isEncrypted === undefined ? defaultE2EEStatusForType(this.type) : isEncrypted; - if (explicitName) { - this._name = explicitName; + this.isEncrypted = options.isEncrypted === undefined ? defaultE2EEStatusForType(options.type) : options.isEncrypted; + if (options.name) { + this._calculatedName = options.name; } else { const summaryData = { joinCount: 1, // ourselves - inviteCount: (this.inviteUserIds?.length || 0) + inviteCount: (options.invites?.length || 0) }; - const userIdProfiles = (inviteUserIds || []).map(userId => new UserIdProfile(userId)); - this._name = calculateRoomName(userIdProfiles, summaryData, log); + const userIdProfiles = (options.invites || []).map(userId => new UserIdProfile(userId)); + this._calculatedName = calculateRoomName(userIdProfiles, summaryData, log); } } /** @internal */ async create(hsApi: HomeServerApi, log: ILogItem): Promise { - const options: CreateRoomPayload = { - is_direct: this.type === RoomType.DirectMessage, - preset: presetForType(this.type) + let avatarEventContent; + if (this.options.avatar) { + const {avatar} = this.options; + const attachment = new AttachmentUpload({filename: avatar.name, blob: avatar.blob, platform: this.platform}); + await attachment.upload(hsApi, () => {}, log); + avatarEventContent = { + info: avatar.info + }; + attachment.applyToContent("url", avatarEventContent); + } + const createOptions: CreateRoomPayload = { + is_direct: this.options.type === RoomType.DirectMessage, + preset: presetForType(this.options.type), + initial_state: [] }; - if (this.explicitName) { - options.name = this.explicitName; + if (this.options.name) { + createOptions.name = this.options.name; } - if (this.topic) { - options.topic = this.topic; + if (this.options.topic) { + createOptions.topic = this.options.topic; } - if (this.inviteUserIds) { - options.invite = this.inviteUserIds; + if (this.options.invites) { + createOptions.invite = this.options.invites; + } + if (this.options.alias) { + createOptions.room_alias_name = this.options.alias; } if (this.isEncrypted) { - options.initial_state = [createRoomEncryptionEvent()]; + createOptions.initial_state.push(createRoomEncryptionEvent()); + } + if (avatarEventContent) { + createOptions.initial_state.push({ + type: "m.room.avatar", + state_key: "", + content: avatarEventContent + }); } try { - const response = await hsApi.createRoom(options, {log}).response(); + const response = await hsApi.createRoom(createOptions, {log}).response(); this._roomId = response["room_id"]; } catch (err) { this._error = err; @@ -127,13 +171,13 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { /** @internal */ async loadProfiles(hsApi: HomeServerApi, log: ILogItem): Promise { // only load profiles if we need it for the room name and avatar - if (!this.explicitName && this.inviteUserIds) { - this.profiles = await loadProfiles(this.inviteUserIds, hsApi, log); + if (!this.options.name && this.options.invites) { + this.profiles = await loadProfiles(this.options.invites, hsApi, log); const summaryData = { joinCount: 1, // ourselves - inviteCount: this.inviteUserIds.length + inviteCount: this.options.invites.length }; - this._name = calculateRoomName(this.profiles, summaryData, log); + this._calculatedName = calculateRoomName(this.profiles, summaryData, log); this.emitChange(); } } @@ -143,16 +187,23 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { this.emit("change"); } - get avatarColorId(): string { return this.inviteUserIds?.[0] ?? this._roomId ?? this.localId; } - get avatarUrl(): string | undefined { return this.profiles[0]?.avatarUrl; } + get avatarColorId(): string { return this.options.invites?.[0] ?? this._roomId ?? this.localId; } + get avatarUrl(): string | undefined { return this.profiles?.[0].avatarUrl; } + get avatarBlobUrl(): string | undefined { return this.options.avatar?.blob?.url; } get roomId(): string | undefined { return this._roomId; } - get name() { return this._name; } + get name() { return this._calculatedName; } get isBeingCreated(): boolean { return true; } get error(): Error | undefined { return this._error; } cancel() { // TODO: remove from collection somehow } + + dispose() { + if (this.options.avatar) { + this.options.avatar.blob.dispose(); + } + } } export async function loadProfiles(userIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise { diff --git a/src/platform/types/types.ts b/src/platform/types/types.ts index 58d2216f..91884117 100644 --- a/src/platform/types/types.ts +++ b/src/platform/types/types.ts @@ -31,3 +31,17 @@ export interface IRequestOptions { } export type RequestFunction = (url: string, options: IRequestOptions) => RequestResult; + +export interface IBlobHandle { + nativeBlob: any; + url: string; + size: number; + mimeType: string; + readAsBuffer(): BufferSource; + dispose() +} + +export type File = { + readonly name: string; + readonly blob: IBlobHandle; +}