From 49a577991b7925c901a548b655df031fc839cb5a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 29 Jul 2019 22:39:56 +0200 Subject: [PATCH 1/7] login and session picker view models, sessions store --- doc/LOGIN.md | 11 ++ doc/viewhierarchy.md | 8 - src/domain/BrawlViewModel.js | 157 ++++++++++++++++++ src/domain/LoginViewModel.js | 38 +++++ src/domain/SessionPickerViewModel.js | 29 ++++ src/domain/session/SessionViewModel.js | 2 +- .../localstorage/SessionsStore.js | 45 +++++ src/ui/web/login/LoginView.js | 7 +- 8 files changed, 285 insertions(+), 12 deletions(-) create mode 100644 doc/LOGIN.md create mode 100644 src/domain/BrawlViewModel.js create mode 100644 src/domain/LoginViewModel.js create mode 100644 src/domain/SessionPickerViewModel.js create mode 100644 src/matrix/sessions-store/localstorage/SessionsStore.js diff --git a/doc/LOGIN.md b/doc/LOGIN.md new file mode 100644 index 00000000..c5a8915e --- /dev/null +++ b/doc/LOGIN.md @@ -0,0 +1,11 @@ +LoginView + LoginViewModel +SessionPickerView + SessionPickerViewModel + +matrix: + SessionStorage (could be in keychain, ... for now we go with localstorage) + getAll() + + Login + \ No newline at end of file diff --git a/doc/viewhierarchy.md b/doc/viewhierarchy.md index 0605297d..69c69679 100644 --- a/doc/viewhierarchy.md +++ b/doc/viewhierarchy.md @@ -16,11 +16,3 @@ view hierarchy: SessionPickView LoginView ``` - - - DONE: support isOwn on message view model - - DONE: put syncstatusbar in sessionview - - DONE: apply css to app - - DONE: keep scroll at bottom - - DONE: hide sender if repeated - - DONE: show date somehow - - DONE: start scrolled down when opening room diff --git a/src/domain/BrawlViewModel.js b/src/domain/BrawlViewModel.js new file mode 100644 index 00000000..5518f7e5 --- /dev/null +++ b/src/domain/BrawlViewModel.js @@ -0,0 +1,157 @@ +import Session from "../matrix/session.js"; +import Sync from "../matrix/sync.js"; +import SessionViewModel from "./session/SessionViewModel.js"; +import LoginViewModel from "./LoginViewModel.js"; +import SessionPickerViewModel from "./SessionPickerViewModel.js"; +import EventEmitter from "../EventEmitter.js"; + +export default class BrawlViewModel extends EventEmitter { + constructor({createStorage, sessionsStore, createHsApi, clock}) { + super(); + this._createStorage = createStorage; + this._sessionsStore = sessionsStore; + this._createHsApi = createHsApi; + this._clock = clock; + + this._loading = false; + this._error = null; + this._sessionViewModel = null; + this._loginViewModel = null; + this._sessionPickerViewModel = null; + } + + async load() { + if (await this._sessionsStore.hasAnySession()) { + this._showPicker(); + } else { + this._showLogin(); + } + } + + async _showPicker() { + this._clearSections(); + this._sessionPickerViewModel = new SessionPickerViewModel({ + sessionsStore: this._sessionsStore, + sessionCallback: sessionInfo => this._onSessionPicked(sessionInfo) + }); + this.emit("change", "activeSection"); + try { + await this._sessionPickerViewModel.load(); + } catch (err) { + this._clearSections(); + this._error = err; + this.emit("change", "activeSection"); + } + } + + _showLogin() { + this._clearSections(); + this._loginViewModel = new LoginViewModel({ + createHsApi: this._createHsApi, + defaultHomeServer: "matrix.org", + loginCallback: loginData => this._onLoginFinished(loginData) + }); + this.emit("change", "activeSection"); + + } + + _showSession(session, sync) { + this._clearSections(); + this._sessionViewModel = new SessionViewModel({session, sync}); + this.emit("change", "activeSection"); + } + + _clearSections() { + this._error = null; + this._loading = false; + this._sessionViewModel = null; + this._loginViewModel = null; + this._sessionPickerViewModel = null; + } + + get activeSection() { + if (this._error) { + return "error"; + } else if(this._loading) { + return "loading"; + } else if (this._sessionViewModel) { + return "session"; + } else if (this._loginViewModel) { + return "login"; + } else { + return "picker"; + } + } + + get loadingText() { return this._loadingText; } + get sessionViewModel() { return this._sessionViewModel; } + get loginViewModel() { return this._loginViewModel; } + get sessionPickerViewModel() { return this._sessionPickerViewModel; } + get errorText() { return this._error && this._error.message; } + + async _onLoginFinished(loginData) { + if (loginData) { + // TODO: extract random() as it is a source of non-determinism + const sessionId = (Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)).toString(); + const sessionInfo = { + id: sessionId, + deviceId: loginData.device_id, + userId: loginData.user_id, + homeServer: loginData.home_server, + accessToken: loginData.access_token, + lastUsed: this._clock.now() + }; + await this._sessionsStore.add(sessionInfo); + this._loadSession(sessionInfo); + } else { + this._showPicker(); + } + } + + _onSessionPicked(sessionInfo) { + if (sessionInfo) { + this._loadSession(sessionInfo); + this._sessionsStore.updateLastUsed(sessionInfo.id, this._clock.now()); + } else { + this._showLogin(); + } + } + + async _loadSession(sessionInfo) { + try { + this._loading = true; + this._loadingText = "Loading your conversations…"; + const hsApi = this._createHsApi(sessionInfo.homeServer); + const storage = await this._createStorage(sessionInfo.id); + // no need to pass access token to session + const filteredSessionInfo = { + deviceId: sessionInfo.deviceId, + userId: sessionInfo.userId, + homeServer: sessionInfo.homeServer, + }; + const session = new Session({storage, sessionInfo: filteredSessionInfo, hsApi}); + // show spinner now, with title loading stored data? + + this.emit("change", "activeSection"); + await session.load(); + const sync = new Sync({hsApi, storage, session}); + + const needsInitialSync = !session.syncToken; + if (!needsInitialSync) { + this._showSession(session, sync); + } + this._loadingText = "Getting your conversations from the server…"; + this.emit("change", "loadingText"); + // update spinner title to initial sync + await sync.start(); + if (needsInitialSync) { + this._showSession(session, sync); + } + // start sending pending messages + session.notifyNetworkAvailable(); + } catch (err) { + this._error = err; + } + this.emit("change", "activeSection"); + } +} diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js new file mode 100644 index 00000000..c6bc6356 --- /dev/null +++ b/src/domain/LoginViewModel.js @@ -0,0 +1,38 @@ +import EventEmitter from "../EventEmitter.js"; + +export default class LoginViewModel extends EventEmitter { + constructor({loginCallback, defaultHomeServer, createHsApi}) { + super(); + this._loginCallback = loginCallback; + this._defaultHomeServer = defaultHomeServer; + this._createHsApi = createHsApi; + this._loading = false; + this._error = null; + } + + get usernamePlaceholder() { return "Username"; } + get passwordPlaceholder() { return "Password"; } + get hsPlaceholder() { return "Your matrix homeserver"; } + get defaultHomeServer() { return this._defaultHomeServer; } + get error() { return this._error; } + get loading() { return this._loading; } + + async login(username, password, homeserver) { + const hsApi = this._createHsApi(homeserver); + try { + this._loading = true; + this.emit("change", "loading"); + const loginData = await hsApi.passwordLogin(username, password).response(); + this._loginCallback(loginData); + // wait for parent view model to switch away here + } catch (err) { + this._error = err; + this._loading = false; + this.emit("change", "loading"); + } + } + + cancel() { + this._loginCallback(); + } +} diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js new file mode 100644 index 00000000..922e8775 --- /dev/null +++ b/src/domain/SessionPickerViewModel.js @@ -0,0 +1,29 @@ +import {SortedArray} from "./observables/index.js"; + +export default class SessionPickerViewModel { + constructor({sessionStore, sessionCallback}) { + this._sessionsStore = sessionStore; + this._sessionCallback = sessionCallback; + this._sessions = new SortedArray((s1, s2) => (s1.lastUsed || 0) - (s2.lastUsed || 0)); + } + + async load() { + const sessions = await this._sessionsStore.getAll(); + this._sessions.setManyUnsorted(sessions); + } + + pick(id) { + const session = this._sessions.array.find(s => s.id === id); + if (session) { + this._sessionCallback(session); + } + } + + get sessions() { + return this._sessions; + } + + cancel() { + this._sessionCallback(); + } +} diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 65750280..28bc1277 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -4,7 +4,7 @@ import RoomViewModel from "./room/RoomViewModel.js"; import SyncStatusViewModel from "./SyncStatusViewModel.js"; export default class SessionViewModel extends EventEmitter { - constructor(session, sync) { + constructor({session, sync}) { super(); this._session = session; this._syncStatusViewModel = new SyncStatusViewModel(sync); diff --git a/src/matrix/sessions-store/localstorage/SessionsStore.js b/src/matrix/sessions-store/localstorage/SessionsStore.js new file mode 100644 index 00000000..424dac62 --- /dev/null +++ b/src/matrix/sessions-store/localstorage/SessionsStore.js @@ -0,0 +1,45 @@ +export default class SessionsStore { + constructor(name) { + this._name = name; + } + + getAll() { + const sessionsJson = localStorage.getItem(this._name); + if (sessionsJson) { + const sessions = JSON.parse(sessionsJson); + if (Array.isArray(sessions)) { + return Promise.resolve(sessions); + } + } + return Promise.resolve([]); + } + + async hasAnySession() { + const all = await this.getAll(); + return all && all.length > 0; + } + + async updateLastUsed(id, timestamp) { + const sessions = await this.getAll(); + if (sessions) { + const session = sessions.find(session => session.id === id); + if (session) { + session.lastUsed = timestamp; + localStorage.setItem(this._name, JSON.stringify(sessions)); + } + } + } + + async get(id) { + const sessions = await this.getAll(); + if (sessions) { + return sessions.find(session => session.id === id); + } + } + + async add(sessionInfo) { + const sessions = await this.getAll(); + sessions.push(sessionInfo); + localStorage.setItem(this._name, JSON.stringify(sessions)); + } +} diff --git a/src/ui/web/login/LoginView.js b/src/ui/web/login/LoginView.js index a9ccf667..77dfde53 100644 --- a/src/ui/web/login/LoginView.js +++ b/src/ui/web/login/LoginView.js @@ -4,7 +4,7 @@ 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}); + const homeserver = t.input({type: "text", placeholder: vm.hsPlaceholder, value: vm.defaultHomeServer}); return t.div({className: "login form"}, [ t.if(vm => vm.error, t => t.div({className: "error"}, vm => vm.error)), t.div(username), @@ -12,8 +12,9 @@ export default class LoginView extends TemplateView { t.div(homeserver), t.div(t.button({ onClick: () => vm.login(username.value, password.value, homeserver.value), - disabled: vm => vm.isBusy - }, "Log In")) + disabled: vm => vm.loading + }, "Log In")), + t.div(t.button({onClick: () => vm.cancel()}), "Cancel") ]); } } From c27172fc4e2b797914632ec9f3b17092387ff984 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 31 Jul 2019 00:07:04 +0200 Subject: [PATCH 2/7] adjust main to new view --- src/main.js | 87 ++++++++--------------------------------------------- 1 file changed, 12 insertions(+), 75 deletions(-) diff --git a/src/main.js b/src/main.js index 9f957513..9b4333a7 100644 --- a/src/main.js +++ b/src/main.js @@ -1,83 +1,20 @@ 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/session/SessionView.js"; -import SessionViewModel from "./domain/session/SessionViewModel.js"; - -const HOST = "127.0.0.1"; -const HOMESERVER = `http://${HOST}:8008`; -const USERNAME = "bruno1"; -const USER_ID = `@${USERNAME}:localhost`; -const PASSWORD = "testtest"; - -function getSessionInfo(userId) { - const sessionsJson = localStorage.getItem("brawl_sessions_v1"); - if (sessionsJson) { - const sessions = JSON.parse(sessionsJson); - const session = sessions.find(session => session.userId === userId); - if (session) { - return session; - } - } -} - -function storeSessionInfo(loginData) { - const sessionsJson = localStorage.getItem("brawl_sessions_v1"); - const sessions = sessionsJson ? JSON.parse(sessionsJson) : []; - const sessionId = (Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)).toString(); - const sessionInfo = { - id: sessionId, - deviceId: loginData.device_id, - userId: loginData.user_id, - homeServer: loginData.home_server, - accessToken: loginData.access_token, - }; - sessions.push(sessionInfo); - localStorage.setItem("brawl_sessions_v1", JSON.stringify(sessions)); - return sessionInfo; -} - -async function login(username, password, homeserver) { - const hsApi = new HomeServerApi(homeserver); - const loginData = await hsApi.passwordLogin(username, password).response(); - return storeSessionInfo(loginData); -} - -function showSession(container, session, sync) { - const vm = new SessionViewModel(session, sync); - const view = new SessionView(vm); - container.appendChild(view.mount()); -} +import SessionsStore from "./matrix/sessions-store/localstorage/SessionsStore.js"; +import BrawlViewModel from "./domain/BrawlViewModel.js"; +import BrawlView from "./ui/web/BrawlView.js"; export default async function main(container) { try { - let sessionInfo = getSessionInfo(USER_ID); - if (!sessionInfo) { - sessionInfo = await login(USERNAME, PASSWORD, HOMESERVER); - } - const storage = await createIdbStorage(`brawl_session_${sessionInfo.id}`); - const hsApi = new HomeServerApi(HOMESERVER, sessionInfo.accessToken); - const session = new Session({storage, hsApi, sessionInfo: { - deviceId: sessionInfo.deviceId, - userId: sessionInfo.userId, - homeServer: sessionInfo.homeServer, //only pass relevant fields to Session - }}); - 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, sync); - } - await sync.start(); - if (needsInitialSync) { - showSession(container, session, sync); - } - // this will start sending unsent messages - session.notifyNetworkAvailable(); + const vm = new BrawlViewModel({ + createStorage: sessionId => createIdbStorage(`brawl_session_${sessionId}`), + createHsApi: homeServer => new HomeServerApi(homeServer), + sessionsStore: new SessionsStore("brawl_sessions_v1"), + clock: Date //just for `now` fn + }); + await vm.load(); + const view = new BrawlView(vm); + container.appendChild(view.mount()); } catch(err) { console.error(`${err.message}:\n${err.stack}`); } From 1082233de465183a6ff02a8d29c776aa3c07510a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 31 Jul 2019 00:07:12 +0200 Subject: [PATCH 3/7] start of session picker view --- src/ui/web/login/SessionPickerView.js | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/ui/web/login/SessionPickerView.js diff --git a/src/ui/web/login/SessionPickerView.js b/src/ui/web/login/SessionPickerView.js new file mode 100644 index 00000000..1b53fe31 --- /dev/null +++ b/src/ui/web/login/SessionPickerView.js @@ -0,0 +1,28 @@ +import * as h from "../general/html.js"; +import ListView from "../general/ListView.js"; + +class SessionPickerItem { + +} + +export default class SessionPickerView extends TemplateView { + mount() { + this._sessionList = new ListView({list: this._viewModel.sessions}, sessionInfo => { + return new SessionPickerItem(sessionInfo); + }); + super.mount(); + } + + render(t, vm) { + this._root = h.div({className: "SessionPickerView"}, [ + this._sessionList.mount(), + h.button() + ]); + + } + + unmount() { + super.unmount(); + this._sessionList.unmount(); + } +} From a7194e0c7aeb7514c22e422b7aff60cc7688d945 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sun, 8 Sep 2019 10:18:59 +0200 Subject: [PATCH 4/7] implement missing views --- src/ui/web/BrawlView.js | 64 +++++++++++++++++++++++++++ src/ui/web/login/SessionPickerView.js | 24 ++++++---- 2 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 src/ui/web/BrawlView.js diff --git a/src/ui/web/BrawlView.js b/src/ui/web/BrawlView.js new file mode 100644 index 00000000..c5642e15 --- /dev/null +++ b/src/ui/web/BrawlView.js @@ -0,0 +1,64 @@ +import SessionView from "./session/SessionView.js"; +import LoginView from "./login/LoginView.js"; +import SessionPickerView from "./login/SessionPickerView.js"; +import TemplateView from "./general/TemplateView.js"; +import SwitchView from "./general/SwitchView.js"; + +export default class BrawlView { + constructor(vm) { + this._vm = vm; + this._switcher = null; + this._root = null; + this._onViewModelChange = this._onViewModelChange.bind(this); + } + + _getView() { + switch (this._vm.activeSection) { + case "error": + return new StatusView({header: "Something went wrong", message: this._vm.errorText}); + case "loading": + return new StatusView({header: "Loading", message: this._vm.loadingText}); + case "session": + return new SessionView(this._vm.sessionViewModel); + case "login": + return new LoginView(this._vm.loginViewModel); + case "picker": + return new SessionPickerView(this._vm.sessionPickerViewModel); + default: + throw new Error(`Unknown section: ${this._vm.activeSection}`); + } + } + + _onViewModelChange(prop) { + if (prop === "activeSection") { + this._switcher.switch(this._getView()); + } + } + + mount() { + this._switcher = new SwitchView(this._getView()); + this._root = this._switcher.mount(); + this._vm.on("change", this._onViewModelChange); + return this._root; + } + + unmount() { + this._vm.off("change", this._onViewModelChange); + this._switcher.unmount(); + } + + root() { + return this._root; + } + + update() {} +} + +class StatusView extends TemplateView { + render(t, vm) { + return t.div({className: "StatusView"}, [ + t.h1(vm.header), + t.p(vm.message), + ]); + } +} diff --git a/src/ui/web/login/SessionPickerView.js b/src/ui/web/login/SessionPickerView.js index 1b53fe31..b2030bd2 100644 --- a/src/ui/web/login/SessionPickerView.js +++ b/src/ui/web/login/SessionPickerView.js @@ -1,24 +1,30 @@ -import * as h from "../general/html.js"; import ListView from "../general/ListView.js"; +import TemplateView from "../general/TemplateView.js"; -class SessionPickerItem { - +class SessionPickerItem extends TemplateView { + render(t) { + return t.li([vm => `${vm.userId}@${vm.homeServer}`]); + } } export default class SessionPickerView extends TemplateView { mount() { - this._sessionList = new ListView({list: this._viewModel.sessions}, sessionInfo => { + this._sessionList = new ListView({ + list: this.viewModel.sessions, + onItemClick: (item) => { + this.viewModel.pick(item.viewModel.id); + }, + }, sessionInfo => { return new SessionPickerItem(sessionInfo); }); - super.mount(); + return super.mount(); } - render(t, vm) { - this._root = h.div({className: "SessionPickerView"}, [ + render(t) { + return t.div({className: "SessionPickerView"}, [ this._sessionList.mount(), - h.button() + t.button({onClick: () => this.viewModel.cancel()}, ["Cancel"]) ]); - } unmount() { From 09b9eff7c148dcf3132b5e41a025f51a1d91b932 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sun, 8 Sep 2019 10:19:16 +0200 Subject: [PATCH 5/7] fix remaining errors to make login work --- src/domain/BrawlViewModel.js | 15 ++++++++------- src/domain/SessionPickerViewModel.js | 6 +++--- src/main.js | 4 ++-- src/matrix/sync.js | 2 +- src/ui/web/login/LoginView.js | 10 +++++++--- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/domain/BrawlViewModel.js b/src/domain/BrawlViewModel.js index 5518f7e5..1d8d2f09 100644 --- a/src/domain/BrawlViewModel.js +++ b/src/domain/BrawlViewModel.js @@ -6,10 +6,10 @@ import SessionPickerViewModel from "./SessionPickerViewModel.js"; import EventEmitter from "../EventEmitter.js"; export default class BrawlViewModel extends EventEmitter { - constructor({createStorage, sessionsStore, createHsApi, clock}) { + constructor({createStorage, sessionStore, createHsApi, clock}) { super(); this._createStorage = createStorage; - this._sessionsStore = sessionsStore; + this._sessionStore = sessionStore; this._createHsApi = createHsApi; this._clock = clock; @@ -21,7 +21,7 @@ export default class BrawlViewModel extends EventEmitter { } async load() { - if (await this._sessionsStore.hasAnySession()) { + if (await this._sessionStore.hasAnySession()) { this._showPicker(); } else { this._showLogin(); @@ -31,7 +31,7 @@ export default class BrawlViewModel extends EventEmitter { async _showPicker() { this._clearSections(); this._sessionPickerViewModel = new SessionPickerViewModel({ - sessionsStore: this._sessionsStore, + sessionStore: this._sessionStore, sessionCallback: sessionInfo => this._onSessionPicked(sessionInfo) }); this.emit("change", "activeSection"); @@ -101,7 +101,7 @@ export default class BrawlViewModel extends EventEmitter { accessToken: loginData.access_token, lastUsed: this._clock.now() }; - await this._sessionsStore.add(sessionInfo); + await this._sessionStore.add(sessionInfo); this._loadSession(sessionInfo); } else { this._showPicker(); @@ -111,7 +111,7 @@ export default class BrawlViewModel extends EventEmitter { _onSessionPicked(sessionInfo) { if (sessionInfo) { this._loadSession(sessionInfo); - this._sessionsStore.updateLastUsed(sessionInfo.id, this._clock.now()); + this._sessionStore.updateLastUsed(sessionInfo.id, this._clock.now()); } else { this._showLogin(); } @@ -121,7 +121,7 @@ export default class BrawlViewModel extends EventEmitter { try { this._loading = true; this._loadingText = "Loading your conversations…"; - const hsApi = this._createHsApi(sessionInfo.homeServer); + const hsApi = this._createHsApi(sessionInfo.homeServer, sessionInfo.accessToken); const storage = await this._createStorage(sessionInfo.id); // no need to pass access token to session const filteredSessionInfo = { @@ -150,6 +150,7 @@ export default class BrawlViewModel extends EventEmitter { // start sending pending messages session.notifyNetworkAvailable(); } catch (err) { + console.error(err); this._error = err; } this.emit("change", "activeSection"); diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index 922e8775..3c7dff80 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -1,14 +1,14 @@ -import {SortedArray} from "./observables/index.js"; +import {SortedArray} from "../observable/index.js"; export default class SessionPickerViewModel { constructor({sessionStore, sessionCallback}) { - this._sessionsStore = sessionStore; + this._sessionStore = sessionStore; this._sessionCallback = sessionCallback; this._sessions = new SortedArray((s1, s2) => (s1.lastUsed || 0) - (s2.lastUsed || 0)); } async load() { - const sessions = await this._sessionsStore.getAll(); + const sessions = await this._sessionStore.getAll(); this._sessions.setManyUnsorted(sessions); } diff --git a/src/main.js b/src/main.js index 9b4333a7..78f5f881 100644 --- a/src/main.js +++ b/src/main.js @@ -8,8 +8,8 @@ export default async function main(container) { try { const vm = new BrawlViewModel({ createStorage: sessionId => createIdbStorage(`brawl_session_${sessionId}`), - createHsApi: homeServer => new HomeServerApi(homeServer), - sessionsStore: new SessionsStore("brawl_sessions_v1"), + createHsApi: (homeServer, accessToken = null) => new HomeServerApi(`https://${homeServer}`, accessToken), + sessionStore: new SessionsStore("brawl_sessions_v1"), clock: Date //just for `now` fn }); await vm.load(); diff --git a/src/matrix/sync.js b/src/matrix/sync.js index 270d72e3..33711c91 100644 --- a/src/matrix/sync.js +++ b/src/matrix/sync.js @@ -20,7 +20,7 @@ function parseRooms(roomsSection, roomCallback) { } export default class Sync extends EventEmitter { - constructor(hsApi, session, storage) { + constructor({hsApi, session, storage}) { super(); this._hsApi = hsApi; this._session = session; diff --git a/src/ui/web/login/LoginView.js b/src/ui/web/login/LoginView.js index 77dfde53..7718d72b 100644 --- a/src/ui/web/login/LoginView.js +++ b/src/ui/web/login/LoginView.js @@ -1,9 +1,13 @@ -import TemplateView from "./general/TemplateView.js"; +import TemplateView from "../general/TemplateView.js"; export default class LoginView extends TemplateView { + constructor(vm) { + super(vm, true); + } + render(t, vm) { const username = t.input({type: "text", placeholder: vm.usernamePlaceholder}); - const password = t.input({type: "password", placeholder: vm.usernamePlaceholder}); + const password = t.input({type: "password", placeholder: vm.passwordPlaceholder}); const homeserver = t.input({type: "text", placeholder: vm.hsPlaceholder, value: vm.defaultHomeServer}); return t.div({className: "login form"}, [ t.if(vm => vm.error, t => t.div({className: "error"}, vm => vm.error)), @@ -14,7 +18,7 @@ export default class LoginView extends TemplateView { onClick: () => vm.login(username.value, password.value, homeserver.value), disabled: vm => vm.loading }, "Log In")), - t.div(t.button({onClick: () => vm.cancel()}), "Cancel") + t.div(t.button({onClick: () => vm.cancel()}, ["Cancel"])) ]); } } From ed67689fdf326cd3e264da3641aa8b052e1c60c8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sun, 8 Sep 2019 10:40:05 +0200 Subject: [PATCH 6/7] some minimal styling for login and session picker --- src/ui/web/css/main.css | 32 +++++++++++++++++++++++++++ src/ui/web/login/LoginView.js | 5 +++-- src/ui/web/login/SessionPickerView.js | 5 +++-- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/ui/web/css/main.css b/src/ui/web/css/main.css index 18e1ba6c..64ba8698 100644 --- a/src/ui/web/css/main.css +++ b/src/ui/web/css/main.css @@ -26,3 +26,35 @@ body { justify-content: center; flex-direction: row; } + +.SessionPickerView { + padding: 0.4em; +} + +.SessionPickerView ul { + list-style: none; + padding: 0; +} + +.SessionPickerView li { + margin: 0.4em 0; + font-size: 1.2em; + background-color: grey; + padding: 0.5em; + cursor: pointer; +} + +.LoginView { + padding: 0.4em; +} + +.form > div { + margin: 0.4em 0; +} + +.form input { + display: block; + width: 100%; + box-sizing: border-box; +} + diff --git a/src/ui/web/login/LoginView.js b/src/ui/web/login/LoginView.js index 7718d72b..1285020c 100644 --- a/src/ui/web/login/LoginView.js +++ b/src/ui/web/login/LoginView.js @@ -9,7 +9,8 @@ export default class LoginView extends TemplateView { const username = t.input({type: "text", placeholder: vm.usernamePlaceholder}); const password = t.input({type: "password", placeholder: vm.passwordPlaceholder}); const homeserver = t.input({type: "text", placeholder: vm.hsPlaceholder, value: vm.defaultHomeServer}); - return t.div({className: "login form"}, [ + return t.div({className: "LoginView form"}, [ + t.h1(["Log in to your homeserver"]), t.if(vm => vm.error, t => t.div({className: "error"}, vm => vm.error)), t.div(username), t.div(password), @@ -18,7 +19,7 @@ export default class LoginView extends TemplateView { onClick: () => vm.login(username.value, password.value, homeserver.value), disabled: vm => vm.loading }, "Log In")), - t.div(t.button({onClick: () => vm.cancel()}, ["Cancel"])) + t.div(t.button({onClick: () => vm.cancel()}, ["Pick an existing session"])) ]); } } diff --git a/src/ui/web/login/SessionPickerView.js b/src/ui/web/login/SessionPickerView.js index b2030bd2..521c2f92 100644 --- a/src/ui/web/login/SessionPickerView.js +++ b/src/ui/web/login/SessionPickerView.js @@ -3,7 +3,7 @@ import TemplateView from "../general/TemplateView.js"; class SessionPickerItem extends TemplateView { render(t) { - return t.li([vm => `${vm.userId}@${vm.homeServer}`]); + return t.li([vm => vm.userId]); } } @@ -22,8 +22,9 @@ export default class SessionPickerView extends TemplateView { render(t) { return t.div({className: "SessionPickerView"}, [ + t.h1(["Pick a session"]), this._sessionList.mount(), - t.button({onClick: () => this.viewModel.cancel()}, ["Cancel"]) + t.button({onClick: () => this.viewModel.cancel()}, ["Log in to a new session instead"]) ]); } From 57bdec7237ec740ba84bede8c8f1812bf1b8ec5c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sun, 8 Sep 2019 10:52:06 +0200 Subject: [PATCH 7/7] update README --- README.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 09676d19..e95beeb9 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,15 @@ # Brawl -A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB + +A minimal [Matrix](https://matrix.org/) chat client, focused on performance, offline functionality and working on my Lumia 950 Windows Phone. ## Status -Syncing & storing rooms with state and timeline, with a minimal UI syncing room list and timeline on screen. Filling gaps supported, detecting overlapping events. The `[0/1]` in the gif below is the local event key, consisting of a fragment id and event index. No sending yet. Using Fractal here to update the room name and send messages: +Brawl can currently log you in, or pick an existing session, sync already joined rooms, fill gaps in the timeline, and send text messages. Everything is stored locally. -![Rooms and timeline syncing on-screen, gaps filling](https://bwindels.github.io/brawl-chat/images/morpheus-gaps.gif) +![Showing multiple sessions, and sending messages](https://bwindels.github.io/brawl-chat/images/brawl-sending.gif) -## Features that this approach would be well suited for +## Why - - store all fetched messages, not just synced ones - - fast local search (with words index) - - scroll timeline with date tooltip? - - jump to timestamp - - multi-account +I started writing Brawl both to have a functional matrix client on my aging phone, and to play around with some ideas I had how to use indexeddb optimally in a matrix client. For every interaction or network response (syncing, filling a gap), Brawl starts a transaction in indexedb, and only commits it once everything went well. This helps to keep your storage always in a consistent state. As little data is kept in memory as well, and while scrolling in the above GIF, everything is loaded straight from the storage. + +If you find this interesting, feel free to reach me at `@bwindels:matrix.org`.