Merge pull request #14 from vector-im/bwindels/element-theme
Implement basic element theme
This commit is contained in:
commit
2a0e78cf68
26 changed files with 455 additions and 152 deletions
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
import {SortedArray} from "../observable/index.js";
|
import {SortedArray} from "../observable/index.js";
|
||||||
import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
|
import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
|
||||||
import {ViewModel} from "./ViewModel.js";
|
import {ViewModel} from "./ViewModel.js";
|
||||||
|
import {avatarInitials, getIdentifierColorNumber} from "./avatar.js";
|
||||||
|
|
||||||
class SessionItemViewModel extends ViewModel {
|
class SessionItemViewModel extends ViewModel {
|
||||||
constructor(sessionInfo, pickerVM) {
|
constructor(sessionInfo, pickerVM) {
|
||||||
|
@ -112,6 +113,14 @@ class SessionItemViewModel extends ViewModel {
|
||||||
this.emitChange("exportDataUrl");
|
this.emitChange("exportDataUrl");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get avatarColorNumber() {
|
||||||
|
return getIdentifierColorNumber(this._sessionInfo.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
get avatarInitials() {
|
||||||
|
return avatarInitials(this._sessionInfo.userId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
56
src/domain/avatar.js
Normal file
56
src/domain/avatar.js
Normal 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;
|
||||||
|
}
|
|
@ -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(), "");
|
|
||||||
}
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {TimelineViewModel} from "./timeline/TimelineViewModel.js";
|
import {TimelineViewModel} from "./timeline/TimelineViewModel.js";
|
||||||
import {avatarInitials} from "../avatar.js";
|
import {avatarInitials, getIdentifierColorNumber} from "../../avatar.js";
|
||||||
import {ViewModel} from "../../ViewModel.js";
|
import {ViewModel} from "../../ViewModel.js";
|
||||||
|
|
||||||
export class RoomViewModel extends ViewModel {
|
export class RoomViewModel extends ViewModel {
|
||||||
|
@ -90,7 +90,9 @@ export class RoomViewModel extends ViewModel {
|
||||||
return avatarInitials(this._room.name);
|
return avatarInitials(this._room.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get avatarColorNumber() {
|
||||||
|
return getIdentifierColorNumber(this._room.id)
|
||||||
|
}
|
||||||
|
|
||||||
async _sendMessage(message) {
|
async _sendMessage(message) {
|
||||||
if (message) {
|
if (message) {
|
||||||
|
@ -113,12 +115,28 @@ export class RoomViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ComposerViewModel {
|
class ComposerViewModel extends ViewModel {
|
||||||
constructor(roomVM) {
|
constructor(roomVM) {
|
||||||
|
super();
|
||||||
this._roomVM = roomVM;
|
this._roomVM = roomVM;
|
||||||
|
this._isEmpty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(message) {
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {SimpleTile} from "./SimpleTile.js";
|
import {SimpleTile} from "./SimpleTile.js";
|
||||||
|
import {getIdentifierColorNumber} from "../../../../avatar.js";
|
||||||
|
|
||||||
export class MessageTile extends SimpleTile {
|
export class MessageTile extends SimpleTile {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
|
@ -32,6 +33,10 @@ export class MessageTile extends SimpleTile {
|
||||||
return this._entry.sender;
|
return this._entry.sender;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get senderColorNumber() {
|
||||||
|
return getIdentifierColorNumber(this._entry.sender);
|
||||||
|
}
|
||||||
|
|
||||||
get date() {
|
get date() {
|
||||||
return this._date.toLocaleDateString({}, {month: "numeric", day: "numeric"});
|
return this._date.toLocaleDateString({}, {month: "numeric", day: "numeric"});
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {avatarInitials} from "../avatar.js";
|
import {avatarInitials, getIdentifierColorNumber} from "../../avatar.js";
|
||||||
import {ViewModel} from "../../ViewModel.js";
|
import {ViewModel} from "../../ViewModel.js";
|
||||||
|
|
||||||
export class RoomTileViewModel extends ViewModel {
|
export class RoomTileViewModel extends ViewModel {
|
||||||
|
@ -60,4 +60,8 @@ export class RoomTileViewModel extends ViewModel {
|
||||||
get avatarInitials() {
|
get avatarInitials() {
|
||||||
return avatarInitials(this._room.name);
|
return avatarInitials(this._room.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get avatarColorNumber() {
|
||||||
|
return getIdentifierColorNumber(this._room.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,10 +15,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.form > div {
|
|
||||||
margin: 0.4em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form input {
|
.form input {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -19,6 +19,16 @@ html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media screen and (min-width: 600px) {
|
||||||
|
.PreSessionScreen {
|
||||||
|
width: 600px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.SessionView {
|
.SessionView {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -28,19 +28,20 @@ limitations under the License.
|
||||||
|
|
||||||
.SessionPickerView li {
|
.SessionPickerView li {
|
||||||
margin: 0.4em 0;
|
margin: 0.4em 0;
|
||||||
padding: 0.5em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.SessionPickerView .sessionInfo {
|
.SessionPickerView .session-info {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SessionPickerView li span.userId {
|
.SessionPickerView li .user-id {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SessionPickerView li span.error {
|
.SessionPickerView li .error {
|
||||||
margin: 0 20px;
|
margin: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,8 +64,12 @@ limitations under the License.
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.MessageComposer {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.MessageComposer > input {
|
.MessageComposer > input {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
flex: 1;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ limitations under the License.
|
||||||
* TODO
|
* TODO
|
||||||
* see if with IE11 we can just set a static stroke state and make it rotate?
|
* 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;
|
fill: none;
|
||||||
stroke: currentcolor;
|
stroke: currentcolor;
|
||||||
|
|
6
src/ui/web/css/themes/element/element-logo.svg
Normal file
6
src/ui/web/css/themes/element/element-logo.svg
Normal 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 |
3
src/ui/web/css/themes/element/icons/chevron-right.svg
Normal file
3
src/ui/web/css/themes/element/icons/chevron-right.svg
Normal 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 |
3
src/ui/web/css/themes/element/icons/send.svg
Normal file
3
src/ui/web/css/themes/element/icons/send.svg
Normal 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 |
|
@ -19,44 +19,140 @@ limitations under the License.
|
||||||
|
|
||||||
.hydrogen {
|
.hydrogen {
|
||||||
font-family: 'Inter', sans-serif, 'emoji';
|
font-family: 'Inter', sans-serif, 'emoji';
|
||||||
|
font-size: 15px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
color: black;
|
color: #2e2f32;
|
||||||
|
|
||||||
|
--usercolor1: #368BD6;
|
||||||
|
--usercolor2: #AC3BA8;
|
||||||
|
--usercolor3: #03B381;
|
||||||
|
--usercolor4: #E64F7A;
|
||||||
|
--usercolor5: #FF812D;
|
||||||
|
--usercolor6: #2DC2C5;
|
||||||
|
--usercolor7: #5C56F5;
|
||||||
|
--usercolor8: #74D12C;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
background: black;
|
background: #3D88FA;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.LeftPanel {
|
.hydrogen .avatar.usercolor1 { background-color: var(--usercolor1); }
|
||||||
background: #333;
|
.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;
|
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 {
|
.LeftPanel ul {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.LeftPanel li {
|
.LeftPanel li {
|
||||||
margin: 5px;
|
margin: 5px 10px;
|
||||||
padding: 10px;
|
padding: 5px;
|
||||||
/* vertical align */
|
/* vertical align */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.LeftPanel li.active {
|
.LeftPanel li.active {
|
||||||
background: lightgray;
|
background: rgba(141, 151, 165, 0.1);
|
||||||
color: black;
|
border-radius: 5px;
|
||||||
}
|
|
||||||
|
|
||||||
.LeftPanel li {
|
|
||||||
border-bottom: 1px #555 solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.LeftPanel li:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.LeftPanel li > * {
|
.LeftPanel li > * {
|
||||||
|
@ -68,12 +164,21 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: white;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SessionStatusView {
|
.SessionStatusView {
|
||||||
padding: 5px;
|
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 {
|
.RoomPlaceholderView {
|
||||||
|
@ -83,13 +188,44 @@ a {
|
||||||
|
|
||||||
.SessionPickerView li {
|
.SessionPickerView li {
|
||||||
font-size: 1.2em;
|
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 {
|
.RoomHeader {
|
||||||
|
background: rgba(245, 245, 245, 0.90);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.RoomHeader button {
|
.RoomHeader button {
|
||||||
|
@ -115,25 +251,48 @@ a {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.RoomHeader {
|
|
||||||
padding: 10px;
|
|
||||||
background-color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.RoomView_error {
|
.RoomView_error {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.MessageComposer {
|
||||||
|
border-top: 1px solid rgba(245, 245, 245, 0.90);
|
||||||
|
}
|
||||||
|
|
||||||
.MessageComposer > input {
|
.MessageComposer > input {
|
||||||
padding: 0.8em;
|
padding: 0.8em;
|
||||||
border: none;
|
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 {
|
.message-container {
|
||||||
max-width: 80%;
|
padding: 2px 10px;
|
||||||
padding: 5px 10px;
|
margin: 5px 10px 0 10px;
|
||||||
margin: 5px 10px;
|
}
|
||||||
background: blue;
|
|
||||||
|
.TextMessageView.continuation .message-container {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-container .sender {
|
.message-container .sender {
|
||||||
|
@ -142,35 +301,23 @@ a {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.TextMessageView .message-container time {
|
.hydrogen .sender.usercolor1 { color: var(--usercolor1); }
|
||||||
padding: 2px 0 0px 20px;
|
.hydrogen .sender.usercolor2 { color: var(--usercolor2); }
|
||||||
font-size: 0.9em;
|
.hydrogen .sender.usercolor3 { color: var(--usercolor3); }
|
||||||
color: lightblue;
|
.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 {
|
.message-container time {
|
||||||
font-size: 0.9em;
|
padding: 2px 0 0px 10px;
|
||||||
color: lightblue;
|
font-size: 0.8em;
|
||||||
}
|
color: #aaa;
|
||||||
|
|
||||||
.own time {
|
|
||||||
color: lightgreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
.own .message-container {
|
|
||||||
background-color: darkgreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TextMessageView.own .message-container {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.TextMessageView.pending .message-container {
|
.TextMessageView.pending .message-container {
|
||||||
background-color: #333;
|
color: #ccc;
|
||||||
}
|
|
||||||
|
|
||||||
.TextMessageView .message-container time {
|
|
||||||
float: right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-container p {
|
.message-container p {
|
||||||
|
@ -185,8 +332,8 @@ a {
|
||||||
.AnnouncementView > div {
|
.AnnouncementView > div {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background-color: #333;
|
background-color: rgba(245, 245, 245, 0.90);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: #CCC;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,7 +94,7 @@ export const TAG_NAMES = {
|
||||||
[HTML_NS]: [
|
[HTML_NS]: [
|
||||||
"a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6",
|
"a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||||
"p", "strong", "em", "span", "img", "section", "main", "article", "aside",
|
"p", "strong", "em", "span", "img", "section", "main", "article", "aside",
|
||||||
"pre", "button", "time", "input", "textarea"],
|
"pre", "button", "time", "input", "textarea", "label"],
|
||||||
[SVG_NS]: ["svg", "circle"]
|
[SVG_NS]: ["svg", "circle"]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -21,23 +21,49 @@ import {SessionLoadView} from "./SessionLoadView.js";
|
||||||
export class LoginView extends TemplateView {
|
export class LoginView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
const disabled = vm => !!vm.isBusy;
|
const disabled = vm => !!vm.isBusy;
|
||||||
const username = t.input({type: "text", placeholder: vm.i18n`Username`, disabled});
|
const username = t.input({
|
||||||
const password = t.input({type: "password", placeholder: vm.i18n`Password`, disabled});
|
id: "username",
|
||||||
const homeserver = t.input({type: "text", placeholder: vm.i18n`Your matrix homeserver`, value: vm.defaultHomeServer, disabled});
|
type: "text",
|
||||||
return t.div({className: "LoginView form"}, [
|
placeholder: vm.i18n`Username`,
|
||||||
t.h1([vm.i18n`Log in to your homeserver`]),
|
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.if(vm => vm.error, t.createTemplate(t => t.div({className: "error"}, vm => vm.error))),
|
||||||
t.div(username),
|
t.div({className: "form-row"}, [t.label({for: "username"}, vm.i18n`Username`), username]),
|
||||||
t.div(password),
|
t.div({className: "form-row"}, [t.label({for: "password"}, vm.i18n`Password`), password]),
|
||||||
t.div(homeserver),
|
t.div({className: "form-row"}, [t.label({for: "homeserver"}, vm.i18n`Homeserver`), homeserver]),
|
||||||
t.div(t.button({
|
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),
|
onClick: () => vm.login(username.value, password.value, homeserver.value),
|
||||||
disabled
|
disabled
|
||||||
}, vm.i18n`Log In`)),
|
}, 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
|
// 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))
|
t.p(hydrogenGithubLink(t))
|
||||||
|
])
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,9 +52,10 @@ class SessionPickerItemView extends TemplateView {
|
||||||
|
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
const deleteButton = t.button({
|
const deleteButton = t.button({
|
||||||
|
className: "destructive",
|
||||||
disabled: vm => vm.isDeleting,
|
disabled: vm => vm.isDeleting,
|
||||||
onClick: this._onDeleteClick.bind(this),
|
onClick: this._onDeleteClick.bind(this),
|
||||||
}, "Delete");
|
}, "Sign Out");
|
||||||
const clearButton = t.button({
|
const clearButton = t.button({
|
||||||
disabled: vm => vm.isClearing,
|
disabled: vm => vm.isClearing,
|
||||||
onClick: () => vm.clear(),
|
onClick: () => vm.clear(),
|
||||||
|
@ -70,17 +71,20 @@ class SessionPickerItemView extends TemplateView {
|
||||||
onClick: () => setTimeout(() => vm.clearExport(), 100),
|
onClick: () => setTimeout(() => vm.clearExport(), 100),
|
||||||
}, "Download");
|
}, "Download");
|
||||||
}));
|
}));
|
||||||
|
const errorMessage = t.if(vm => vm.error, t.createTemplate(t => t.p({className: "error"}, vm => vm.error)));
|
||||||
const userName = t.span({className: "userId"}, vm => vm.label);
|
return t.li([
|
||||||
const errorMessage = t.if(vm => vm.error, t.createTemplate(t => t.span({className: "error"}, vm => vm.error)));
|
t.div({className: "session-info"}, [
|
||||||
return t.li([t.div({className: "sessionInfo"}, [
|
t.div({className: `avatar usercolor${vm.avatarColorNumber}`}, vm => vm.avatarInitials),
|
||||||
userName,
|
t.div({className: "user-id"}, vm => vm.label),
|
||||||
errorMessage,
|
]),
|
||||||
downloadExport,
|
t.div({className: "session-actions"}, [
|
||||||
exportButton,
|
|
||||||
clearButton,
|
|
||||||
deleteButton,
|
deleteButton,
|
||||||
])]);
|
exportButton,
|
||||||
|
downloadExport,
|
||||||
|
clearButton,
|
||||||
|
]),
|
||||||
|
errorMessage
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +93,7 @@ export class SessionPickerView extends TemplateView {
|
||||||
const sessionList = new ListView({
|
const sessionList = new ListView({
|
||||||
list: vm.sessions,
|
list: vm.sessions,
|
||||||
onItemClick: (item, event) => {
|
onItemClick: (item, event) => {
|
||||||
if (event.target.closest(".userId")) {
|
if (event.target.closest(".session-info")) {
|
||||||
vm.pick(item.value.id);
|
vm.pick(item.value.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -98,13 +102,24 @@ export class SessionPickerView extends TemplateView {
|
||||||
return new SessionPickerItemView(sessionInfo);
|
return new SessionPickerItemView(sessionInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
return t.div({className: "SessionPickerView"}, [
|
return t.div({className: "PreSessionScreen"}, [
|
||||||
t.h1(["Pick a session"]),
|
t.div({className: "logo"}),
|
||||||
|
t.div({className: "SessionPickerView"}, [
|
||||||
|
t.h1(["Continue as …"]),
|
||||||
t.view(sessionList),
|
t.view(sessionList),
|
||||||
t.p(t.button({onClick: () => vm.cancel()}, ["Log in to a new session instead"])),
|
t.div({className: "button-row"}, [
|
||||||
t.p(t.button({onClick: async () => vm.import(await selectFileAsText("application/json"))}, "Import")),
|
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.if(vm => vm.loadViewModel, vm => new SessionLoadView(vm.loadViewModel)),
|
||||||
t.p(hydrogenGithubLink(t))
|
t.p(hydrogenGithubLink(t))
|
||||||
|
])
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,9 @@ limitations under the License.
|
||||||
import {TemplateView} from "../general/TemplateView.js";
|
import {TemplateView} from "../general/TemplateView.js";
|
||||||
|
|
||||||
export class RoomTile extends TemplateView {
|
export class RoomTile extends TemplateView {
|
||||||
render(t) {
|
render(t, vm) {
|
||||||
return t.li({"className": {"active": vm => vm.isOpen}}, [
|
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))
|
t.div({className: "description"}, t.div({className: "name"}, vm => vm.name))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,19 +22,32 @@ export class MessageComposer extends TemplateView {
|
||||||
this._input = null;
|
this._input = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(t) {
|
render(t, vm) {
|
||||||
this._input = t.input({
|
this._input = t.input({
|
||||||
placeholder: "Send a message ...",
|
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`),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onKeyDown(event) {
|
_trySend() {
|
||||||
if (event.key === "Enter") {
|
|
||||||
if (this.value.sendMessage(this._input.value)) {
|
if (this.value.sendMessage(this._input.value)) {
|
||||||
this._input.value = "";
|
this._input.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onKeyDown(event) {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
this._trySend();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ export class RoomView extends TemplateView {
|
||||||
t.div({className: "TimelinePanel"}, [
|
t.div({className: "TimelinePanel"}, [
|
||||||
t.div({className: "RoomHeader"}, [
|
t.div({className: "RoomHeader"}, [
|
||||||
t.button({className: "back", onClick: () => vm.close()}),
|
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.div({className: "room-description"}, [
|
||||||
t.h2(vm => vm.name),
|
t.h2(vm => vm.name),
|
||||||
]),
|
]),
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {TemplateView} from "../../../general/TemplateView.js";
|
import {TemplateView} from "../../../general/TemplateView.js";
|
||||||
|
import {renderMessage} from "./common.js";
|
||||||
|
|
||||||
export class ImageView extends TemplateView {
|
export class ImageView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
|
@ -33,13 +34,8 @@ export class ImageView extends TemplateView {
|
||||||
style: `padding-top: ${heightRatioPercent}%; width: ${vm.thumbnailWidth}px;`
|
style: `padding-top: ${heightRatioPercent}%; width: ${vm.thumbnailWidth}px;`
|
||||||
}, image);
|
}, image);
|
||||||
|
|
||||||
return t.li(
|
return renderMessage(t, vm,
|
||||||
{className: {"TextMessageView": true, own: vm.isOwn, pending: vm.isPending}},
|
[t.div(linkContainer), t.p(t.time(vm.date + " " + vm.time))]
|
||||||
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)),
|
|
||||||
])
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,15 +15,12 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {TemplateView} from "../../../general/TemplateView.js";
|
import {TemplateView} from "../../../general/TemplateView.js";
|
||||||
|
import {renderMessage} from "./common.js";
|
||||||
|
|
||||||
export class TextMessageView extends TemplateView {
|
export class TextMessageView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
return t.li(
|
return renderMessage(t, vm,
|
||||||
{className: {"TextMessageView": true, own: vm.isOwn, pending: vm.isPending}},
|
[t.p([vm.text, t.time(vm.date + " " + vm.time)])]
|
||||||
t.div({className: "message-container"}, [
|
|
||||||
t.div({className: "sender"}, vm => vm.isContinuation ? "" : vm.sender),
|
|
||||||
t.p([vm.text, t.time(vm.date + " " + vm.time)]),
|
|
||||||
])
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
14
src/ui/web/session/room/timeline/common.js
Normal file
14
src/ui/web/session/room/timeline/common.js
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link rel="stylesheet" type="text/css" href="css/main.css">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
Reference in a new issue