Merge pull request #14 from vector-im/bwindels/element-theme

Implement basic element theme
This commit is contained in:
Bruno Windels 2020-08-14 12:55:19 +00:00 committed by GitHub
commit 2a0e78cf68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 455 additions and 152 deletions

View file

@ -25,6 +25,6 @@
navigator.serviceWorker.register('sw.js')
.then(function() { console.log("Service Worker registered"); });
}
</script>
</script>
</body>
</html>

View file

@ -17,6 +17,7 @@ limitations under the License.
import {SortedArray} from "../observable/index.js";
import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
import {ViewModel} from "./ViewModel.js";
import {avatarInitials, getIdentifierColorNumber} from "./avatar.js";
class SessionItemViewModel extends ViewModel {
constructor(sessionInfo, pickerVM) {
@ -112,6 +113,14 @@ class SessionItemViewModel extends ViewModel {
this.emitChange("exportDataUrl");
}
}
get avatarColorNumber() {
return getIdentifierColorNumber(this._sessionInfo.userId);
}
get avatarInitials() {
return avatarInitials(this._sessionInfo.userId);
}
}

56
src/domain/avatar.js Normal file
View file

@ -0,0 +1,56 @@
/*
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.
*/
export function avatarInitials(name) {
let words = name.split(" ");
if (words.length === 1) {
words = words[0].split("-");
}
words = words.slice(0, 2);
return words.reduce((i, w) => {
let firstChar = w.charAt(0);
if (firstChar === "!" || firstChar === "@" || firstChar === "#") {
firstChar = w.charAt(1);
}
return i + firstChar.toUpperCase();
}, "");
}
/**
* calculates a numeric hash for a given string
*
* @param {string} str string to hash
*
* @return {number}
*/
function hashCode(str) {
let hash = 0;
let i;
let chr;
if (str.length === 0) {
return hash;
}
for (i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0;
}
return Math.abs(hash);
}
export function getIdentifierColorNumber(id) {
return (hashCode(id) % 8) + 1;
}

View file

@ -1,20 +0,0 @@
/*
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.
*/
export function avatarInitials(name) {
const words = name.split(" ").slice(0, 2);
return words.reduce((i, w) => i + w.charAt(0).toUpperCase(), "");
}

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import {TimelineViewModel} from "./timeline/TimelineViewModel.js";
import {avatarInitials} from "../avatar.js";
import {avatarInitials, getIdentifierColorNumber} from "../../avatar.js";
import {ViewModel} from "../../ViewModel.js";
export class RoomViewModel extends ViewModel {
@ -90,7 +90,9 @@ export class RoomViewModel extends ViewModel {
return avatarInitials(this._room.name);
}
get avatarColorNumber() {
return getIdentifierColorNumber(this._room.id)
}
async _sendMessage(message) {
if (message) {
@ -113,12 +115,28 @@ export class RoomViewModel extends ViewModel {
}
}
class ComposerViewModel {
class ComposerViewModel extends ViewModel {
constructor(roomVM) {
super();
this._roomVM = roomVM;
this._isEmpty = true;
}
sendMessage(message) {
return this._roomVM._sendMessage(message);
const success = this._roomVM._sendMessage(message);
if (success) {
this._isEmpty = true;
this.emitChange("canSend");
}
return success;
}
get canSend() {
return !this._isEmpty;
}
setInput(text) {
this._isEmpty = text.length === 0;
this.emitChange("canSend");
}
}

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import {SimpleTile} from "./SimpleTile.js";
import {getIdentifierColorNumber} from "../../../../avatar.js";
export class MessageTile extends SimpleTile {
constructor(options) {
@ -32,6 +33,10 @@ export class MessageTile extends SimpleTile {
return this._entry.sender;
}
get senderColorNumber() {
return getIdentifierColorNumber(this._entry.sender);
}
get date() {
return this._date.toLocaleDateString({}, {month: "numeric", day: "numeric"});
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {avatarInitials} from "../avatar.js";
import {avatarInitials, getIdentifierColorNumber} from "../../avatar.js";
import {ViewModel} from "../../ViewModel.js";
export class RoomTileViewModel extends ViewModel {
@ -60,4 +60,8 @@ export class RoomTileViewModel extends ViewModel {
get avatarInitials() {
return avatarInitials(this._room.name);
}
get avatarColorNumber() {
return getIdentifierColorNumber(this._room.id)
}
}

View file

@ -15,10 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.form > div {
margin: 0.4em 0;
}
.form input {
display: block;
width: 100%;

View file

@ -19,6 +19,16 @@ html {
height: 100%;
}
@media screen and (min-width: 600px) {
.PreSessionScreen {
width: 600px;
box-sizing: border-box;
margin: 0 auto;
margin-top: 50px;
}
}
.SessionView {
display: flex;
flex-direction: column;

View file

@ -28,19 +28,20 @@ limitations under the License.
.SessionPickerView li {
margin: 0.4em 0;
padding: 0.5em;
}
.SessionPickerView .sessionInfo {
.SessionPickerView .session-info {
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
}
.SessionPickerView li span.userId {
.SessionPickerView li .user-id {
flex: 1;
}
.SessionPickerView li span.error {
.SessionPickerView li .error {
margin: 0 20px;
}

View file

@ -64,8 +64,12 @@ limitations under the License.
margin: 0;
}
.MessageComposer {
display: flex;
}
.MessageComposer > input {
display: block;
width: 100%;
flex: 1;
box-sizing: border-box;
}

View file

@ -42,7 +42,7 @@ limitations under the License.
* TODO
* see if with IE11 we can just set a static stroke state and make it rotate?
*/
stroke-dasharray: 0 0 10 90;
stroke-dasharray: 0 0 85 85;
fill: none;
stroke: currentcolor;

View file

@ -0,0 +1,6 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.28 2.88C17.28 1.28942 18.5694 0 20.16 0C30.7639 0 39.36 8.59613 39.36 19.2C39.36 20.7906 38.0706 22.08 36.48 22.08C34.8894 22.08 33.6 20.7906 33.6 19.2C33.6 11.7773 27.5827 5.76 20.16 5.76C18.5694 5.76 17.28 4.47058 17.28 2.88Z" fill="#0DBD8B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.72 45.12C30.72 46.7106 29.4306 48 27.84 48C17.2361 48 8.64 39.4039 8.64 28.8C8.64 27.2094 9.92942 25.92 11.52 25.92C13.1106 25.92 14.4 27.2094 14.4 28.8C14.4 36.2227 20.4173 42.24 27.84 42.24C29.4306 42.24 30.72 43.5294 30.72 45.12Z" fill="#0DBD8B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.88 30.72C1.28942 30.72 -5.63623e-08 29.4306 -1.25889e-07 27.84C-5.89399e-07 17.2361 8.59613 8.63997 19.2 8.63997C20.7906 8.63997 22.08 9.92939 22.08 11.52C22.08 13.1106 20.7906 14.4 19.2 14.4C11.7773 14.4 5.76 20.4173 5.76 27.84C5.76 29.4306 4.47058 30.72 2.88 30.72Z" fill="#0DBD8B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M45.12 17.28C46.7106 17.28 48 18.5694 48 20.16C48 30.7639 39.4039 39.36 28.8 39.36C27.2094 39.36 25.92 38.0706 25.92 36.48C25.92 34.8894 27.2094 33.6 28.8 33.6C36.2227 33.6 42.24 27.5827 42.24 20.16C42.24 18.5694 43.5294 17.28 45.12 17.28Z" fill="#0DBD8B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 18L15 12L9 6" stroke="#8D99A5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 212 B

View file

@ -0,0 +1,3 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.7372 8.96458L1.89656 15.8816C0.963879 16.3478 -0.00645849 15.3477 0.449387 14.4353C0.449387 14.4353 2.16481 10.9711 2.63665 10.0637C3.10849 9.15633 3.64864 8.99926 8.6646 8.35098C8.85021 8.32697 9.00215 8.1869 9.00215 7.99982C9.00215 7.81307 8.85021 7.67301 8.6646 7.649C3.64864 7.00071 3.10849 6.84364 2.63665 5.93624C2.16481 5.02918 0.449387 1.56465 0.449387 1.56465C-0.00645849 0.65258 0.963879 -0.347862 1.89656 0.118344L15.7372 7.03573C16.5319 7.43257 16.5319 8.5674 15.7372 8.96458Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 621 B

View file

@ -19,44 +19,140 @@ limitations under the License.
.hydrogen {
font-family: 'Inter', sans-serif, 'emoji';
font-size: 15px;
background-color: white;
color: black;
color: #2e2f32;
--usercolor1: #368BD6;
--usercolor2: #AC3BA8;
--usercolor3: #03B381;
--usercolor4: #E64F7A;
--usercolor5: #FF812D;
--usercolor6: #2DC2C5;
--usercolor7: #5C56F5;
--usercolor8: #74D12C;
}
.avatar {
border-radius: 100%;
background: black;
background: #3D88FA;
color: white;
}
.LeftPanel {
background: #333;
.hydrogen .avatar.usercolor1 { background-color: var(--usercolor1); }
.hydrogen .avatar.usercolor2 { background-color: var(--usercolor2); }
.hydrogen .avatar.usercolor3 { background-color: var(--usercolor3); }
.hydrogen .avatar.usercolor4 { background-color: var(--usercolor4); }
.hydrogen .avatar.usercolor5 { background-color: var(--usercolor5); }
.hydrogen .avatar.usercolor6 { background-color: var(--usercolor6); }
.hydrogen .avatar.usercolor7 { background-color: var(--usercolor7); }
.hydrogen .avatar.usercolor8 { background-color: var(--usercolor8); }
.logo {
height: 48px;
min-width: 48px;
background-image: url('element-logo.svg');
background-repeat: no-repeat;
background-position: center;
}
/** buttons */
.button-row {
display: flex;
}
.button-row > * {
margin-right: 10px;
}
.button-row > *:last-child {
margin-right: 0px;
}
.button-row button {
margin: 10px 0;
flex: 1 0 auto;
}
.form-row {
margin: 12px 0;
}
.form-row input {
padding: 12px;
border: 1px solid rgba(141, 151, 165, 0.15);
border-radius: 8px;
margin-top: 5px;
font-size: 1em;
}
.form-row label, .form-row input {
display: block;
}
button.styled.secondary {
color: #03B381;
}
button.styled.primary {
background-color: #03B381;
border-radius: 8px;
color: white;
}
button.styled.primary.destructive {
background-color: #FF4B55;
}
button.styled.secondary.destructive {
color: #FF4B55;
}
button.styled {
border: none;
padding: 10px;
background: none;
font-weight: 500;
}
.PreSessionScreen {
padding: 30px;
}
.PreSessionScreen h1 {
font-size: 16px;
text-align: center;
}
@media screen and (min-width: 600px) {
.PreSessionScreen {
box-shadow: 0px 6px 32px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
}
.PreSessionScreen .logo {
height: 48px;
min-width: 48px;
}
.LeftPanel {
background: rgba(245, 245, 245, 0.90);
}
.LeftPanel ul {
padding: 0;
margin: 0;
}
.LeftPanel li {
margin: 5px;
padding: 10px;
margin: 5px 10px;
padding: 5px;
/* vertical align */
align-items: center;
}
.LeftPanel li.active {
background: lightgray;
color: black;
}
.LeftPanel li {
border-bottom: 1px #555 solid;
}
.LeftPanel li:last-child {
border-bottom: none;
background: rgba(141, 151, 165, 0.1);
border-radius: 5px;
}
.LeftPanel li > * {
@ -68,12 +164,21 @@ limitations under the License.
}
a {
color: white;
color: inherit;
}
.SessionStatusView {
padding: 5px;
background-color: #555;
position: absolute;
top: 20px;
right: 20px;
background-color: #3D88FA;
color: white;
border-radius: 10px;
}
.room-shown .SessionStatusView {
top: 72px;
}
.RoomPlaceholderView {
@ -83,13 +188,44 @@ a {
.SessionPickerView li {
font-size: 1.2em;
background-color: grey;
}
.SessionPickerView .session-info {
padding: 12px;
border: 1px solid rgba(141, 151, 165, 0.15);
border-radius: 8px;
background-image: url('icons/chevron-right.svg');
background-position: center right 30px;
background-repeat: no-repeat;
font-weight: 500;
}
.SessionPickerView .session-actions {
margin: 10px 0 20px 0;
display: flex;
}
.SessionPickerView .session-actions > * {
margin-right: 10px;
}
.SessionPickerView .session-actions > *:last-child {
margin-right: 0px;
}
.SessionPickerView .session-actions button {
border: none;
background: none;
color: inherit;
}
.SessionPickerView button.destructive {
color: #FF4B55;
}
.RoomHeader {
background: rgba(245, 245, 245, 0.90);
padding: 10px;
background-color: #333;
}
.RoomHeader button {
@ -115,25 +251,48 @@ a {
font-size: 0.8em;
}
.RoomHeader {
padding: 10px;
background-color: #333;
}
.RoomView_error {
color: red;
}
.MessageComposer {
border-top: 1px solid rgba(245, 245, 245, 0.90);
}
.MessageComposer > input {
padding: 0.8em;
border: none;
}
.MessageComposer > button.send {
margin: 8px;
width: 32px;
height: 32px;
display: block;
border-radius: 100%;
border: none;
text-indent: 200%;
overflow: hidden;
background-color: #03B381;
background-image: url('icons/send.svg');
background-repeat: no-repeat;
background-position: center;
}
.MessageComposer > button.send:disabled {
background-color: #E3E8F0;
}
.message-container {
max-width: 80%;
padding: 5px 10px;
margin: 5px 10px;
background: blue;
padding: 2px 10px;
margin: 5px 10px 0 10px;
}
.TextMessageView.continuation .message-container {
margin-top: 0;
margin-bottom: 0;
}
.message-container .sender {
@ -142,35 +301,23 @@ a {
font-weight: bold;
}
.TextMessageView .message-container time {
padding: 2px 0 0px 20px;
font-size: 0.9em;
color: lightblue;
}
.hydrogen .sender.usercolor1 { color: var(--usercolor1); }
.hydrogen .sender.usercolor2 { color: var(--usercolor2); }
.hydrogen .sender.usercolor3 { color: var(--usercolor3); }
.hydrogen .sender.usercolor4 { color: var(--usercolor4); }
.hydrogen .sender.usercolor5 { color: var(--usercolor5); }
.hydrogen .sender.usercolor6 { color: var(--usercolor6); }
.hydrogen .sender.usercolor7 { color: var(--usercolor7); }
.hydrogen .sender.usercolor8 { color: var(--usercolor8); }
.message-container time {
font-size: 0.9em;
color: lightblue;
}
.own time {
color: lightgreen;
}
.own .message-container {
background-color: darkgreen;
}
.TextMessageView.own .message-container {
margin-left: auto;
padding: 2px 0 0px 10px;
font-size: 0.8em;
color: #aaa;
}
.TextMessageView.pending .message-container {
background-color: #333;
}
.TextMessageView .message-container time {
float: right;
color: #ccc;
}
.message-container p {
@ -185,8 +332,8 @@ a {
.AnnouncementView > div {
margin: 0 auto;
padding: 10px 20px;
background-color: #333;
background-color: rgba(245, 245, 245, 0.90);
font-size: 0.9em;
color: #CCC;
text-align: center;
border-radius: 10px;
}

View file

@ -94,7 +94,7 @@ export const TAG_NAMES = {
[HTML_NS]: [
"a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6",
"p", "strong", "em", "span", "img", "section", "main", "article", "aside",
"pre", "button", "time", "input", "textarea"],
"pre", "button", "time", "input", "textarea", "label"],
[SVG_NS]: ["svg", "circle"]
};

View file

@ -21,23 +21,49 @@ import {SessionLoadView} from "./SessionLoadView.js";
export class LoginView extends TemplateView {
render(t, vm) {
const disabled = vm => !!vm.isBusy;
const username = t.input({type: "text", placeholder: vm.i18n`Username`, disabled});
const password = t.input({type: "password", placeholder: vm.i18n`Password`, disabled});
const homeserver = t.input({type: "text", placeholder: vm.i18n`Your matrix homeserver`, value: vm.defaultHomeServer, disabled});
return t.div({className: "LoginView form"}, [
t.h1([vm.i18n`Log in to your homeserver`]),
t.if(vm => vm.error, t.createTemplate(t => t.div({className: "error"}, vm => vm.error))),
t.div(username),
t.div(password),
t.div(homeserver),
t.div(t.button({
onClick: () => vm.login(username.value, password.value, homeserver.value),
disabled
}, vm.i18n`Log In`)),
t.div(t.button({onClick: () => vm.cancel(), disabled}, [vm.i18n`Pick an existing session`])),
// use t.mapView rather than t.if to create a new view when the view model changes too
t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadView(loadViewModel) : null),
t.p(hydrogenGithubLink(t))
const username = t.input({
id: "username",
type: "text",
placeholder: vm.i18n`Username`,
disabled
});
const password = t.input({
id: "password",
type: "password",
placeholder: vm.i18n`Password`,
disabled
});
const homeserver = t.input({
id: "homeserver",
type: "text",
placeholder: vm.i18n`Your matrix homeserver`,
value: vm.defaultHomeServer,
disabled
});
return t.div({className: "PreSessionScreen"}, [
t.div({className: "logo"}),
t.div({className: "LoginView form"}, [
t.h1([vm.i18n`Sign In`]),
t.if(vm => vm.error, t.createTemplate(t => t.div({className: "error"}, vm => vm.error))),
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"}, [t.label({for: "homeserver"}, vm.i18n`Homeserver`), homeserver]),
t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadView(loadViewModel) : null),
t.div({className: "button-row"}, [
t.button({
className: "styled secondary",
onClick: () => vm.cancel(), disabled
}, [vm.i18n`Go Back`]),
t.button({
className: "styled primary",
onClick: () => vm.login(username.value, password.value, homeserver.value),
disabled
}, vm.i18n`Log In`),
]),
// use t.mapView rather than t.if to create a new view when the view model changes too
t.p(hydrogenGithubLink(t))
])
]);
}
}

View file

@ -52,9 +52,10 @@ class SessionPickerItemView extends TemplateView {
render(t, vm) {
const deleteButton = t.button({
className: "destructive",
disabled: vm => vm.isDeleting,
onClick: this._onDeleteClick.bind(this),
}, "Delete");
}, "Sign Out");
const clearButton = t.button({
disabled: vm => vm.isClearing,
onClick: () => vm.clear(),
@ -70,17 +71,20 @@ class SessionPickerItemView extends TemplateView {
onClick: () => setTimeout(() => vm.clearExport(), 100),
}, "Download");
}));
const userName = t.span({className: "userId"}, vm => vm.label);
const errorMessage = t.if(vm => vm.error, t.createTemplate(t => t.span({className: "error"}, vm => vm.error)));
return t.li([t.div({className: "sessionInfo"}, [
userName,
errorMessage,
downloadExport,
exportButton,
clearButton,
deleteButton,
])]);
const errorMessage = t.if(vm => vm.error, t.createTemplate(t => t.p({className: "error"}, vm => vm.error)));
return t.li([
t.div({className: "session-info"}, [
t.div({className: `avatar usercolor${vm.avatarColorNumber}`}, vm => vm.avatarInitials),
t.div({className: "user-id"}, vm => vm.label),
]),
t.div({className: "session-actions"}, [
deleteButton,
exportButton,
downloadExport,
clearButton,
]),
errorMessage
]);
}
}
@ -89,7 +93,7 @@ export class SessionPickerView extends TemplateView {
const sessionList = new ListView({
list: vm.sessions,
onItemClick: (item, event) => {
if (event.target.closest(".userId")) {
if (event.target.closest(".session-info")) {
vm.pick(item.value.id);
}
},
@ -98,13 +102,24 @@ export class SessionPickerView extends TemplateView {
return new SessionPickerItemView(sessionInfo);
});
return t.div({className: "SessionPickerView"}, [
t.h1(["Pick a session"]),
t.view(sessionList),
t.p(t.button({onClick: () => vm.cancel()}, ["Log in to a new session instead"])),
t.p(t.button({onClick: async () => vm.import(await selectFileAsText("application/json"))}, "Import")),
t.if(vm => vm.loadViewModel, vm => new SessionLoadView(vm.loadViewModel)),
t.p(hydrogenGithubLink(t))
return t.div({className: "PreSessionScreen"}, [
t.div({className: "logo"}),
t.div({className: "SessionPickerView"}, [
t.h1(["Continue as …"]),
t.view(sessionList),
t.div({className: "button-row"}, [
t.button({
className: "styled secondary",
onClick: async () => vm.import(await selectFileAsText("application/json"))
}, vm.i18n`Import a session`),
t.button({
className: "styled primary",
onClick: () => vm.cancel()
}, vm.i18n`Sign In`)
]),
t.if(vm => vm.loadViewModel, vm => new SessionLoadView(vm.loadViewModel)),
t.p(hydrogenGithubLink(t))
])
]);
}
}

View file

@ -17,9 +17,9 @@ limitations under the License.
import {TemplateView} from "../general/TemplateView.js";
export class RoomTile extends TemplateView {
render(t) {
render(t, vm) {
return t.li({"className": {"active": vm => vm.isOpen}}, [
t.div({className: "avatar medium"}, vm => vm.avatarInitials),
t.div({className: `avatar medium usercolor${vm.avatarColorNumber}`}, vm => vm.avatarInitials),
t.div({className: "description"}, t.div({className: "name"}, vm => vm.name))
]);
}

View file

@ -22,19 +22,32 @@ export class MessageComposer extends TemplateView {
this._input = null;
}
render(t) {
render(t, vm) {
this._input = t.input({
placeholder: "Send a message ...",
onKeydown: e => this._onKeyDown(e)
onKeydown: e => this._onKeyDown(e),
onInput: () => vm.setInput(this._input.value),
});
return t.div({className: "MessageComposer"}, [this._input]);
return t.div({className: "MessageComposer"}, [
this._input,
t.button({
className: "send",
title: vm.i18n`Send`,
disabled: vm => !vm.canSend,
onClick: () => this._trySend(),
}, vm.i18n`Send`),
]);
}
_trySend() {
if (this.value.sendMessage(this._input.value)) {
this._input.value = "";
}
}
_onKeyDown(event) {
if (event.key === "Enter") {
if (this.value.sendMessage(this._input.value)) {
this._input.value = "";
}
this._trySend();
}
}
}

View file

@ -30,7 +30,7 @@ export class RoomView extends TemplateView {
t.div({className: "TimelinePanel"}, [
t.div({className: "RoomHeader"}, [
t.button({className: "back", onClick: () => vm.close()}),
t.div({className: "avatar large"}, vm => vm.avatarInitials),
t.div({className: `avatar large usercolor${vm.avatarColorNumber}`}, vm => vm.avatarInitials),
t.div({className: "room-description"}, [
t.h2(vm => vm.name),
]),

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import {TemplateView} from "../../../general/TemplateView.js";
import {renderMessage} from "./common.js";
export class ImageView extends TemplateView {
render(t, vm) {
@ -33,13 +34,8 @@ export class ImageView extends TemplateView {
style: `padding-top: ${heightRatioPercent}%; width: ${vm.thumbnailWidth}px;`
}, image);
return t.li(
{className: {"TextMessageView": true, own: vm.isOwn, pending: vm.isPending}},
t.div({className: "message-container"}, [
t.div({className: "sender"}, vm => vm.isContinuation ? "" : vm.sender),
t.div(linkContainer),
t.p(t.time(vm.date + " " + vm.time)),
])
return renderMessage(t, vm,
[t.div(linkContainer), t.p(t.time(vm.date + " " + vm.time))]
);
}
}

View file

@ -15,15 +15,12 @@ limitations under the License.
*/
import {TemplateView} from "../../../general/TemplateView.js";
import {renderMessage} from "./common.js";
export class TextMessageView extends TemplateView {
render(t, vm) {
return t.li(
{className: {"TextMessageView": true, own: vm.isOwn, pending: vm.isPending}},
t.div({className: "message-container"}, [
t.div({className: "sender"}, vm => vm.isContinuation ? "" : vm.sender),
t.p([vm.text, t.time(vm.date + " " + vm.time)]),
])
return renderMessage(t, vm,
[t.p([vm.text, t.time(vm.date + " " + vm.time)])]
);
}
}

View file

@ -0,0 +1,14 @@
export function renderMessage(t, vm, children) {
const classes = {
"TextMessageView": true,
own: vm.isOwn,
pending: vm.isPending,
continuation: vm.isContinuation,
};
const sender = t.div({className: `sender usercolor${vm.senderColorNumber}`}, vm => vm.isContinuation ? "" : vm.sender);
children = [sender].concat(children);
return t.li(
{className: classes},
t.div({className: "message-container"}, children)
);
}

View file

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="css/main.css">
<link rel="stylesheet" type="text/css" href="css/themes/bubbles/theme.css">
<link rel="stylesheet" type="text/css" href="css/themes/element/theme.css">
</head>
<body>
<script type="text/javascript">