From 16fed27a8a9085d8df7ca5e332c5bf303304772c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sat, 15 Jun 2019 17:49:45 +0200 Subject: [PATCH 01/15] SwitchView, to alternate between different views --- src/ui/web/SwitchView.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/ui/web/SwitchView.js diff --git a/src/ui/web/SwitchView.js b/src/ui/web/SwitchView.js new file mode 100644 index 00000000..7789cb1e --- /dev/null +++ b/src/ui/web/SwitchView.js @@ -0,0 +1,36 @@ +export default class SwitchView { + constructor(defaultView) { + this._childView = defaultView; + } + + mount() { + return this._childView.mount(); + } + + unmount() { + return this._childView.unmount(); + } + + root() { + return this._childView.root(); + } + + update() { + return this._childView.update(); + } + + switch(newView) { + const oldRoot = this.root(); + this._childView.unmount(); + this._childView = newView; + const newRoot = this._childView.mount(); + const parent = oldRoot.parentElement; + if (parent) { + parent.replaceChild(newRoot, oldRoot); + } + } + + get childView() { + return this._childView; + } +} From 03df472c53ab43a6345bcd04a8f41b54aed4ddbf Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sat, 15 Jun 2019 17:50:15 +0200 Subject: [PATCH 02/15] show placeholder in middle panel when no room is selected --- src/ui/web/RoomPlaceholderView.js | 19 +++++++++++++++++++ src/ui/web/SessionView.js | 30 ++++++++---------------------- 2 files changed, 27 insertions(+), 22 deletions(-) create mode 100644 src/ui/web/RoomPlaceholderView.js diff --git a/src/ui/web/RoomPlaceholderView.js b/src/ui/web/RoomPlaceholderView.js new file mode 100644 index 00000000..eaeadb57 --- /dev/null +++ b/src/ui/web/RoomPlaceholderView.js @@ -0,0 +1,19 @@ +import {tag} from "./html.js"; + +export default class RoomPlaceholderView { + constructor() { + this._root = null; + } + + mount() { + this._root = tag.div(tag.h2("Choose a room on the left side.")); + return this._root; + } + + root() { + return this._root; + } + + unmount() {} + update() {} +} diff --git a/src/ui/web/SessionView.js b/src/ui/web/SessionView.js index ba92f80b..12ef2e1c 100644 --- a/src/ui/web/SessionView.js +++ b/src/ui/web/SessionView.js @@ -1,11 +1,14 @@ import ListView from "./ListView.js"; import RoomTile from "./RoomTile.js"; import RoomView from "./RoomView.js"; +import SwitchView from "./SwitchView.js"; +import RoomPlaceholderView from "./RoomPlaceholderView.js"; import {tag} from "./html.js"; export default class SessionView { constructor(viewModel) { this._viewModel = viewModel; + this._middleSwitcher = null; this._roomList = null; this._currentRoom = null; this._root = null; @@ -27,41 +30,24 @@ export default class SessionView { }, (room) => new RoomTile(room) ); - this._roomList.mount(); - this._root.appendChild(this._roomList.root()); - - this._updateCurrentRoom(); + this._root.appendChild(this._roomList.mount()); + this._middleSwitcher = new SwitchView(new RoomPlaceholderView()); + this._root.appendChild(this._middleSwitcher.mount()); return this._root; } unmount() { this._roomList.unmount(); - if (this._room) { - this._room.unmount(); - } - + this._middleSwitcher.unmount(); this._viewModel.off("change", this._onViewModelChange); } _onViewModelChange(prop) { if (prop === "currentRoom") { - this._updateCurrentRoom(); + this._middleSwitcher.switch(new RoomView(this._viewModel.currentRoom)); } } // changing viewModel not supported for now update() {} - - _updateCurrentRoom() { - if (this._currentRoom) { - this._currentRoom.root().remove(); - this._currentRoom.unmount(); - this._currentRoom = null; - } - if (this._viewModel.currentRoom) { - this._currentRoom = new RoomView(this._viewModel.currentRoom); - this._currentRoom.mount(); - this.root().appendChild(this._currentRoom.root()); - } - } } From 95e1d55b977297f0ff0d904e8f9067ea3c48d267 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sat, 15 Jun 2019 17:50:54 +0200 Subject: [PATCH 03/15] extract argument detection for el and use it in both html and Template --- src/ui/web/Template.js | 13 ++++--------- src/ui/web/html.js | 17 ++++++++++++++--- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/ui/web/Template.js b/src/ui/web/Template.js index de47a6f0..820c5775 100644 --- a/src/ui/web/Template.js +++ b/src/ui/web/Template.js @@ -1,4 +1,4 @@ -import { setAttribute, text, TAG_NAMES } from "./html.js"; +import { setAttribute, text, isChildren, TAG_NAMES } from "./html.js"; function classNames(obj, value) { @@ -130,14 +130,9 @@ export default class Template { } el(name, attributes, children) { - if (attributes) { - // valid attributes is only object that is not a DOM node - // anything else (string, fn, array, dom node) is presumed - // to be children with no attributes passed - if (typeof attributes !== "object" || !!attributes.nodeType || Array.isArray(attributes)) { - children = attributes; - attributes = null; - } + if (attributes && isChildren(attributes)) { + children = attributes; + attributes = null; } const node = document.createElement(name); diff --git a/src/ui/web/html.js b/src/ui/web/html.js index 604090f2..0816a689 100644 --- a/src/ui/web/html.js +++ b/src/ui/web/html.js @@ -1,5 +1,9 @@ // DOM helper functions +export function isChildren(children) { + return typeof children !== "object" || !!children.nodeType || Array.isArray(children); +} + export function setAttribute(el, name, value) { if (name === "className") { name = "class"; @@ -14,13 +18,20 @@ export function setAttribute(el, name, value) { } } -export function el(elementName, attrs, children) { +export function el(elementName, attributes, children) { + if (attributes && isChildren(attributes)) { + children = attributes; + attributes = null; + } + const e = document.createElement(elementName); - if (typeof attrs === "object" && attrs !== null) { - for (let [name, value] of Object.entries(attrs)) { + + if (attributes) { + for (let [name, value] of Object.entries(attributes)) { setAttribute(e, name, value); } } + if (children) { if (!Array.isArray(children)) { children = [children]; From a5a333b71aea35406553fa1dcc3b70e43c7f6087 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sun, 16 Jun 2019 10:52:02 +0200 Subject: [PATCH 04/15] organize view code in directory like viewmodels --- .../room/timeline/tiles/RoomMemberTile.js | 2 +- .../room/timeline/tiles/RoomNameTile.js | 4 +-- .../session/room/timeline/tilesCreator.js | 3 +- src/ui/web/{ => general}/ListView.js | 0 src/ui/web/{ => general}/SwitchView.js | 0 src/ui/web/{ => general}/Template.js | 0 src/ui/web/{ => general}/TemplateView.js | 11 +++++-- src/ui/web/{ => general}/html.js | 1 + src/ui/web/login/LoginView.js | 19 +++++++++++ .../web/{ => session}/RoomPlaceholderView.js | 2 +- src/ui/web/{ => session}/RoomTile.js | 2 +- src/ui/web/{ => session}/SessionView.js | 23 ++++++++----- src/ui/web/session/SyncStatusBar.js | 16 +++++++++ src/ui/web/{ => session/room}/RoomView.js | 6 ++-- .../session/room/timeline/AnnoucementView.js | 7 ++++ .../{ => session/room}/timeline/GapView.js | 9 +++-- .../session/room/timeline/TextMessageView.js | 13 ++++++++ .../web/session/room/timeline/TimelineTile.js | 33 +++++++++++++++++++ 18 files changed, 129 insertions(+), 22 deletions(-) rename src/ui/web/{ => general}/ListView.js (100%) rename src/ui/web/{ => general}/SwitchView.js (100%) rename src/ui/web/{ => general}/Template.js (100%) rename src/ui/web/{ => general}/TemplateView.js (56%) rename src/ui/web/{ => general}/html.js (94%) create mode 100644 src/ui/web/login/LoginView.js rename src/ui/web/{ => session}/RoomPlaceholderView.js (88%) rename src/ui/web/{ => session}/RoomTile.js (78%) rename src/ui/web/{ => session}/SessionView.js (68%) create mode 100644 src/ui/web/session/SyncStatusBar.js rename src/ui/web/{ => session/room}/RoomView.js (91%) create mode 100644 src/ui/web/session/room/timeline/AnnoucementView.js rename src/ui/web/{ => session/room}/timeline/GapView.js (60%) create mode 100644 src/ui/web/session/room/timeline/TextMessageView.js create mode 100644 src/ui/web/session/room/timeline/TimelineTile.js diff --git a/src/domain/session/room/timeline/tiles/RoomMemberTile.js b/src/domain/session/room/timeline/tiles/RoomMemberTile.js index cc9796e9..e3492882 100644 --- a/src/domain/session/room/timeline/tiles/RoomMemberTile.js +++ b/src/domain/session/room/timeline/tiles/RoomMemberTile.js @@ -6,7 +6,7 @@ export default class RoomNameTile extends SimpleTile { return "announcement"; } - get label() { + get announcement() { const event = this._entry.event; const content = event.content; return `${event.sender} changed membership to ${content.membership}`; diff --git a/src/domain/session/room/timeline/tiles/RoomNameTile.js b/src/domain/session/room/timeline/tiles/RoomNameTile.js index f415efe6..32cd5adf 100644 --- a/src/domain/session/room/timeline/tiles/RoomNameTile.js +++ b/src/domain/session/room/timeline/tiles/RoomNameTile.js @@ -6,9 +6,9 @@ export default class RoomNameTile extends SimpleTile { return "announcement"; } - get label() { + get announcement() { const event = this._entry.event; const content = event.content; - return `${event.sender} changed the room name to "${content.name}"` + return `${event.sender} named the room "${content.name}"` } } diff --git a/src/domain/session/room/timeline/tilesCreator.js b/src/domain/session/room/timeline/tilesCreator.js index 60ce42ed..71009576 100644 --- a/src/domain/session/room/timeline/tilesCreator.js +++ b/src/domain/session/room/timeline/tilesCreator.js @@ -22,7 +22,8 @@ export default function ({timeline}) { case "m.emote": return new TextTile(options); case "m.image": - return new ImageTile(options); + return null; // not supported yet + // return new ImageTile(options); case "m.location": return new LocationTile(options); default: diff --git a/src/ui/web/ListView.js b/src/ui/web/general/ListView.js similarity index 100% rename from src/ui/web/ListView.js rename to src/ui/web/general/ListView.js diff --git a/src/ui/web/SwitchView.js b/src/ui/web/general/SwitchView.js similarity index 100% rename from src/ui/web/SwitchView.js rename to src/ui/web/general/SwitchView.js diff --git a/src/ui/web/Template.js b/src/ui/web/general/Template.js similarity index 100% rename from src/ui/web/Template.js rename to src/ui/web/general/Template.js diff --git a/src/ui/web/TemplateView.js b/src/ui/web/general/TemplateView.js similarity index 56% rename from src/ui/web/TemplateView.js rename to src/ui/web/general/TemplateView.js index 96d1a500..a697a9a7 100644 --- a/src/ui/web/TemplateView.js +++ b/src/ui/web/general/TemplateView.js @@ -1,8 +1,9 @@ import Template from "./Template.js"; export default class TemplateView { - constructor(value) { - this.viewModel = value; + constructor(vm, bindToChangeEvent) { + this.viewModel = vm; + this._changeEventHandler = bindToChangeEvent ? this.update.bind(this, this.viewModel) : null; this._template = null; } @@ -11,6 +12,9 @@ export default class TemplateView { } mount() { + if (this._changeEventHandler) { + this.viewModel.on("change", this._changeEventHandler); + } this._template = new Template(this.viewModel, (t, value) => this.render(t, value)); return this.root(); } @@ -20,6 +24,9 @@ export default class TemplateView { } unmount() { + if (this._changeEventHandler) { + this.viewModel.off("change", this._changeEventHandler); + } this._template.dispose(); this._template = null; } diff --git a/src/ui/web/html.js b/src/ui/web/general/html.js similarity index 94% rename from src/ui/web/html.js rename to src/ui/web/general/html.js index 0816a689..a4f816c9 100644 --- a/src/ui/web/html.js +++ b/src/ui/web/general/html.js @@ -1,6 +1,7 @@ // DOM helper functions export function isChildren(children) { + // children should be an not-object (that's the attributes), or a domnode, or an array return typeof children !== "object" || !!children.nodeType || Array.isArray(children); } diff --git a/src/ui/web/login/LoginView.js b/src/ui/web/login/LoginView.js new file mode 100644 index 00000000..a9ccf667 --- /dev/null +++ b/src/ui/web/login/LoginView.js @@ -0,0 +1,19 @@ +import TemplateView from "./general/TemplateView.js"; + +export default class LoginView extends TemplateView { + render(t, vm) { + const username = t.input({type: "text", placeholder: vm.usernamePlaceholder}); + const password = t.input({type: "password", placeholder: vm.usernamePlaceholder}); + const homeserver = t.input({type: "text", placeholder: vm.hsPlaceholder, value: vm.defaultHS}); + return t.div({className: "login form"}, [ + t.if(vm => vm.error, 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 => vm.isBusy + }, "Log In")) + ]); + } +} diff --git a/src/ui/web/RoomPlaceholderView.js b/src/ui/web/session/RoomPlaceholderView.js similarity index 88% rename from src/ui/web/RoomPlaceholderView.js rename to src/ui/web/session/RoomPlaceholderView.js index eaeadb57..fb5f9b61 100644 --- a/src/ui/web/RoomPlaceholderView.js +++ b/src/ui/web/session/RoomPlaceholderView.js @@ -1,4 +1,4 @@ -import {tag} from "./html.js"; +import {tag} from "../general/html.js"; export default class RoomPlaceholderView { constructor() { diff --git a/src/ui/web/RoomTile.js b/src/ui/web/session/RoomTile.js similarity index 78% rename from src/ui/web/RoomTile.js rename to src/ui/web/session/RoomTile.js index a68049ab..cd880593 100644 --- a/src/ui/web/RoomTile.js +++ b/src/ui/web/session/RoomTile.js @@ -1,4 +1,4 @@ -import TemplateView from "./TemplateView.js"; +import TemplateView from "../general/TemplateView.js"; export default class RoomTile extends TemplateView { render(t) { diff --git a/src/ui/web/SessionView.js b/src/ui/web/session/SessionView.js similarity index 68% rename from src/ui/web/SessionView.js rename to src/ui/web/session/SessionView.js index 12ef2e1c..be19ca30 100644 --- a/src/ui/web/SessionView.js +++ b/src/ui/web/session/SessionView.js @@ -1,9 +1,10 @@ -import ListView from "./ListView.js"; +import ListView from "../general/ListView.js"; import RoomTile from "./RoomTile.js"; -import RoomView from "./RoomView.js"; -import SwitchView from "./SwitchView.js"; +import RoomView from "./room/RoomView.js"; +import SwitchView from "../general/SwitchView.js"; import RoomPlaceholderView from "./RoomPlaceholderView.js"; -import {tag} from "./html.js"; +import SyncStatusBar from "./SyncStatusBar.js"; +import {tag} from "../general/html.js"; export default class SessionView { constructor(viewModel) { @@ -21,8 +22,7 @@ export default class SessionView { mount() { this._viewModel.on("change", this._onViewModelChange); - - this._root = tag.div({className: "SessionView"}); + this._syncStatusBar = new SyncStatusBar(this._viewModel.syncStatusViewModel); this._roomList = new ListView( { list: this._viewModel.roomList, @@ -30,9 +30,16 @@ export default class SessionView { }, (room) => new RoomTile(room) ); - this._root.appendChild(this._roomList.mount()); this._middleSwitcher = new SwitchView(new RoomPlaceholderView()); - this._root.appendChild(this._middleSwitcher.mount()); + + this._root = tag.div({className: "SessionView"}, [ + this._syncStatusBar.mount(), + tag.div({className: "main"}, [ + this._roomList.mount(), + this._middleSwitcher.mount() + ]) + ]); + return this._root; } diff --git a/src/ui/web/session/SyncStatusBar.js b/src/ui/web/session/SyncStatusBar.js new file mode 100644 index 00000000..7630cbe1 --- /dev/null +++ b/src/ui/web/session/SyncStatusBar.js @@ -0,0 +1,16 @@ +import TemplateView from "../general/TemplateView.js"; + +export default class SyncStatusBar extends TemplateView { + constructor(vm) { + super(vm, true); + } + + render(t, vm) { + return t.div({className: { + "SyncStatusBar": true, + }}, [ + vm => vm.status, + t.if(vm => !vm.isSyncing, t => t.button({onClick: () => vm.trySync()}, "Try syncing")) + ]); + } +} diff --git a/src/ui/web/RoomView.js b/src/ui/web/session/room/RoomView.js similarity index 91% rename from src/ui/web/RoomView.js rename to src/ui/web/session/room/RoomView.js index 35523c9f..03f3ddd5 100644 --- a/src/ui/web/RoomView.js +++ b/src/ui/web/session/room/RoomView.js @@ -1,6 +1,6 @@ -import TimelineTile from "./TimelineTile.js"; -import ListView from "./ListView.js"; -import {tag} from "./html.js"; +import TimelineTile from "./timeline/TimelineTile.js"; +import ListView from "../../general/ListView.js"; +import {tag} from "../../general/html.js"; import GapView from "./timeline/GapView.js"; export default class RoomView { diff --git a/src/ui/web/session/room/timeline/AnnoucementView.js b/src/ui/web/session/room/timeline/AnnoucementView.js new file mode 100644 index 00000000..1bb08db0 --- /dev/null +++ b/src/ui/web/session/room/timeline/AnnoucementView.js @@ -0,0 +1,7 @@ +import TemplateView from "../../../general/TemplateView.js"; + +export default class AnnouncementView extends TemplateView { + render(t) { + return t.li({className: "AnnouncementView"}, vm => vm.announcement); + } +} diff --git a/src/ui/web/timeline/GapView.js b/src/ui/web/session/room/timeline/GapView.js similarity index 60% rename from src/ui/web/timeline/GapView.js rename to src/ui/web/session/room/timeline/GapView.js index 258cc167..62cde6a6 100644 --- a/src/ui/web/timeline/GapView.js +++ b/src/ui/web/session/room/timeline/GapView.js @@ -1,14 +1,17 @@ -import TemplateView from "../TemplateView.js"; +import TemplateView from "../../../general/TemplateView.js"; export default class GapView extends TemplateView { render(t, vm) { const className = { - gap: true, + GapView: true, isLoading: vm => vm.isLoading }; const label = (vm.isUp ? "🠝" : "🠟") + " fill gap"; //no binding return t.li({className}, [ - t.button({onClick: () => this.viewModel.fill(), disabled: vm => vm.isLoading}, label), + t.button({ + onClick: () => this.viewModel.fill(), + disabled: vm => vm.isLoading + }, label), t.if(vm => vm.error, t => t.strong(vm => vm.error)) ]); } diff --git a/src/ui/web/session/room/timeline/TextMessageView.js b/src/ui/web/session/room/timeline/TextMessageView.js new file mode 100644 index 00000000..86f2813b --- /dev/null +++ b/src/ui/web/session/room/timeline/TextMessageView.js @@ -0,0 +1,13 @@ +import TemplateView from "../../../general/TemplateView.js"; + +export default class TextMessageView extends TemplateView { + render(t, vm) { + return t.li( + {className: {"TextMessageView": true, own: vm.isOwn}}, + t.div({className: "message-container"}, [ + t.div({className: "sender"}, vm.sender), + t.p([vm.text, t.time(vm.time)]), + ]) + ); + } +} diff --git a/src/ui/web/session/room/timeline/TimelineTile.js b/src/ui/web/session/room/timeline/TimelineTile.js new file mode 100644 index 00000000..4e87f182 --- /dev/null +++ b/src/ui/web/session/room/timeline/TimelineTile.js @@ -0,0 +1,33 @@ +import {tag} from "../../../general/html.js"; + +export default class TimelineTile { + constructor(tileVM) { + this._tileVM = tileVM; + this._root = null; + } + + root() { + return this._root; + } + + mount() { + this._root = renderTile(this._tileVM); + return this._root; + } + + unmount() {} + + update(vm, paramName) { + } +} + +function renderTile(tile) { + switch (tile.shape) { + case "message": + return tag.li([tag.strong(tile.internalId+" "), tile.label]); + case "announcement": + return tag.li([tag.strong(tile.internalId+" "), tile.announcement]); + default: + return tag.li([tag.strong(tile.internalId+" "), "unknown tile shape: " + tile.shape]); + } +} From a4bc2dd2b0817504481d2329ea0f8a83a9fce738 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sun, 16 Jun 2019 10:53:23 +0200 Subject: [PATCH 05/15] support isOwn on messages --- src/domain/session/SessionViewModel.js | 2 +- src/domain/session/room/RoomViewModel.js | 5 +++-- src/domain/session/room/timeline/TimelineViewModel.js | 4 ++-- src/domain/session/room/timeline/tiles/MessageTile.js | 5 +++++ src/domain/session/room/timeline/tilesCreator.js | 4 ++-- src/matrix/session.js | 4 ++++ 6 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 03a985ce..7bbcfe75 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -29,7 +29,7 @@ export default class SessionViewModel extends EventEmitter { if (this._currentRoomViewModel) { this._currentRoomViewModel.disable(); } - this._currentRoomViewModel = new RoomViewModel(room); + this._currentRoomViewModel = new RoomViewModel(room, this._session.userId); this._currentRoomViewModel.enable(); this.emit("change", "currentRoom"); } diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 0e5f9b1c..899f13af 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -2,9 +2,10 @@ import EventEmitter from "../../../EventEmitter.js"; import TimelineViewModel from "./timeline/TimelineViewModel.js"; export default class RoomViewModel extends EventEmitter { - constructor(room) { + constructor(room, ownUserId) { super(); this._room = room; + this._ownUserId = ownUserId; this._timeline = null; this._timelineVM = null; this._onRoomChange = this._onRoomChange.bind(this); @@ -15,7 +16,7 @@ export default class RoomViewModel extends EventEmitter { this._room.on("change", this._onRoomChange); try { this._timeline = await this._room.openTimeline(); - this._timelineVM = new TimelineViewModel(this._timeline); + this._timelineVM = new TimelineViewModel(this._timeline, this._ownUserId); this.emit("change", "timelineViewModel"); } catch (err) { console.error(`room.openTimeline(): ${err.message}:\n${err.stack}`); diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js index 5c179673..0025c563 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -18,12 +18,12 @@ import TilesCollection from "./TilesCollection.js"; import tilesCreator from "./tilesCreator.js"; export default class TimelineViewModel { - constructor(timeline) { + constructor(timeline, ownUserId) { this._timeline = timeline; // once we support sending messages we could do // timeline.entries.concat(timeline.pendingEvents) // for an ObservableList that also contains local echos - this._tiles = new TilesCollection(timeline.entries, tilesCreator({timeline})); + this._tiles = new TilesCollection(timeline.entries, tilesCreator({timeline, ownUserId})); } // doesn't fill gaps, only loads stored entries/tiles diff --git a/src/domain/session/room/timeline/tiles/MessageTile.js b/src/domain/session/room/timeline/tiles/MessageTile.js index 8ada2310..937074fc 100644 --- a/src/domain/session/room/timeline/tiles/MessageTile.js +++ b/src/domain/session/room/timeline/tiles/MessageTile.js @@ -4,6 +4,7 @@ export default class MessageTile extends SimpleTile { constructor(options) { super(options); + this._isOwn = this._entry.event.sender === options.ownUserId; this._date = new Date(this._entry.event.origin_server_ts); } @@ -23,6 +24,10 @@ export default class MessageTile extends SimpleTile { return this._date.toLocaleTimeString(); } + get isOwn() { + return this._isOwn; + } + _getContent() { const event = this._entry.event; return event && event.content; diff --git a/src/domain/session/room/timeline/tilesCreator.js b/src/domain/session/room/timeline/tilesCreator.js index 71009576..8af1f18f 100644 --- a/src/domain/session/room/timeline/tilesCreator.js +++ b/src/domain/session/room/timeline/tilesCreator.js @@ -5,9 +5,9 @@ import LocationTile from "./tiles/LocationTile.js"; import RoomNameTile from "./tiles/RoomNameTile.js"; import RoomMemberTile from "./tiles/RoomMemberTile.js"; -export default function ({timeline}) { +export default function ({timeline, ownUserId}) { return function tilesCreator(entry, emitUpdate) { - const options = {entry, emitUpdate}; + const options = {entry, emitUpdate, ownUserId}; if (entry.isGap) { return new GapTile(options, timeline); } else if (entry.event) { diff --git a/src/matrix/session.js b/src/matrix/session.js index 763d0265..a6b7b354 100644 --- a/src/matrix/session.js +++ b/src/matrix/session.js @@ -59,4 +59,8 @@ export default class Session { get syncToken() { return this._session.syncToken; } + + get userId() { + return this._sessionInfo.userId; + } } From 1917a528c7d92f272034728450030232842abdd8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sun, 16 Jun 2019 10:54:16 +0200 Subject: [PATCH 06/15] replace ad hoc buttons and label with SyncStatusBar --- index.html | 11 +---- src/EventEmitter.js | 12 ++++-- src/domain/session/SessionViewModel.js | 8 +++- src/domain/session/SyncStatusViewModel.js | 51 +++++++++++++++++++++++ src/main.js | 26 ++++-------- src/matrix/sync.js | 12 ++++-- 6 files changed, 85 insertions(+), 35 deletions(-) create mode 100644 src/domain/session/SyncStatusViewModel.js diff --git a/index.html b/index.html index 477d7c05..0ab41b2d 100644 --- a/index.html +++ b/index.html @@ -52,18 +52,9 @@ -

-
-
diff --git a/src/EventEmitter.js b/src/EventEmitter.js index 460d75a0..27a4e17d 100644 --- a/src/EventEmitter.js +++ b/src/EventEmitter.js @@ -3,11 +3,11 @@ export default class EventEmitter { this._handlersByName = {}; } - emit(name, value) { + emit(name, ...values) { const handlers = this._handlersByName[name]; if (handlers) { for(const h of handlers) { - h(value); + h(...values); } } } @@ -15,6 +15,7 @@ export default class EventEmitter { on(name, callback) { let handlers = this._handlersByName[name]; if (!handlers) { + this.onFirstSubscriptionAdded(name); this._handlersByName[name] = handlers = new Set(); } handlers.add(callback); @@ -26,9 +27,14 @@ export default class EventEmitter { handlers.delete(callback); if (handlers.length === 0) { delete this._handlersByName[name]; + this.onLastSubscriptionRemoved(name); } } } + + onFirstSubscriptionAdded(name) {} + + onLastSubscriptionRemoved(name) {} } //#ifdef TESTS export function tests() { @@ -66,4 +72,4 @@ export function tests() { } }; } -//#endif \ No newline at end of file +//#endif diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 7bbcfe75..5339ffc9 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -1,11 +1,13 @@ import EventEmitter from "../../EventEmitter.js"; import RoomTileViewModel from "./roomlist/RoomTileViewModel.js"; import RoomViewModel from "./room/RoomViewModel.js"; +import SyncStatusViewModel from "./SyncStatusViewModel.js"; export default class SessionViewModel extends EventEmitter { - constructor(session) { + constructor(session, sync) { super(); this._session = session; + this._syncStatusViewModel = new SyncStatusViewModel(sync); this._currentRoomViewModel = null; const roomTileVMs = this._session.rooms.mapValues((room, emitUpdate) => { return new RoomTileViewModel({ @@ -17,6 +19,10 @@ export default class SessionViewModel extends EventEmitter { this._roomList = roomTileVMs.sortValues((a, b) => a.compare(b)); } + get syncStatusViewModel() { + return this._syncStatusViewModel; + } + get roomList() { return this._roomList; } diff --git a/src/domain/session/SyncStatusViewModel.js b/src/domain/session/SyncStatusViewModel.js new file mode 100644 index 00000000..ef227873 --- /dev/null +++ b/src/domain/session/SyncStatusViewModel.js @@ -0,0 +1,51 @@ +import EventEmitter from "../../EventEmitter.js"; + +export default class SyncStatusViewModel extends EventEmitter { + constructor(sync) { + super(); + this._sync = sync; + this._onStatus = this._onStatus.bind(this); + } + + _onStatus(status, err) { + if (status === "error") { + this._error = err; + } else if (status === "started") { + this._error = null; + } + this.emit("change"); + } + + onFirstSubscriptionAdded(name) { + if (name === "change") { + this._sync.on("status", this._onStatus); + } + } + + onLastSubscriptionRemoved(name) { + if (name === "change") { + this._sync.on("status", this._onStatus); + } + } + + trySync() { + this._sync.start(); + this.emit("change"); + } + + get status() { + if (!this.isSyncing) { + if (this._error) { + return `Error while syncing: ${this._error.message}`; + } else { + return "Sync stopped"; + } + } else { + return "Sync running"; + } + } + + get isSyncing() { + return this._sync.isSyncing; + } +} diff --git a/src/main.js b/src/main.js index 6ecef32c..6500420c 100644 --- a/src/main.js +++ b/src/main.js @@ -2,7 +2,7 @@ import HomeServerApi from "./matrix/hs-api.js"; import Session from "./matrix/session.js"; import createIdbStorage from "./matrix/storage/idb/create.js"; import Sync from "./matrix/sync.js"; -import SessionView from "./ui/web/SessionView.js"; +import SessionView from "./ui/web/session/SessionView.js"; import SessionViewModel from "./domain/session/SessionViewModel.js"; const HOST = "localhost"; @@ -44,15 +44,14 @@ async function login(username, password, homeserver) { return storeSessionInfo(loginData); } -function showSession(container, session) { - const vm = new SessionViewModel(session); +function showSession(container, session, sync) { + const vm = new SessionViewModel(session, sync); const view = new SessionView(vm); - view.mount(); - container.appendChild(view.root()); + container.appendChild(view.mount()); } // eslint-disable-next-line no-unused-vars -export default async function main(label, button, container) { +export default async function main(container) { try { let sessionInfo = getSessionInfo(USER_ID); if (!sessionInfo) { @@ -67,26 +66,17 @@ export default async function main(label, button, container) { }}); await session.load(); console.log("session loaded"); + const sync = new Sync(hsApi, session, storage); const needsInitialSync = !session.syncToken; if (needsInitialSync) { console.log("session needs initial sync"); } else { - showSession(container, session); + showSession(container, session, sync); } - const sync = new Sync(hsApi, session, storage); await sync.start(); if (needsInitialSync) { - showSession(container, session); + showSession(container, session, sync); } - label.innerText = "sync running"; - button.addEventListener("click", () => sync.stop()); - sync.on("error", err => { - label.innerText = "sync error"; - console.error("sync error", err); - }); - sync.on("stopped", () => { - label.innerText = "sync stopped"; - }); } catch(err) { console.error(`${err.message}:\n${err.stack}`); } diff --git a/src/matrix/sync.js b/src/matrix/sync.js index 37425090..21e2e472 100644 --- a/src/matrix/sync.js +++ b/src/matrix/sync.js @@ -32,12 +32,18 @@ export default class Sync extends EventEmitter { this._isSyncing = false; this._currentRequest = null; } + + get isSyncing() { + return this._isSyncing; + } + // returns when initial sync is done async start() { if (this._isSyncing) { return; } this._isSyncing = true; + this.emit("status", "started"); let syncToken = this._session.syncToken; // do initial sync if needed if (!syncToken) { @@ -56,12 +62,12 @@ export default class Sync extends EventEmitter { } catch (err) { this._isSyncing = false; if (!(err instanceof RequestAbortError)) { - console.warn("stopping sync because of error"); - this.emit("error", err); + console.warn("stopping sync because of error", err.message); + this.emit("status", "error", err); } } } - this.emit("stopped"); + this.emit("status", "stopped"); } async _syncRequest(syncToken, timeout) { From 590ed56d684b4c09143b842e433dceca22a81b00 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sun, 16 Jun 2019 10:54:37 +0200 Subject: [PATCH 07/15] leftover things that got moved during directory org --- src/ui/web/TimelineTile.js | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 src/ui/web/TimelineTile.js diff --git a/src/ui/web/TimelineTile.js b/src/ui/web/TimelineTile.js deleted file mode 100644 index 8ee411d2..00000000 --- a/src/ui/web/TimelineTile.js +++ /dev/null @@ -1,33 +0,0 @@ -import {tag} from "./html.js"; - -export default class TimelineTile { - constructor(tileVM) { - this._tileVM = tileVM; - this._root = null; - } - - root() { - return this._root; - } - - mount() { - this._root = renderTile(this._tileVM); - return this._root; - } - - unmount() {} - - update(vm, paramName) { - } -} - -function renderTile(tile) { - switch (tile.shape) { - case "message": - return tag.li(null, [tag.strong(null, tile.internalId+" "), tile.label]); - case "announcement": - return tag.li(null, [tag.strong(null, tile.internalId+" "), tile.label]); - default: - return tag.li(null, [tag.strong(null, tile.internalId+" "), "unknown tile shape: " + tile.shape]); - } -} From d72a7102b2ee3e59385c8dec00312bfadc44ff8b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sun, 16 Jun 2019 15:12:54 +0200 Subject: [PATCH 08/15] only bind className when the obj has at least 1 fn, also support it html --- src/ui/web/general/Template.js | 24 ++++++++++++------------ src/ui/web/general/html.js | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/ui/web/general/Template.js b/src/ui/web/general/Template.js index 820c5775..b8c566c2 100644 --- a/src/ui/web/general/Template.js +++ b/src/ui/web/general/Template.js @@ -1,17 +1,13 @@ -import { setAttribute, text, isChildren, TAG_NAMES } from "./html.js"; +import { setAttribute, text, isChildren, classNames, TAG_NAMES } from "./html.js"; -function classNames(obj, value) { - return Object.entries(obj).reduce((cn, [name, enabled]) => { - if (typeof enabled === "function") { - enabled = enabled(value); +function objHasFns(obj) { + for(const value of Object.values(obj)) { + if (typeof value === "function") { + return true; } - if (enabled) { - return (cn.length ? " " : "") + name; - } else { - return cn; - } - }, ""); + } + return false; } /** Bindable template. Renders once, and allows bindings for given nodes. If you need @@ -152,7 +148,11 @@ export default class Template { const isFn = typeof value === "function"; // binding for className as object of className => enabled if (key === "className" && typeof value === "object" && value !== null) { - this._addClassNamesBinding(node, value); + if (objHasFns(value)) { + this._addClassNamesBinding(node, value); + } else { + setAttribute(node, key, classNames(value)); + } } else if (key.startsWith("on") && key.length > 2 && isFn) { const eventName = key.substr(2, 1).toLowerCase() + key.substr(3); const handler = value; diff --git a/src/ui/web/general/html.js b/src/ui/web/general/html.js index a4f816c9..b003b5a1 100644 --- a/src/ui/web/general/html.js +++ b/src/ui/web/general/html.js @@ -5,6 +5,19 @@ export function isChildren(children) { return typeof children !== "object" || !!children.nodeType || Array.isArray(children); } +export function classNames(obj, value) { + return Object.entries(obj).reduce((cn, [name, enabled]) => { + if (typeof enabled === "function") { + enabled = enabled(value); + } + if (enabled) { + return cn + (cn.length ? " " : "") + name; + } else { + return cn; + } + }, ""); +} + export function setAttribute(el, name, value) { if (name === "className") { name = "class"; @@ -29,6 +42,9 @@ export function el(elementName, attributes, children) { if (attributes) { for (let [name, value] of Object.entries(attributes)) { + if (name === "className" && typeof value === "object" && value !== null) { + value = classNames(value); + } setAttribute(e, name, value); } } From 4a657b279d257a847978a4379cce4c632beb6e97 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sun, 16 Jun 2019 15:21:20 +0200 Subject: [PATCH 09/15] apply css from prototype, other small changes, keep scroll at bottom --- index.html | 49 +----------- src/domain/session/avatar.js | 4 + src/domain/session/room/RoomViewModel.js | 7 +- .../room/timeline/tiles/MessageTile.js | 2 +- .../room/timeline/tiles/RoomMemberTile.js | 15 +++- .../session/room/timeline/tiles/TextTile.js | 4 +- .../session/roomlist/RoomTileViewModel.js | 6 ++ src/ui/web/css/avatar.css | 27 +++++++ src/ui/web/css/layout.css | 56 ++++++++++++++ src/ui/web/css/left-panel.css | 47 ++++++++++++ src/ui/web/css/main.css | 21 ++++++ src/ui/web/css/room.css | 65 ++++++++++++++++ src/ui/web/css/timeline.css | 74 +++++++++++++++++++ src/ui/web/general/ListView.js | 18 ++++- src/ui/web/general/TemplateView.js | 2 +- src/ui/web/general/html.js | 2 +- src/ui/web/session/RoomPlaceholderView.js | 2 +- src/ui/web/session/RoomTile.js | 5 +- src/ui/web/session/SessionView.js | 4 +- src/ui/web/session/SyncStatusBar.js | 1 + src/ui/web/session/room/RoomView.js | 70 ++++++++---------- src/ui/web/session/room/TimelineList.js | 31 ++++++++ ...AnnoucementView.js => AnnouncementView.js} | 2 +- .../session/room/timeline/TextMessageView.js | 1 + 24 files changed, 415 insertions(+), 100 deletions(-) create mode 100644 src/domain/session/avatar.js create mode 100644 src/ui/web/css/avatar.css create mode 100644 src/ui/web/css/layout.css create mode 100644 src/ui/web/css/left-panel.css create mode 100644 src/ui/web/css/main.css create mode 100644 src/ui/web/css/room.css create mode 100644 src/ui/web/css/timeline.css create mode 100644 src/ui/web/session/room/TimelineList.js rename src/ui/web/session/room/timeline/{AnnoucementView.js => AnnouncementView.js} (63%) diff --git a/index.html b/index.html index 0ab41b2d..8ae1e5cf 100644 --- a/index.html +++ b/index.html @@ -2,54 +2,7 @@ - +