Merge branch 'develop' into travis/room-list/hover-state

This commit is contained in:
Travis Ralston 2020-06-10 07:42:41 -06:00
commit a3391d9a08
23 changed files with 806 additions and 54 deletions

View file

@ -0,0 +1,53 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient } from "matrix-js-sdk/src/client";
import { AsyncStore } from "./AsyncStore";
import { ActionPayload } from "../dispatcher/payloads";
export abstract class AsyncStoreWithClient<T extends Object> extends AsyncStore<T> {
protected matrixClient: MatrixClient;
protected abstract async onAction(payload: ActionPayload);
protected async onReady() {
// Default implementation is to do nothing.
}
protected async onNotReady() {
// Default implementation is to do nothing.
}
protected async onDispatch(payload: ActionPayload) {
await this.onAction(payload);
if (payload.action === 'MatrixActions.sync') {
// Filter out anything that isn't the first PREPARED sync.
if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) {
return;
}
this.matrixClient = payload.matrixClient;
await this.onReady();
} else if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') {
if (this.matrixClient) {
await this.onNotReady();
this.matrixClient = null;
}
}
}
}

View file

@ -0,0 +1,166 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import SettingsStore, { SettingLevel } from "../settings/SettingsStore";
import { Room } from "matrix-js-sdk/src/models/room";
import { ActionPayload } from "../dispatcher/payloads";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import { arrayHasDiff } from "../utils/arrays";
import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy";
const MAX_ROOMS = 20; // arbitrary
const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up
interface IState {
enabled?: boolean;
rooms?: Room[];
}
export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
private static internalInstance = new BreadcrumbsStore();
private waitingRooms: { roomId: string, addedTs: number }[] = [];
private constructor() {
super(defaultDispatcher);
SettingsStore.monitorSetting("breadcrumb_rooms", null);
SettingsStore.monitorSetting("breadcrumbs", null);
}
public static get instance(): BreadcrumbsStore {
return BreadcrumbsStore.internalInstance;
}
public get rooms(): Room[] {
return this.state.rooms || [];
}
public get visible(): boolean {
return this.state.enabled;
}
protected async onAction(payload: ActionPayload) {
if (!this.matrixClient) return;
// TODO: Remove when new room list is made the default
if (!RoomListStoreTempProxy.isUsingNewStore()) return;
if (payload.action === 'setting_updated') {
if (payload.settingName === 'breadcrumb_rooms') {
await this.updateRooms();
} else if (payload.settingName === 'breadcrumbs') {
await this.updateState({enabled: SettingsStore.getValue("breadcrumbs", null)});
}
} else if (payload.action === 'view_room') {
if (payload.auto_join && !this.matrixClient.getRoom(payload.room_id)) {
// Queue the room instead of pushing it immediately. We're probably just
// waiting for a room join to complete.
this.waitingRooms.push({roomId: payload.room_id, addedTs: Date.now()});
} else {
// The tests might not result in a valid room object.
const room = this.matrixClient.getRoom(payload.room_id);
if (room) await this.appendRoom(room);
}
}
}
protected async onReady() {
// TODO: Remove when new room list is made the default
if (!RoomListStoreTempProxy.isUsingNewStore()) return;
await this.updateRooms();
await this.updateState({enabled: SettingsStore.getValue("breadcrumbs", null)});
this.matrixClient.on("Room.myMembership", this.onMyMembership);
this.matrixClient.on("Room", this.onRoom);
}
protected async onNotReady() {
// TODO: Remove when new room list is made the default
if (!RoomListStoreTempProxy.isUsingNewStore()) return;
this.matrixClient.removeListener("Room.myMembership", this.onMyMembership);
this.matrixClient.removeListener("Room", this.onRoom);
}
private onMyMembership = async (room: Room) => {
// We turn on breadcrumbs by default once the user has at least 1 room to show.
if (!this.state.enabled) {
await SettingsStore.setValue("breadcrumbs", null, SettingLevel.ACCOUNT, true);
}
};
private onRoom = async (room: Room) => {
const waitingRoom = this.waitingRooms.find(r => r.roomId === room.roomId);
if (!waitingRoom) return;
this.waitingRooms.splice(this.waitingRooms.indexOf(waitingRoom), 1);
if ((Date.now() - waitingRoom.addedTs) > AUTOJOIN_WAIT_THRESHOLD_MS) return; // Too long ago.
await this.appendRoom(room);
};
private async updateRooms() {
let roomIds = SettingsStore.getValue("breadcrumb_rooms");
if (!roomIds || roomIds.length === 0) roomIds = [];
const rooms = roomIds.map(r => this.matrixClient.getRoom(r)).filter(r => !!r);
const currentRooms = this.state.rooms || [];
if (!arrayHasDiff(rooms, currentRooms)) return; // no change (probably echo)
await this.updateState({rooms});
}
private async appendRoom(room: Room) {
const rooms = (this.state.rooms || []).slice(); // cheap clone
// If the room is upgraded, use that room instead. We'll also splice out
// any children of the room.
const history = this.matrixClient.getRoomUpgradeHistory(room.roomId);
if (history.length > 1) {
room = history[history.length - 1]; // Last room is most recent in history
// Take out any room that isn't the most recent room
for (let i = 0; i < history.length - 1; i++) {
const idx = rooms.findIndex(r => r.roomId === history[i].roomId);
if (idx !== -1) rooms.splice(idx, 1);
}
}
// Remove the existing room, if it is present
const existingIdx = rooms.findIndex(r => r.roomId === room.roomId);
if (existingIdx !== -1) {
rooms.splice(existingIdx, 1);
}
// Splice the room to the start of the list
rooms.splice(0, 0, room);
if (rooms.length > MAX_ROOMS) {
// This looks weird, but it's saying to start at the MAX_ROOMS point in the
// list and delete everything after it.
rooms.splice(MAX_ROOMS, rooms.length - MAX_ROOMS);
}
// Update the breadcrumbs
await this.updateState({rooms});
const roomIds = rooms.map(r => r.roomId);
if (roomIds.length > 0) {
await SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds);
}
}
}

View file

@ -64,7 +64,24 @@ export class ListLayout {
}
public get minVisibleTiles(): number {
return 3;
// the .65 comes from the CSS where the show more button is
// mathematically 65% of a tile when floating.
return 4.65;
}
public calculateTilesToPixelsMin(maxTiles: number, n: number, possiblePadding: number): number {
// Only apply the padding if we're about to use maxTiles as we need to
// plan for the padding. If we're using n, the padding is already accounted
// for by the resizing stuff.
let padding = 0;
if (maxTiles < n) {
padding = possiblePadding;
}
return this.tilesToPixels(Math.min(maxTiles, n)) + padding;
}
public tilesToPixelsWithPadding(n: number, padding: number): number {
return this.tilesToPixels(n) + padding;
}
public tilesToPixels(n: number): number {

View file

@ -31,11 +31,14 @@ export class RoomListStoreTempProxy {
return SettingsStore.isFeatureEnabled("feature_new_room_list");
}
public static addListener(handler: () => void) {
public static addListener(handler: () => void): RoomListStoreTempToken {
if (RoomListStoreTempProxy.isUsingNewStore()) {
return RoomListStore.instance.on(UPDATE_EVENT, handler);
const offFn = () => RoomListStore.instance.off(UPDATE_EVENT, handler);
RoomListStore.instance.on(UPDATE_EVENT, handler);
return new RoomListStoreTempToken(offFn);
} else {
return OldRoomListStore.addListener(handler);
const token = OldRoomListStore.addListener(handler);
return new RoomListStoreTempToken(() => token.remove());
}
}
@ -47,3 +50,12 @@ export class RoomListStoreTempProxy {
}
}
}
export class RoomListStoreTempToken {
constructor(private offFn: () => void) {
}
public remove(): void {
this.offFn();
}
}

View file

@ -74,6 +74,11 @@ export class TagWatcher {
this.store.removeFilter(filter);
}
// Destroy any and all old filter conditions to prevent resource leaks
for (const filter of this.filters.values()) {
filter.destroy();
}
this.filters = newFilters;
}
};