forked from mystiq/hydrogen-web
Merge pull request #654 from vector-im/bwindels/create-room
Create rooms
This commit is contained in:
commit
a184ad528f
61 changed files with 1384 additions and 277 deletions
|
@ -32,7 +32,7 @@ function allowsChild(parent, child) {
|
|||
// allowed root segments
|
||||
return type === "login" || type === "session" || type === "sso" || type === "logout";
|
||||
case "session":
|
||||
return type === "room" || type === "rooms" || type === "settings";
|
||||
return type === "room" || type === "rooms" || type === "settings" || type === "create-room";
|
||||
case "rooms":
|
||||
// downside of the approach: both of these will control which tile is selected
|
||||
return type === "room" || type === "empty-grid-tile";
|
||||
|
|
144
src/domain/session/CreateRoomViewModel.js
Normal file
144
src/domain/session/CreateRoomViewModel.js
Normal file
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
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 {ViewModel} from "../ViewModel.js";
|
||||
import {imageToInfo} from "./common.js";
|
||||
import {RoomType} from "../../matrix/room/common";
|
||||
|
||||
export class CreateRoomViewModel extends ViewModel {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
const {session} = options;
|
||||
this._session = session;
|
||||
this._name = undefined;
|
||||
this._topic = undefined;
|
||||
this._roomAlias = undefined;
|
||||
this._isPublic = false;
|
||||
this._isEncrypted = true;
|
||||
this._isAdvancedShown = false;
|
||||
this._isFederationDisabled = false;
|
||||
this._avatarScaledBlob = undefined;
|
||||
this._avatarFileName = undefined;
|
||||
this._avatarInfo = undefined;
|
||||
}
|
||||
|
||||
get isPublic() { return this._isPublic; }
|
||||
get isEncrypted() { return this._isEncrypted; }
|
||||
get canCreate() { return !!this._name; }
|
||||
avatarUrl() { return this._avatarScaledBlob.url; }
|
||||
get avatarTitle() { return this._name; }
|
||||
get avatarLetter() { return ""; }
|
||||
get avatarColorNumber() { return 0; }
|
||||
get hasAvatar() { return !!this._avatarScaledBlob; }
|
||||
get isFederationDisabled() { return this._isFederationDisabled; }
|
||||
get isAdvancedShown() { return this._isAdvancedShown; }
|
||||
|
||||
setName(name) {
|
||||
this._name = name;
|
||||
this.emitChange("canCreate");
|
||||
}
|
||||
|
||||
setRoomAlias(roomAlias) {
|
||||
this._roomAlias = roomAlias;
|
||||
}
|
||||
|
||||
setTopic(topic) {
|
||||
this._topic = topic;
|
||||
}
|
||||
|
||||
setPublic(isPublic) {
|
||||
this._isPublic = isPublic;
|
||||
this.emitChange("isPublic");
|
||||
}
|
||||
|
||||
setEncrypted(isEncrypted) {
|
||||
this._isEncrypted = isEncrypted;
|
||||
this.emitChange("isEncrypted");
|
||||
}
|
||||
|
||||
setFederationDisabled(disable) {
|
||||
this._isFederationDisabled = disable;
|
||||
this.emitChange("isFederationDisabled");
|
||||
}
|
||||
|
||||
toggleAdvancedShown() {
|
||||
this._isAdvancedShown = !this._isAdvancedShown;
|
||||
this.emitChange("isAdvancedShown");
|
||||
}
|
||||
|
||||
create() {
|
||||
let avatar;
|
||||
if (this._avatarScaledBlob) {
|
||||
avatar = {
|
||||
info: this._avatarInfo,
|
||||
name: this._avatarFileName,
|
||||
blob: this._avatarScaledBlob
|
||||
}
|
||||
}
|
||||
const roomBeingCreated = this._session.createRoom({
|
||||
type: this.isPublic ? RoomType.Public : RoomType.Private,
|
||||
name: this._name ?? undefined,
|
||||
topic: this._topic ?? undefined,
|
||||
isEncrypted: !this.isPublic && this._isEncrypted,
|
||||
isFederationDisabled: this._isFederationDisabled,
|
||||
alias: this.isPublic ? ensureAliasIsLocalPart(this._roomAlias) : undefined,
|
||||
avatar,
|
||||
});
|
||||
this.navigation.push("room", roomBeingCreated.id);
|
||||
}
|
||||
|
||||
async selectAvatar() {
|
||||
if (!this.platform.hasReadPixelPermission()) {
|
||||
alert("Please allow canvas image data access, so we can scale your images down.");
|
||||
return;
|
||||
}
|
||||
if (this._avatarScaledBlob) {
|
||||
this._avatarScaledBlob.dispose();
|
||||
}
|
||||
this._avatarScaledBlob = undefined;
|
||||
this._avatarFileName = undefined;
|
||||
this._avatarInfo = undefined;
|
||||
|
||||
const file = await this.platform.openFile("image/*");
|
||||
if (!file || !file.blob.mimeType.startsWith("image/")) {
|
||||
// allow to clear the avatar by not selecting an image
|
||||
this.emitChange("hasAvatar");
|
||||
return;
|
||||
}
|
||||
let image = await this.platform.loadImage(file.blob);
|
||||
const limit = 800;
|
||||
if (image.maxDimension > limit) {
|
||||
const scaledImage = await image.scale(limit);
|
||||
image.dispose();
|
||||
image = scaledImage;
|
||||
}
|
||||
this._avatarScaledBlob = image.blob;
|
||||
this._avatarInfo = imageToInfo(image);
|
||||
this._avatarFileName = file.name;
|
||||
this.emitChange("hasAvatar");
|
||||
}
|
||||
}
|
||||
|
||||
function ensureAliasIsLocalPart(roomAliasLocalPart) {
|
||||
if (roomAliasLocalPart.startsWith("#")) {
|
||||
roomAliasLocalPart = roomAliasLocalPart.substr(1);
|
||||
}
|
||||
const colonIdx = roomAliasLocalPart.indexOf(":");
|
||||
if (colonIdx !== -1) {
|
||||
roomAliasLocalPart = roomAliasLocalPart.substr(0, colonIdx);
|
||||
}
|
||||
return roomAliasLocalPart;
|
||||
}
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import {ObservableValue} from "../../observable/ObservableValue";
|
||||
import {RoomStatus} from "../../matrix/room/common";
|
||||
|
||||
/**
|
||||
Depending on the status of a room (invited, joined, archived, or none),
|
||||
|
@ -34,11 +35,11 @@ the now transferred child view model.
|
|||
This is also why there is an explicit initialize method, see comment there.
|
||||
*/
|
||||
export class RoomViewModelObservable extends ObservableValue {
|
||||
constructor(sessionViewModel, roomId) {
|
||||
constructor(sessionViewModel, roomIdOrLocalId) {
|
||||
super(null);
|
||||
this._statusSubscription = null;
|
||||
this._sessionViewModel = sessionViewModel;
|
||||
this.id = roomId;
|
||||
this.id = roomIdOrLocalId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,11 +60,21 @@ export class RoomViewModelObservable extends ObservableValue {
|
|||
}
|
||||
|
||||
async _statusToViewModel(status) {
|
||||
if (status.invited) {
|
||||
if (status & RoomStatus.Replaced) {
|
||||
if (status & RoomStatus.BeingCreated) {
|
||||
const {session} = this._sessionViewModel._client;
|
||||
const roomBeingCreated = session.roomsBeingCreated.get(this.id);
|
||||
this._sessionViewModel.notifyRoomReplaced(roomBeingCreated.id, roomBeingCreated.roomId);
|
||||
} else {
|
||||
throw new Error("Don't know how to replace a room with this status: " + (status ^ RoomStatus.Replaced));
|
||||
}
|
||||
} else if (status & RoomStatus.BeingCreated) {
|
||||
return this._sessionViewModel._createRoomBeingCreatedViewModel(this.id);
|
||||
} else if (status & RoomStatus.Invited) {
|
||||
return this._sessionViewModel._createInviteViewModel(this.id);
|
||||
} else if (status.joined) {
|
||||
return this._sessionViewModel._createRoomViewModel(this.id);
|
||||
} else if (status.archived) {
|
||||
} else if (status & RoomStatus.Joined) {
|
||||
return this._sessionViewModel._createRoomViewModelInstance(this.id);
|
||||
} else if (status & RoomStatus.Archived) {
|
||||
return await this._sessionViewModel._createArchivedRoomViewModel(this.id);
|
||||
} else {
|
||||
return this._sessionViewModel._createUnknownRoomViewModel(this.id);
|
||||
|
|
|
@ -19,10 +19,12 @@ import {LeftPanelViewModel} from "./leftpanel/LeftPanelViewModel.js";
|
|||
import {RoomViewModel} from "./room/RoomViewModel.js";
|
||||
import {UnknownRoomViewModel} from "./room/UnknownRoomViewModel.js";
|
||||
import {InviteViewModel} from "./room/InviteViewModel.js";
|
||||
import {RoomBeingCreatedViewModel} from "./room/RoomBeingCreatedViewModel.js";
|
||||
import {LightboxViewModel} from "./room/LightboxViewModel.js";
|
||||
import {SessionStatusViewModel} from "./SessionStatusViewModel.js";
|
||||
import {RoomGridViewModel} from "./RoomGridViewModel.js";
|
||||
import {SettingsViewModel} from "./settings/SettingsViewModel.js";
|
||||
import {CreateRoomViewModel} from "./CreateRoomViewModel.js";
|
||||
import {ViewModel} from "../ViewModel.js";
|
||||
import {RoomViewModelObservable} from "./RoomViewModelObservable.js";
|
||||
import {RightPanelViewModel} from "./rightpanel/RightPanelViewModel.js";
|
||||
|
@ -37,13 +39,11 @@ export class SessionViewModel extends ViewModel {
|
|||
reconnector: client.reconnector,
|
||||
session: client.session,
|
||||
})));
|
||||
this._leftPanelViewModel = this.track(new LeftPanelViewModel(this.childOptions({
|
||||
invites: this._client.session.invites,
|
||||
rooms: this._client.session.rooms
|
||||
})));
|
||||
this._leftPanelViewModel = this.track(new LeftPanelViewModel(this.childOptions({session: this._client.session})));
|
||||
this._settingsViewModel = null;
|
||||
this._roomViewModelObservable = null;
|
||||
this._gridViewModel = null;
|
||||
this._createRoomViewModel = null;
|
||||
this._setupNavigation();
|
||||
}
|
||||
|
||||
|
@ -75,6 +75,12 @@ export class SessionViewModel extends ViewModel {
|
|||
}));
|
||||
this._updateSettings(settings.get());
|
||||
|
||||
const createRoom = this.navigation.observe("create-room");
|
||||
this.track(createRoom.subscribe(createRoomOpen => {
|
||||
this._updateCreateRoom(createRoomOpen);
|
||||
}));
|
||||
this._updateCreateRoom(createRoom.get());
|
||||
|
||||
const lightbox = this.navigation.observe("lightbox");
|
||||
this.track(lightbox.subscribe(eventId => {
|
||||
this._updateLightbox(eventId);
|
||||
|
@ -96,7 +102,7 @@ export class SessionViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
get activeMiddleViewModel() {
|
||||
return this._roomViewModelObservable?.get() || this._gridViewModel || this._settingsViewModel;
|
||||
return this._roomViewModelObservable?.get() || this._gridViewModel || this._settingsViewModel || this._createRoomViewModel;
|
||||
}
|
||||
|
||||
get roomGridViewModel() {
|
||||
|
@ -119,11 +125,14 @@ export class SessionViewModel extends ViewModel {
|
|||
return this._roomViewModelObservable?.get();
|
||||
}
|
||||
|
||||
|
||||
get rightPanelViewModel() {
|
||||
return this._rightPanelViewModel;
|
||||
}
|
||||
|
||||
get createRoomViewModel() {
|
||||
return this._createRoomViewModel;
|
||||
}
|
||||
|
||||
_updateGrid(roomIds) {
|
||||
const changed = !(this._gridViewModel && roomIds);
|
||||
const currentRoomId = this.navigation.path.get("room");
|
||||
|
@ -162,7 +171,7 @@ export class SessionViewModel extends ViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
_createRoomViewModel(roomId) {
|
||||
_createRoomViewModelInstance(roomId) {
|
||||
const room = this._client.session.rooms.get(roomId);
|
||||
if (room) {
|
||||
const roomVM = new RoomViewModel(this.childOptions({room}));
|
||||
|
@ -200,6 +209,17 @@ export class SessionViewModel extends ViewModel {
|
|||
return null;
|
||||
}
|
||||
|
||||
_createRoomBeingCreatedViewModel(localId) {
|
||||
const roomBeingCreated = this._client.session.roomsBeingCreated.get(localId);
|
||||
if (roomBeingCreated) {
|
||||
return new RoomBeingCreatedViewModel(this.childOptions({
|
||||
roomBeingCreated,
|
||||
mediaRepository: this._client.session.mediaRepository,
|
||||
}));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_updateRoom(roomId) {
|
||||
// opening a room and already open?
|
||||
if (this._roomViewModelObservable?.id === roomId) {
|
||||
|
@ -237,6 +257,16 @@ export class SessionViewModel extends ViewModel {
|
|||
this.emitChange("activeMiddleViewModel");
|
||||
}
|
||||
|
||||
_updateCreateRoom(createRoomOpen) {
|
||||
if (this._createRoomViewModel) {
|
||||
this._createRoomViewModel = this.disposeTracked(this._createRoomViewModel);
|
||||
}
|
||||
if (createRoomOpen) {
|
||||
this._createRoomViewModel = this.track(new CreateRoomViewModel(this.childOptions({session: this._client.session})));
|
||||
}
|
||||
this.emitChange("activeMiddleViewModel");
|
||||
}
|
||||
|
||||
_updateLightbox(eventId) {
|
||||
if (this._lightboxViewModel) {
|
||||
this._lightboxViewModel = this.disposeTracked(this._lightboxViewModel);
|
||||
|
@ -263,9 +293,12 @@ export class SessionViewModel extends ViewModel {
|
|||
const enable = !!this.navigation.path.get("right-panel")?.value;
|
||||
if (enable) {
|
||||
const room = this._roomFromNavigation();
|
||||
this._rightPanelViewModel = this.track(new RightPanelViewModel(this.childOptions({room})));
|
||||
this._rightPanelViewModel = this.track(new RightPanelViewModel(this.childOptions({room, session: this._client.session})));
|
||||
}
|
||||
this.emitChange("rightPanelViewModel");
|
||||
}
|
||||
|
||||
notifyRoomReplaced(oldId, newId) {
|
||||
this.navigation.push("room", newId);
|
||||
}
|
||||
}
|
||||
|
|
24
src/domain/session/common.js
Normal file
24
src/domain/session/common.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
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 function imageToInfo(image) {
|
||||
return {
|
||||
w: image.width,
|
||||
h: image.height,
|
||||
mimetype: image.blob.mimeType,
|
||||
size: image.blob.size
|
||||
};
|
||||
}
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js";
|
||||
import {ViewModel} from "../../ViewModel.js";
|
||||
|
||||
const KIND_ORDER = ["invite", "room"];
|
||||
const KIND_ORDER = ["roomBeingCreated", "invite", "room"];
|
||||
|
||||
export class BaseTileViewModel extends ViewModel {
|
||||
constructor(options) {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
@ -16,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import {BaseTileViewModel} from "./BaseTileViewModel.js";
|
||||
import {comparePrimitive} from "./common";
|
||||
|
||||
export class InviteTileViewModel extends BaseTileViewModel {
|
||||
constructor(options) {
|
||||
|
@ -25,31 +25,40 @@ export class InviteTileViewModel extends BaseTileViewModel {
|
|||
this._url = this.urlCreator.openRoomActionUrl(this._invite.id);
|
||||
}
|
||||
|
||||
get busy() {
|
||||
return this._invite.accepting || this._invite.rejecting;
|
||||
}
|
||||
|
||||
get kind() {
|
||||
return "invite";
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this._url;
|
||||
}
|
||||
get busy() { return this._invite.accepting || this._invite.rejecting; }
|
||||
get kind() { return "invite"; }
|
||||
get url() { return this._url; }
|
||||
get name() { return this._invite.name; }
|
||||
get isHighlighted() { return true; }
|
||||
get isUnread() { return true; }
|
||||
get badgeCount() { return this.i18n`!`; }
|
||||
get _avatarSource() { return this._invite; }
|
||||
|
||||
/** very important that sorting order is stable and that comparing
|
||||
* to itself always returns 0, otherwise SortedMapList will
|
||||
* remove the wrong children, etc ... */
|
||||
compare(other) {
|
||||
const parentComparison = super.compare(other);
|
||||
if (parentComparison !== 0) {
|
||||
return parentComparison;
|
||||
}
|
||||
return other._invite.timestamp - this._invite.timestamp;
|
||||
const timeDiff = other._invite.timestamp - this._invite.timestamp;
|
||||
if (timeDiff !== 0) {
|
||||
return timeDiff;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this._invite.name;
|
||||
}
|
||||
|
||||
get _avatarSource() {
|
||||
return this._invite;
|
||||
return comparePrimitive(this._invite.id, other._invite.id);
|
||||
}
|
||||
}
|
||||
|
||||
export function tests() {
|
||||
return {
|
||||
"test compare with timestamp": assert => {
|
||||
const urlCreator = {openRoomActionUrl() { return "";}}
|
||||
const vm1 = new InviteTileViewModel({invite: {timestamp: 500, id: "1"}, urlCreator});
|
||||
const vm2 = new InviteTileViewModel({invite: {timestamp: 250, id: "2"}, urlCreator});
|
||||
assert(vm1.compare(vm2) < 0);
|
||||
assert(vm2.compare(vm1) > 0);
|
||||
assert.equal(vm1.compare(vm1), 0);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
import {ViewModel} from "../../ViewModel.js";
|
||||
import {RoomTileViewModel} from "./RoomTileViewModel.js";
|
||||
import {InviteTileViewModel} from "./InviteTileViewModel.js";
|
||||
import {RoomBeingCreatedTileViewModel} from "./RoomBeingCreatedTileViewModel.js";
|
||||
import {RoomFilter} from "./RoomFilter.js";
|
||||
import {ApplyMap} from "../../../observable/map/ApplyMap.js";
|
||||
import {addPanelIfNeeded} from "../../navigation/index.js";
|
||||
|
@ -25,32 +26,36 @@ import {addPanelIfNeeded} from "../../navigation/index.js";
|
|||
export class LeftPanelViewModel extends ViewModel {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
const {rooms, invites} = options;
|
||||
this._tileViewModelsMap = this._mapTileViewModels(rooms, invites);
|
||||
const {session} = options;
|
||||
this._tileViewModelsMap = this._mapTileViewModels(session.roomsBeingCreated, session.invites, session.rooms);
|
||||
this._tileViewModelsFilterMap = new ApplyMap(this._tileViewModelsMap);
|
||||
this._tileViewModels = this._tileViewModelsFilterMap.sortValues((a, b) => a.compare(b));
|
||||
this._currentTileVM = null;
|
||||
this._setupNavigation();
|
||||
this._closeUrl = this.urlCreator.urlForSegment("session");
|
||||
this._settingsUrl = this.urlCreator.urlForSegment("settings");
|
||||
this._createRoomUrl = this.urlCreator.urlForSegment("create-room");
|
||||
}
|
||||
|
||||
_mapTileViewModels(rooms, invites) {
|
||||
_mapTileViewModels(roomsBeingCreated, invites, rooms) {
|
||||
// join is not commutative, invites will take precedence over rooms
|
||||
return invites.join(rooms).mapValues((roomOrInvite, emitChange) => {
|
||||
const allTiles = invites.join(roomsBeingCreated, rooms).mapValues((item, emitChange) => {
|
||||
let vm;
|
||||
if (roomOrInvite.isInvite) {
|
||||
vm = new InviteTileViewModel(this.childOptions({invite: roomOrInvite, emitChange}));
|
||||
if (item.isBeingCreated) {
|
||||
vm = new RoomBeingCreatedTileViewModel(this.childOptions({roomBeingCreated: item, emitChange}));
|
||||
} else if (item.isInvite) {
|
||||
vm = new InviteTileViewModel(this.childOptions({invite: item, emitChange}));
|
||||
} else {
|
||||
vm = new RoomTileViewModel(this.childOptions({room: roomOrInvite, emitChange}));
|
||||
vm = new RoomTileViewModel(this.childOptions({room: item, emitChange}));
|
||||
}
|
||||
const isOpen = this.navigation.path.get("room")?.value === roomOrInvite.id;
|
||||
const isOpen = this.navigation.path.get("room")?.value === item.id;
|
||||
if (isOpen) {
|
||||
vm.open();
|
||||
this._updateCurrentVM(vm);
|
||||
}
|
||||
return vm;
|
||||
});
|
||||
return allTiles;
|
||||
}
|
||||
|
||||
_updateCurrentVM(vm) {
|
||||
|
@ -69,6 +74,8 @@ export class LeftPanelViewModel extends ViewModel {
|
|||
return this._settingsUrl;
|
||||
}
|
||||
|
||||
get createRoomUrl() { return this._createRoomUrl; }
|
||||
|
||||
_setupNavigation() {
|
||||
const roomObservable = this.navigation.observe("room");
|
||||
this.track(roomObservable.subscribe(roomId => this._open(roomId)));
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
Copyright 2020, 2021 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 {BaseTileViewModel} from "./BaseTileViewModel.js";
|
||||
import {comparePrimitive} from "./common";
|
||||
|
||||
export class RoomBeingCreatedTileViewModel extends BaseTileViewModel {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
const {roomBeingCreated} = options;
|
||||
this._roomBeingCreated = roomBeingCreated;
|
||||
this._url = this.urlCreator.openRoomActionUrl(this._roomBeingCreated.id);
|
||||
}
|
||||
|
||||
get busy() { return !this._roomBeingCreated.error; }
|
||||
get kind() { return "roomBeingCreated"; }
|
||||
get isHighlighted() { return !this.busy; }
|
||||
get badgeCount() { return !this.busy && this.i18n`Failed`; }
|
||||
get url() { return this._url; }
|
||||
get name() { return this._roomBeingCreated.name; }
|
||||
get _avatarSource() { return this._roomBeingCreated; }
|
||||
|
||||
/** very important that sorting order is stable and that comparing
|
||||
* to itself always returns 0, otherwise SortedMapList will
|
||||
* remove the wrong children, etc ... */
|
||||
compare(other) {
|
||||
const parentCmp = super.compare(other);
|
||||
if (parentCmp !== 0) {
|
||||
return parentCmp;
|
||||
}
|
||||
const nameCmp = comparePrimitive(this.name, other.name);
|
||||
if (nameCmp === 0) {
|
||||
return comparePrimitive(this._roomBeingCreated.id, other._roomBeingCreated.id);
|
||||
} else {
|
||||
return nameCmp;
|
||||
}
|
||||
}
|
||||
|
||||
avatarUrl(size) {
|
||||
// allow blob url which doesn't need mxc => http resolution
|
||||
return this._roomBeingCreated.avatarBlobUrl ?? super.avatarUrl(size);
|
||||
}
|
||||
}
|
||||
|
||||
export function tests() {
|
||||
return {
|
||||
"test compare with names": assert => {
|
||||
const urlCreator = {openRoomActionUrl() { return "";}}
|
||||
const vm1 = new RoomBeingCreatedTileViewModel({roomBeingCreated: {name: "A", id: "1"}, urlCreator});
|
||||
const vm2 = new RoomBeingCreatedTileViewModel({roomBeingCreated: {name: "B", id: "2"}, urlCreator});
|
||||
assert(vm1.compare(vm2) < 0);
|
||||
assert(vm2.compare(vm1) > 0);
|
||||
assert.equal(vm1.compare(vm1), 0);
|
||||
},
|
||||
}
|
||||
}
|
|
@ -33,6 +33,9 @@ export class RoomTileViewModel extends BaseTileViewModel {
|
|||
return this._url;
|
||||
}
|
||||
|
||||
/** very important that sorting order is stable and that comparing
|
||||
* to itself always returns 0, otherwise SortedMapList will
|
||||
* remove the wrong children, etc ... */
|
||||
compare(other) {
|
||||
const parentComparison = super.compare(other);
|
||||
if (parentComparison !== 0) {
|
||||
|
|
23
src/domain/session/leftpanel/common.js
Normal file
23
src/domain/session/leftpanel/common.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
Copyright 2020, 2021 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 function comparePrimitive(a, b) {
|
||||
if (a === b) {
|
||||
return 0;
|
||||
} else {
|
||||
return a < b ? -1 : 1;
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import {ViewModel} from "../../ViewModel.js";
|
||||
import {RoomType} from "../../../matrix/room/common";
|
||||
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js";
|
||||
|
||||
export class MemberDetailsViewModel extends ViewModel {
|
||||
|
@ -25,6 +26,7 @@ export class MemberDetailsViewModel extends ViewModel {
|
|||
this._member = this._observableMember.get();
|
||||
this._isEncrypted = options.isEncrypted;
|
||||
this._powerLevelsObservable = options.powerLevelsObservable;
|
||||
this._session = options.session;
|
||||
this.track(this._powerLevelsObservable.subscribe(() => this._onPowerLevelsChange()));
|
||||
this.track(this._observableMember.subscribe( () => this._onMemberChange()));
|
||||
}
|
||||
|
@ -77,6 +79,19 @@ export class MemberDetailsViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
get linkToUser() {
|
||||
return `https://matrix.to/#/${this._member.userId}`;
|
||||
return `https://matrix.to/#/${encodeURIComponent(this._member.userId)}`;
|
||||
}
|
||||
|
||||
async openDirectMessage() {
|
||||
const room = this._session.findDirectMessageForUserId(this.userId);
|
||||
let roomId = room?.id;
|
||||
if (!roomId) {
|
||||
const roomBeingCreated = await this._session.createRoom({
|
||||
type: RoomType.DirectMessage,
|
||||
invites: [this.userId]
|
||||
});
|
||||
roomId = roomBeingCreated.id;
|
||||
}
|
||||
this.navigation.push("room", roomId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ export class RightPanelViewModel extends ViewModel {
|
|||
constructor(options) {
|
||||
super(options);
|
||||
this._room = options.room;
|
||||
this._session = options.session;
|
||||
this._members = null;
|
||||
this._setupNavigation();
|
||||
}
|
||||
|
@ -48,7 +49,13 @@ export class RightPanelViewModel extends ViewModel {
|
|||
}
|
||||
const isEncrypted = this._room.isEncrypted;
|
||||
const powerLevelsObservable = await this._room.observePowerLevels();
|
||||
return {observableMember, isEncrypted, powerLevelsObservable, mediaRepository: this._room.mediaRepository};
|
||||
return {
|
||||
observableMember,
|
||||
isEncrypted,
|
||||
powerLevelsObservable,
|
||||
mediaRepository: this._room.mediaRepository,
|
||||
session: this._session
|
||||
};
|
||||
}
|
||||
|
||||
_setupNavigation() {
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
# "Room" view models
|
||||
|
||||
InviteViewModel and RoomViewModel are interchangebly used as "room view model":
|
||||
- SessionViewModel.roomViewModel can be an instance of either
|
||||
- RoomGridViewModel.roomViewModelAt(i) can return an instance of either
|
||||
InviteViewModel, RoomViewModel and RoomBeingCreatedViewModel are interchangebly used as "room view model":
|
||||
- SessionViewModel.roomViewModel can be an instance of any
|
||||
- RoomGridViewModel.roomViewModelAt(i) can return an instance of any
|
||||
|
||||
This is because they are accessed by the same url and need to transition into each other, in these two locations. Having two methods, especially in RoomGridViewModel would have been more cumbersome, even though this is not in line with how different view models are exposed in SessionViewModel.
|
||||
|
||||
They share an `id` and `kind` property, the latter can be used to differentiate them from the view, and a `focus` method.
|
||||
Once we convert this folder to typescript, we should use this interface for all the view models:
|
||||
```ts
|
||||
interface IGridItemViewModel {
|
||||
id: string;
|
||||
kind: string;
|
||||
focus();
|
||||
}
|
||||
```
|
||||
|
|
75
src/domain/session/room/RoomBeingCreatedViewModel.js
Normal file
75
src/domain/session/room/RoomBeingCreatedViewModel.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
Copyright 2020, 2021 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 {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js";
|
||||
import {ViewModel} from "../../ViewModel.js";
|
||||
|
||||
export class RoomBeingCreatedViewModel extends ViewModel {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
const {roomBeingCreated, mediaRepository} = options;
|
||||
this._roomBeingCreated = roomBeingCreated;
|
||||
this._mediaRepository = mediaRepository;
|
||||
this._onRoomChange = this._onRoomChange.bind(this);
|
||||
this._closeUrl = this.urlCreator.urlUntilSegment("session");
|
||||
this._roomBeingCreated.on("change", this._onRoomChange);
|
||||
}
|
||||
|
||||
get kind() { return "roomBeingCreated"; }
|
||||
get closeUrl() { return this._closeUrl; }
|
||||
get name() { return this._roomBeingCreated.name; }
|
||||
get id() { return this._roomBeingCreated.id; }
|
||||
get isEncrypted() { return this._roomBeingCreated.isEncrypted; }
|
||||
get error() {
|
||||
const {error} = this._roomBeingCreated;
|
||||
if (error) {
|
||||
if (error.name === "ConnectionError") {
|
||||
return this.i18n`You seem to be offline`;
|
||||
} else {
|
||||
return error.message;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
get avatarLetter() { return avatarInitials(this.name); }
|
||||
get avatarColorNumber() { return getIdentifierColorNumber(this._roomBeingCreated.avatarColorId); }
|
||||
get avatarTitle() { return this.name; }
|
||||
|
||||
avatarUrl(size) {
|
||||
// allow blob url which doesn't need mxc => http resolution
|
||||
return this._roomBeingCreated.avatarBlobUrl ??
|
||||
getAvatarHttpUrl(this._roomBeingCreated.avatarUrl, size, this.platform, this._mediaRepository);
|
||||
}
|
||||
|
||||
focus() {}
|
||||
|
||||
_onRoomChange() {
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this._roomBeingCreated.cancel();
|
||||
// navigate away from the room
|
||||
this.navigation.applyPath(this.navigation.path.until("session"));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
super.dispose();
|
||||
this._roomBeingCreated.off("change", this._onRoomChange);
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import {ComposerViewModel} from "./ComposerViewModel.js"
|
|||
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js";
|
||||
import {tilesCreator} from "./timeline/tilesCreator.js";
|
||||
import {ViewModel} from "../../ViewModel.js";
|
||||
import {imageToInfo} from "../common.js";
|
||||
|
||||
export class RoomViewModel extends ViewModel {
|
||||
constructor(options) {
|
||||
|
@ -97,9 +98,8 @@ export class RoomViewModel extends ViewModel {
|
|||
// room doesn't tell us yet which fields changed,
|
||||
// so emit all fields originating from summary
|
||||
_onRoomChange() {
|
||||
if (this._room.isArchived) {
|
||||
// propagate the update to the child view models so it's bindings can update based on room changes
|
||||
this._composerVM.emitChange();
|
||||
}
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
|
@ -274,7 +274,9 @@ export class RoomViewModel extends ViewModel {
|
|||
let image = await this.platform.loadImage(file.blob);
|
||||
const limit = await this.platform.settingsStorage.getInt("sentImageSizeLimit");
|
||||
if (limit && image.maxDimension > limit) {
|
||||
image = await image.scale(limit);
|
||||
const scaledImage = await image.scale(limit);
|
||||
image.dispose();
|
||||
image = scaledImage;
|
||||
}
|
||||
const content = {
|
||||
body: file.name,
|
||||
|
@ -320,15 +322,6 @@ export class RoomViewModel extends ViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
function imageToInfo(image) {
|
||||
return {
|
||||
w: image.width,
|
||||
h: image.height,
|
||||
mimetype: image.blob.mimeType,
|
||||
size: image.blob.size
|
||||
};
|
||||
}
|
||||
|
||||
function videoToInfo(video) {
|
||||
const info = imageToInfo(video);
|
||||
info.duration = video.duration;
|
||||
|
|
|
@ -108,13 +108,14 @@ export class LogItem implements ILogItem {
|
|||
return item;
|
||||
}
|
||||
|
||||
set(key: string | object, value?: unknown): void {
|
||||
set(key: string | object, value?: unknown): ILogItem {
|
||||
if(typeof key === "object") {
|
||||
const values = key;
|
||||
Object.assign(this._values, values);
|
||||
} else {
|
||||
this._values[key] = value;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
serialize(filter: LogFilter, parentStartTime: number | undefined, forced: boolean): ISerializedItem | undefined {
|
||||
|
|
|
@ -67,7 +67,7 @@ export class NullLogItem implements ILogItem {
|
|||
log(): ILogItem {
|
||||
return this;
|
||||
}
|
||||
set(): void {}
|
||||
set(): ILogItem { return this; }
|
||||
|
||||
runDetached(_: LabelOrValues, callback: LogCallback<unknown>): ILogItem {
|
||||
new Promise(r => r(callback(this))).then(noop, noop);
|
||||
|
|
|
@ -43,7 +43,7 @@ export interface ILogItem {
|
|||
readonly values: LogItemValues;
|
||||
wrap<T>(labelOrValues: LabelOrValues, callback: LogCallback<T>, logLevel?: LogLevel, filterCreator?: FilterCreator): T;
|
||||
log(labelOrValues: LabelOrValues, logLevel?: LogLevel): ILogItem;
|
||||
set(key: string | object, value: unknown): void;
|
||||
set(key: string | object, value: unknown): ILogItem;
|
||||
runDetached(labelOrValues: LabelOrValues, callback: LogCallback<unknown>, logLevel?: LogLevel, filterCreator?: FilterCreator): ILogItem;
|
||||
wrapDetached(labelOrValues: LabelOrValues, callback: LogCallback<unknown>, logLevel?: LogLevel, filterCreator?: FilterCreator): void;
|
||||
refDetached(logItem: ILogItem, logLevel?: LogLevel): void;
|
||||
|
|
|
@ -17,7 +17,8 @@ limitations under the License.
|
|||
|
||||
import {Room} from "./room/Room.js";
|
||||
import {ArchivedRoom} from "./room/ArchivedRoom.js";
|
||||
import {RoomStatus} from "./room/RoomStatus.js";
|
||||
import {RoomStatus} from "./room/common";
|
||||
import {RoomBeingCreated} from "./room/RoomBeingCreated";
|
||||
import {Invite} from "./room/Invite.js";
|
||||
import {Pusher} from "./push/Pusher";
|
||||
import { ObservableMap } from "../observable/index.js";
|
||||
|
@ -63,6 +64,14 @@ export class Session {
|
|||
this._activeArchivedRooms = new Map();
|
||||
this._invites = new ObservableMap();
|
||||
this._inviteUpdateCallback = (invite, params) => this._invites.update(invite.id, params);
|
||||
this._roomsBeingCreatedUpdateCallback = (rbc, params) => {
|
||||
if (rbc.isCancelled) {
|
||||
this._roomsBeingCreated.remove(rbc.id);
|
||||
} else {
|
||||
this._roomsBeingCreated.update(rbc.id, params)
|
||||
}
|
||||
};
|
||||
this._roomsBeingCreated = new ObservableMap();
|
||||
this._user = new User(sessionInfo.userId);
|
||||
this._deviceMessageHandler = new DeviceMessageHandler({storage});
|
||||
this._olm = olm;
|
||||
|
@ -421,7 +430,7 @@ export class Session {
|
|||
// load rooms
|
||||
const rooms = await txn.roomSummary.getAll();
|
||||
const roomLoadPromise = Promise.all(rooms.map(async summary => {
|
||||
const room = this.createRoom(summary.roomId, pendingEventsByRoomId.get(summary.roomId));
|
||||
const room = this.createJoinedRoom(summary.roomId, pendingEventsByRoomId.get(summary.roomId));
|
||||
await log.wrap("room", log => room.load(summary, txn, log));
|
||||
this._rooms.add(room.id, room);
|
||||
}));
|
||||
|
@ -529,8 +538,21 @@ export class Session {
|
|||
return this._rooms;
|
||||
}
|
||||
|
||||
findDirectMessageForUserId(userId) {
|
||||
for (const [,room] of this._rooms) {
|
||||
if (room.isDirectMessageForUserId(userId)) {
|
||||
return room;
|
||||
}
|
||||
}
|
||||
for (const [,invite] of this._invites) {
|
||||
if (invite.isDirectMessageForUserId(userId)) {
|
||||
return invite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
createRoom(roomId, pendingEvents) {
|
||||
createJoinedRoom(roomId, pendingEvents) {
|
||||
return new Room({
|
||||
roomId,
|
||||
getSyncToken: this._getSyncToken,
|
||||
|
@ -580,6 +602,36 @@ export class Session {
|
|||
});
|
||||
}
|
||||
|
||||
get roomsBeingCreated() {
|
||||
return this._roomsBeingCreated;
|
||||
}
|
||||
|
||||
createRoom(options) {
|
||||
let roomBeingCreated;
|
||||
this._platform.logger.runDetached("create room", async log => {
|
||||
const id = `local-${Math.floor(this._platform.random() * Number.MAX_SAFE_INTEGER)}`;
|
||||
roomBeingCreated = new RoomBeingCreated(
|
||||
id, options, this._roomsBeingCreatedUpdateCallback,
|
||||
this._mediaRepository, this._platform, log);
|
||||
this._roomsBeingCreated.set(id, 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));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
// we should now know the roomId, check if the room was synced before we received
|
||||
// the room id. Replace the room being created with the synced room.
|
||||
if (roomBeingCreated.roomId) {
|
||||
if (this.rooms.get(roomBeingCreated.roomId)) {
|
||||
this._tryReplaceRoomBeingCreated(roomBeingCreated.roomId, log);
|
||||
}
|
||||
await roomBeingCreated.adjustDirectMessageMapIfNeeded(this._user, this._storage, this._hsApi, log);
|
||||
}
|
||||
});
|
||||
return roomBeingCreated;
|
||||
}
|
||||
|
||||
async obtainSyncLock(syncResponse) {
|
||||
const toDeviceEvents = syncResponse.to_device?.events;
|
||||
if (Array.isArray(toDeviceEvents) && toDeviceEvents.length) {
|
||||
|
@ -662,11 +714,29 @@ export class Session {
|
|||
}
|
||||
}
|
||||
|
||||
applyRoomCollectionChangesAfterSync(inviteStates, roomStates, archivedRoomStates) {
|
||||
_tryReplaceRoomBeingCreated(roomId, log) {
|
||||
for (const [,roomBeingCreated] of this._roomsBeingCreated) {
|
||||
if (roomBeingCreated.roomId === roomId) {
|
||||
const observableStatus = this._observedRoomStatus.get(roomBeingCreated.id);
|
||||
if (observableStatus) {
|
||||
log.log(`replacing room being created`)
|
||||
.set("localId", roomBeingCreated.id)
|
||||
.set("roomId", roomBeingCreated.roomId);
|
||||
observableStatus.set(observableStatus.get() | RoomStatus.Replaced);
|
||||
}
|
||||
roomBeingCreated.dispose();
|
||||
this._roomsBeingCreated.remove(roomBeingCreated.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applyRoomCollectionChangesAfterSync(inviteStates, roomStates, archivedRoomStates, log) {
|
||||
// update the collections after sync
|
||||
for (const rs of roomStates) {
|
||||
if (rs.shouldAdd) {
|
||||
this._rooms.add(rs.id, rs.room);
|
||||
this._tryReplaceRoomBeingCreated(rs.id, log);
|
||||
} else if (rs.shouldRemove) {
|
||||
this._rooms.remove(rs.id);
|
||||
}
|
||||
|
@ -684,21 +754,23 @@ export class Session {
|
|||
if (this._observedRoomStatus.size !== 0) {
|
||||
for (const ars of archivedRoomStates) {
|
||||
if (ars.shouldAdd) {
|
||||
this._observedRoomStatus.get(ars.id)?.set(RoomStatus.archived);
|
||||
this._observedRoomStatus.get(ars.id)?.set(RoomStatus.Archived);
|
||||
}
|
||||
}
|
||||
for (const rs of roomStates) {
|
||||
if (rs.shouldAdd) {
|
||||
this._observedRoomStatus.get(rs.id)?.set(RoomStatus.joined);
|
||||
this._observedRoomStatus.get(rs.id)?.set(RoomStatus.Joined);
|
||||
}
|
||||
}
|
||||
for (const is of inviteStates) {
|
||||
const statusObservable = this._observedRoomStatus.get(is.id);
|
||||
if (statusObservable) {
|
||||
const withInvited = statusObservable.get() | RoomStatus.Invited;
|
||||
if (is.shouldAdd) {
|
||||
statusObservable.set(statusObservable.get().withInvited());
|
||||
statusObservable.set(withInvited);
|
||||
} else if (is.shouldRemove) {
|
||||
statusObservable.set(statusObservable.get().withoutInvited());
|
||||
const withoutInvited = withInvited ^ RoomStatus.Invited;
|
||||
statusObservable.set(withoutInvited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -708,7 +780,7 @@ export class Session {
|
|||
_forgetArchivedRoom(roomId) {
|
||||
const statusObservable = this._observedRoomStatus.get(roomId);
|
||||
if (statusObservable) {
|
||||
statusObservable.set(statusObservable.get().withoutArchived());
|
||||
statusObservable.set((statusObservable.get() | RoomStatus.Archived) ^ RoomStatus.Archived);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -797,21 +869,25 @@ export class Session {
|
|||
}
|
||||
|
||||
async getRoomStatus(roomId) {
|
||||
const isBeingCreated = !!this._roomsBeingCreated.get(roomId);
|
||||
if (isBeingCreated) {
|
||||
return RoomStatus.BeingCreated;
|
||||
}
|
||||
const isJoined = !!this._rooms.get(roomId);
|
||||
if (isJoined) {
|
||||
return RoomStatus.joined;
|
||||
return RoomStatus.Joined;
|
||||
} else {
|
||||
const isInvited = !!this._invites.get(roomId);
|
||||
const txn = await this._storage.readTxn([this._storage.storeNames.archivedRoomSummary]);
|
||||
const isArchived = await txn.archivedRoomSummary.has(roomId);
|
||||
if (isInvited && isArchived) {
|
||||
return RoomStatus.invitedAndArchived;
|
||||
return RoomStatus.Invited | RoomStatus.Archived;
|
||||
} else if (isInvited) {
|
||||
return RoomStatus.invited;
|
||||
return RoomStatus.Invited;
|
||||
} else if (isArchived) {
|
||||
return RoomStatus.archived;
|
||||
return RoomStatus.Archived;
|
||||
} else {
|
||||
return RoomStatus.none;
|
||||
return RoomStatus.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -823,6 +899,7 @@ export class Session {
|
|||
observable = new RetainedObservableValue(status, () => {
|
||||
this._observedRoomStatus.delete(roomId);
|
||||
});
|
||||
|
||||
this._observedRoomStatus.set(roomId, observable);
|
||||
}
|
||||
return observable;
|
||||
|
|
|
@ -249,7 +249,7 @@ export class Sync {
|
|||
if (!isRoomInResponse) {
|
||||
let room = this._session.rooms.get(roomId);
|
||||
if (room) {
|
||||
roomStates.push(new RoomSyncProcessState(room, false, null, {}, room.membership));
|
||||
roomStates.push(new RoomSyncProcessState(room, false, {}, room.membership));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -264,7 +264,7 @@ export class Sync {
|
|||
await rs.room.load(null, prepareTxn, log);
|
||||
}
|
||||
return rs.room.prepareSync(
|
||||
rs.roomResponse, rs.membership, rs.invite, newKeys, prepareTxn, log)
|
||||
rs.roomResponse, rs.membership, newKeys, prepareTxn, log)
|
||||
}, log.level.Detail);
|
||||
}));
|
||||
|
||||
|
@ -316,7 +316,7 @@ export class Sync {
|
|||
for(let is of inviteStates) {
|
||||
log.wrap("invite", log => is.invite.afterSync(is.changes, log), log.level.Detail);
|
||||
}
|
||||
this._session.applyRoomCollectionChangesAfterSync(inviteStates, roomStates, archivedRoomStates);
|
||||
this._session.applyRoomCollectionChangesAfterSync(inviteStates, roomStates, archivedRoomStates, log);
|
||||
}
|
||||
|
||||
_openSyncTxn() {
|
||||
|
@ -366,7 +366,7 @@ export class Sync {
|
|||
if (invite) {
|
||||
inviteStates.push(new InviteSyncProcessState(invite, false, null, membership));
|
||||
}
|
||||
const roomState = this._createRoomSyncState(roomId, invite, roomResponse, membership, isInitialSync);
|
||||
const roomState = this._createRoomSyncState(roomId, roomResponse, membership, isInitialSync);
|
||||
if (roomState) {
|
||||
roomStates.push(roomState);
|
||||
}
|
||||
|
@ -381,7 +381,7 @@ export class Sync {
|
|||
return {roomStates, archivedRoomStates};
|
||||
}
|
||||
|
||||
_createRoomSyncState(roomId, invite, roomResponse, membership, isInitialSync) {
|
||||
_createRoomSyncState(roomId, roomResponse, membership, isInitialSync) {
|
||||
let isNewRoom = false;
|
||||
let room = this._session.rooms.get(roomId);
|
||||
// create room only either on new join,
|
||||
|
@ -392,12 +392,12 @@ export class Sync {
|
|||
// we receive also gets written.
|
||||
// In any case, don't create a room for a rejected invite
|
||||
if (!room && (membership === "join" || (isInitialSync && membership === "leave"))) {
|
||||
room = this._session.createRoom(roomId);
|
||||
room = this._session.createJoinedRoom(roomId);
|
||||
isNewRoom = true;
|
||||
}
|
||||
if (room) {
|
||||
return new RoomSyncProcessState(
|
||||
room, isNewRoom, invite, roomResponse, membership);
|
||||
room, isNewRoom, roomResponse, membership);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -468,10 +468,9 @@ class SessionSyncProcessState {
|
|||
}
|
||||
|
||||
class RoomSyncProcessState {
|
||||
constructor(room, isNewRoom, invite, roomResponse, membership) {
|
||||
constructor(room, isNewRoom, roomResponse, membership) {
|
||||
this.room = room;
|
||||
this.isNewRoom = isNewRoom;
|
||||
this.invite = invite;
|
||||
this.roomResponse = roomResponse;
|
||||
this.membership = membership;
|
||||
this.preparation = null;
|
||||
|
|
|
@ -57,3 +57,15 @@ export function verifyEd25519Signature(olmUtil, userId, deviceOrKeyId, ed25519Ke
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function createRoomEncryptionEvent() {
|
||||
return {
|
||||
"type": "m.room.encryption",
|
||||
"state_key": "",
|
||||
"content": {
|
||||
"algorithm": MEGOLM_ALGORITHM,
|
||||
"rotation_period_ms": 604800000,
|
||||
"rotation_period_msgs": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -279,20 +279,32 @@ export class HomeServerApi {
|
|||
return this._post(`/logout`, {}, {}, options);
|
||||
}
|
||||
|
||||
getDehydratedDevice(options: BaseRequestOptions): IHomeServerRequest {
|
||||
getDehydratedDevice(options: BaseRequestOptions = {}): IHomeServerRequest {
|
||||
options.prefix = DEHYDRATION_PREFIX;
|
||||
return this._get(`/dehydrated_device`, undefined, undefined, options);
|
||||
}
|
||||
|
||||
createDehydratedDevice(payload: Record<string, any>, options: BaseRequestOptions): IHomeServerRequest {
|
||||
createDehydratedDevice(payload: Record<string, any>, options: BaseRequestOptions = {}): IHomeServerRequest {
|
||||
options.prefix = DEHYDRATION_PREFIX;
|
||||
return this._put(`/dehydrated_device`, {}, payload, options);
|
||||
}
|
||||
|
||||
claimDehydratedDevice(deviceId: string, options: BaseRequestOptions): IHomeServerRequest {
|
||||
claimDehydratedDevice(deviceId: string, options: BaseRequestOptions = {}): IHomeServerRequest {
|
||||
options.prefix = DEHYDRATION_PREFIX;
|
||||
return this._post(`/dehydrated_device/claim`, {}, {device_id: deviceId}, options);
|
||||
}
|
||||
|
||||
profile(userId: string, options?: BaseRequestOptions): IHomeServerRequest {
|
||||
return this._get(`/profile/${encodeURIComponent(userId)}`);
|
||||
}
|
||||
|
||||
createRoom(payload: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
|
||||
return this._post(`/createRoom`, {}, payload, options);
|
||||
}
|
||||
|
||||
setAccountData(ownUserId: string, type: string, content: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
|
||||
return this._put(`/user/${encodeURIComponent(ownUserId)}/account_data/${encodeURIComponent(type)}`, {}, content, options);
|
||||
}
|
||||
}
|
||||
|
||||
import {Request as MockRequest} from "../../mocks/Request.js";
|
||||
|
|
51
src/matrix/profile.ts
Normal file
51
src/matrix/profile.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import type {HomeServerApi} from "./net/HomeServerApi";
|
||||
import type {ILogItem} from "../logging/types";
|
||||
|
||||
export async function loadProfiles(userIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise<Profile[]> {
|
||||
const profiles = await Promise.all(userIds.map(async userId => {
|
||||
const response = await hsApi.profile(userId, {log}).response();
|
||||
return new Profile(userId, response.displayname as string, response.avatar_url as string);
|
||||
}));
|
||||
profiles.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return profiles;
|
||||
}
|
||||
|
||||
export interface IProfile {
|
||||
get userId(): string;
|
||||
get displayName(): string | undefined;
|
||||
get avatarUrl(): string | undefined;
|
||||
get name(): string;
|
||||
}
|
||||
|
||||
export class Profile implements IProfile {
|
||||
constructor(
|
||||
public readonly userId: string,
|
||||
public readonly displayName: string,
|
||||
public readonly avatarUrl: string | undefined
|
||||
) {}
|
||||
|
||||
get name() { return this.displayName || this.userId; }
|
||||
}
|
||||
|
||||
export class UserIdProfile implements IProfile {
|
||||
constructor(public readonly userId: string) {}
|
||||
get displayName() { return undefined; }
|
||||
get name() { return this.userId; }
|
||||
get avatarUrl() { return undefined; }
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -420,6 +420,21 @@ export class BaseRoom extends EventEmitter {
|
|||
return this._summary.data.membership;
|
||||
}
|
||||
|
||||
isDirectMessageForUserId(userId) {
|
||||
if (this._summary.data.dmUserId === userId) {
|
||||
return true;
|
||||
} else {
|
||||
// fall back to considering any room a DM containing heroes (e.g. no name) and 2 members,
|
||||
// on of which the userId we're looking for.
|
||||
// We need this because we're not yet processing m.direct account data correctly.
|
||||
const {heroes, joinCount, inviteCount} = this._summary.data;
|
||||
if (heroes && heroes.includes(userId) && (joinCount + inviteCount) === 2) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async _loadPowerLevels() {
|
||||
const txn = await this._storage.readTxn([this._storage.storeNames.roomState]);
|
||||
const powerLevelsState = await txn.roomState.get(this._roomId, "m.room.power_levels", "");
|
||||
|
|
|
@ -73,6 +73,10 @@ export class Invite extends EventEmitter {
|
|||
return this._inviter;
|
||||
}
|
||||
|
||||
isDirectMessageForUserId(userId) {
|
||||
return this.isDirectMessage && this._inviter.userId === userId;
|
||||
}
|
||||
|
||||
get isPublic() {
|
||||
return this._inviteData.joinRule === "public";
|
||||
}
|
||||
|
@ -184,7 +188,7 @@ export class Invite extends EventEmitter {
|
|||
return {
|
||||
roomId: this.id,
|
||||
isEncrypted: !!summaryData.encryption,
|
||||
isDirectMessage: this._isDirectMessage(myInvite),
|
||||
isDirectMessage: summaryData.isDirectMessage,
|
||||
// type:
|
||||
name,
|
||||
avatarUrl,
|
||||
|
@ -196,12 +200,8 @@ export class Invite extends EventEmitter {
|
|||
};
|
||||
}
|
||||
|
||||
_isDirectMessage(myInvite) {
|
||||
return !!(myInvite?.content?.is_direct);
|
||||
}
|
||||
|
||||
_createSummaryData(inviteState) {
|
||||
return inviteState.reduce(processStateEvent, new SummaryData(null, this.id));
|
||||
return inviteState.reduce((data, event) => processStateEvent(data, event, this._user.id), new SummaryData(null, this.id));
|
||||
}
|
||||
|
||||
async _createHeroes(inviteState, log) {
|
||||
|
|
|
@ -54,15 +54,12 @@ export class Room extends BaseRoom {
|
|||
return false;
|
||||
}
|
||||
|
||||
async prepareSync(roomResponse, membership, invite, newKeys, txn, log) {
|
||||
async prepareSync(roomResponse, membership, newKeys, txn, log) {
|
||||
log.set("id", this.id);
|
||||
if (newKeys) {
|
||||
log.set("newKeys", newKeys.length);
|
||||
}
|
||||
let summaryChanges = this._summary.data.applySyncResponse(roomResponse, membership);
|
||||
if (membership === "join" && invite) {
|
||||
summaryChanges = summaryChanges.applyInvite(invite);
|
||||
}
|
||||
let summaryChanges = this._summary.data.applySyncResponse(roomResponse, membership, this._user.id);
|
||||
let roomEncryption = this._roomEncryption;
|
||||
// encryption is enabled in this sync
|
||||
if (!roomEncryption && summaryChanges.encryption) {
|
||||
|
|
258
src/matrix/room/RoomBeingCreated.ts
Normal file
258
src/matrix/room/RoomBeingCreated.ts
Normal file
|
@ -0,0 +1,258 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
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 {loadProfiles, Profile, UserIdProfile} from "../profile";
|
||||
import {RoomType} from "./common";
|
||||
|
||||
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";
|
||||
import type {User} from "../User";
|
||||
import type {Storage} from "../storage/idb/Storage";
|
||||
|
||||
type CreateRoomPayload = {
|
||||
is_direct?: boolean;
|
||||
preset?: string;
|
||||
name?: string;
|
||||
topic?: string;
|
||||
invite?: string[];
|
||||
room_alias_name?: string;
|
||||
creation_content?: {"m.federate": boolean};
|
||||
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;
|
||||
isFederationDisabled?: boolean;
|
||||
name?: string;
|
||||
topic?: string;
|
||||
invites?: string[];
|
||||
avatar?: Avatar;
|
||||
alias?: string;
|
||||
}
|
||||
|
||||
function defaultE2EEStatusForType(type: RoomType): boolean {
|
||||
switch (type) {
|
||||
case RoomType.DirectMessage:
|
||||
case RoomType.Private:
|
||||
return true;
|
||||
case RoomType.Public:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function presetForType(type: RoomType): string {
|
||||
switch (type) {
|
||||
case RoomType.DirectMessage:
|
||||
return "trusted_private_chat";
|
||||
case RoomType.Private:
|
||||
return "private_chat";
|
||||
case RoomType.Public:
|
||||
return "public_chat";
|
||||
}
|
||||
}
|
||||
|
||||
export class RoomBeingCreated extends EventEmitter<{change: never}> {
|
||||
private _roomId?: string;
|
||||
private profiles: Profile[] = [];
|
||||
|
||||
public readonly isEncrypted: boolean;
|
||||
private _calculatedName: string;
|
||||
private _error?: Error;
|
||||
private _isCancelled = false;
|
||||
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
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 = options.isEncrypted === undefined ? defaultE2EEStatusForType(options.type) : options.isEncrypted;
|
||||
if (options.name) {
|
||||
this._calculatedName = options.name;
|
||||
} else {
|
||||
const summaryData = {
|
||||
joinCount: 1, // ourselves
|
||||
inviteCount: (options.invites?.length || 0)
|
||||
};
|
||||
const userIdProfiles = (options.invites || []).map(userId => new UserIdProfile(userId));
|
||||
this._calculatedName = calculateRoomName(userIdProfiles, summaryData, log);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async create(hsApi: HomeServerApi, log: ILogItem): Promise<void> {
|
||||
try {
|
||||
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.options.name) {
|
||||
createOptions.name = this.options.name;
|
||||
}
|
||||
if (this.options.topic) {
|
||||
createOptions.topic = this.options.topic;
|
||||
}
|
||||
if (this.options.invites) {
|
||||
createOptions.invite = this.options.invites;
|
||||
}
|
||||
if (this.options.alias) {
|
||||
createOptions.room_alias_name = this.options.alias;
|
||||
}
|
||||
if (this.options.isFederationDisabled === true) {
|
||||
createOptions.creation_content = {
|
||||
"m.federate": false
|
||||
};
|
||||
}
|
||||
if (this.isEncrypted) {
|
||||
createOptions.initial_state.push(createRoomEncryptionEvent());
|
||||
}
|
||||
if (avatarEventContent) {
|
||||
createOptions.initial_state.push({
|
||||
type: "m.room.avatar",
|
||||
state_key: "",
|
||||
content: avatarEventContent
|
||||
});
|
||||
}
|
||||
const response = await hsApi.createRoom(createOptions, {log}).response();
|
||||
this._roomId = response["room_id"];
|
||||
} catch (err) {
|
||||
this._error = err;
|
||||
}
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
/** requests the profiles of the invitees if needed to give an accurate
|
||||
* estimated room name in case an explicit room name is not set.
|
||||
* The room is being created in the background whether this is called
|
||||
* or not, and this just gives a more accurate name while that request
|
||||
* is running. */
|
||||
/** @internal */
|
||||
async loadProfiles(hsApi: HomeServerApi, log: ILogItem): Promise<void> {
|
||||
try {
|
||||
// only load profiles if we need it for the room name and avatar
|
||||
if (!this.options.name && this.options.invites) {
|
||||
this.profiles = await loadProfiles(this.options.invites, hsApi, log);
|
||||
const summaryData = {
|
||||
joinCount: 1, // ourselves
|
||||
inviteCount: this.options.invites.length
|
||||
};
|
||||
this._calculatedName = calculateRoomName(this.profiles, summaryData, log);
|
||||
this.emitChange();
|
||||
}
|
||||
} catch (err) {} // swallow error, loading profiles is not essential
|
||||
}
|
||||
|
||||
private emitChange(params?: string) {
|
||||
this.updateCallback(this, params);
|
||||
this.emit("change");
|
||||
}
|
||||
|
||||
get avatarColorId(): string { return this.options.invites?.[0] ?? this._roomId ?? this.id; }
|
||||
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._calculatedName; }
|
||||
get isBeingCreated(): boolean { return true; }
|
||||
get error(): Error | undefined { return this._error; }
|
||||
|
||||
cancel() {
|
||||
if (!this._isCancelled) {
|
||||
this.dispose();
|
||||
this._isCancelled = true;
|
||||
this.emitChange("isCancelled");
|
||||
}
|
||||
}
|
||||
// called from Session when updateCallback is invoked to remove it from the collection
|
||||
get isCancelled() { return this._isCancelled; }
|
||||
|
||||
/** @internal */
|
||||
dispose() {
|
||||
if (this.options.avatar) {
|
||||
this.options.avatar.blob.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
async adjustDirectMessageMapIfNeeded(user: User, storage: Storage, hsApi: HomeServerApi, log: ILogItem): Promise<void> {
|
||||
if (!this.options.invites || this.options.type !== RoomType.DirectMessage) {
|
||||
return;
|
||||
}
|
||||
const userId = this.options.invites[0];
|
||||
const DM_MAP_TYPE = "m.direct";
|
||||
await log.wrap("set " + DM_MAP_TYPE, async log => {
|
||||
try {
|
||||
const txn = await storage.readWriteTxn([storage.storeNames.accountData]);
|
||||
let mapEntry;
|
||||
try {
|
||||
mapEntry = await txn.accountData.get(DM_MAP_TYPE);
|
||||
if (!mapEntry) {
|
||||
mapEntry = {type: DM_MAP_TYPE, content: {}};
|
||||
}
|
||||
const map = mapEntry.content;
|
||||
let userRooms = map[userId];
|
||||
if (!userRooms) {
|
||||
map[userId] = userRooms = [];
|
||||
}
|
||||
// this is a new room id so no need to check if it's already there
|
||||
userRooms.push(this._roomId);
|
||||
txn.accountData.set(mapEntry);
|
||||
await txn.complete();
|
||||
} catch (err) {
|
||||
txn.abort();
|
||||
throw err;
|
||||
}
|
||||
await hsApi.setAccountData(user.id, DM_MAP_TYPE, mapEntry.content, {log}).response();
|
||||
} catch (err) {
|
||||
// we can't really do anything else but logging here
|
||||
log.catch(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
Copyright 2021 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 class RoomStatus {
|
||||
constructor(joined, invited, archived) {
|
||||
this.joined = joined;
|
||||
this.invited = invited;
|
||||
this.archived = archived;
|
||||
}
|
||||
|
||||
withInvited() {
|
||||
if (this.invited) {
|
||||
return this;
|
||||
} else if (this.archived) {
|
||||
return RoomStatus.invitedAndArchived;
|
||||
} else {
|
||||
return RoomStatus.invited;
|
||||
}
|
||||
}
|
||||
|
||||
withoutInvited() {
|
||||
if (!this.invited) {
|
||||
return this;
|
||||
} else if (this.joined) {
|
||||
return RoomStatus.joined;
|
||||
} else if (this.archived) {
|
||||
return RoomStatus.archived;
|
||||
} else {
|
||||
return RoomStatus.none;
|
||||
}
|
||||
}
|
||||
|
||||
withoutArchived() {
|
||||
if (!this.archived) {
|
||||
return this;
|
||||
} else if (this.invited) {
|
||||
return RoomStatus.invited;
|
||||
} else {
|
||||
return RoomStatus.none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RoomStatus.joined = new RoomStatus(true, false, false);
|
||||
RoomStatus.archived = new RoomStatus(false, false, true);
|
||||
RoomStatus.invited = new RoomStatus(false, true, false);
|
||||
RoomStatus.invitedAndArchived = new RoomStatus(false, true, true);
|
||||
RoomStatus.none = new RoomStatus(false, false, false);
|
|
@ -46,7 +46,7 @@ export function reduceStateEvents(roomResponse, callback, value) {
|
|||
return value;
|
||||
}
|
||||
|
||||
function applySyncResponse(data, roomResponse, membership) {
|
||||
function applySyncResponse(data, roomResponse, membership, ownUserId) {
|
||||
if (roomResponse.summary) {
|
||||
data = updateSummary(data, roomResponse.summary);
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ function applySyncResponse(data, roomResponse, membership) {
|
|||
// process state events in state and in timeline.
|
||||
// non-state events are handled by applyTimelineEntries
|
||||
// so decryption is handled properly
|
||||
data = reduceStateEvents(roomResponse, processStateEvent, data);
|
||||
data = reduceStateEvents(roomResponse, (data, event) => processStateEvent(data, event, ownUserId), data);
|
||||
const unreadNotifications = roomResponse.unread_notifications;
|
||||
if (unreadNotifications) {
|
||||
data = processNotificationCounts(data, unreadNotifications);
|
||||
|
@ -95,8 +95,11 @@ function processRoomAccountData(data, event) {
|
|||
return data;
|
||||
}
|
||||
|
||||
export function processStateEvent(data, event) {
|
||||
if (event.type === "m.room.encryption") {
|
||||
export function processStateEvent(data, event, ownUserId) {
|
||||
if (event.type === "m.room.create") {
|
||||
data = data.cloneIfNeeded();
|
||||
data.lastMessageTimestamp = event.origin_server_ts;
|
||||
} else if (event.type === "m.room.encryption") {
|
||||
const algorithm = event.content?.algorithm;
|
||||
if (!data.encryption && algorithm === MEGOLM_ALGORITHM) {
|
||||
data = data.cloneIfNeeded();
|
||||
|
@ -118,6 +121,25 @@ export function processStateEvent(data, event) {
|
|||
const content = event.content;
|
||||
data = data.cloneIfNeeded();
|
||||
data.canonicalAlias = content.alias;
|
||||
} else if (event.type === "m.room.member") {
|
||||
const content = event.content;
|
||||
if (content.is_direct === true && content.membership === "invite" && !data.isDirectMessage) {
|
||||
let other;
|
||||
if (event.sender === ownUserId) {
|
||||
other = event.state_key;
|
||||
} else if (event.state_key === ownUserId) {
|
||||
other = event.sender;
|
||||
}
|
||||
if (other) {
|
||||
data = data.cloneIfNeeded();
|
||||
data.isDirectMessage = true;
|
||||
data.dmUserId = other;
|
||||
}
|
||||
} else if (content.membership === "leave" && data.isDirectMessage && data.dmUserId === event.state_key) {
|
||||
data = data.cloneIfNeeded();
|
||||
data.isDirectMessage = false;
|
||||
data.dmUserId = null;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
@ -158,19 +180,6 @@ function updateSummary(data, summary) {
|
|||
return data;
|
||||
}
|
||||
|
||||
function applyInvite(data, invite) {
|
||||
if (data.isDirectMessage !== invite.isDirectMessage) {
|
||||
data = data.cloneIfNeeded();
|
||||
data.isDirectMessage = invite.isDirectMessage;
|
||||
if (invite.isDirectMessage) {
|
||||
data.dmUserId = invite.inviter?.userId;
|
||||
} else {
|
||||
data.dmUserId = null;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export class SummaryData {
|
||||
constructor(copy, roomId) {
|
||||
this.roomId = copy ? copy.roomId : roomId;
|
||||
|
@ -227,12 +236,8 @@ export class SummaryData {
|
|||
return applyTimelineEntries(this, timelineEntries, isInitialSync, canMarkUnread, ownUserId);
|
||||
}
|
||||
|
||||
applySyncResponse(roomResponse, membership) {
|
||||
return applySyncResponse(this, roomResponse, membership);
|
||||
}
|
||||
|
||||
applyInvite(invite) {
|
||||
return applyInvite(this, invite);
|
||||
applySyncResponse(roomResponse, membership, ownUserId) {
|
||||
return applySyncResponse(this, roomResponse, membership, ownUserId);
|
||||
}
|
||||
|
||||
get needsHeroes() {
|
||||
|
|
|
@ -25,3 +25,18 @@ export const REDACTION_TYPE = "m.room.redaction";
|
|||
export function isRedacted(event) {
|
||||
return !!event?.unsigned?.redacted_because;
|
||||
}
|
||||
|
||||
export enum RoomStatus {
|
||||
None = 1 << 0,
|
||||
BeingCreated = 1 << 1,
|
||||
Invited = 1 << 2,
|
||||
Joined = 1 << 3,
|
||||
Replaced = 1 << 4,
|
||||
Archived = 1 << 5,
|
||||
}
|
||||
|
||||
export enum RoomType {
|
||||
DirectMessage,
|
||||
Private,
|
||||
Public
|
||||
}
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import {RoomMember} from "./RoomMember.js";
|
||||
|
||||
function calculateRoomName(sortedMembers, summaryData, log) {
|
||||
export function calculateRoomName(sortedMembers, summaryData, log) {
|
||||
const countWithoutMe = summaryData.joinCount + summaryData.inviteCount - 1;
|
||||
if (sortedMembers.length >= countWithoutMe) {
|
||||
if (sortedMembers.length > 1) {
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {getPrevContentFromStateEvent} from "../common.js";
|
||||
import {getPrevContentFromStateEvent} from "../common";
|
||||
|
||||
export const EVENT_TYPE = "m.room.member";
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
import {createEnum} from "../../../utils/enum";
|
||||
import {AbortError} from "../../../utils/error";
|
||||
import {REDACTION_TYPE} from "../common.js";
|
||||
import {REDACTION_TYPE} from "../common";
|
||||
import {getRelationFromContent, getRelationTarget, setRelationTarget} from "../timeline/relations.js";
|
||||
|
||||
export const SendStatus = createEnum(
|
||||
|
|
|
@ -18,7 +18,7 @@ import {SortedArray} from "../../../observable/list/SortedArray";
|
|||
import {ConnectionError} from "../../error.js";
|
||||
import {PendingEvent, SendStatus} from "./PendingEvent.js";
|
||||
import {makeTxnId, isTxnId} from "../../common.js";
|
||||
import {REDACTION_TYPE} from "../common.js";
|
||||
import {REDACTION_TYPE} from "../common";
|
||||
import {getRelationFromContent, getRelationTarget, setRelationTarget, REACTION_TYPE, ANNOTATION_RELATION_TYPE} from "../timeline/relations.js";
|
||||
|
||||
export class SendQueue {
|
||||
|
|
|
@ -22,7 +22,7 @@ import {TimelineReader} from "./persistence/TimelineReader.js";
|
|||
import {PendingEventEntry} from "./entries/PendingEventEntry.js";
|
||||
import {RoomMember} from "../members/RoomMember.js";
|
||||
import {getRelation, ANNOTATION_RELATION_TYPE} from "./relations.js";
|
||||
import {REDACTION_TYPE} from "../common.js";
|
||||
import {REDACTION_TYPE} from "../common";
|
||||
import {NonPersistedEventEntry} from "./entries/NonPersistedEventEntry.js";
|
||||
import {DecryptionSource} from "../../e2ee/common.js";
|
||||
import {EVENT_TYPE as MEMBER_EVENT_TYPE} from "../members/RoomMember.js";
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import {BaseEntry} from "./BaseEntry";
|
||||
import {REDACTION_TYPE} from "../../common.js";
|
||||
import {REDACTION_TYPE} from "../../common";
|
||||
import {createAnnotation, ANNOTATION_RELATION_TYPE, getRelationFromContent} from "../relations.js";
|
||||
import {PendingAnnotation} from "../PendingAnnotation.js";
|
||||
import {createReplyContent} from "./reply.js"
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import {BaseEventEntry} from "./BaseEventEntry.js";
|
||||
import {getPrevContentFromStateEvent, isRedacted} from "../../common.js";
|
||||
import {getPrevContentFromStateEvent, isRedacted} from "../../common";
|
||||
import {getRelationFromContent, getRelatedEventId} from "../relations.js";
|
||||
|
||||
export class EventEntry extends BaseEventEntry {
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import {EventEntry} from "../entries/EventEntry.js";
|
||||
import {REDACTION_TYPE, isRedacted} from "../../common.js";
|
||||
import {REDACTION_TYPE, isRedacted} from "../../common";
|
||||
import {ANNOTATION_RELATION_TYPE, getRelation} from "../relations.js";
|
||||
import {redactEvent} from "../common.js";
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {REDACTION_TYPE} from "../common.js";
|
||||
import {REDACTION_TYPE} from "../common";
|
||||
|
||||
export const REACTION_TYPE = "m.reaction";
|
||||
export const ANNOTATION_RELATION_TYPE = "m.annotation";
|
||||
|
|
70
src/observable/map/LogMap.js
Normal file
70
src/observable/map/LogMap.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import {BaseObservableMap} from "./BaseObservableMap.js";
|
||||
|
||||
export class LogMap extends BaseObservableMap {
|
||||
constructor(source, log) {
|
||||
super();
|
||||
this._source = source;
|
||||
this.log = log;
|
||||
this._subscription = null;
|
||||
}
|
||||
|
||||
onAdd(key, value) {
|
||||
this.log("add", key, value);
|
||||
this.emitAdd(key, value);
|
||||
}
|
||||
|
||||
onRemove(key, value) {
|
||||
this.log("remove", key, value);
|
||||
this.emitRemove(key, value);
|
||||
}
|
||||
|
||||
onUpdate(key, value, params) {
|
||||
this.log("update", key, value, params);
|
||||
this.emitUpdate(key, value, params);
|
||||
}
|
||||
|
||||
onSubscribeFirst() {
|
||||
this.log("subscribeFirst");
|
||||
this._subscription = this._source.subscribe(this);
|
||||
super.onSubscribeFirst();
|
||||
}
|
||||
|
||||
onUnsubscribeLast() {
|
||||
super.onUnsubscribeLast();
|
||||
this._subscription = this._subscription();
|
||||
this.log("unsubscribeLast");
|
||||
}
|
||||
|
||||
onReset() {
|
||||
this.log("reset");
|
||||
this.emitReset();
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this._source[Symbol.iterator]();
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this._source.size;
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this._source.get(key);
|
||||
}
|
||||
}
|
|
@ -29,3 +29,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;
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ limitations under the License.
|
|||
.hydrogen .avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* work around postcss-css-variables limitations and repeat variable usage */
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.form input {
|
||||
.form-row.text > input, .form-row.text > textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
|
|
|
@ -49,7 +49,7 @@ main {
|
|||
grid-template:
|
||||
"status status" auto
|
||||
"left middle" 1fr /
|
||||
300px 1fr;
|
||||
320px 1fr;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ main {
|
|||
grid-template:
|
||||
"status status status" auto
|
||||
"left middle right" 1fr /
|
||||
300px 1fr 300px;
|
||||
320px 1fr 300px;
|
||||
}
|
||||
|
||||
/* resize and reposition session view to account for mobile Safari which shifts
|
||||
|
@ -216,3 +216,14 @@ the layout viewport up without resizing it when the keyboard shows */
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vertical-layout {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.vertical-layout > .stretch {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
|
3
src/platform/web/ui/css/themes/element/icons/plus.svg
Normal file
3
src/platform/web/ui/css/themes/element/icons/plus.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.74986 3.55554C8.74986 3.14133 8.41408 2.80554 7.99986 2.80554C7.58565 2.80554 7.24986 3.14133 7.24986 3.55554V7.24999L3.55542 7.24999C3.14121 7.24999 2.80542 7.58577 2.80542 7.99999C2.80542 8.4142 3.14121 8.74999 3.55542 8.74999L7.24987 8.74999V12.4444C7.24987 12.8586 7.58565 13.1944 7.99987 13.1944C8.41408 13.1944 8.74987 12.8586 8.74987 12.4444V8.74999L12.4443 8.74999C12.8585 8.74999 13.1943 8.4142 13.1943 7.99999C13.1943 7.58577 12.8585 7.24999 12.4443 7.24999L8.74986 7.24999V3.55554Z" fill="#8E99A4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 670 B |
|
@ -80,22 +80,43 @@ limitations under the License.
|
|||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.form-row.text textarea {
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.form-row input {
|
||||
.form-row.text > input, .form-row.text > textarea {
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(141, 151, 165, 0.15);
|
||||
border-radius: 8px;
|
||||
margin-top: 5px;
|
||||
font-size: 1em;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-row label, .form-row input {
|
||||
.form-row.check {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.form-row.text > label, .form-row.text > input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-row .form-row-description {
|
||||
font-size: 1rem;
|
||||
color: #777;
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
|
||||
.button-action {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -157,6 +178,10 @@ a.button-action {
|
|||
background-image: url('icons/settings.svg');
|
||||
}
|
||||
|
||||
.button-utility.create {
|
||||
background-image: url('icons/plus.svg');
|
||||
}
|
||||
|
||||
.button-utility.grid.on {
|
||||
background-image: url('icons/disable-grid.svg');
|
||||
}
|
||||
|
@ -1049,10 +1074,14 @@ button.RoomDetailsView_row::after {
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.MemberDetailsView_options a{
|
||||
.MemberDetailsView_options a, .MemberDetailsView_options button {
|
||||
color: #0dbd8b;
|
||||
text-decoration: none;
|
||||
margin-bottom: 3px;
|
||||
margin: 0 0 3px 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.LazyListParent {
|
||||
|
@ -1075,3 +1104,35 @@ button.RoomDetailsView_row::after {
|
|||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.CreateRoomView, .RoomBeingCreated_error {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.RoomBeingCreated_error {
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
.centered-column {
|
||||
padding: 0 12px;
|
||||
align-self: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.CreateRoomView_selectAvatar {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.CreateRoomView_selectAvatarPlaceholder {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 100%;
|
||||
background-color: #e1e3e6;
|
||||
background-image: url('icons/plus.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 36px;
|
||||
}
|
||||
|
|
|
@ -14,11 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {TemplateView} from "./TemplateView";
|
||||
import {StaticView} from "./StaticView";
|
||||
import {spinner} from "../common.js";
|
||||
|
||||
export class LoadingView extends TemplateView {
|
||||
render(t) {
|
||||
return t.div({ className: "LoadingView" }, [spinner(t), "Loading"]);
|
||||
export class LoadingView extends StaticView {
|
||||
constructor(label = "Loading") {
|
||||
super(label, (t, label) => {
|
||||
return t.div({ className: "LoadingView" }, [spinner(t), label]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ export class LoginView extends TemplateView {
|
|||
t.div({className: "logo"}),
|
||||
t.h1([vm.i18n`Sign In`]),
|
||||
t.mapView(vm => vm.completeSSOLoginViewModel, vm => vm ? new CompleteSSOView(vm) : null),
|
||||
t.if(vm => vm.showHomeserver, (t, vm) => t.div({ className: "LoginView_sso form form-row" },
|
||||
t.if(vm => vm.showHomeserver, (t, vm) => t.div({ className: "LoginView_sso form-row text" },
|
||||
[
|
||||
t.label({for: "homeserver"}, vm.i18n`Homeserver`),
|
||||
t.input({
|
||||
|
|
|
@ -41,8 +41,8 @@ export class PasswordLoginView extends TemplateView {
|
|||
}
|
||||
}, [
|
||||
t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))),
|
||||
t.div({ className: "form-row" }, [t.label({ for: "username" }, vm.i18n`Username`), username]),
|
||||
t.div({ className: "form-row" }, [t.label({ for: "password" }, vm.i18n`Password`), password]),
|
||||
t.div({ className: "form-row text" }, [t.label({ for: "username" }, vm.i18n`Username`), username]),
|
||||
t.div({ className: "form-row text" }, [t.label({ for: "password" }, vm.i18n`Password`), password]),
|
||||
t.div({ className: "button-row" }, [
|
||||
t.button({
|
||||
className: "button-action primary",
|
||||
|
|
120
src/platform/web/ui/session/CreateRoomView.js
Normal file
120
src/platform/web/ui/session/CreateRoomView.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
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 {TemplateView} from "../general/TemplateView";
|
||||
import {AvatarView} from "../AvatarView";
|
||||
import {StaticView} from "../general/StaticView";
|
||||
|
||||
export class CreateRoomView extends TemplateView {
|
||||
render(t, vm) {
|
||||
return t.main({className: "middle"},
|
||||
t.div({className: "CreateRoomView centered-column"}, [
|
||||
t.h2("Create room"),
|
||||
//t.div({className: "RoomView_error"}, vm => vm.error),
|
||||
t.form({className: "CreateRoomView_detailsForm form", onChange: evt => this.onFormChange(evt), onSubmit: evt => this.onSubmit(evt)}, [
|
||||
t.div({className: "vertical-layout"}, [
|
||||
t.button({type: "button", className: "CreateRoomView_selectAvatar", onClick: () => vm.selectAvatar()},
|
||||
t.mapView(vm => vm.hasAvatar, hasAvatar => {
|
||||
if (hasAvatar) {
|
||||
return new AvatarView(vm, 64);
|
||||
} else {
|
||||
return new StaticView(undefined, t => {
|
||||
return t.div({className: "CreateRoomView_selectAvatarPlaceholder"})
|
||||
});
|
||||
}
|
||||
})
|
||||
),
|
||||
t.div({className: "stretch form-row text"}, [
|
||||
t.label({for: "name"}, vm.i18n`Room name`),
|
||||
t.input({
|
||||
onInput: evt => vm.setName(evt.target.value),
|
||||
type: "text", name: "name", id: "name",
|
||||
placeholder: vm.i18n`Enter a room name`
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
t.div({className: "form-row text"}, [
|
||||
t.label({for: "topic"}, vm.i18n`Topic (optional)`),
|
||||
t.textarea({
|
||||
onInput: evt => vm.setTopic(evt.target.value),
|
||||
name: "topic", id: "topic",
|
||||
placeholder: vm.i18n`Topic`
|
||||
}),
|
||||
]),
|
||||
t.div({className: "form-group"}, [
|
||||
t.div({className: "form-row check"}, [
|
||||
t.input({type: "radio", name: "isPublic", id: "isPrivate", value: "false", checked: !vm.isPublic}),
|
||||
t.label({for: "isPrivate"}, vm.i18n`Private room, only upon invitation.`)
|
||||
]),
|
||||
t.div({className: "form-row check"}, [
|
||||
t.input({type: "radio", name: "isPublic", id: "isPublic", value: "true", checked: vm.isPublic}),
|
||||
t.label({for: "isPublic"}, vm.i18n`Public room, anyone can join`)
|
||||
]),
|
||||
]),
|
||||
t.div({className: {"form-row check": true, hidden: vm => vm.isPublic}}, [
|
||||
t.input({type: "checkbox", name: "isEncrypted", id: "isEncrypted", checked: vm.isEncrypted}),
|
||||
t.label({for: "isEncrypted"}, vm.i18n`Enable end-to-end encryption`)
|
||||
]),
|
||||
t.div({className: {"form-row text": true, hidden: vm => !vm.isPublic}}, [
|
||||
t.label({for: "roomAlias"}, vm.i18n`Room alias`),
|
||||
t.input({
|
||||
onInput: evt => vm.setRoomAlias(evt.target.value),
|
||||
type: "text", name: "roomAlias", id: "roomAlias",
|
||||
placeholder: vm.i18n`Room alias (<alias>, or #<alias> or #<alias>:hs.tld`}),
|
||||
]),
|
||||
t.div({className: "form-group"}, [
|
||||
t.div(t.button({className: "link", type: "button", onClick: () => vm.toggleAdvancedShown()},
|
||||
vm => vm.isAdvancedShown ? vm.i18n`Hide advanced settings` : vm.i18n`Show advanced settings`)),
|
||||
t.div({className: {"form-row check": true, hidden: vm => !vm.isAdvancedShown}}, [
|
||||
t.input({type: "checkbox", name: "isFederationDisabled", id: "isFederationDisabled", checked: vm.isFederationDisabled}),
|
||||
t.label({for: "isFederationDisabled"}, [
|
||||
vm.i18n`Disable federation`,
|
||||
t.p({className: "form-row-description"}, vm.i18n`Can't be changed later. This will prevent people on other homeservers from joining the room. This is typically used when only people from your own organisation (if applicable) should be allowed in the room, and is otherwise not needed.`)
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
t.div({className: "button-row"}, [
|
||||
t.button({
|
||||
className: "button-action primary",
|
||||
type: "submit",
|
||||
disabled: vm => !vm.canCreate
|
||||
}, vm.i18n`Create room`),
|
||||
]),
|
||||
])
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
onFormChange(evt) {
|
||||
switch (evt.target.name) {
|
||||
case "isEncrypted":
|
||||
this.value.setEncrypted(evt.target.checked);
|
||||
break;
|
||||
case "isPublic":
|
||||
this.value.setPublic(evt.currentTarget.isPublic.value === "true");
|
||||
break;
|
||||
case "isFederationDisabled":
|
||||
this.value.setFederationDisabled(evt.target.checked);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit(evt) {
|
||||
evt.preventDefault();
|
||||
this.value.create();
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import {RoomView} from "./room/RoomView.js";
|
||||
import {RoomBeingCreatedView} from "./room/RoomBeingCreatedView.js";
|
||||
import {InviteView} from "./room/InviteView.js";
|
||||
import {TemplateView} from "../general/TemplateView";
|
||||
import {StaticView} from "../general/StaticView.js";
|
||||
|
@ -33,7 +34,9 @@ export class RoomGridView extends TemplateView {
|
|||
},
|
||||
}, t.mapView(vm => vm.roomViewModelAt(i), roomVM => {
|
||||
if (roomVM) {
|
||||
if (roomVM.kind === "invite") {
|
||||
if (roomVM.kind === "roomBeingCreated") {
|
||||
return new RoomBeingCreatedView(roomVM);
|
||||
} else if (roomVM.kind === "invite") {
|
||||
return new InviteView(roomVM);
|
||||
} else {
|
||||
return new RoomView(roomVM);
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
import {LeftPanelView} from "./leftpanel/LeftPanelView.js";
|
||||
import {RoomView} from "./room/RoomView.js";
|
||||
import {UnknownRoomView} from "./room/UnknownRoomView.js";
|
||||
import {RoomBeingCreatedView} from "./room/RoomBeingCreatedView.js";
|
||||
import {InviteView} from "./room/InviteView.js";
|
||||
import {LightboxView} from "./room/LightboxView.js";
|
||||
import {TemplateView} from "../general/TemplateView";
|
||||
|
@ -25,6 +26,7 @@ import {StaticView} from "../general/StaticView.js";
|
|||
import {SessionStatusView} from "./SessionStatusView.js";
|
||||
import {RoomGridView} from "./RoomGridView.js";
|
||||
import {SettingsView} from "./settings/SettingsView.js";
|
||||
import {CreateRoomView} from "./CreateRoomView.js";
|
||||
import {RightPanelView} from "./rightpanel/RightPanelView.js";
|
||||
|
||||
export class SessionView extends TemplateView {
|
||||
|
@ -43,11 +45,15 @@ export class SessionView extends TemplateView {
|
|||
return new RoomGridView(vm.roomGridViewModel);
|
||||
} else if (vm.settingsViewModel) {
|
||||
return new SettingsView(vm.settingsViewModel);
|
||||
} else if (vm.createRoomViewModel) {
|
||||
return new CreateRoomView(vm.createRoomViewModel);
|
||||
} else if (vm.currentRoomViewModel) {
|
||||
if (vm.currentRoomViewModel.kind === "invite") {
|
||||
return new InviteView(vm.currentRoomViewModel);
|
||||
} else if (vm.currentRoomViewModel.kind === "room") {
|
||||
return new RoomView(vm.currentRoomViewModel);
|
||||
} else if (vm.currentRoomViewModel.kind === "roomBeingCreated") {
|
||||
return new RoomBeingCreatedView(vm.currentRoomViewModel);
|
||||
} else {
|
||||
return new UnknownRoomView(vm.currentRoomViewModel);
|
||||
}
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
Copyright 2020, 2021 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 {TemplateView} from "../../general/TemplateView";
|
||||
import {renderStaticAvatar} from "../../avatar.js";
|
||||
import {spinner} from "../../common.js";
|
||||
|
||||
export class InviteTileView extends TemplateView {
|
||||
render(t, vm) {
|
||||
const classes = {
|
||||
"active": vm => vm.isOpen,
|
||||
"hidden": vm => vm.hidden
|
||||
};
|
||||
return t.li({"className": classes}, [
|
||||
t.a({href: vm.url}, [
|
||||
renderStaticAvatar(vm, 32),
|
||||
t.div({className: "description"}, [
|
||||
t.div({className: "name"}, vm.name),
|
||||
t.map(vm => vm.busy, busy => {
|
||||
if (busy) {
|
||||
return spinner(t);
|
||||
} else {
|
||||
return t.div({className: "badge highlighted"}, "!");
|
||||
}
|
||||
})
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
import {ListView} from "../../general/ListView";
|
||||
import {TemplateView} from "../../general/TemplateView";
|
||||
import {RoomTileView} from "./RoomTileView.js";
|
||||
import {InviteTileView} from "./InviteTileView.js";
|
||||
|
||||
class FilterField extends TemplateView {
|
||||
render(t, options) {
|
||||
|
@ -63,13 +62,7 @@ export class LeftPanelView extends TemplateView {
|
|||
className: "RoomList",
|
||||
list: vm.tileViewModels,
|
||||
},
|
||||
tileVM => {
|
||||
if (tileVM.kind === "invite") {
|
||||
return new InviteTileView(tileVM);
|
||||
} else {
|
||||
return new RoomTileView(tileVM);
|
||||
}
|
||||
}
|
||||
tileVM => new RoomTileView(tileVM)
|
||||
));
|
||||
const utilitiesRow = t.div({className: "utilities"}, [
|
||||
t.a({className: "button-utility close-session", href: vm.closeUrl, "aria-label": vm.i18n`Back to account list`, title: vm.i18n`Back to account list`}),
|
||||
|
@ -97,6 +90,7 @@ export class LeftPanelView extends TemplateView {
|
|||
"aria-label": gridButtonLabel
|
||||
}),
|
||||
t.a({className: "button-utility settings", href: vm.settingsUrl, "aria-label": vm.i18n`Settings`, title: vm.i18n`Settings`}),
|
||||
t.a({className: "button-utility create", href: vm.createRoomUrl, "aria-label": vm.i18n`Create room`, title: vm.i18n`Create room`}),
|
||||
]);
|
||||
|
||||
return t.div({className: "LeftPanel"}, [
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
|
||||
import {TemplateView} from "../../general/TemplateView";
|
||||
import {AvatarView} from "../../AvatarView.js";
|
||||
import {spinner} from "../../common.js";
|
||||
|
||||
export class RoomTileView extends TemplateView {
|
||||
render(t, vm) {
|
||||
|
@ -29,13 +30,19 @@ export class RoomTileView extends TemplateView {
|
|||
t.view(new AvatarView(vm, 32), {parentProvidesUpdates: true}),
|
||||
t.div({className: "description"}, [
|
||||
t.div({className: {"name": true, unread: vm => vm.isUnread}}, vm => vm.name),
|
||||
t.div({
|
||||
t.map(vm => vm.busy, busy => {
|
||||
if (busy) {
|
||||
return spinner(t);
|
||||
} else {
|
||||
return t.div({
|
||||
className: {
|
||||
badge: true,
|
||||
highlighted: vm => vm.isHighlighted,
|
||||
hidden: vm => !vm.badgeCount
|
||||
}
|
||||
}, vm => vm.badgeCount),
|
||||
}, vm => vm.badgeCount);
|
||||
}
|
||||
})
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
|
|
@ -46,7 +46,8 @@ export class MemberDetailsView extends TemplateView {
|
|||
t.div({className: "MemberDetailsView_label"}, vm.i18n`Options`),
|
||||
t.div({className: "MemberDetailsView_options"},
|
||||
[
|
||||
t.a({href: vm.linkToUser, target: "_blank", rel: "noopener"}, vm.i18n`Open Link to User`)
|
||||
t.a({href: vm.linkToUser, target: "_blank", rel: "noopener"}, vm.i18n`Open Link to User`),
|
||||
t.button({className: "text", onClick: () => vm.openDirectMessage()}, vm.i18n`Open direct message`)
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ export class MessageComposer extends TemplateView {
|
|||
this._clearHeight();
|
||||
}
|
||||
},
|
||||
placeholder: vm.isEncrypted ? "Send an encrypted message…" : "Send a message…",
|
||||
placeholder: vm => vm.isEncrypted ? "Send an encrypted message…" : "Send a message…",
|
||||
rows: "1"
|
||||
});
|
||||
this._focusInput = () => this._input.focus();
|
||||
|
|
57
src/platform/web/ui/session/room/RoomBeingCreatedView.js
Normal file
57
src/platform/web/ui/session/room/RoomBeingCreatedView.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
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 {TemplateView} from "../../general/TemplateView";
|
||||
import {LoadingView} from "../../general/LoadingView";
|
||||
import {AvatarView} from "../../AvatarView";
|
||||
|
||||
export class RoomBeingCreatedView extends TemplateView {
|
||||
render(t, vm) {
|
||||
return t.main({className: "RoomView middle"}, [
|
||||
t.div({className: "RoomHeader middle-header"}, [
|
||||
t.a({className: "button-utility close-middle", href: vm.closeUrl, title: vm.i18n`Close room`}),
|
||||
t.view(new AvatarView(vm, 32)),
|
||||
t.div({className: "room-description"}, [
|
||||
t.h2(vm => vm.name),
|
||||
])
|
||||
]),
|
||||
t.div({className: "RoomView_body"}, [
|
||||
t.mapView(vm => vm.error, error => {
|
||||
if (error) {
|
||||
return new ErrorView(vm);
|
||||
} else {
|
||||
return new LoadingView(vm.i18n`Setting up the room…`);
|
||||
}
|
||||
})
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorView extends TemplateView {
|
||||
render(t,vm) {
|
||||
return t.div({className: "RoomBeingCreated_error centered-column"}, [
|
||||
t.h3(vm.i18n`Could not create the room, something went wrong:`),
|
||||
t.div({className: "RoomView_error form-group"}, vm.error),
|
||||
t.div({className: "button-row"},
|
||||
t.button({
|
||||
className: "button-action primary destructive",
|
||||
onClick: () => vm.cancel()
|
||||
}, vm.i18n`Cancel`))
|
||||
]);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue