mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-23 03:25:12 +01:00
show timeline when clicking room in roomlist
This commit is contained in:
parent
5cafb92fee
commit
6b4ed65a57
14
src/main.js
14
src/main.js
@ -2,8 +2,8 @@ import HomeServerApi from "./matrix/hs-api.js";
|
|||||||
import Session from "./matrix/session.js";
|
import Session from "./matrix/session.js";
|
||||||
import createIdbStorage from "./matrix/storage/idb/create.js";
|
import createIdbStorage from "./matrix/storage/idb/create.js";
|
||||||
import Sync from "./matrix/sync.js";
|
import Sync from "./matrix/sync.js";
|
||||||
import ListView from "./ui/web/ListView.js";
|
import SessionView from "./ui/web/SessionView.js";
|
||||||
import RoomTile from "./ui/web/RoomTile.js";
|
import SessionViewModel from "./ui/viewmodels/SessionViewModel.js";
|
||||||
|
|
||||||
const HOST = "localhost";
|
const HOST = "localhost";
|
||||||
const HOMESERVER = `http://${HOST}:8008`;
|
const HOMESERVER = `http://${HOST}:8008`;
|
||||||
@ -34,10 +34,10 @@ async function login(username, password, homeserver) {
|
|||||||
return {sessionId, loginData};
|
return {sessionId, loginData};
|
||||||
}
|
}
|
||||||
|
|
||||||
function showRooms(container, rooms) {
|
function showSession(container, session) {
|
||||||
const sortedRooms = rooms.sortValues((a, b) => a.name.localeCompare(b.name));
|
const vm = new SessionViewModel(session);
|
||||||
const listView = new ListView(sortedRooms, (room) => new RoomTile(room));
|
const view = new SessionView(vm);
|
||||||
container.appendChild(listView.mount());
|
container.appendChild(view.mount());
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
@ -54,7 +54,7 @@ export default async function main(label, button, container) {
|
|||||||
await session.setLoginData(loginData);
|
await session.setLoginData(loginData);
|
||||||
}
|
}
|
||||||
await session.load();
|
await session.load();
|
||||||
showRooms(container, session.rooms);
|
showSession(container, session);
|
||||||
const hsApi = new HomeServerApi(HOMESERVER, session.accessToken);
|
const hsApi = new HomeServerApi(HOMESERVER, session.accessToken);
|
||||||
console.log("session loaded");
|
console.log("session loaded");
|
||||||
if (!session.syncToken) {
|
if (!session.syncToken) {
|
||||||
|
@ -21,23 +21,27 @@ export default class RoomPersister {
|
|||||||
|
|
||||||
// }
|
// }
|
||||||
|
|
||||||
async persistSync(roomResponse, txn) {
|
persistSync(roomResponse, txn) {
|
||||||
let nextKey = this._lastSortKey;
|
let nextKey = this._lastSortKey;
|
||||||
const timeline = roomResponse.timeline;
|
const timeline = roomResponse.timeline;
|
||||||
|
const entries = [];
|
||||||
// is limited true for initial sync???? or do we need to handle that as a special case?
|
// is limited true for initial sync???? or do we need to handle that as a special case?
|
||||||
// I suppose it will, yes
|
// I suppose it will, yes
|
||||||
if (timeline.limited) {
|
if (timeline.limited) {
|
||||||
nextKey = nextKey.nextKeyWithGap();
|
nextKey = nextKey.nextKeyWithGap();
|
||||||
txn.roomTimeline.appendGap(this._roomId, nextKey, {prev_batch: timeline.prev_batch});
|
entries.push(this._createGapEntry(nextKey, timeline.prev_batch));
|
||||||
}
|
}
|
||||||
// const startOfChunkSortKey = nextKey;
|
// const startOfChunkSortKey = nextKey;
|
||||||
|
if (timeline.events) {
|
||||||
if (timeline.events) {
|
for(const event of timeline.events) {
|
||||||
for(const event of timeline.events) {
|
nextKey = nextKey.nextKey();
|
||||||
nextKey = nextKey.nextKey();
|
entries.push(this._createEventEntry(nextKey, event));
|
||||||
txn.roomTimeline.appendEvent(this._roomId, nextKey, event);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// write to store
|
||||||
|
for(const entry of entries) {
|
||||||
|
txn.roomTimeline.append(entry);
|
||||||
|
}
|
||||||
// right thing to do? if the txn fails, not sure we'll continue anyways ...
|
// right thing to do? if the txn fails, not sure we'll continue anyways ...
|
||||||
// only advance the key once the transaction has
|
// only advance the key once the transaction has
|
||||||
// succeeded
|
// succeeded
|
||||||
@ -55,13 +59,30 @@ export default class RoomPersister {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (timeline.events) {
|
if (timeline.events) {
|
||||||
if (state.events) {
|
for (const event of timeline.events) {
|
||||||
for (const event of timeline.events) {
|
if (typeof event.state_key === "string") {
|
||||||
if (typeof event.state_key === "string") {
|
txn.roomState.setStateEvent(this._roomId, event);
|
||||||
txn.roomState.setStateEvent(this._roomId, event);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return entries;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
_createGapEntry(sortKey, prevBatch) {
|
||||||
|
return {
|
||||||
|
roomId: this._roomId,
|
||||||
|
sortKey: sortKey.buffer,
|
||||||
|
event: null,
|
||||||
|
gap: {prev_batch: prevBatch}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_createEventEntry(sortKey, event) {
|
||||||
|
return {
|
||||||
|
roomId: this._roomId,
|
||||||
|
sortKey: sortKey.buffer,
|
||||||
|
event: event,
|
||||||
|
gap: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import EventEmitter from "../../EventEmitter.js";
|
||||||
import RoomSummary from "./summary.js";
|
import RoomSummary from "./summary.js";
|
||||||
import RoomPersister from "./persister.js";
|
import RoomPersister from "./persister.js";
|
||||||
import EventEmitter from "../../EventEmitter.js";
|
import Timeline from "./timeline.js";
|
||||||
|
|
||||||
export default class Room extends EventEmitter {
|
export default class Room extends EventEmitter {
|
||||||
constructor(roomId, storage, emitCollectionChange) {
|
constructor(roomId, storage, emitCollectionChange) {
|
||||||
@ -10,19 +11,23 @@ export default class Room extends EventEmitter {
|
|||||||
this._summary = new RoomSummary(roomId);
|
this._summary = new RoomSummary(roomId);
|
||||||
this._persister = new RoomPersister(roomId);
|
this._persister = new RoomPersister(roomId);
|
||||||
this._emitCollectionChange = emitCollectionChange;
|
this._emitCollectionChange = emitCollectionChange;
|
||||||
|
this._timeline = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
persistSync(roomResponse, membership, txn) {
|
persistSync(roomResponse, membership, txn) {
|
||||||
const changed = this._summary.applySync(roomResponse, membership, txn);
|
const summaryChanged = this._summary.applySync(roomResponse, membership, txn);
|
||||||
this._persister.persistSync(roomResponse, txn);
|
const newTimelineEntries = this._persister.persistSync(roomResponse, txn);
|
||||||
return changed;
|
return {summaryChanged, newTimelineEntries};
|
||||||
}
|
}
|
||||||
|
|
||||||
emitSync(changed) {
|
emitSync({summaryChanged, newTimelineEntries}) {
|
||||||
if (changed) {
|
if (summaryChanged) {
|
||||||
this.emit("change");
|
this.emit("change");
|
||||||
(this._emitCollectionChange)(this);
|
(this._emitCollectionChange)(this);
|
||||||
}
|
}
|
||||||
|
if (this._timeline) {
|
||||||
|
this._timeline.appendLiveEntries(newTimelineEntries);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
load(summary, txn) {
|
load(summary, txn) {
|
||||||
@ -37,4 +42,18 @@ export default class Room extends EventEmitter {
|
|||||||
get id() {
|
get id() {
|
||||||
return this._roomId;
|
return this._roomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async openTimeline() {
|
||||||
|
if (this._timeline) {
|
||||||
|
throw new Error("not dealing with load race here for now");
|
||||||
|
}
|
||||||
|
this._timeline = new Timeline({
|
||||||
|
roomId: this.id,
|
||||||
|
storage: this._storage,
|
||||||
|
closeCallback: () => this._timeline = null,
|
||||||
|
});
|
||||||
|
await this._timeline.load();
|
||||||
|
return this._timeline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +87,7 @@ export default class RoomSummary {
|
|||||||
this._membership = membership;
|
this._membership = membership;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
// state comes before timeline
|
||||||
if (roomResponse.state) {
|
if (roomResponse.state) {
|
||||||
changed = roomResponse.state.events.reduce((changed, e) => {
|
changed = roomResponse.state.events.reduce((changed, e) => {
|
||||||
return this._processEvent(e) || changed;
|
return this._processEvent(e) || changed;
|
||||||
|
36
src/matrix/room/timeline.js
Normal file
36
src/matrix/room/timeline.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { ObservableArray } from "../../observable/index.js";
|
||||||
|
|
||||||
|
export default class Timeline {
|
||||||
|
constructor({roomId, storage, closeCallback}) {
|
||||||
|
this._roomId = roomId;
|
||||||
|
this._storage = storage;
|
||||||
|
this._closeCallback = closeCallback;
|
||||||
|
this._entriesList = new ObservableArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @package */
|
||||||
|
async load() {
|
||||||
|
const txn = await this._storage.readTxn([this._storage.storeNames.roomTimeline]);
|
||||||
|
const entries = await txn.roomTimeline.lastEvents(this._roomId, 100);
|
||||||
|
for (const entry of entries) {
|
||||||
|
this._entriesList.append(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @package */
|
||||||
|
appendLiveEntries(newEntries) {
|
||||||
|
for (const entry of newEntries) {
|
||||||
|
this._entriesList.append(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
get entries() {
|
||||||
|
return this._entriesList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
close() {
|
||||||
|
this._closeCallback();
|
||||||
|
}
|
||||||
|
}
|
@ -18,20 +18,26 @@ export default class RoomTimelineStore {
|
|||||||
return this._timelineStore.selectLimit(range, amount);
|
return this._timelineStore.selectLimit(range, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
async eventsBefore(roomId, sortKey, amount) {
|
async eventsBefore(roomId, sortKey, amount) {
|
||||||
const range = IDBKeyRange.bound([roomId, SortKey.minKey.buffer], [roomId, sortKey.buffer], false, true);
|
const range = IDBKeyRange.bound([roomId, SortKey.minKey.buffer], [roomId, sortKey.buffer], false, true);
|
||||||
const events = await this._timelineStore.selectLimitReverse(range, amount);
|
const events = await this._timelineStore.selectLimitReverse(range, amount);
|
||||||
events.reverse(); // because we fetched them backwards
|
events.reverse(); // because we fetched them backwards
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// entry should have roomId, sortKey, event & gap keys
|
||||||
|
append(entry) {
|
||||||
|
this._timelineStore.add(entry);
|
||||||
|
}
|
||||||
|
// should this happen as part of a transaction that stores all synced in changes?
|
||||||
|
// e.g.:
|
||||||
|
// - timeline events for all rooms
|
||||||
|
// - latest sync token
|
||||||
|
// - new members
|
||||||
|
// - new room state
|
||||||
|
// - updated/new account data
|
||||||
|
|
||||||
|
|
||||||
// should this happen as part of a transaction that stores all synced in changes?
|
|
||||||
// e.g.:
|
|
||||||
// - timeline events for all rooms
|
|
||||||
// - latest sync token
|
|
||||||
// - new members
|
|
||||||
// - new room state
|
|
||||||
// - updated/new account data
|
|
||||||
|
|
||||||
appendGap(roomId, sortKey, gap) {
|
appendGap(roomId, sortKey, gap) {
|
||||||
this._timelineStore.add({
|
this._timelineStore.add({
|
||||||
|
@ -3,6 +3,7 @@ import FilteredMap from "./map/FilteredMap.js";
|
|||||||
import MappedMap from "./map/MappedMap.js";
|
import MappedMap from "./map/MappedMap.js";
|
||||||
import BaseObservableMap from "./map/BaseObservableMap.js";
|
import BaseObservableMap from "./map/BaseObservableMap.js";
|
||||||
// re-export "root" (of chain) collections
|
// re-export "root" (of chain) collections
|
||||||
|
export { default as ObservableArray} from "./list/ObservableArray.js";
|
||||||
export { default as ObservableMap } from "./map/ObservableMap.js";
|
export { default as ObservableMap } from "./map/ObservableMap.js";
|
||||||
|
|
||||||
// avoid circular dependency between these classes
|
// avoid circular dependency between these classes
|
||||||
|
21
src/observable/list/ObservableArray.js
Normal file
21
src/observable/list/ObservableArray.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import BaseObservableList from "./BaseObservableList.js";
|
||||||
|
|
||||||
|
export default class ObservableArray extends BaseObservableList {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._items = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
append(item) {
|
||||||
|
this._items.push(item);
|
||||||
|
this.emitAdd(this._items.length - 1, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
get length() {
|
||||||
|
return this._items.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this._items.values();
|
||||||
|
}
|
||||||
|
}
|
@ -55,6 +55,6 @@ export default class MappedMap extends BaseObservableMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator]() {
|
[Symbol.iterator]() {
|
||||||
return this._mappedValues.entries()[Symbol.iterator];
|
return this._mappedValues.entries();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
src/ui/viewmodels/RoomTileViewModel.js
Normal file
24
src/ui/viewmodels/RoomTileViewModel.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export default class RoomTileViewModel {
|
||||||
|
// we use callbacks to parent VM instead of emit because
|
||||||
|
// it would be annoying to keep track of subscriptions in
|
||||||
|
// parent for all RoomTileViewModels
|
||||||
|
// emitUpdate is ObservableMap/ObservableList update mechanism
|
||||||
|
constructor({room, emitUpdate, emitOpen}) {
|
||||||
|
this._room = room;
|
||||||
|
this._emitUpdate = emitUpdate;
|
||||||
|
this._emitOpen = emitOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
this._emitOpen(this._room);
|
||||||
|
}
|
||||||
|
|
||||||
|
compare(other) {
|
||||||
|
// sort by name for now
|
||||||
|
return this._room.name.localeCompare(other._room.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this._room.name;
|
||||||
|
}
|
||||||
|
}
|
36
src/ui/viewmodels/RoomViewModel.js
Normal file
36
src/ui/viewmodels/RoomViewModel.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import EventEmitter from "../../EventEmitter.js";
|
||||||
|
|
||||||
|
export default class RoomViewModel extends EventEmitter {
|
||||||
|
constructor(room) {
|
||||||
|
super();
|
||||||
|
this._room = room;
|
||||||
|
this._timeline = null;
|
||||||
|
this._onRoomChange = this._onRoomChange.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async enable() {
|
||||||
|
this._room.on("change", this._onRoomChange);
|
||||||
|
this._timeline = await this._room.openTimeline();
|
||||||
|
this.emit("change", "timelineEntries");
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (this._timeline) {
|
||||||
|
this._timeline.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// room doesn't tell us yet which fields changed,
|
||||||
|
// so emit all fields originating from summary
|
||||||
|
_onRoomChange() {
|
||||||
|
this.emit("change", "name");
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this._room.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get timelineEntries() {
|
||||||
|
return this._timeline && this._timeline.entries;
|
||||||
|
}
|
||||||
|
}
|
37
src/ui/viewmodels/SessionViewModel.js
Normal file
37
src/ui/viewmodels/SessionViewModel.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import EventEmitter from "../../EventEmitter.js";
|
||||||
|
import RoomTileViewModel from "./RoomTileViewModel.js";
|
||||||
|
import RoomViewModel from "./RoomViewModel.js";
|
||||||
|
|
||||||
|
export default class SessionViewModel extends EventEmitter {
|
||||||
|
constructor(session) {
|
||||||
|
super();
|
||||||
|
this._session = session;
|
||||||
|
this._currentRoomViewModel = null;
|
||||||
|
const roomTileVMs = this._session.rooms.mapValues((room, emitUpdate) => {
|
||||||
|
return new RoomTileViewModel({
|
||||||
|
room,
|
||||||
|
emitUpdate,
|
||||||
|
emitOpen: room => this._openRoom(room)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this._roomList = roomTileVMs.sortValues((a, b) => a.compare(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
get roomList() {
|
||||||
|
return this._roomList;
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentRoom() {
|
||||||
|
return this._currentRoomViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
_openRoom(room) {
|
||||||
|
if (this._currentRoomViewModel) {
|
||||||
|
this._currentRoomViewModel.disable();
|
||||||
|
}
|
||||||
|
this._currentRoomViewModel = new RoomViewModel(room);
|
||||||
|
this._currentRoomViewModel.enable();
|
||||||
|
this.emit("change", "currentRoom");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,34 +19,57 @@ function insertAt(parentNode, idx, childNode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class ListView {
|
export default class ListView {
|
||||||
constructor(collection, childCreator) {
|
constructor({list, onItemClick}, childCreator) {
|
||||||
this._collection = collection;
|
this._onItemClick = onItemClick;
|
||||||
|
this._list = list;
|
||||||
this._root = null;
|
this._root = null;
|
||||||
this._subscription = null;
|
this._subscription = null;
|
||||||
this._childCreator = childCreator;
|
this._childCreator = childCreator;
|
||||||
this._childInstances = null;
|
this._childInstances = null;
|
||||||
|
this._onClick = this._onClick.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
root() {
|
root() {
|
||||||
return this._root;
|
return this._root;
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {}
|
update(attributes) {
|
||||||
|
if (attributes.hasOwnProperty("list")) {
|
||||||
|
if (this._subscription) {
|
||||||
|
this._unloadList();
|
||||||
|
while (this._root.lastChild) {
|
||||||
|
this._root.lastChild.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._list = attributes.list;
|
||||||
|
this._loadList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mount() {
|
mount() {
|
||||||
this._subscription = this._collection.subscribe(this);
|
|
||||||
this._root = html.ul({className: "ListView"});
|
this._root = html.ul({className: "ListView"});
|
||||||
this._childInstances = [];
|
this._loadList();
|
||||||
for (let item of this._collection) {
|
if (this._onItemClick) {
|
||||||
const child = this._childCreator(item);
|
this._root.addEventListener("click", this._onClick);
|
||||||
this._childInstances.push(child);
|
|
||||||
const childDomNode = child.mount();
|
|
||||||
this._root.appendChild(childDomNode);
|
|
||||||
}
|
}
|
||||||
return this._root;
|
return this._root;
|
||||||
}
|
}
|
||||||
|
|
||||||
unmount() {
|
unmount() {
|
||||||
|
this._unloadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onClick(event) {
|
||||||
|
let childNode = event.target;
|
||||||
|
while (childNode.parentNode !== this._root) {
|
||||||
|
childNode = childNode.parentNode;
|
||||||
|
}
|
||||||
|
const index = Array.prototype.indexOf.call(this._root.childNodes, childNode);
|
||||||
|
const childView = this._childInstances[index];
|
||||||
|
this._onItemClick(childView);
|
||||||
|
}
|
||||||
|
|
||||||
|
_unloadList() {
|
||||||
this._subscription = this._subscription();
|
this._subscription = this._subscription();
|
||||||
for (let child of this._childInstances) {
|
for (let child of this._childInstances) {
|
||||||
child.unmount();
|
child.unmount();
|
||||||
@ -54,6 +77,20 @@ export default class ListView {
|
|||||||
this._childInstances = null;
|
this._childInstances = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_loadList() {
|
||||||
|
if (!this._list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._subscription = this._list.subscribe(this);
|
||||||
|
this._childInstances = [];
|
||||||
|
for (let item of this._list) {
|
||||||
|
const child = this._childCreator(item);
|
||||||
|
this._childInstances.push(child);
|
||||||
|
const childDomNode = child.mount();
|
||||||
|
this._root.appendChild(childDomNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onAdd(idx, value) {
|
onAdd(idx, value) {
|
||||||
const child = this._childCreator(value);
|
const child = this._childCreator(value);
|
||||||
this._childInstances.splice(idx, 0, child);
|
this._childInstances.splice(idx, 0, child);
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { li } from "./html.js";
|
import { li } from "./html.js";
|
||||||
|
|
||||||
export default class RoomTile {
|
export default class RoomTile {
|
||||||
constructor(room) {
|
constructor(viewModel) {
|
||||||
this._room = room;
|
this._viewModel = viewModel;
|
||||||
this._root = null;
|
this._root = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
mount() {
|
mount() {
|
||||||
this._root = li(null, this._room.name);
|
this._root = li(null, this._viewModel.name);
|
||||||
return this._root;
|
return this._root;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,7 +16,11 @@ export default class RoomTile {
|
|||||||
|
|
||||||
update() {
|
update() {
|
||||||
// no data-binding yet
|
// no data-binding yet
|
||||||
this._root.innerText = this._room.name;
|
this._root.innerText = this._viewModel.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
clicked() {
|
||||||
|
this._viewModel.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
root() {
|
root() {
|
||||||
|
49
src/ui/web/RoomView.js
Normal file
49
src/ui/web/RoomView.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import TimelineTile from "./TimelineTile.js";
|
||||||
|
import ListView from "./ListView.js";
|
||||||
|
import * as html from "./html.js";
|
||||||
|
|
||||||
|
export default class RoomView {
|
||||||
|
constructor(viewModel) {
|
||||||
|
this._viewModel = viewModel;
|
||||||
|
this._root = null;
|
||||||
|
this._timelineList = null;
|
||||||
|
this._nameLabel = null;
|
||||||
|
this._onViewModelUpdate = this._onViewModelUpdate.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
mount() {
|
||||||
|
this._viewModel.on("change", this._onViewModelUpdate);
|
||||||
|
this._nameLabel = html.h2(null, this._viewModel.name);
|
||||||
|
this._timelineList = new ListView({
|
||||||
|
list: this._viewModel.timelineEntries
|
||||||
|
}, entry => new TimelineTile(entry));
|
||||||
|
this._timelineList.mount();
|
||||||
|
|
||||||
|
this._root = html.div({className: "RoomView"}, [
|
||||||
|
this._nameLabel,
|
||||||
|
this._timelineList.root()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return this._root;
|
||||||
|
}
|
||||||
|
|
||||||
|
unmount() {
|
||||||
|
this._timelineList.unmount();
|
||||||
|
this._viewModel.off("change", this._onViewModelUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
root() {
|
||||||
|
return this._root;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onViewModelUpdate(prop) {
|
||||||
|
if (prop === "name") {
|
||||||
|
this._nameLabel.innerText = this._viewModel.name;
|
||||||
|
}
|
||||||
|
else if (prop === "timelineEntries") {
|
||||||
|
this._timelineList.update({list: this._viewModel.timelineEntries});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {}
|
||||||
|
}
|
67
src/ui/web/SessionView.js
Normal file
67
src/ui/web/SessionView.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import ListView from "./ListView.js";
|
||||||
|
import RoomTile from "./RoomTile.js";
|
||||||
|
import RoomView from "./RoomView.js";
|
||||||
|
import { div } from "./html.js";
|
||||||
|
|
||||||
|
export default class SessionView {
|
||||||
|
constructor(viewModel) {
|
||||||
|
this._viewModel = viewModel;
|
||||||
|
this._roomList = null;
|
||||||
|
this._currentRoom = null;
|
||||||
|
this._root = null;
|
||||||
|
this._onViewModelChange = this._onViewModelChange.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
root() {
|
||||||
|
return this._root;
|
||||||
|
}
|
||||||
|
|
||||||
|
mount() {
|
||||||
|
this._viewModel.on("change", this._onViewModelChange);
|
||||||
|
|
||||||
|
this._root = div({className: "SessionView"});
|
||||||
|
this._roomList = new ListView(
|
||||||
|
{
|
||||||
|
list: this._viewModel.roomList,
|
||||||
|
onItemClick: roomTile => roomTile.clicked()
|
||||||
|
},
|
||||||
|
(room) => new RoomTile(room)
|
||||||
|
);
|
||||||
|
this._roomList.mount();
|
||||||
|
this._root.appendChild(this._roomList.root());
|
||||||
|
|
||||||
|
this._updateCurrentRoom();
|
||||||
|
return this._root;
|
||||||
|
}
|
||||||
|
|
||||||
|
unmount() {
|
||||||
|
this._roomList.unmount();
|
||||||
|
if (this._room) {
|
||||||
|
this._room.unmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._viewModel.off("change", this._onViewModelChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onViewModelChange(prop) {
|
||||||
|
if (prop === "currentRoom") {
|
||||||
|
this._updateCurrentRoom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
src/ui/web/TimelineTile.js
Normal file
56
src/ui/web/TimelineTile.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import * as html from "./html.js";
|
||||||
|
|
||||||
|
function tileText(event) {
|
||||||
|
const content = event.content;
|
||||||
|
switch (event.type) {
|
||||||
|
case "m.room.message": {
|
||||||
|
const msgtype = content.msgtype;
|
||||||
|
switch (msgtype) {
|
||||||
|
case "m.text":
|
||||||
|
return content.body;
|
||||||
|
default:
|
||||||
|
return `unsupported msgtype: ${msgtype}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "m.room.name":
|
||||||
|
return `changed the room name to "${content.name}"`;
|
||||||
|
case "m.room.member":
|
||||||
|
return `changed membership to ${content.membership}`;
|
||||||
|
default:
|
||||||
|
return `unsupported event type: ${event.type}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TimelineTile {
|
||||||
|
constructor(entry) {
|
||||||
|
this._entry = entry;
|
||||||
|
this._root = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
root() {
|
||||||
|
return this._root;
|
||||||
|
}
|
||||||
|
|
||||||
|
mount() {
|
||||||
|
let children;
|
||||||
|
if (this._entry.gap) {
|
||||||
|
children = [
|
||||||
|
html.strong(null, "Gap"),
|
||||||
|
" with prev_batch ",
|
||||||
|
html.strong(null, this._entry.gap.prev_batch)
|
||||||
|
];
|
||||||
|
} else if (this._entry.event) {
|
||||||
|
const event = this._entry.event;
|
||||||
|
children = [
|
||||||
|
html.strong(null, event.sender),
|
||||||
|
`: ${tileText(event)}`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
this._root = html.li(null, children);
|
||||||
|
return this._root;
|
||||||
|
}
|
||||||
|
|
||||||
|
unmount() {}
|
||||||
|
|
||||||
|
update() {}
|
||||||
|
}
|
@ -47,3 +47,4 @@ export function section(... params) { return el("section", ... params); }
|
|||||||
export function main(... params) { return el("main", ... params); }
|
export function main(... params) { return el("main", ... params); }
|
||||||
export function article(... params) { return el("article", ... params); }
|
export function article(... params) { return el("article", ... params); }
|
||||||
export function aside(... params) { return el("aside", ... params); }
|
export function aside(... params) { return el("aside", ... params); }
|
||||||
|
export function pre(... params) { return el("pre", ... params); }
|
||||||
|
Loading…
Reference in New Issue
Block a user