first draft of url navigation for grid

This commit is contained in:
Bruno Windels 2020-10-12 17:49:06 +02:00
parent 6c2c29a7da
commit b2d6b7014b
11 changed files with 539 additions and 201 deletions

View file

@ -98,7 +98,7 @@ export class RootViewModel extends ViewModel {
createSessionContainer: this._createSessionContainer,
ready: sessionContainer => {
const url = this.urlRouter.urlForSegment("session", sessionContainer.sessionId);
this.urlRouter.replaceUrl(url);
this.urlRouter.history.replaceUrl(url);
this._showSession(sessionContainer);
},
}));

View file

@ -28,10 +28,26 @@ export class Navigation {
}
applyPath(path) {
const oldPath = this._path;
this._path = path;
for (const [type, observable] of this._observables) {
// if the value did not change, this won't emit
observable.set(this._path.get(type)?.value);
// clear values not in the new path in reverse order of path
for (let i = oldPath.segments.length - 1; i >= 0; i -= 1) {
const segment = oldPath[i];
if (!this._path.get(segment.type)) {
const observable = this._observables.get(segment.type);
if (observable) {
observable.set(segment.type, undefined);
}
}
}
// change values in order of path
for (const segment of this._path.segments) {
const observable = this._observables.get(segment.type);
if (observable) {
if (!segmentValueEqual(segment?.value, observable.get())) {
observable.set(segment.type, segment.value);
}
}
}
}
@ -55,6 +71,27 @@ export class Navigation {
}
return new Path(segments, this._allowsChild);
}
segment(type, value) {
return new Segment(type, value);
}
}
function segmentValueEqual(a, b) {
if (a === b) {
return true;
}
// allow (sparse) arrays
if (Array.isArray(a) && Array.isArray(b)) {
const len = Math.max(a.length, b.length);
for (let i = 0; i < len; i += 1) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
return false;
}
export class Segment {
@ -89,6 +126,14 @@ class Path {
return null;
}
until(type) {
const index = this._segments.findIndex(s => s.type === type);
if (index !== -1) {
return new Path(this._segments.slice(0, index + 1), this._allowsChild)
}
return new Path([], this._allowsChild);
}
get(type) {
return this._segments.find(s => s.type === type);
}

View file

@ -17,69 +17,78 @@ limitations under the License.
import {Segment} from "./Navigation.js";
export class URLRouter {
constructor({history, navigation, redirect}) {
constructor({history, navigation, parseUrlPath, stringifyPath}) {
this._subscription = null;
this._history = history;
this._navigation = navigation;
this._redirect = redirect;
this._parseUrlPath = parseUrlPath;
this._stringifyPath = stringifyPath;
}
attach() {
this._subscription = this._history.subscribe(url => {
this.applyUrl(url);
const redirectedUrl = this.applyUrl(url);
if (redirectedUrl !== url) {
this._history.replaceUrl(redirectedUrl);
}
});
this.applyUrl(this._history.get());
}
applyUrl(url) {
const segments = this._segmentsFromUrl(url);
const path = this._redirect(segments, this._navigation);
this._navigation.applyPath(path);
}
stop() {
dispose() {
this._subscription = this._subscription();
}
_segmentsFromUrl(url) {
const path = this._history.urlAsPath(url);
const parts = path.split("/").filter(p => !!p);
let index = 0;
const segments = [];
while (index < parts.length) {
const type = parts[index];
if ((index + 1) < parts.length) {
index += 1;
const value = parts[index];
segments.push(new Segment(type, value));
} else {
segments.push(new Segment(type));
}
index += 1;
}
return segments;
applyUrl(url) {
const urlPath = this._history.urlAsPath(url)
const navPath = this._navigation.pathFrom(this._parseUrlPath(urlPath));
this._navigation.applyPath(navPath);
return this._history.pathAsUrl(this._stringifyPath(navPath));
}
get history() {
return this._history;
}
urlForSegment(type, value) {
const path = this._navigation.path.with(new Segment(type, value));
if (path) {
return this.urlForPath(path);
urlForSegments(segments) {
let path = this._navigation.path;
for (const segment of segments) {
path = path.with(segment);
if (!path) {
return;
}
}
return this.urlForPath(path);
}
urlForSegment(type, value) {
return this.urlForSegments([this._navigation.segment(type, value)]);
}
urlForPath(path) {
let urlPath = "";
for (const {type, value} of path.segments) {
if (typeof value === "boolean") {
urlPath += `/${type}`;
} else {
urlPath += `/${type}/${value}`;
}
}
return this.history.pathAsUrl(this._stringifyPath(path));
}
openRoomActionUrl(roomId) {
// not a segment to navigation knowns about, so append it manually
const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${roomId}`;
return this._history.pathAsUrl(urlPath);
}
disableGridUrl() {
}
enableGridUrl() {
let path = this._navigation.path.until("session");
const room = this._navigation.get("room");
if (room) {
path = path.with(this._navigation.segment("rooms", [room.value]));
path = path.with(room);
} else {
path = path.with(this._navigation.segment("rooms", []));
path = path.with(this._navigation.segment("empty-grid-tile", 0));
}
return this.urlForPath(path);
}
}

View file

@ -18,49 +18,30 @@ import {Navigation, Segment} from "./Navigation.js";
import {URLRouter} from "./URLRouter.js";
export function createNavigation() {
return new Navigation(function allowsChild(parent, child) {
const {type} = child;
switch (parent?.type) {
case undefined:
// allowed root segments
return type === "login" || type === "session";
case "session":
return type === "room" || type === "rooms" || type === "settings";
case "rooms":
// downside of the approach: both of these will control which tile is selected
return type === "room" || type === "empty-grid-tile";
default:
return false;
}
});
return new Navigation(allowsChild);
}
export function createRouter({history, navigation}) {
return new URLRouter({history, navigation, redirect});
return new URLRouter({history, navigation, stringifyPath, parseUrlPath});
}
function redirect(urlParts, navigation) {
const {path} = navigation;
const segments = urlParts.reduce((output, s) => {
// redirect open-room action to grid/non-grid url
if (s.type === "open-room") {
const rooms = path.get("rooms");
if (rooms) {
output = output.concat(roomsSegmentWithRoom(rooms, s.value, path));
}
return rooms.concat(new Segment("room", s.value));
}
return output.concat(s);
}, []);
return navigation.pathFrom(segments);
function allowsChild(parent, child) {
const {type} = child;
switch (parent?.type) {
case undefined:
// allowed root segments
return type === "login" || type === "session";
case "session":
return type === "room" || type === "rooms" || type === "settings";
case "rooms":
// downside of the approach: both of these will control which tile is selected
return type === "room" || type === "empty-grid-tile";
default:
return false;
}
}
function roomsSegmentWithRoom(rooms, roomId, path) {
// find the index of either the current room,
// or the current selected empty tile,
// to put the new room in
// TODO: is rooms.value a string or an array?
const room = path.get("room");
let index = 0;
if (room) {
@ -71,20 +52,157 @@ function roomsSegmentWithRoom(rooms, roomId, path) {
index = emptyGridTile.value;
}
}
const newRooms = rooms.slice();
const newRooms = rooms.value.slice();
newRooms[index] = roomId;
return new Segment("rooms", newRooms);
}
function parseUrlValue(type, iterator) {
if (type === "rooms") {
const roomIds = iterator.next().value.split(",");
const selectedIndex = parseInt(iterator.next().value, 10);
const roomId = roomIds[selectedIndex];
if (roomId) {
return [new Segment(type, roomIds), new Segment("room", roomId)];
export function parseUrlPath(urlPath, currentNavPath) {
// substr(1) to take of initial /
const parts = urlPath.substr(1).split("/");
const iterator = parts[Symbol.iterator]();
const segments = [];
let next;
while (!(next = iterator.next()).done) {
const type = next.value;
if (type === "rooms") {
const roomsValue = iterator.next().value;
if (!roomsValue) { break; }
const roomIds = roomsValue.split(",");
segments.push(new Segment(type, roomIds));
const selectedIndex = parseInt(iterator.next().value || "0", 10);
const roomId = roomIds[selectedIndex];
if (roomId) {
segments.push(new Segment("room", roomId));
} else {
segments.push(new Segment("empty-grid-tile", selectedIndex));
}
} else if (type === "open-room") {
const roomId = iterator.next().value;
if (!roomId) { break; }
const rooms = currentNavPath.get("rooms");
if (rooms) {
segments.push(roomsSegmentWithRoom(rooms, roomId, currentNavPath));
}
segments.push(new Segment("room", roomId));
} else {
return [new Segment(type, roomIds), new Segment("empty-grid-tile", selectedIndex)];
// might be undefined, which will be turned into true by Segment
const value = iterator.next().value;
segments.push(new Segment(type, value));
}
}
return segments;
}
export function stringifyPath(path) {
let urlPath = "";
let prevSegment;
for (const segment of path.segments) {
switch (segment.type) {
case "rooms":
urlPath += `/rooms/${segment.value.join(",")}`;
break;
case "empty-grid-tile":
urlPath += `/${segment.value}`;
break;
case "room":
if (prevSegment?.type === "rooms") {
const index = prevSegment.value.indexOf(segment.value);
urlPath += `/${index}`;
} else {
urlPath += `/${segment.type}/${segment.value}`;
}
break;
default:
urlPath += `/${segment.type}`;
if (segment.value && segment.value !== true) {
urlPath += `/${segment.value}`;
}
}
prevSegment = segment;
}
return urlPath;
}
export function tests() {
return {
"stringify grid url with focused empty tile": assert => {
const nav = new Navigation(allowsChild);
const path = nav.pathFrom([
new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]),
new Segment("empty-grid-tile", 3)
]);
const urlPath = stringifyPath(path);
assert.equal(urlPath, "/session/1/rooms/a,b,c/3");
},
"stringify grid url with focused room": assert => {
const nav = new Navigation(allowsChild);
const path = nav.pathFrom([
new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]),
new Segment("room", "b")
]);
const urlPath = stringifyPath(path);
assert.equal(urlPath, "/session/1/rooms/a,b,c/1");
},
"parse grid url path with focused empty tile": assert => {
const segments = parseUrlPath("/session/1/rooms/a,b,c/3");
assert.equal(segments.length, 3);
assert.equal(segments[0].type, "session");
assert.equal(segments[0].value, "1");
assert.equal(segments[1].type, "rooms");
assert.deepEqual(segments[1].value, ["a", "b", "c"]);
assert.equal(segments[2].type, "empty-grid-tile");
assert.equal(segments[2].value, 3);
},
"parse grid url path with focused room": assert => {
const segments = parseUrlPath("/session/1/rooms/a,b,c/1");
assert.equal(segments.length, 3);
assert.equal(segments[0].type, "session");
assert.equal(segments[0].value, "1");
assert.equal(segments[1].type, "rooms");
assert.deepEqual(segments[1].value, ["a", "b", "c"]);
assert.equal(segments[2].type, "room");
assert.equal(segments[2].value, "b");
},
"parse open-room action replacing the current focused room": assert => {
const nav = new Navigation(allowsChild);
const path = nav.pathFrom([
new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]),
new Segment("room", "b")
]);
const segments = parseUrlPath("/session/1/open-room/d", path);
assert.equal(segments.length, 3);
assert.equal(segments[0].type, "session");
assert.equal(segments[0].value, "1");
assert.equal(segments[1].type, "rooms");
assert.deepEqual(segments[1].value, ["a", "d", "c"]);
assert.equal(segments[2].type, "room");
assert.equal(segments[2].value, "d");
},
"parse open-room action setting a room in an empty tile": assert => {
const nav = new Navigation(allowsChild);
const path = nav.pathFrom([
new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]),
new Segment("empty-grid-tile", 4)
]);
const segments = parseUrlPath("/session/1/open-room/d", path);
assert.equal(segments.length, 3);
assert.equal(segments[0].type, "session");
assert.equal(segments[0].value, "1");
assert.equal(segments[1].type, "rooms");
assert.deepEqual(segments[1].value, ["a", "b", "c", , "d"]); //eslint-disable-line no-sparse-arrays
assert.equal(segments[2].type, "room");
assert.equal(segments[2].value, "d");
},
"parse session url path without id": assert => {
const segments = parseUrlPath("/session");
assert.equal(segments.length, 1);
assert.equal(segments[0].type, "session");
assert.strictEqual(segments[0].value, true);
}
}
}

View file

@ -19,10 +19,46 @@ import {ViewModel} from "../ViewModel.js";
export class RoomGridViewModel extends ViewModel {
constructor(options) {
super(options);
this._width = options.width;
this._height = options.height;
this._createRoomViewModel = options.createRoomViewModel;
this._selectedIndex = 0;
this._viewModels = [];
this._viewModels = (options.roomIds || []).map(roomId => {
if (roomId) {
const vm = this._createRoomViewModel(roomId);
if (vm) {
return this.track(vm);
}
}
});
this._setupNavigation();
}
_setupNavigation() {
const focusTileIndex = this.navigation.observe("empty-grid-tile");
this.track(focusTileIndex.subscribe(index => {
if (typeof index === "number") {
this._setFocusIndex(index);
}
}));
if (typeof focusTileIndex.get() === "number") {
this._selectedIndex = focusTileIndex.get();
}
const focusedRoom = this.navigation.get("room");
this.track(focusedRoom.subscribe(roomId => {
if (roomId) {
this._openRoom(roomId);
}
}));
if (focusedRoom.get()) {
const index = this._viewModels.findIndex(vm => vm && vm.id === focusedRoom.get());
if (index >= 0) {
this._selectedIndex = index;
}
}
}
roomViewModelAt(i) {
@ -33,15 +69,6 @@ export class RoomGridViewModel extends ViewModel {
return this._selectedIndex;
}
setFocusIndex(idx) {
if (idx === this._selectedIndex) {
return;
}
this._selectedIndex = idx;
const vm = this._viewModels[this._selectedIndex];
vm?.focus();
this.emitChange("focusedIndex");
}
get width() {
return this._width;
}
@ -50,41 +77,91 @@ export class RoomGridViewModel extends ViewModel {
return this._height;
}
/**
* Sets a pair of room and room tile view models at the current index
* @param {RoomViewModel} vm
* @package
*/
setRoomViewModel(vm) {
const old = this._viewModels[this._selectedIndex];
this.disposeTracked(old);
this._viewModels[this._selectedIndex] = this.track(vm);
this.emitChange(`${this._selectedIndex}`);
focusTile(index) {
if (index === this._selectedIndex) {
return;
}
let path = this.navigation.path;
const vm = this._viewModels[index];
if (vm) {
path = path.with(this.navigation.segment("room", vm.id));
} else {
path = path.with(this.navigation.segment("empty-grid-tile", index));
}
let url = this.urlRouter.urlForPath(path);
url = this.urlRouter.applyUrl(url);
this.urlRouter.history.pushUrl(url);
}
/**
* @package
*/
tryFocusRoom(roomId) {
/** called from SessionViewModel */
setRoomIds(roomIds) {
let changed = false;
const len = this._height * this._width;
for (let i = 0; i < len; i += 1) {
const newId = roomIds[i];
const vm = this._viewModels[i];
if (newId && !vm) {
this._viewModels[i] = this.track(this._createRoomViewModel(newId));
changed = true;
} else if (newId !== vm?.id) {
this._viewModels[i] = this.disposeTracked(this._viewModels[i]);
if (newId) {
this._viewModels[i] = this.track(this._createRoomViewModel(newId));
}
changed = true;
}
}
if (changed) {
this.emitChange();
}
}
/** called from SessionViewModel */
transferRoomViewModel(index, roomVM) {
const oldVM = this._viewModels[index];
this.disposeTracked(oldVM);
this._viewModels[index] = this.track(roomVM);
}
/** called from SessionViewModel */
releaseRoomViewModel(roomId) {
const index = this._viewModels.findIndex(vm => vm.id === roomId);
if (index !== -1) {
const vm = this._viewModels[index];
this.untrack(vm);
this._viewModels[index] = null;
return vm;
}
}
_setFocusIndex(idx) {
if (idx === this._selectedIndex || idx >= (this._width * this._height)) {
return;
}
this._selectedIndex = idx;
const vm = this._viewModels[this._selectedIndex];
vm?.focus();
this.emitChange("focusedIndex");
}
_setFocusRoom(roomId) {
const index = this._viewModels.findIndex(vm => vm.id === roomId);
if (index >= 0) {
this.setFocusIndex(index);
this._setFocusIndex(index);
return true;
}
return false;
}
/**
* Returns the first set of room vm,
* and untracking it so it is not owned by this view model anymore.
* @package
*/
getAndUntrackFirst() {
for (const vm of this._viewModels) {
_openRoom(roomId) {
if (!this._setFocusRoom(roomId)) {
// replace vm at focused index
const vm = this._viewModels[this._selectedIndex];
if (vm) {
this.untrack(vm);
return vm;
this.disposeTracked(vm);
}
this._viewModels[this._selectedIndex] = this.track(this._createRoomViewModel(roomId));
this.emitChange();
}
}
}

View file

@ -32,27 +32,33 @@ export class SessionViewModel extends ViewModel {
session: sessionContainer.session,
})));
this._leftPanelViewModel = new LeftPanelViewModel(this.childOptions({
rooms: this._sessionContainer.session.rooms,
// this will go over navigation as well
gridEnabled: {
get: () => !!this._gridViewModel,
set: value => this._enabledGrid(value)
}
rooms: this._sessionContainer.session.rooms
}));
this._currentRoomViewModel = null;
this._gridViewModel = null;
// this gives us the active room, also in the grid?
this.track(this.navigator.observe("room").subscribe(roomId => {
this._setupNavigation();
}
}));
_setupNavigation() {
const gridRooms = this.navigation.observe("rooms");
// this gives us a set of room ids in the grid
this.track(this.navigator.observe("rooms").subscribe(value => {
if (value) {
const roomIds = typeof value === "string" ? value.split(",") : [];
// also update grid
this._enabledGrid(roomIds);
this.track(gridRooms.subscribe(roomIds => {
this._updateGrid(roomIds);
}));
if (gridRooms.get()) {
this._updateGrid(gridRooms.get());
}
const currentRoomId = this.navigation.observe("room");
// this gives us the active room
this.track(currentRoomId.subscribe(roomId => {
if (!this._gridViewModel) {
this._openRoom(roomId);
}
}));
if (currentRoomId.get() && !this._gridViewModel) {
this._openRoom(currentRoomId.get());
}
}
start() {
@ -88,53 +94,109 @@ export class SessionViewModel extends ViewModel {
return this._currentRoomViewModel;
}
// TODO: this should also happen based on URLs
_enabledGrid(enabled) {
if (enabled) {
this._gridViewModel = this.track(new RoomGridViewModel(this.childOptions({width: 3, height: 2})));
// transfer current room
if (this._currentRoomViewModel) {
this.untrack(this._currentRoomViewModel);
this._gridViewModel.setRoomViewModel(this._currentRoomViewModel);
this._currentRoomViewModel = null;
// _transitionToGrid() {
// if (this._gridViewModel) {
// return;
// }
// this._gridViewModel = this.track(new RoomGridViewModel(this.childOptions({width: 3, height: 2})));
// let path;
// if (this._currentRoomViewModel) {
// this.untrack(this._currentRoomViewModel);
// this._gridViewModel.transferRoomViewModel(0, this._currentRoomViewModel);
// const roomId = this._currentRoomViewModel.id;
// this._currentRoomViewModel = null;
// path = this.navigation.path
// .with(this.navigation.segment("rooms", [roomId]))
// .with(this.navigation.segment("room", roomId));
// } else {
// path = this.navigation.path
// .with(this.navigation.segment("rooms", []))
// .with(this.navigation.segment("empty-grid-tile", 0));
// }
// const url = this.urlRouter.urlForPath(path);
// this.urlRouter.history.pushUrl(url);
// this.emitChange("middlePanelViewType");
// this.navigation.applyPath(path);
// }
// _transitionFromGrid() {
// if (!this._gridViewModel) {
// return;
// }
// const vm = this._gridViewModel.releaseFirstRoomViewModel();
// let path = this.navigation.path.until("session");
// if (vm) {
// path = path.with(this.navigation.segment("room", vm.id));
// this._currentRoomViewModel = this.track(vm);
// }
// this._gridViewModel = this.disposeTracked(this._gridViewModel);
// const url = this.urlRouter.urlForPath(path);
// this.urlRouter.history.pushUrl(url);
// this.emitChange("middlePanelViewType");
// this.navigation.applyPath(path);
// }
_updateGrid(roomIds) {
const changed = !(this._gridViewModel && roomIds);
const currentRoomId = this.navigation.path.get("room");
if (roomIds) {
if (!this._gridViewModel) {
this._gridViewModel = this.track(new RoomGridViewModel(this.childOptions({
width: 3,
height: 2,
createRoomViewModel: roomId => this._createRoomViewModel(roomId),
roomIds: roomIds
})));
const vm = this._currentRoomViewModel;
const index = roomIds.indexOf(vm.id);
if (vm && index !== -1) {
this.untrack(vm);
this._gridViewModel.transferRoomViewModel(index, vm);
this._currentRoomViewModel = null;
}
} else {
this._gridViewModel.setRoomIds(roomIds);
}
} else {
const vm = this._gridViewModel.getAndUntrackFirst();
if (vm) {
} else if (this._gridViewModel && !roomIds) {
if (currentRoomId) {
const vm = this._gridViewModel.releaseRoomViewModel(currentRoomId.value);
this._currentRoomViewModel = this.track(vm);
}
this._gridViewModel = this.disposeTracked(this._gridViewModel);
}
this.emitChange("middlePanelViewType");
if (changed) {
this.emitChange("middlePanelViewType");
}
}
_openRoom(roomId) {
// already open?
if (this._gridViewModel?.tryFocusRoom(roomId)) {
return;
} else if (this._currentRoomViewModel?.id === roomId) {
return;
}
_createRoomViewModel(roomId) {
const room = this._session.rooms.get(roomId);
// not found? close current room and show placeholder
if (!room) {
if (this._gridViewModel) {
this._gridViewModel.setRoomViewModel(null);
} else {
this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel);
}
return;
return null;
}
const roomVM = new RoomViewModel(this.childOptions({
room,
ownUserId: this._sessionContainer.session.user.id,
}));
roomVM.load();
return roomVM;
}
_openRoom(roomId) {
// already open?
if (this._currentRoomViewModel?.id === roomId) {
return;
}
this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel);
const roomVM = this._createRoomViewModel(roomId);
if (roomVM) {
this._currentRoomViewModel = this.track(roomVM);
}
if (this._gridViewModel) {
this._gridViewModel.setRoomViewModel(roomVM);
} else {
this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel);
this._currentRoomViewModel = this.track(roomVM);
this.emitChange("currentRoom");
}
}

View file

@ -34,15 +34,44 @@ export class LeftPanelViewModel extends ViewModel {
});
this._roomListFilterMap = new ApplyMap(roomTileVMs);
this._roomList = this._roomListFilterMap.sortValues((a, b) => a.compare(b));
this._currentTileVM = null;
this._setupNavigation();
}
get gridEnabled() {
return this._gridEnabled.get();
_setupNavigation() {
const roomObservable = this.navigation.observe("room");
this.track(roomObservable.subscribe(roomId => this._open(roomId)));
this._open(roomObservable.get());
const gridObservable = this.navigation.observe("rooms");
this.gridEnabled = !!gridObservable.get();
this.track(gridObservable.subscribe(roomIds => {
const changed = this.gridEnabled ^ !!roomIds;
this.gridEnabled = !!roomIds;
if (changed) {
this.emitChange("gridEnabled");
}
}));
}
_open(roomId) {
this._currentTileVM?.close();
this._currentTileVM = null;
if (roomId) {
this._currentTileVM = this._roomListFilterMap.get(roomId);
this._currentTileVM?.open();
}
}
toggleGrid() {
this._gridEnabled.set(!this._gridEnabled.get());
this.emitChange("gridEnabled");
let url;
if (this._gridEnabled) {
url = this.urlRouter.disableGridUrl();
} else {
url = this.urlRouter.enableGridUrl();
}
url = this.urlRouter.applyUrl(url);
this.urlRouter.history.pushUrl(url);
}
get roomList() {

View file

@ -31,7 +31,7 @@ export class RoomTileViewModel extends ViewModel {
this._isOpen = false;
this._wasUnreadWhenOpening = false;
this._hidden = false;
this._url = this.urlRouter.urlForSegment("room", this._room.id);
this._url = this.urlRouter.openRoomActionUrl(this._room.id);
}
get hidden() {

View file

@ -35,26 +35,27 @@ export class History extends BaseObservableValue {
return document.location.hash;
}
/** does not emit */
replaceUrl(url) {
window.history.replaceState(null, null, url);
// replaceState does not cause hashchange
this.emit(url);
}
/** does not emit */
pushUrl(url) {
const hash = this.urlAsPath(url);
// important to check before we expect an echo
// as setting the hash to it's current value doesn't
// trigger onhashchange
if (hash === document.location.hash) {
return;
}
// this operation is silent,
// so avoid emitting on echo hashchange event
if (this._boundOnHashChange) {
this._expectSetEcho = true;
}
document.location.hash = hash;
window.history.pushState(null, null, url);
// const hash = this.urlAsPath(url);
// // important to check before we expect an echo
// // as setting the hash to it's current value doesn't
// // trigger onhashchange
// if (hash === document.location.hash) {
// return;
// }
// // this operation is silent,
// // so avoid emitting on echo hashchange event
// if (this._boundOnHashChange) {
// this._expectSetEcho = true;
// }
// document.location.hash = hash;
}
urlAsPath(url) {

View file

@ -23,8 +23,8 @@ export class RoomGridView extends TemplateView {
const children = [];
for (let i = 0; i < (vm.height * vm.width); i+=1) {
children.push(t.div({
onClick: () => vm.setFocusIndex(i),
onFocusin: () => vm.setFocusIndex(i),
onClick: () => vm.focusTile(i),
onFocusin: () => vm.focusTile(i),
className: {
"container": true,
[`tile${i}`]: true,

View file

@ -25,22 +25,19 @@ export class RoomTileView extends TemplateView {
"hidden": vm => vm.hidden
};
return t.li({"className": classes}, [
renderAvatar(t, vm, 32),
t.div({className: "description"}, [
t.div({className: {"name": true, unread: vm => vm.isUnread}}, vm => vm.name),
t.div({
className: {
"badge": true,
highlighted: vm => vm.isHighlighted,
hidden: vm => !vm.badgeCount
}
}, vm => vm.badgeCount),
t.a({href: vm.url}, [
renderAvatar(t, vm, 32),
t.div({className: "description"}, [
t.div({className: {"name": true, unread: vm => vm.isUnread}}, vm => vm.name),
t.div({
className: {
"badge": true,
highlighted: vm => vm.isHighlighted,
hidden: vm => !vm.badgeCount
}
}, vm => vm.badgeCount),
])
])
]);
}
// called from ListView
clicked() {
this.value.open();
}
}