Merge branch 'develop' into travis/room-list/showmore2

This commit is contained in:
Travis Ralston 2020-07-10 10:57:17 -06:00
commit 477472c178
17 changed files with 363 additions and 157 deletions

View file

@ -54,7 +54,11 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
flex-direction: column;
.mx_LeftPanel2_userHeader {
padding: 12px 12px 20px; // 12px top, 12px sides, 20px bottom
/* 12px top, 12px sides, 20px bottom (using 13px bottom to account
* for internal whitespace in the breadcrumbs)
*/
padding: 12px 12px 13px;
flex-shrink: 0; // to convince safari's layout engine the flexbox is fine
// Create another flexbox column for the rows to stack within
display: flex;
@ -72,7 +76,20 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
width: 100%;
overflow-y: hidden;
overflow-x: scroll;
margin-top: 8px;
margin-top: 20px;
padding-bottom: 2px;
&.mx_IndicatorScrollbar_leftOverflow {
mask-image: linear-gradient(90deg, transparent, black 10%);
}
&.mx_IndicatorScrollbar_rightOverflow {
mask-image: linear-gradient(90deg, black, black 90%, transparent);
}
&.mx_IndicatorScrollbar_rightOverflow.mx_IndicatorScrollbar_leftOverflow {
mask-image: linear-gradient(90deg, transparent, black 10%, black 90%, transparent);
}
}
}
@ -80,6 +97,8 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
margin-left: 12px;
margin-right: 12px;
flex-shrink: 0; // to convince safari's layout engine the flexbox is fine
// Create a flexbox to organize the inputs
display: flex;
align-items: center;

View file

@ -24,7 +24,7 @@ limitations under the License.
right: 0;
}
.mx_NotificationBadge {
.mx_NotificationBadge, .mx_RoomTile2_badgeContainer {
position: absolute;
top: 0;
right: 0;

View file

@ -24,6 +24,8 @@ limitations under the License.
margin-left: 8px;
width: 100%;
flex-shrink: 0; // to convince safari's layout engine the flexbox is fine
.mx_RoomSublist2_headerContainer {
// Create a flexbox to make alignment easy
display: flex;

View file

@ -89,7 +89,6 @@ limitations under the License.
height: 16px;
// don't set width so that it takes no space when there is no badge to show
margin: auto 0; // vertically align
position: relative; // fixes badge alignment in some scenarios
// Create a flexbox to make aligning dot badges easier
display: flex;

View file

@ -37,6 +37,9 @@ declare global {
mx_RoomListStore2: RoomListStore2;
mx_RoomListLayoutStore: RoomListLayoutStore;
mxPlatformPeg: PlatformPeg;
// TODO: Remove flag before launch: https://github.com/vector-im/riot-web/issues/14231
mx_QuietRoomListLogging: boolean;
}
// workaround for https://github.com/microsoft/TypeScript/issues/30933

View file

@ -105,7 +105,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
ev.target.select();
};
private onBlur = () => {
private onBlur = (ev: React.FocusEvent<HTMLInputElement>) => {
this.setState({focused: false});
};

View file

@ -219,7 +219,10 @@ export default class RoomList2 extends React.Component<IProps, IState> {
private updateLists = () => {
const newLists = RoomListStore.instance.orderedLists;
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log("new lists", newLists);
}
this.setState({sublists: newLists}, () => {
this.props.onResize();
@ -293,6 +296,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
isMinimized={this.props.isMinimized}
onResize={this.props.onResize}
extraBadTilesThatShouldntExist={extraTiles}
isFiltered={!!this.searchFilter.search}
/>
);
}

View file

@ -78,6 +78,7 @@ interface IProps {
isMinimized: boolean;
tagId: TagID;
onResize: () => void;
isFiltered: boolean;
// TODO: Don't use this. It's for community invites, and community invites shouldn't be here.
// You should feel bad if you use this.
@ -98,6 +99,7 @@ interface IState {
notificationState: ListNotificationState;
contextMenuPosition: PartialDOMRect;
isResizing: boolean;
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
height: number;
}
@ -118,6 +120,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
notificationState: RoomNotificationStateStore.instance.getListState(this.props.tagId),
contextMenuPosition: null,
isResizing: false,
isExpanded: this.props.isFiltered ? this.props.isFiltered : !this.layout.isCollapsed
height,
};
this.state.notificationState.setRooms(this.props.rooms);
@ -156,8 +159,15 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
return Math.min(nVisible, this.numTiles);
}
public componentDidUpdate(prevProps) {
public componentDidUpdate(prevProps: Readonly<IProps>) {
this.state.notificationState.setRooms(this.props.rooms);
if (prevProps.isFiltered !== this.props.isFiltered) {
if (this.props.isFiltered) {
this.setState({isExpanded: true});
} else {
this.setState({isExpanded: !this.layout.isCollapsed});
}
}
// as the rooms can come in one by one we need to reevaluate
// the amount of available rooms to cap the amount of requested visible rooms by the layout
if (RoomSublist2.calcNumTiles(prevProps) !== this.numTiles) {
@ -175,10 +185,9 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// XXX: we have to do this a tick later because we have incorrect intermediate props during a room change
// where we lose the room we are changing from temporarily and then it comes back in an update right after.
setImmediate(() => {
const isCollapsed = this.layout.isCollapsed;
const roomIndex = this.props.rooms.findIndex((r) => r.roomId === payload.room_id);
if (isCollapsed && roomIndex > -1) {
if (!this.state.isExpanded && roomIndex > -1) {
this.toggleCollapsed();
}
// extend the visible section to include the room if it is entirely invisible
@ -336,24 +345,23 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
};
private toggleCollapsed = () => {
this.layout.isCollapsed = !this.layout.isCollapsed;
this.forceUpdate(); // because the layout doesn't trigger an update
this.layout.isCollapsed = this.state.isExpanded;
this.setState({isExpanded: !this.layout.isCollapsed});
setImmediate(() => this.props.onResize()); // needs to happen when the DOM is updated
};
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
const isCollapsed = this.layout && this.layout.isCollapsed;
switch (ev.key) {
case Key.ARROW_LEFT:
ev.stopPropagation();
if (!isCollapsed) {
if (this.state.isExpanded) {
// On ARROW_LEFT collapse the room sublist if it isn't already
this.toggleCollapsed();
}
break;
case Key.ARROW_RIGHT: {
ev.stopPropagation();
if (isCollapsed) {
if (!this.state.isExpanded) {
// On ARROW_RIGHT expand the room sublist if it isn't already
this.toggleCollapsed();
} else if (this.sublistRef.current) {
@ -382,7 +390,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
};
private renderVisibleTiles(): React.ReactElement[] {
if (this.layout && this.layout.isCollapsed) {
if (!this.state.isExpanded) {
// don't waste time on rendering
return [];
}
@ -539,7 +547,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const collapseClasses = classNames({
'mx_RoomSublist2_collapseBtn': true,
'mx_RoomSublist2_collapseBtn_collapsed': this.layout && this.layout.isCollapsed,
'mx_RoomSublist2_collapseBtn_collapsed': !this.state.isExpanded,
});
const classes = classNames({
@ -567,7 +575,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
tabIndex={tabIndex}
className="mx_RoomSublist2_headerText"
role="treeitem"
aria-expanded={!this.layout.isCollapsed}
aria-expanded={this.state.isExpanded}
aria-level={1}
onClick={this.onHeaderClick}
onContextMenu={this.onContextMenu}

View file

@ -256,7 +256,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
0
));
} else {
console.log(`Unexpected tag ${tagId} applied to ${this.props.room.room_id}`);
console.warn(`Unexpected tag ${tagId} applied to ${this.props.room.room_id}`);
}
if ((ev as React.KeyboardEvent).key === Key.ENTER) {

View file

@ -33,6 +33,7 @@ import { EffectiveMembership, getEffectiveMembership } from "./membership";
import { ListLayout } from "./ListLayout";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import RoomListLayoutStore from "./RoomListLayoutStore";
import { MarkedExecution } from "../../utils/MarkedExecution";
interface IState {
tagsEnabled?: boolean;
@ -51,7 +52,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
private algorithm = new Algorithm();
private filterConditions: IFilterCondition[] = [];
private tagWatcher = new TagWatcher(this);
private layoutMap: Map<TagID, ListLayout> = new Map<TagID, ListLayout>();
private updateFn = new MarkedExecution(() => this.emit(LISTS_UPDATE_EVENT));
private readonly watchedSettings = [
'feature_custom_tags',
@ -62,7 +63,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
this.checkEnabled();
for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null);
RoomViewStore.addListener(this.onRVSUpdate);
RoomViewStore.addListener(() => this.handleRVSUpdate({}));
this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
}
@ -91,27 +92,42 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
await this.updateAlgorithmInstances();
}
private onRVSUpdate = () => {
/**
* Handles suspected RoomViewStore changes.
* @param trigger Set to false to prevent a list update from being sent. Should only
* be used if the calling code will manually trigger the update.
*/
private async handleRVSUpdate({trigger = true}) {
if (!this.enabled) return; // TODO: Remove with https://github.com/vector-im/riot-web/issues/14231
if (!this.matrixClient) return; // We assume there won't be RVS updates without a client
const activeRoomId = RoomViewStore.getRoomId();
if (!activeRoomId && this.algorithm.stickyRoom) {
this.algorithm.stickyRoom = null;
await this.algorithm.setStickyRoom(null);
} else if (activeRoomId) {
const activeRoom = this.matrixClient.getRoom(activeRoomId);
if (!activeRoom) {
console.warn(`${activeRoomId} is current in RVS but missing from client - clearing sticky room`);
this.algorithm.stickyRoom = null;
await this.algorithm.setStickyRoom(null);
} else if (activeRoom !== this.algorithm.stickyRoom) {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`Changing sticky room to ${activeRoomId}`);
this.algorithm.stickyRoom = activeRoom;
}
await this.algorithm.setStickyRoom(activeRoom);
}
}
};
protected async onDispatch(payload: ActionPayload) {
if (trigger) this.updateFn.trigger();
}
protected onDispatch(payload: ActionPayload) {
// We do this to intentionally break out of the current event loop task, allowing
// us to instead wait for a more convenient time to run our updates.
setImmediate(() => this.onDispatchAsync(payload));
}
protected async onDispatchAsync(payload: ActionPayload) {
if (payload.action === 'MatrixActions.sync') {
// Filter out anything that isn't the first PREPARED sync.
if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) {
@ -127,8 +143,12 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
// Update any settings here, as some may have happened before we were logically ready.
console.log("Regenerating room lists: Startup");
await this.readAndCacheSettingsFromStore();
await this.regenerateAllLists();
this.onRVSUpdate(); // fake an RVS update to adjust sticky room, if needed
await this.regenerateAllLists({trigger: false});
await this.handleRVSUpdate({trigger: false}); // fake an RVS update to adjust sticky room, if needed
this.updateFn.trigger();
return; // no point in running the next conditions - they won't match
}
// TODO: Remove this once the RoomListStore becomes default
@ -137,7 +157,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') {
// Reset state without causing updates as the client will have been destroyed
// and downstream code will throw NPE errors.
this.reset(null, true);
await this.reset(null, true);
this._matrixClient = null;
this.initialListsGenerated = false; // we'll want to regenerate them
}
@ -151,7 +171,8 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
console.log("Regenerating room lists: Settings changed");
await this.readAndCacheSettingsFromStore();
await this.regenerateAllLists(); // regenerate the lists now
await this.regenerateAllLists({trigger: false}); // regenerate the lists now
this.updateFn.trigger();
}
}
@ -169,16 +190,22 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
console.warn(`Own read receipt was in unknown room ${room.roomId}`);
return;
}
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Got own read receipt in ${room.roomId}`);
}
await this.handleRoomUpdate(room, RoomUpdateCause.ReadReceipt);
this.updateFn.trigger();
return;
}
} else if (payload.action === 'MatrixActions.Room.tags') {
const roomPayload = (<any>payload); // TODO: Type out the dispatcher types
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Got tag change in ${roomPayload.room.roomId}`);
}
await this.handleRoomUpdate(roomPayload.room, RoomUpdateCause.PossibleTagChange);
this.updateFn.trigger();
} else if (payload.action === 'MatrixActions.Room.timeline') {
const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
@ -188,12 +215,16 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
const roomId = eventPayload.event.getRoomId();
const room = this.matrixClient.getRoom(roomId);
const tryUpdate = async (updatedRoom: Room) => {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()}` +
` in ${updatedRoom.roomId}`);
}
if (eventPayload.event.getType() === 'm.room.tombstone' && eventPayload.event.getStateKey() === '') {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Got tombstone event - trying to remove now-dead room`);
}
const newRoom = this.matrixClient.getRoom(eventPayload.event.getContent()['replacement_room']);
if (newRoom) {
// If we have the new room, then the new room check will have seen the predecessor
@ -202,6 +233,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
}
}
await this.handleRoomUpdate(updatedRoom, RoomUpdateCause.Timeline);
this.updateFn.trigger();
};
if (!room) {
console.warn(`Live timeline event ${eventPayload.event.getId()} received without associated room`);
@ -222,13 +254,18 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
console.warn(`Event ${eventPayload.event.getId()} was decrypted in an unknown room ${roomId}`);
return;
}
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Decrypted timeline event ${eventPayload.event.getId()} in ${roomId}`);
}
await this.handleRoomUpdate(room, RoomUpdateCause.Timeline);
this.updateFn.trigger();
} else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') {
const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Received updated DM map`);
}
const dmMap = eventPayload.event.getContent();
for (const userId of Object.keys(dmMap)) {
const roomIds = dmMap[userId];
@ -246,51 +283,73 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
await this.handleRoomUpdate(room, RoomUpdateCause.PossibleTagChange);
}
}
this.updateFn.trigger();
} else if (payload.action === 'MatrixActions.Room.myMembership') {
const membershipPayload = (<any>payload); // TODO: Type out the dispatcher types
const oldMembership = getEffectiveMembership(membershipPayload.oldMembership);
const newMembership = getEffectiveMembership(membershipPayload.membership);
if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Handling new room ${membershipPayload.room.roomId}`);
}
// If we're joining an upgraded room, we'll want to make sure we don't proliferate
// the dead room in the list.
const createEvent = membershipPayload.room.currentState.getStateEvents("m.room.create", "");
if (createEvent && createEvent.getContent()['predecessor']) {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Room has a predecessor`);
}
const prevRoom = this.matrixClient.getRoom(createEvent.getContent()['predecessor']['room_id']);
if (prevRoom) {
const isSticky = this.algorithm.stickyRoom === prevRoom;
if (isSticky) {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Clearing sticky room due to room upgrade`);
await this.algorithm.setStickyRoomAsync(null);
}
await this.algorithm.setStickyRoom(null);
}
// Note: we hit the algorithm instead of our handleRoomUpdate() function to
// avoid redundant updates.
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Removing previous room from room list`);
}
await this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved);
}
}
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Adding new room to room list`);
}
await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
this.updateFn.trigger();
return;
}
if (oldMembership !== EffectiveMembership.Invite && newMembership === EffectiveMembership.Invite) {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Handling invite to ${membershipPayload.room.roomId}`);
}
await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
this.updateFn.trigger();
return;
}
// If it's not a join, it's transitioning into a different list (possibly historical)
if (oldMembership !== newMembership) {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Handling membership change in ${membershipPayload.room.roomId}`);
}
await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.PossibleTagChange);
this.updateFn.trigger();
return;
}
}
@ -299,9 +358,11 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<any> {
const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);
if (shouldUpdate) {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[DEBUG] Room "${room.name}" (${room.roomId}) triggered by ${cause} requires list update`);
this.emit(LISTS_UPDATE_EVENT, this);
}
this.updateFn.mark();
}
}
@ -309,6 +370,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
await this.algorithm.setTagSorting(tagId, sort);
// TODO: Per-account? https://github.com/vector-im/riot-web/issues/14114
localStorage.setItem(`mx_tagSort_${tagId}`, sort);
this.updateFn.triggerIfWillMark();
}
public getTagSorting(tagId: TagID): SortAlgorithm {
@ -347,6 +409,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
await this.algorithm.setListOrdering(tagId, order);
// TODO: Per-account? https://github.com/vector-im/riot-web/issues/14114
localStorage.setItem(`mx_listOrder_${tagId}`, order);
this.updateFn.triggerIfWillMark();
}
public getListOrder(tagId: TagID): ListAlgorithm {
@ -382,6 +445,10 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
}
private async updateAlgorithmInstances() {
// We'll require an update, so mark for one. Marking now also prevents the calls
// to setTagSorting and setListOrder from causing triggers.
this.updateFn.mark();
for (const tag of Object.keys(this.orderedLists)) {
const definedSort = this.getTagSorting(tag);
const definedOrder = this.getListOrder(tag);
@ -405,12 +472,19 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
}
private onAlgorithmListUpdated = () => {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log("Underlying algorithm has triggered a list update - refiring");
this.emit(LISTS_UPDATE_EVENT, this);
console.log("Underlying algorithm has triggered a list update - marking");
}
this.updateFn.mark();
};
private async regenerateAllLists() {
/**
* Regenerates the room whole room list, discarding any previous results.
* @param trigger Set to false to prevent a list update from being sent. Should only
* be used if the calling code will manually trigger the update.
*/
private async regenerateAllLists({trigger = true}) {
console.warn("Regenerating all room lists");
const sorts: ITagSortingMap = {};
@ -435,21 +509,26 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
this.initialListsGenerated = true;
this.emit(LISTS_UPDATE_EVENT, this);
if (trigger) this.updateFn.trigger();
}
public addFilter(filter: IFilterCondition): void {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log("Adding filter condition:", filter);
}
this.filterConditions.push(filter);
if (this.algorithm) {
this.algorithm.addFilterCondition(filter);
}
this.updateFn.trigger();
}
public removeFilter(filter: IFilterCondition): void {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log("Removing filter condition:", filter);
}
const idx = this.filterConditions.indexOf(filter);
if (idx >= 0) {
this.filterConditions.splice(idx, 1);
@ -458,6 +537,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
this.algorithm.removeFilterCondition(filter);
}
}
this.updateFn.trigger();
}
/**

View file

@ -41,6 +41,17 @@ import { getListAlgorithmInstance } from "./list-ordering";
*/
export const LIST_UPDATED_EVENT = "list_updated_event";
// These are the causes which require a room to be known in order for us to handle them. If
// a cause in this list is raised and we don't know about the room, we don't handle the update.
//
// Note: these typically happen when a new room is coming in, such as the user creating or
// joining the room. For these cases, we need to know about the room prior to handling it otherwise
// we'll make bad assumptions.
const CAUSES_REQUIRING_ROOM = [
RoomUpdateCause.Timeline,
RoomUpdateCause.ReadReceipt,
];
interface IStickyRoom {
room: Room;
position: number;
@ -76,12 +87,6 @@ export class Algorithm extends EventEmitter {
return this._stickyRoom ? this._stickyRoom.room : null;
}
public set stickyRoom(val: Room) {
// setters can't be async, so we call a private function to do the work
// noinspection JSIgnoredPromiseFromCall
this.updateStickyRoom(val);
}
protected get hasFilters(): boolean {
return this.allowedByFilter.size > 0;
}
@ -104,7 +109,7 @@ export class Algorithm extends EventEmitter {
* Awaitable version of the sticky room setter.
* @param val The new room to sticky.
*/
public async setStickyRoomAsync(val: Room) {
public async setStickyRoom(val: Room) {
await this.updateStickyRoom(val);
}
@ -310,9 +315,11 @@ export class Algorithm extends EventEmitter {
}
newMap[tagId] = allowedRoomsInThisTag;
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[DEBUG] ${newMap[tagId].length}/${rooms.length} rooms filtered into ${tagId}`);
}
}
const allowedRooms = Object.values(newMap).reduce((rv, v) => { rv.push(...v); return rv; }, <Room[]>[]);
this.allowedRoomsByFilters = new Set(allowedRooms);
@ -320,26 +327,13 @@ export class Algorithm extends EventEmitter {
this.emit(LIST_UPDATED_EVENT);
}
// TODO: Remove or use.
protected addPossiblyFilteredRoomsToTag(tagId: TagID, added: Room[]): void {
const filters = this.allowedByFilter.keys();
for (const room of added) {
for (const filter of filters) {
if (filter.isVisible(room)) {
this.allowedRoomsByFilters.add(room);
break;
}
}
}
// Now that we've updated the allowed rooms, recalculate the tag
this.recalculateFilteredRoomsForTag(tagId);
}
protected recalculateFilteredRoomsForTag(tagId: TagID): void {
if (!this.hasFilters) return; // don't bother doing work if there's nothing to do
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`Recalculating filtered rooms for ${tagId}`);
}
delete this.filteredRooms[tagId];
const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone
this.tryInsertStickyRoomToFilterSet(rooms, tagId);
@ -348,9 +342,11 @@ export class Algorithm extends EventEmitter {
this.filteredRooms[tagId] = filteredRooms;
}
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`);
}
}
protected tryInsertStickyRoomToFilterSet(rooms: Room[], tagId: TagID) {
if (!this._stickyRoom || !this._stickyRoom.room || this._stickyRoom.tag !== tagId) return;
@ -388,8 +384,10 @@ export class Algorithm extends EventEmitter {
}
if (!this._cachedStickyRooms || !updatedTag) {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`Generating clone of cached rooms for sticky room handling`);
}
const stickiedTagMap: ITagMap = {};
for (const tagId of Object.keys(this.cachedRooms)) {
stickiedTagMap[tagId] = this.cachedRooms[tagId].map(r => r); // shallow clone
@ -400,8 +398,10 @@ export class Algorithm extends EventEmitter {
if (updatedTag) {
// Update the tag indicated by the caller, if possible. This is mostly to ensure
// our cache is up to date.
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`Replacing cached sticky rooms for ${updatedTag}`);
}
this._cachedStickyRooms[updatedTag] = this.cachedRooms[updatedTag].map(r => r); // shallow clone
}
@ -410,8 +410,10 @@ export class Algorithm extends EventEmitter {
// we might have updated from the cache is also our sticky room.
const sticky = this._stickyRoom;
if (!updatedTag || updatedTag === sticky.tag) {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`Inserting sticky room ${sticky.room.roomId} at position ${sticky.position} in ${sticky.tag}`);
}
this._cachedStickyRooms[sticky.tag].splice(sticky.position, 0, sticky.room);
}
@ -636,8 +638,10 @@ export class Algorithm extends EventEmitter {
* processing.
*/
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`Handle room update for ${room.roomId} called with cause ${cause}`);
}
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
// Note: check the isSticky against the room ID just in case the reference is wrong
@ -666,18 +670,6 @@ export class Algorithm extends EventEmitter {
}
}
if (hasTags && isForLastSticky && !knownRoomRef) {
// we have a fairly good chance at losing a room right now. Under some circumstances,
// we can end up with a room which transitions references and tag changes, then gets
// lost when the sticky room changes. To counter this, we try and add the room to the
// list manually as the condition below to update the reference will fail.
//
// Other conditions *should* result in the room being sorted into the right place.
console.warn(`${room.roomId} was about to be lost - inserting at end of room list`);
this.rooms.push(room);
knownRoomRef = true;
}
// If we have tags for a room and don't have the room referenced, something went horribly
// wrong - the reference should have been updated above.
if (hasTags && !knownRoomRef && !isSticky) {
@ -690,6 +682,13 @@ export class Algorithm extends EventEmitter {
// to trigger a sticky room update ourselves.
this._stickyRoom.room = room;
}
// If after all that we're still a NewRoom update, add the room if applicable.
// We don't do this for the sticky room (because it causes duplication issues)
// or if we know about the reference (as it should be replaced).
if (cause === RoomUpdateCause.NewRoom && !isSticky && !knownRoomRef) {
this.rooms.push(room);
}
}
if (cause === RoomUpdateCause.PossibleTagChange) {
@ -699,30 +698,40 @@ export class Algorithm extends EventEmitter {
const diff = arrayDiff(oldTags, newTags);
if (diff.removed.length > 0 || diff.added.length > 0) {
for (const rmTag of diff.removed) {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`Removing ${room.roomId} from ${rmTag}`);
}
const algorithm: OrderingAlgorithm = this.algorithms[rmTag];
if (!algorithm) throw new Error(`No algorithm for ${rmTag}`);
await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
this.cachedRooms[rmTag] = algorithm.orderedRooms;
}
for (const addTag of diff.added) {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`Adding ${room.roomId} to ${addTag}`);
}
const algorithm: OrderingAlgorithm = this.algorithms[addTag];
if (!algorithm) throw new Error(`No algorithm for ${addTag}`);
await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom);
this.cachedRooms[addTag] = algorithm.orderedRooms;
}
// Update the tag map so we don't regen it in a moment
this.roomIdsToTags[room.roomId] = newTags;
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`);
}
cause = RoomUpdateCause.Timeline;
didTagChange = true;
} else {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.warn(`Received no-op update for ${room.roomId} - changing to Timeline update`);
console.log(`Received no-op update for ${room.roomId} - changing to Timeline update`);
}
cause = RoomUpdateCause.Timeline;
}
@ -738,7 +747,7 @@ export class Algorithm extends EventEmitter {
};
} else {
// We have to clear the lock as the sticky room change will trigger updates.
await this.setStickyRoomAsync(room);
await this.setStickyRoom(room);
}
}
}
@ -748,15 +757,27 @@ export class Algorithm extends EventEmitter {
// as the sticky room relies on this.
if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) {
if (this.stickyRoom === room) {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`);
}
return false;
}
}
if (!this.roomIdsToTags[room.roomId]) {
if (CAUSES_REQUIRING_ROOM.includes(cause)) {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.warn(`Skipping tag update for ${room.roomId} because we don't know about the room`);
}
return false;
}
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`);
}
// Get the tags for the room and populate the cache
const roomTags = this.getTagsForRoom(room).filter(t => !isNullOrUndefined(this.cachedRooms[t]));
@ -767,12 +788,16 @@ export class Algorithm extends EventEmitter {
this.roomIdsToTags[room.roomId] = roomTags;
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags);
}
}
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`);
}
const tags = this.roomIdsToTags[room.roomId];
if (!tags) {
@ -794,8 +819,10 @@ export class Algorithm extends EventEmitter {
changed = true;
}
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`);
}
return changed;
}
}

View file

@ -87,9 +87,6 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
super(tagId, initialSortingAlgorithm);
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Constructed an ImportanceAlgorithm for ${tagId}`);
}
// noinspection JSMethodCanBeStatic
@ -160,7 +157,10 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
} else if (cause === RoomUpdateCause.RoomRemoved) {
const roomIdx = this.getRoomIndex(room);
if (roomIdx === -1) return false; // no change
if (roomIdx === -1) {
console.warn(`Tried to remove unknown room from ${this.tagId}: ${room.roomId}`);
return false; // no change
}
const oldCategory = this.getCategoryFromIndices(roomIdx, this.indices);
this.alterCategoryPositionBy(oldCategory, -1, this.indices);
this.cachedOrderedRooms.splice(roomIdx, 1); // remove the room
@ -169,15 +169,6 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
}
}
private getRoomIndex(room: Room): number {
let roomIdx = this.cachedOrderedRooms.indexOf(room);
if (roomIdx === -1) { // can only happen if the js-sdk's store goes sideways.
console.warn(`Degrading performance to find missing room in "${this.tagId}": ${room.roomId}`);
roomIdx = this.cachedOrderedRooms.findIndex(r => r.roomId === room.roomId);
}
return roomIdx;
}
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
try {
await this.updateLock.acquireAsync();

View file

@ -28,9 +28,6 @@ export class NaturalAlgorithm extends OrderingAlgorithm {
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
super(tagId, initialSortingAlgorithm);
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Constructed a NaturalAlgorithm for ${tagId}`);
}
public async setRooms(rooms: Room[]): Promise<any> {
@ -50,8 +47,12 @@ export class NaturalAlgorithm extends OrderingAlgorithm {
if (cause === RoomUpdateCause.NewRoom) {
this.cachedOrderedRooms.push(room);
} else if (cause === RoomUpdateCause.RoomRemoved) {
const idx = this.cachedOrderedRooms.indexOf(room);
if (idx >= 0) this.cachedOrderedRooms.splice(idx, 1);
const idx = this.getRoomIndex(room);
if (idx >= 0) {
this.cachedOrderedRooms.splice(idx, 1);
} else {
console.warn(`Tried to remove unknown room from ${this.tagId}: ${room.roomId}`);
}
}
// TODO: Optimize this to avoid useless operations: https://github.com/vector-im/riot-web/issues/14035

View file

@ -70,4 +70,13 @@ export abstract class OrderingAlgorithm {
* @returns True if the update requires the Algorithm to update the presentation layers.
*/
public abstract handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean>;
protected getRoomIndex(room: Room): number {
let roomIdx = this.cachedOrderedRooms.indexOf(room);
if (roomIdx === -1) { // can only happen if the js-sdk's store goes sideways.
console.warn(`Degrading performance to find missing room in "${this.tagId}": ${room.roomId}`);
roomIdx = this.cachedOrderedRooms.findIndex(r => r.roomId === room.roomId);
}
return roomIdx;
}
}

View file

@ -52,8 +52,6 @@ export class CommunityFilterCondition extends EventEmitter implements IFilterCon
const beforeRoomIds = this.roomIds;
this.roomIds = (await GroupStore.getGroupRooms(this.community.groupId)).map(r => r.roomId);
if (arrayHasDiff(beforeRoomIds, this.roomIds)) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log("Updating filter for group: ", this.community.groupId);
this.emit(FILTER_CHANGED);
}
};

View file

@ -41,8 +41,6 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
public set search(val: string) {
this._search = val;
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log("Updating filter for room name search:", this._search);
this.emit(FILTER_CHANGED);
}

View file

@ -0,0 +1,67 @@
/*
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.
*/
/**
* A utility to ensure that a function is only called once triggered with
* a mark applied. Multiple marks can be applied to the function, however
* the function will only be called once upon trigger().
*
* The function starts unmarked.
*/
export class MarkedExecution {
private marked = false;
/**
* Creates a MarkedExecution for the provided function.
* @param fn The function to be called upon trigger if marked.
*/
constructor(private fn: () => void) {
}
/**
* Resets the mark without calling the function.
*/
public reset() {
this.marked = false;
}
/**
* Marks the function to be called upon trigger().
*/
public mark() {
this.marked = true;
}
/**
* If marked, the function will be called, otherwise this does nothing.
*/
public trigger() {
if (!this.marked) return;
this.reset(); // reset first just in case the fn() causes a trigger()
this.fn();
}
/**
* Triggers the function if a mark() call would mark it. If the function
* has already been marked this will do nothing.
*/
public triggerIfWillMark() {
if (!this.marked) {
this.mark();
this.trigger();
}
}
}