create room view and view model

This commit is contained in:
Bruno Windels 2022-02-09 19:02:51 +01:00
parent a1e14c4eec
commit 5c085efc10
8 changed files with 294 additions and 12 deletions

View File

@ -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");
}
}

View 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
};
}

View File

@ -20,6 +20,7 @@ import {ComposerViewModel} from "./ComposerViewModel.js"
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js";
import {tilesCreator} from "./timeline/tilesCreator.js"; import {tilesCreator} from "./timeline/tilesCreator.js";
import {ViewModel} from "../../ViewModel.js"; import {ViewModel} from "../../ViewModel.js";
import {imageToInfo} from "../common.js";
export class RoomViewModel extends ViewModel { export class RoomViewModel extends ViewModel {
constructor(options) { constructor(options) {
@ -273,7 +274,9 @@ export class RoomViewModel extends ViewModel {
let image = await this.platform.loadImage(file.blob); let image = await this.platform.loadImage(file.blob);
const limit = await this.platform.settingsStorage.getInt("sentImageSizeLimit"); const limit = await this.platform.settingsStorage.getInt("sentImageSizeLimit");
if (limit && image.maxDimension > limit) { if (limit && image.maxDimension > limit) {
image = await image.scale(limit); const scaledImage = await image.scale(limit);
image.dispose();
image = scaledImage;
} }
const content = { const content = {
body: file.name, 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) { function videoToInfo(video) {
const info = imageToInfo(video); const info = imageToInfo(video);
info.duration = video.duration; info.duration = video.duration;

View File

@ -34,6 +34,7 @@ limitations under the License.
.hydrogen .avatar img { .hydrogen .avatar img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover;
} }
/* work around postcss-css-variables limitations and repeat variable usage */ /* work around postcss-css-variables limitations and repeat variable usage */

View File

@ -49,7 +49,7 @@ main {
grid-template: grid-template:
"status status" auto "status status" auto
"left middle" 1fr / "left middle" 1fr /
300px 1fr; 320px 1fr;
min-height: 0; min-height: 0;
min-width: 0; min-width: 0;
} }
@ -58,7 +58,7 @@ main {
grid-template: grid-template:
"status status status" auto "status status status" auto
"left middle right" 1fr / "left middle right" 1fr /
300px 1fr 300px; 320px 1fr 300px;
} }
/* resize and reposition session view to account for mobile Safari which shifts /* resize and reposition session view to account for mobile Safari which shifts

View 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

View File

@ -171,6 +171,10 @@ a.button-action {
background-image: url('icons/settings.svg'); background-image: url('icons/settings.svg');
} }
.button-utility.create {
background-image: url('icons/plus.svg');
}
.button-utility.grid.on { .button-utility.grid.on {
background-image: url('icons/disable-grid.svg'); background-image: url('icons/disable-grid.svg');
} }
@ -1089,3 +1093,28 @@ button.RoomDetailsView_row::after {
display: flex; display: flex;
gap: 12px; 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;
}

View File

@ -0,0 +1,105 @@
/*
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: "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();
}
}