diff --git a/src/domain/BrawlViewModel.js b/src/domain/BrawlViewModel.js index cab4846e..d24c4b3d 100644 --- a/src/domain/BrawlViewModel.js +++ b/src/domain/BrawlViewModel.js @@ -6,9 +6,9 @@ import SessionPickerViewModel from "./SessionPickerViewModel.js"; import EventEmitter from "../EventEmitter.js"; export default class BrawlViewModel extends EventEmitter { - constructor({createStorage, sessionStore, createHsApi, clock}) { + constructor({storageFactory, sessionStore, createHsApi, clock}) { super(); - this._createStorage = createStorage; + this._storageFactory = storageFactory; this._sessionStore = sessionStore; this._createHsApi = createHsApi; this._clock = clock; @@ -32,6 +32,7 @@ export default class BrawlViewModel extends EventEmitter { this._clearSections(); this._sessionPickerViewModel = new SessionPickerViewModel({ sessionStore: this._sessionStore, + storageFactory: this._storageFactory, sessionCallback: sessionInfo => this._onSessionPicked(sessionInfo) }); this.emit("change", "activeSection"); @@ -122,7 +123,7 @@ export default class BrawlViewModel extends EventEmitter { this._loading = true; this._loadingText = "Loading your conversations…"; const hsApi = this._createHsApi(sessionInfo.homeServer, sessionInfo.accessToken); - const storage = await this._createStorage(sessionInfo.id); + const storage = await this._storageFactory.create(sessionInfo.id); // no need to pass access token to session const filteredSessionInfo = { deviceId: sessionInfo.deviceId, diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index 3c7dff80..2fd25e22 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -1,7 +1,54 @@ import {SortedArray} from "../observable/index.js"; +import EventEmitter from "../EventEmitter.js"; + +class SessionItemViewModel extends EventEmitter { + constructor(sessionInfo, pickerVM) { + super(); + this._pickerVM = pickerVM; + this._sessionInfo = sessionInfo; + this._isDeleting = false; + this._error = null; + } + + get error() { + return this._error && this._error.message; + } + + async delete() { + this._isDeleting = true; + this.emit("change", "isDeleting"); + try { + await this._pickerVM.delete(this.id); + } catch(err) { + this._error = err; + console.error(err); + this.emit("change", "error"); + } finally { + this._isDeleting = false; + this.emit("change", "isDeleting"); + } + } + + get isDeleting() { + return this._isDeleting; + } + + get id() { + return this._sessionInfo.id; + } + + get userId() { + return this._sessionInfo.userId; + } + + get lastUsed() { + return this._sessionInfo.lastUsed; + } +} export default class SessionPickerViewModel { - constructor({sessionStore, sessionCallback}) { + constructor({storageFactory, sessionStore, sessionCallback}) { + this._storageFactory = storageFactory; this._sessionStore = sessionStore; this._sessionCallback = sessionCallback; this._sessions = new SortedArray((s1, s2) => (s1.lastUsed || 0) - (s2.lastUsed || 0)); @@ -9,7 +56,7 @@ export default class SessionPickerViewModel { async load() { const sessions = await this._sessionStore.getAll(); - this._sessions.setManyUnsorted(sessions); + this._sessions.setManyUnsorted(sessions.map(s => new SessionItemViewModel(s, this))); } pick(id) { @@ -19,6 +66,13 @@ export default class SessionPickerViewModel { } } + async delete(id) { + const idx = this._sessions.array.findIndex(s => s.id === id); + await this._sessionStore.delete(id); + await this._storageFactory.delete(id); + this._sessions.remove(idx); + } + get sessions() { return this._sessions; } diff --git a/src/main.js b/src/main.js index 6ce6cc32..6d6ed505 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,5 @@ import HomeServerApi from "./matrix/hs-api.js"; -import createIdbStorage from "./matrix/storage/idb/create.js"; +import StorageFactory from "./matrix/storage/idb/create.js"; import SessionsStore from "./matrix/sessions-store/localstorage/SessionsStore.js"; import BrawlViewModel from "./domain/BrawlViewModel.js"; import BrawlView from "./ui/web/BrawlView.js"; @@ -7,7 +7,7 @@ import BrawlView from "./ui/web/BrawlView.js"; export default async function main(container) { try { const vm = new BrawlViewModel({ - createStorage: sessionId => createIdbStorage(`brawl_session_${sessionId}`), + storageFactory: new StorageFactory(), createHsApi: (homeServer, accessToken = null) => new HomeServerApi(homeServer, accessToken), sessionStore: new SessionsStore("brawl_sessions_v1"), clock: Date //just for `now` fn diff --git a/src/matrix/sessions-store/localstorage/SessionsStore.js b/src/matrix/sessions-store/localstorage/SessionsStore.js index 424dac62..dbe6fda5 100644 --- a/src/matrix/sessions-store/localstorage/SessionsStore.js +++ b/src/matrix/sessions-store/localstorage/SessionsStore.js @@ -42,4 +42,11 @@ export default class SessionsStore { sessions.push(sessionInfo); localStorage.setItem(this._name, JSON.stringify(sessions)); } + + async delete(sessionId) { + let sessions = await this.getAll(); + sessions = sessions.filter(s => s.id !== sessionId); + localStorage.setItem(this._name, JSON.stringify(sessions)); + } + } diff --git a/src/matrix/storage/idb/create.js b/src/matrix/storage/idb/create.js index 5c476679..633b24f7 100644 --- a/src/matrix/storage/idb/create.js +++ b/src/matrix/storage/idb/create.js @@ -1,9 +1,18 @@ import Storage from "./storage.js"; -import { openDatabase } from "./utils.js"; +import { openDatabase, reqAsPromise } from "./utils.js"; -export default async function createIdbStorage(databaseName) { - const db = await openDatabase(databaseName, createStores, 1); - return new Storage(db); +export default class StorageFactory { + async create(sessionId) { + const databaseName = `brawl_session_${sessionId}`; + const db = await openDatabase(databaseName, createStores, 1); + return new Storage(db); + } + + delete(sessionId) { + const databaseName = `brawl_session_${sessionId}`; + const req = window.indexedDB.deleteDatabase(databaseName); + return reqAsPromise(req); + } } function createStores(db) { diff --git a/src/ui/web/css/main.css b/src/ui/web/css/main.css index 64ba8698..da9f5e23 100644 --- a/src/ui/web/css/main.css +++ b/src/ui/web/css/main.css @@ -42,6 +42,15 @@ body { background-color: grey; padding: 0.5em; cursor: pointer; + display: flex; +} + +.SessionPickerView li span.userId { + flex: 1; +} + +.SessionPickerView li span.error { + margin: 0 20px; } .LoginView { diff --git a/src/ui/web/general/TemplateView.js b/src/ui/web/general/TemplateView.js index da7f4938..f6be9292 100644 --- a/src/ui/web/general/TemplateView.js +++ b/src/ui/web/general/TemplateView.js @@ -32,6 +32,8 @@ export default class TemplateView { } update(value, prop) { - this._template.update(value); + if (this._template) { + this._template.update(value); + } } } diff --git a/src/ui/web/login/SessionPickerView.js b/src/ui/web/login/SessionPickerView.js index 521c2f92..363158ba 100644 --- a/src/ui/web/login/SessionPickerView.js +++ b/src/ui/web/login/SessionPickerView.js @@ -2,8 +2,25 @@ import ListView from "../general/ListView.js"; import TemplateView from "../general/TemplateView.js"; class SessionPickerItem extends TemplateView { + constructor(vm) { + super(vm, true); + this._onDeleteClick = this._onDeleteClick.bind(this); + } + + _onDeleteClick(event) { + event.stopPropagation(); + event.preventDefault(); + this.viewModel.delete(); + } + render(t) { - return t.li([vm => vm.userId]); + const deleteButton = t.button({ + disabled: vm => vm.isDeleting, + onClick: event => this._onDeleteClick(event) + }, "Delete"); + const userName = t.span({className: "userId"}, vm => vm.userId); + const errorMessage = t.if(vm => vm.error, t => t.span({className: "error"}, vm => vm.error)); + return t.li([userName, errorMessage, deleteButton]); } }