add avatar support to creating room

This commit is contained in:
Bruno Windels 2022-02-09 18:58:30 +01:00
parent afe8e17a6f
commit 83d2b58bad
5 changed files with 111 additions and 41 deletions

View file

@ -50,4 +50,9 @@ export class RoomBeingCreatedTileViewModel extends BaseTileViewModel {
get _avatarSource() { get _avatarSource() {
return this._roomBeingCreated; return this._roomBeingCreated;
} }
avatarUrl(size) {
// allow blob url which doesn't need mxc => http resolution
return this._roomBeingCreated.avatarBlobUrl ?? super.avatarUrl(size);
}
} }

View file

@ -600,15 +600,16 @@ export class Session {
return this._roomsBeingCreated; return this._roomsBeingCreated;
} }
createRoom({type, isEncrypted, explicitName, topic, invites, loadProfiles = true}, log = undefined) { createRoom(options, log = undefined) {
let roomBeingCreated; let roomBeingCreated;
this._platform.logger.runDetached("create room", async log => { this._platform.logger.runDetached("create room", async log => {
const localId = `local-${Math.floor(this._platform.random() * Number.MAX_SAFE_INTEGER)}`; const localId = `local-${Math.floor(this._platform.random() * Number.MAX_SAFE_INTEGER)}`;
roomBeingCreated = new RoomBeingCreated(localId, type, isEncrypted, roomBeingCreated = new RoomBeingCreated(
explicitName, topic, invites, this._roomsBeingCreatedUpdateCallback, localId, options, this._roomsBeingCreatedUpdateCallback,
this._mediaRepository, log); this._mediaRepository, this._platform, log);
this._roomsBeingCreated.set(localId, roomBeingCreated); this._roomsBeingCreated.set(localId, roomBeingCreated);
const promises = [roomBeingCreated.create(this._hsApi, log)]; const promises = [roomBeingCreated.create(this._hsApi, log)];
const loadProfiles = !(options.loadProfiles === false); // default to true
if (loadProfiles) { if (loadProfiles) {
promises.push(roomBeingCreated.loadProfiles(this._hsApi, log)); promises.push(roomBeingCreated.loadProfiles(this._hsApi, log));
} }
@ -715,6 +716,7 @@ export class Session {
.set("roomId", roomBeingCreated.roomId); .set("roomId", roomBeingCreated.roomId);
observableStatus.set(observableStatus.get() | RoomStatus.Replaced); observableStatus.set(observableStatus.get() | RoomStatus.Replaced);
} }
roomBeingCreated.dispose();
this._roomsBeingCreated.remove(roomBeingCreated.localId); this._roomsBeingCreated.remove(roomBeingCreated.localId);
return; return;
} }

View file

@ -40,17 +40,15 @@ export class AttachmentUpload {
return this._sentBytes; return this._sentBytes;
} }
/** @public */
abort() { abort() {
this._uploadRequest?.abort(); this._uploadRequest?.abort();
} }
/** @public */
get localPreview() { get localPreview() {
return this._unencryptedBlob; return this._unencryptedBlob;
} }
/** @package */ /** @internal */
async encrypt() { async encrypt() {
if (this._encryptionInfo) { if (this._encryptionInfo) {
throw new Error("already encrypted"); throw new Error("already encrypted");
@ -60,7 +58,7 @@ export class AttachmentUpload {
this._encryptionInfo = info; this._encryptionInfo = info;
} }
/** @package */ /** @internal */
async upload(hsApi, progressCallback, log) { async upload(hsApi, progressCallback, log) {
this._uploadRequest = hsApi.uploadAttachment(this._transferredBlob, this._filename, { this._uploadRequest = hsApi.uploadAttachment(this._transferredBlob, this._filename, {
uploadProgress: sentBytes => { uploadProgress: sentBytes => {
@ -73,7 +71,7 @@ export class AttachmentUpload {
this._mxcUrl = content_uri; this._mxcUrl = content_uri;
} }
/** @package */ /** @internal */
applyToContent(urlPath, content) { applyToContent(urlPath, content) {
if (!this._mxcUrl) { if (!this._mxcUrl) {
throw new Error("upload has not finished"); throw new Error("upload has not finished");

View file

@ -18,10 +18,12 @@ import {calculateRoomName} from "./members/Heroes";
import {createRoomEncryptionEvent} from "../e2ee/common"; import {createRoomEncryptionEvent} from "../e2ee/common";
import {MediaRepository} from "../net/MediaRepository"; import {MediaRepository} from "../net/MediaRepository";
import {EventEmitter} from "../../utils/EventEmitter"; import {EventEmitter} from "../../utils/EventEmitter";
import {AttachmentUpload} from "./AttachmentUpload";
import type {StateEvent} from "../storage/types";
import type {HomeServerApi} from "../net/HomeServerApi"; import type {HomeServerApi} from "../net/HomeServerApi";
import type {ILogItem} from "../../logging/types"; import type {ILogItem} from "../../logging/types";
import type {Platform} from "../../platform/web/Platform";
import type {IBlobHandle} from "../../platform/types/types";
type CreateRoomPayload = { type CreateRoomPayload = {
is_direct?: boolean; is_direct?: boolean;
@ -29,7 +31,31 @@ type CreateRoomPayload = {
name?: string; name?: string;
topic?: string; topic?: string;
invite?: string[]; invite?: string[];
initial_state?: StateEvent[] room_alias_name?: string;
initial_state: {type: string; state_key: string; content: Record<string, any>}[]
}
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 { export enum RoomType {
@ -64,54 +90,72 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> {
private profiles: Profile[] = []; private profiles: Profile[] = [];
public readonly isEncrypted: boolean; public readonly isEncrypted: boolean;
private _name: string; private _calculatedName: string;
private _error?: Error; private _error?: Error;
constructor( constructor(
public readonly localId: string, public readonly localId: string,
private readonly type: RoomType, private readonly options: Options,
isEncrypted: boolean | undefined,
private readonly explicitName: string | undefined,
private readonly topic: string | undefined,
private readonly inviteUserIds: string[] | undefined,
private readonly updateCallback: (self: RoomBeingCreated, params: string | undefined) => void, private readonly updateCallback: (self: RoomBeingCreated, params: string | undefined) => void,
public readonly mediaRepository: MediaRepository, public readonly mediaRepository: MediaRepository,
public readonly platform: Platform,
log: ILogItem log: ILogItem
) { ) {
super(); super();
this.isEncrypted = isEncrypted === undefined ? defaultE2EEStatusForType(this.type) : isEncrypted; this.isEncrypted = options.isEncrypted === undefined ? defaultE2EEStatusForType(options.type) : options.isEncrypted;
if (explicitName) { if (options.name) {
this._name = explicitName; this._calculatedName = options.name;
} else { } else {
const summaryData = { const summaryData = {
joinCount: 1, // ourselves joinCount: 1, // ourselves
inviteCount: (this.inviteUserIds?.length || 0) inviteCount: (options.invites?.length || 0)
}; };
const userIdProfiles = (inviteUserIds || []).map(userId => new UserIdProfile(userId)); const userIdProfiles = (options.invites || []).map(userId => new UserIdProfile(userId));
this._name = calculateRoomName(userIdProfiles, summaryData, log); this._calculatedName = calculateRoomName(userIdProfiles, summaryData, log);
} }
} }
/** @internal */ /** @internal */
async create(hsApi: HomeServerApi, log: ILogItem): Promise<void> { async create(hsApi: HomeServerApi, log: ILogItem): Promise<void> {
const options: CreateRoomPayload = { let avatarEventContent;
is_direct: this.type === RoomType.DirectMessage, if (this.options.avatar) {
preset: presetForType(this.type) 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) { if (this.options.name) {
options.name = this.explicitName; createOptions.name = this.options.name;
} }
if (this.topic) { if (this.options.topic) {
options.topic = this.topic; createOptions.topic = this.options.topic;
} }
if (this.inviteUserIds) { if (this.options.invites) {
options.invite = this.inviteUserIds; createOptions.invite = this.options.invites;
}
if (this.options.alias) {
createOptions.room_alias_name = this.options.alias;
} }
if (this.isEncrypted) { 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 { try {
const response = await hsApi.createRoom(options, {log}).response(); const response = await hsApi.createRoom(createOptions, {log}).response();
this._roomId = response["room_id"]; this._roomId = response["room_id"];
} catch (err) { } catch (err) {
this._error = err; this._error = err;
@ -127,13 +171,13 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> {
/** @internal */ /** @internal */
async loadProfiles(hsApi: HomeServerApi, log: ILogItem): Promise<void> { async loadProfiles(hsApi: HomeServerApi, log: ILogItem): Promise<void> {
// only load profiles if we need it for the room name and avatar // only load profiles if we need it for the room name and avatar
if (!this.explicitName && this.inviteUserIds) { if (!this.options.name && this.options.invites) {
this.profiles = await loadProfiles(this.inviteUserIds, hsApi, log); this.profiles = await loadProfiles(this.options.invites, hsApi, log);
const summaryData = { const summaryData = {
joinCount: 1, // ourselves 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(); this.emitChange();
} }
} }
@ -143,16 +187,23 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> {
this.emit("change"); this.emit("change");
} }
get avatarColorId(): string { return this.inviteUserIds?.[0] ?? this._roomId ?? this.localId; } get avatarColorId(): string { return this.options.invites?.[0] ?? this._roomId ?? this.localId; }
get avatarUrl(): string | undefined { return this.profiles[0]?.avatarUrl; } 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 roomId(): string | undefined { return this._roomId; }
get name() { return this._name; } get name() { return this._calculatedName; }
get isBeingCreated(): boolean { return true; } get isBeingCreated(): boolean { return true; }
get error(): Error | undefined { return this._error; } get error(): Error | undefined { return this._error; }
cancel() { cancel() {
// TODO: remove from collection somehow // 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<Profile[]> { export async function loadProfiles(userIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise<Profile[]> {

View file

@ -31,3 +31,17 @@ export interface IRequestOptions {
} }
export type RequestFunction = (url: string, options: IRequestOptions) => RequestResult; 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;
}