diff --git a/index.html b/index.html index 09e51f34..cdf0ad4f 100644 --- a/index.html +++ b/index.html @@ -25,6 +25,6 @@ navigator.serviceWorker.register('sw.js') .then(function() { console.log("Service Worker registered"); }); } - + diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index 52b2d5f7..37fbf767 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -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); + } } diff --git a/src/domain/avatar.js b/src/domain/avatar.js new file mode 100644 index 00000000..9184762b --- /dev/null +++ b/src/domain/avatar.js @@ -0,0 +1,56 @@ +/* +Copyright 2020 Bruno Windels + +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; +} diff --git a/src/domain/session/avatar.js b/src/domain/session/avatar.js deleted file mode 100644 index 355a6cf1..00000000 --- a/src/domain/session/avatar.js +++ /dev/null @@ -1,20 +0,0 @@ -/* -Copyright 2020 Bruno Windels - -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(), ""); -} diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index d27f3737..c29a4cc8 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -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"); } } diff --git a/src/domain/session/room/timeline/tiles/MessageTile.js b/src/domain/session/room/timeline/tiles/MessageTile.js index ffae90b8..802918d7 100644 --- a/src/domain/session/room/timeline/tiles/MessageTile.js +++ b/src/domain/session/room/timeline/tiles/MessageTile.js @@ -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"}); } diff --git a/src/domain/session/roomlist/RoomTileViewModel.js b/src/domain/session/roomlist/RoomTileViewModel.js index d1585262..a638ebd4 100644 --- a/src/domain/session/roomlist/RoomTileViewModel.js +++ b/src/domain/session/roomlist/RoomTileViewModel.js @@ -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) + } } diff --git a/src/ui/web/css/form.css b/src/ui/web/css/form.css index 14dba7f3..741d2bfd 100644 --- a/src/ui/web/css/form.css +++ b/src/ui/web/css/form.css @@ -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%; diff --git a/src/ui/web/css/layout.css b/src/ui/web/css/layout.css index d48dfd98..95fe71da 100644 --- a/src/ui/web/css/layout.css +++ b/src/ui/web/css/layout.css @@ -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; diff --git a/src/ui/web/css/login.css b/src/ui/web/css/login.css index d8387e73..1032ead0 100644 --- a/src/ui/web/css/login.css +++ b/src/ui/web/css/login.css @@ -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; } diff --git a/src/ui/web/css/room.css b/src/ui/web/css/room.css index 2f00ec0c..252313a3 100644 --- a/src/ui/web/css/room.css +++ b/src/ui/web/css/room.css @@ -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; } diff --git a/src/ui/web/css/spinner.css b/src/ui/web/css/spinner.css index 69793447..62974da6 100644 --- a/src/ui/web/css/spinner.css +++ b/src/ui/web/css/spinner.css @@ -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; diff --git a/src/ui/web/css/themes/element/element-logo.svg b/src/ui/web/css/themes/element/element-logo.svg new file mode 100644 index 00000000..7e6c50fb --- /dev/null +++ b/src/ui/web/css/themes/element/element-logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/ui/web/css/themes/element/icons/chevron-right.svg b/src/ui/web/css/themes/element/icons/chevron-right.svg new file mode 100644 index 00000000..a7b862aa --- /dev/null +++ b/src/ui/web/css/themes/element/icons/chevron-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/ui/web/css/themes/element/icons/send.svg b/src/ui/web/css/themes/element/icons/send.svg new file mode 100644 index 00000000..b47ab8ea --- /dev/null +++ b/src/ui/web/css/themes/element/icons/send.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/ui/web/css/themes/element/theme.css b/src/ui/web/css/themes/element/theme.css index 36cc0d77..f11c47b3 100644 --- a/src/ui/web/css/themes/element/theme.css +++ b/src/ui/web/css/themes/element/theme.css @@ -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; } diff --git a/src/ui/web/general/html.js b/src/ui/web/general/html.js index 421b04b7..846539c7 100644 --- a/src/ui/web/general/html.js +++ b/src/ui/web/general/html.js @@ -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"] }; diff --git a/src/ui/web/login/LoginView.js b/src/ui/web/login/LoginView.js index 55dd0f8c..ef2afbb6 100644 --- a/src/ui/web/login/LoginView.js +++ b/src/ui/web/login/LoginView.js @@ -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)) + ]) ]); } } diff --git a/src/ui/web/login/SessionPickerView.js b/src/ui/web/login/SessionPickerView.js index 41c37d15..8b051dcc 100644 --- a/src/ui/web/login/SessionPickerView.js +++ b/src/ui/web/login/SessionPickerView.js @@ -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)) + ]) ]); } } diff --git a/src/ui/web/session/RoomTile.js b/src/ui/web/session/RoomTile.js index bdce01cc..9268bfe5 100644 --- a/src/ui/web/session/RoomTile.js +++ b/src/ui/web/session/RoomTile.js @@ -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)) ]); } diff --git a/src/ui/web/session/room/MessageComposer.js b/src/ui/web/session/room/MessageComposer.js index cdf56dba..e07e77e3 100644 --- a/src/ui/web/session/room/MessageComposer.js +++ b/src/ui/web/session/room/MessageComposer.js @@ -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(); } } } diff --git a/src/ui/web/session/room/RoomView.js b/src/ui/web/session/room/RoomView.js index 6080368d..0c7ace9e 100644 --- a/src/ui/web/session/room/RoomView.js +++ b/src/ui/web/session/room/RoomView.js @@ -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), ]), diff --git a/src/ui/web/session/room/timeline/ImageView.js b/src/ui/web/session/room/timeline/ImageView.js index d5f9279f..4770510c 100644 --- a/src/ui/web/session/room/timeline/ImageView.js +++ b/src/ui/web/session/room/timeline/ImageView.js @@ -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))] ); } } diff --git a/src/ui/web/session/room/timeline/TextMessageView.js b/src/ui/web/session/room/timeline/TextMessageView.js index ed67a1fa..260eaf29 100644 --- a/src/ui/web/session/room/timeline/TextMessageView.js +++ b/src/ui/web/session/room/timeline/TextMessageView.js @@ -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)])] ); } } diff --git a/src/ui/web/session/room/timeline/common.js b/src/ui/web/session/room/timeline/common.js new file mode 100644 index 00000000..848f1cf5 --- /dev/null +++ b/src/ui/web/session/room/timeline/common.js @@ -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) + ); +} diff --git a/src/ui/web/view-gallery.html b/src/ui/web/view-gallery.html index f5db307f..43827afb 100644 --- a/src/ui/web/view-gallery.html +++ b/src/ui/web/view-gallery.html @@ -3,7 +3,7 @@ - +