diff --git a/src/domain/session/CreateRoomViewModel.js b/src/domain/session/CreateRoomViewModel.js
new file mode 100644
index 00000000..47656128
--- /dev/null
+++ b/src/domain/session/CreateRoomViewModel.js
@@ -0,0 +1,126 @@
+/*
+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/create";
+
+export class CreateRoomViewModel extends ViewModel {
+ constructor(options) {
+ super(options);
+ const {session} = options;
+ this._session = session;
+ this._name = "";
+ this._topic = "";
+ this._isPublic = false;
+ this._isEncrypted = true;
+ this._avatarScaledBlob = undefined;
+ this._avatarFileName = undefined;
+ this._avatarInfo = undefined;
+ }
+
+ setName(name) {
+ this._name = name;
+ this.emitChange("name");
+ }
+
+ get name() { return this._name; }
+
+ setTopic(topic) {
+ this._topic = topic;
+ this.emitChange("topic");
+ }
+
+ get topic() { return this._topic; }
+
+ setPublic(isPublic) {
+ this._isPublic = isPublic;
+ this.emitChange("isPublic");
+ }
+
+ get isPublic() { return this._isPublic; }
+
+ setEncrypted(isEncrypted) {
+ this._isEncrypted = isEncrypted;
+ this.emitChange("isEncrypted");
+ }
+
+ get isEncrypted() { return this._isEncrypted; }
+
+ get canCreate() {
+ return !!this.name;
+ }
+
+ 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,
+ alias: this.isPublic ? this.roomAlias : undefined,
+ avatar,
+ invites: ["@bwindels:matrix.org"]
+ });
+ this.navigation.push("room", roomBeingCreated.localId);
+ }
+
+
+ avatarUrl() { return this._avatarScaledBlob.url; }
+ get avatarTitle() { return this.name; }
+ get avatarLetter() { return ""; }
+ get avatarColorNumber() { return 0; }
+ get hasAvatar() { return !!this._avatarScaledBlob; }
+ get error() { return ""; }
+
+ 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");
+ }
+}
diff --git a/src/domain/session/common.js b/src/domain/session/common.js
new file mode 100644
index 00000000..1ab275bb
--- /dev/null
+++ b/src/domain/session/common.js
@@ -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
+ };
+}
diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js
index c81c47ef..2d10c7ca 100644
--- a/src/domain/session/room/RoomViewModel.js
+++ b/src/domain/session/room/RoomViewModel.js
@@ -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) {
@@ -273,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,
@@ -319,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;
diff --git a/src/platform/web/ui/css/avatar.css b/src/platform/web/ui/css/avatar.css
index 143ea899..2ee9ca0c 100644
--- a/src/platform/web/ui/css/avatar.css
+++ b/src/platform/web/ui/css/avatar.css
@@ -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 */
diff --git a/src/platform/web/ui/css/layout.css b/src/platform/web/ui/css/layout.css
index 2b3a04ae..a0f42b01 100644
--- a/src/platform/web/ui/css/layout.css
+++ b/src/platform/web/ui/css/layout.css
@@ -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
diff --git a/src/platform/web/ui/css/themes/element/icons/plus.svg b/src/platform/web/ui/css/themes/element/icons/plus.svg
new file mode 100644
index 00000000..ea197223
--- /dev/null
+++ b/src/platform/web/ui/css/themes/element/icons/plus.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css
index eae02ee8..3a0d34ba 100644
--- a/src/platform/web/ui/css/themes/element/theme.css
+++ b/src/platform/web/ui/css/themes/element/theme.css
@@ -171,6 +171,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');
}
@@ -1089,3 +1093,28 @@ button.RoomDetailsView_row::after {
display: flex;
gap: 12px;
}
+
+.CreateRoomView {
+ padding: 0 12px;
+ justify-self: center;
+ max-width: 400px;
+ 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;
+}
diff --git a/src/platform/web/ui/session/CreateRoomView.js b/src/platform/web/ui/session/CreateRoomView.js
new file mode 100644
index 00000000..6bdc32ca
--- /dev/null
+++ b/src/platform/web/ui/session/CreateRoomView.js
@@ -0,0 +1,105 @@
+/*
+Copyright 2020 Bruno Windels
+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: "CreateRoomView middle"}, [
+ 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`
+ }, vm => vm.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
+ `}),
+ ]),
+ 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;
+ }
+ }
+
+ onSubmit(evt) {
+ evt.preventDefault();
+ this.value.create();
+ }
+}