Merge branch 'develop' into element

This commit is contained in:
Bruno Windels 2020-07-10 19:04:45 +02:00
commit 952200f031
14 changed files with 462 additions and 243 deletions

View file

@ -55,7 +55,11 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations
flex-direction: column; flex-direction: column;
.mx_LeftPanel2_userHeader { .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 // Create another flexbox column for the rows to stack within
display: flex; display: flex;
@ -73,7 +77,20 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations
width: 100%; width: 100%;
overflow-y: hidden; overflow-y: hidden;
overflow-x: scroll; 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);
}
} }
} }
@ -81,6 +98,8 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations
margin-left: 12px; margin-left: 12px;
margin-right: 12px; margin-right: 12px;
flex-shrink: 0; // to convince safari's layout engine the flexbox is fine
// Create a flexbox to organize the inputs // Create a flexbox to organize the inputs
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -24,6 +24,8 @@ limitations under the License.
margin-left: 8px; margin-left: 8px;
width: 100%; width: 100%;
flex-shrink: 0; // to convince safari's layout engine the flexbox is fine
.mx_RoomSublist2_headerContainer { .mx_RoomSublist2_headerContainer {
// Create a flexbox to make alignment easy // Create a flexbox to make alignment easy
display: flex; display: flex;
@ -181,7 +183,6 @@ limitations under the License.
} }
.mx_RoomSublist2_resizeBox { .mx_RoomSublist2_resizeBox {
margin-bottom: 4px; // for the resize handle
position: relative; position: relative;
// Create another flexbox column for the tiles // Create another flexbox column for the tiles
@ -189,55 +190,22 @@ limitations under the License.
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
.mx_RoomSublist2_placeholder { .mx_RoomSublist2_tiles {
height: 44px; // Height of a room tile plus margins flex: 1 0 0;
} overflow: hidden;
// need this to be flex otherwise the overflow hidden from above
.mx_RoomSublist2_showNButton { // sometimes vertically centers the clipped list ... no idea why it would do this
cursor: pointer; // as the box model should be top aligned. Happens in both FF and Chromium
font-size: $font-13px;
line-height: $font-18px;
color: $roomtile2-preview-color;
// Update the render() function for RoomSublist2 if these change
// Update the ListLayout class for minVisibleTiles if these change.
//
// At 24px high, 8px padding on the top and 4px padding on the bottom this equates to 0.73 of
// a tile due to how the padding calculations work.
height: 24px;
padding-top: 8px;
padding-bottom: 4px;
// We force this to the bottom so it will overlap rooms as needed.
// We account for the space it takes up (24px) in the code through padding.
position: absolute;
bottom: 0;
left: 0;
right: 0;
// We create a flexbox to cheat at alignment
display: flex; display: flex;
align-items: center; flex-direction: column;
.mx_RoomSublist2_showNButtonChevron {
position: relative;
width: 16px;
height: 16px;
margin-left: 12px;
margin-right: 18px;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $roomtile2-preview-color;
} }
.mx_RoomSublist2_showMoreButtonChevron { .mx_RoomSublist2_resizerHandles_showNButton {
mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); flex: 0 0 32px;
} }
.mx_RoomSublist2_showLessButtonChevron { .mx_RoomSublist2_resizerHandles {
mask-image: url('$(res)/img/feather-customised/chevron-up.svg'); flex: 0 0 4px;
}
} }
// Class name comes from the ResizableBox component // Class name comes from the ResizableBox component
@ -269,6 +237,42 @@ limitations under the License.
} }
} }
.mx_RoomSublist2_showNButton {
cursor: pointer;
font-size: $font-13px;
line-height: $font-18px;
color: $roomtile2-preview-color;
// Update the render() function for RoomSublist2 if these change
// Update the ListLayout class for minVisibleTiles if these change.
height: 24px;
padding-bottom: 4px;
// We create a flexbox to cheat at alignment
display: flex;
align-items: center;
.mx_RoomSublist2_showNButtonChevron {
position: relative;
width: 16px;
height: 16px;
margin-left: 12px;
margin-right: 18px;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $roomtile2-preview-color;
}
.mx_RoomSublist2_showMoreButtonChevron {
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
}
.mx_RoomSublist2_showLessButtonChevron {
mask-image: url('$(res)/img/feather-customised/chevron-up.svg');
}
}
&.mx_RoomSublist2_hasMenuOpen, &.mx_RoomSublist2_hasMenuOpen,
&:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:focus-within, &:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:focus-within,
&:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:hover { &:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:hover {
@ -314,6 +318,7 @@ limitations under the License.
.mx_RoomSublist2_resizeBox { .mx_RoomSublist2_resizeBox {
align-items: center; align-items: center;
}
.mx_RoomSublist2_showNButton { .mx_RoomSublist2_showNButton {
flex-direction: column; flex-direction: column;
@ -322,7 +327,6 @@ limitations under the License.
margin-right: 12px; // to center margin-right: 12px; // to center
} }
} }
}
.mx_RoomSublist2_menuButton { .mx_RoomSublist2_menuButton {
height: 16px; height: 16px;

View file

@ -37,6 +37,9 @@ declare global {
mx_RoomListStore2: RoomListStore2; mx_RoomListStore2: RoomListStore2;
mx_RoomListLayoutStore: RoomListLayoutStore; mx_RoomListLayoutStore: RoomListLayoutStore;
mxPlatformPeg: PlatformPeg; 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 // workaround for https://github.com/microsoft/TypeScript/issues/30933

View file

@ -219,7 +219,10 @@ export default class RoomList2 extends React.Component<IProps, IState> {
private updateLists = () => { private updateLists = () => {
const newLists = RoomListStore.instance.orderedLists; 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); console.log("new lists", newLists);
}
this.setState({sublists: newLists}, () => { this.setState({sublists: newLists}, () => {
this.props.onResize(); this.props.onResize();

View file

@ -59,7 +59,7 @@ import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
* warning disappears. * * warning disappears. *
*******************************************************************/ *******************************************************************/
const SHOW_N_BUTTON_HEIGHT = 32; // As defined by CSS const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
export const HEADER_HEIGHT = 32; // As defined by CSS export const HEADER_HEIGHT = 32; // As defined by CSS
@ -87,6 +87,12 @@ interface IProps {
// TODO: Account for https://github.com/vector-im/riot-web/issues/14179 // TODO: Account for https://github.com/vector-im/riot-web/issues/14179
} }
// TODO: Use re-resizer's NumberSize when it is exposed as the type
interface ResizeDelta {
width: number;
height: number;
}
type PartialDOMRect = Pick<DOMRect, "left" | "top" | "height">; type PartialDOMRect = Pick<DOMRect, "left" | "top" | "height">;
interface IState { interface IState {
@ -94,6 +100,7 @@ interface IState {
contextMenuPosition: PartialDOMRect; contextMenuPosition: PartialDOMRect;
isResizing: boolean; isResizing: boolean;
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
height: number;
} }
export default class RoomSublist2 extends React.Component<IProps, IState> { export default class RoomSublist2 extends React.Component<IProps, IState> {
@ -101,28 +108,54 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
private sublistRef = createRef<HTMLDivElement>(); private sublistRef = createRef<HTMLDivElement>();
private dispatcherRef: string; private dispatcherRef: string;
private layout: ListLayout; private layout: ListLayout;
private heightAtStart: number;
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);
this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId); this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId);
this.heightAtStart = 0;
const height = this.calculateInitialHeight();
this.state = { this.state = {
notificationState: RoomNotificationStateStore.instance.getListState(this.props.tagId), notificationState: RoomNotificationStateStore.instance.getListState(this.props.tagId),
contextMenuPosition: null, contextMenuPosition: null,
isResizing: false, isResizing: false,
isExpanded: this.props.isFiltered ? this.props.isFiltered : !this.layout.isCollapsed isExpanded: this.props.isFiltered ? this.props.isFiltered : !this.layout.isCollapsed,
height,
}; };
this.state.notificationState.setRooms(this.props.rooms); this.state.notificationState.setRooms(this.props.rooms);
this.dispatcherRef = defaultDispatcher.register(this.onAction); this.dispatcherRef = defaultDispatcher.register(this.onAction);
} }
private calculateInitialHeight() {
const requestedVisibleTiles = Math.max(Math.floor(this.layout.visibleTiles), this.layout.minVisibleTiles);
const tileCount = Math.min(this.numTiles, requestedVisibleTiles);
const height = this.layout.tilesToPixelsWithPadding(tileCount, this.padding);
return height;
}
private get padding() {
let padding = RESIZE_HANDLE_HEIGHT;
// this is used for calculating the max height of the whole container,
// and takes into account whether there should be room reserved for the show less button
// when fully expanded. Note that the show more button might still be shown when not fully expanded,
// but in this case it will take the space of a tile and we don't need to reserve space for it.
if (this.numTiles > this.layout.defaultVisibleTiles) {
padding += SHOW_N_BUTTON_HEIGHT;
}
return padding;
}
private get numTiles(): number { private get numTiles(): number {
return (this.props.rooms || []).length + (this.props.extraBadTilesThatShouldntExist || []).length; return RoomSublist2.calcNumTiles(this.props);
}
private static calcNumTiles(props) {
return (props.rooms || []).length + (props.extraBadTilesThatShouldntExist || []).length;
} }
private get numVisibleTiles(): number { private get numVisibleTiles(): number {
const nVisible = Math.floor(this.layout.visibleTiles); const nVisible = Math.ceil(this.layout.visibleTiles);
return Math.min(nVisible, this.numTiles); return Math.min(nVisible, this.numTiles);
} }
@ -135,6 +168,11 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
this.setState({isExpanded: !this.layout.isCollapsed}); 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) {
this.setState({height: this.calculateInitialHeight()});
}
} }
public componentWillUnmount() { public componentWillUnmount() {
@ -166,47 +204,50 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
if (this.props.onAddRoom) this.props.onAddRoom(); if (this.props.onAddRoom) this.props.onAddRoom();
}; };
private applyHeightChange(newHeight: number) {
const heightInTiles = Math.ceil(this.layout.pixelsToTiles(newHeight - this.padding));
this.layout.visibleTiles = Math.min(this.numTiles, heightInTiles);
}
private onResize = ( private onResize = (
e: MouseEvent | TouchEvent, e: MouseEvent | TouchEvent,
travelDirection: Direction, travelDirection: Direction,
refToElement: HTMLDivElement, refToElement: HTMLDivElement,
delta: { width: number, height: number }, // TODO: Use re-resizer's NumberSize when it is exposed as the type delta: ResizeDelta,
) => { ) => {
// Do some sanity checks, but in reality we shouldn't need these. const newHeight = this.heightAtStart + delta.height;
if (travelDirection !== "bottom") return; this.applyHeightChange(newHeight);
if (delta.height === 0) return; // something went wrong, so just ignore it. this.setState({height: newHeight});
// NOTE: the movement in the MouseEvent (not present on a TouchEvent) is inaccurate
// for our purposes. The delta provided by the library is also a change *from when
// resizing started*, meaning it is fairly useless for us. This is why we just use
// the client height and run with it.
const heightBefore = this.layout.visibleTiles;
const heightInTiles = this.layout.pixelsToTiles(refToElement.clientHeight);
this.layout.setVisibleTilesWithin(heightInTiles, this.numTiles);
if (heightBefore === this.layout.visibleTiles) return; // no-op
this.forceUpdate(); // because the layout doesn't trigger a re-render
}; };
private onResizeStart = () => { private onResizeStart = () => {
this.heightAtStart = this.state.height;
this.setState({isResizing: true}); this.setState({isResizing: true});
}; };
private onResizeStop = () => { private onResizeStop = (
this.setState({isResizing: false}); e: MouseEvent | TouchEvent,
travelDirection: Direction,
refToElement: HTMLDivElement,
delta: ResizeDelta,
) => {
const newHeight = this.heightAtStart + delta.height;
this.applyHeightChange(newHeight);
this.setState({isResizing: false, height: newHeight});
}; };
private onShowAllClick = () => { private onShowAllClick = () => {
const numVisibleTiles = this.numVisibleTiles; const newHeight = this.layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
this.layout.visibleTiles = this.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT); this.applyHeightChange(newHeight);
this.forceUpdate(); // because the layout doesn't trigger a re-render this.setState({height: newHeight}, () => {
setImmediate(this.focusRoomTile, numVisibleTiles); // focus the tile after the current bottom one this.focusRoomTile(this.numTiles - 1);
});
}; };
private onShowLessClick = () => { private onShowLessClick = () => {
this.layout.visibleTiles = this.layout.defaultVisibleTiles; const newHeight = this.layout.tilesToPixelsWithPadding(this.layout.defaultVisibleTiles, this.padding);
this.forceUpdate(); // because the layout doesn't trigger a re-render this.applyHeightChange(newHeight);
// focus will flow to the show more button here this.setState({height: newHeight});
}; };
private focusRoomTile = (index: number) => { private focusRoomTile = (index: number) => {
@ -559,7 +600,6 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// TODO: Error boundary: https://github.com/vector-im/riot-web/issues/14185 // TODO: Error boundary: https://github.com/vector-im/riot-web/issues/14185
const visibleTiles = this.renderVisibleTiles(); const visibleTiles = this.renderVisibleTiles();
const classes = classNames({ const classes = classNames({
'mx_RoomSublist2': true, 'mx_RoomSublist2': true,
'mx_RoomSublist2_hasMenuOpen': !!this.state.contextMenuPosition, 'mx_RoomSublist2_hasMenuOpen': !!this.state.contextMenuPosition,
@ -570,6 +610,11 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
if (visibleTiles.length > 0) { if (visibleTiles.length > 0) {
const layout = this.layout; // to shorten calls const layout = this.layout; // to shorten calls
const minTiles = Math.min(layout.minVisibleTiles, this.numTiles);
const showMoreAtMinHeight = minTiles < this.numTiles;
const minHeightPadding = RESIZE_HANDLE_HEIGHT + (showMoreAtMinHeight ? SHOW_N_BUTTON_HEIGHT : 0);
const minTilesPx = layout.tilesToPixelsWithPadding(minTiles, minHeightPadding);
const maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
const showMoreBtnClasses = classNames({ const showMoreBtnClasses = classNames({
'mx_RoomSublist2_showNButton': true, 'mx_RoomSublist2_showNButton': true,
}); });
@ -578,9 +623,11 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// floats above the resize handle, if we have one present. If the user has all // floats above the resize handle, if we have one present. If the user has all
// tiles visible, it becomes 'show less'. // tiles visible, it becomes 'show less'.
let showNButton = null; let showNButton = null;
if (this.numTiles > visibleTiles.length) {
// we have a cutoff condition - add the button to show all if (maxTilesPx > this.state.height) {
const numMissing = this.numTiles - visibleTiles.length; const nonPaddedHeight = this.state.height - RESIZE_HANDLE_HEIGHT - SHOW_N_BUTTON_HEIGHT;
const amountFullyShown = Math.floor(nonPaddedHeight / this.layout.tileHeight);
const numMissing = this.numTiles - amountFullyShown;
let showMoreText = ( let showMoreText = (
<span className='mx_RoomSublist2_showNButtonText'> <span className='mx_RoomSublist2_showNButtonText'>
{_t("Show %(count)s more", {count: numMissing})} {_t("Show %(count)s more", {count: numMissing})}
@ -595,7 +642,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
{showMoreText} {showMoreText}
</RovingAccessibleButton> </RovingAccessibleButton>
); );
} else if (this.numTiles <= visibleTiles.length && this.numTiles > this.layout.defaultVisibleTiles) { } else if (this.numTiles > this.layout.defaultVisibleTiles) {
// we have all tiles visible - add a button to show less // we have all tiles visible - add a button to show less
let showLessText = ( let showLessText = (
<span className='mx_RoomSublist2_showNButtonText'> <span className='mx_RoomSublist2_showNButtonText'>
@ -639,44 +686,31 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// goes backwards and can become wildly incorrect (visibleTiles says 18 when there's // goes backwards and can become wildly incorrect (visibleTiles says 18 when there's
// only mathematically 7 possible). // only mathematically 7 possible).
// The padding is variable though, so figure out what we need padding for. const handleWrapperClasses = classNames({
let padding = 0; 'mx_RoomSublist2_resizerHandles': true,
if (showNButton) padding += SHOW_N_BUTTON_HEIGHT; 'mx_RoomSublist2_resizerHandles_showNButton': !!showNButton,
padding += RESIZE_HANDLE_HEIGHT; // always append the handle height });
const relativeTiles = layout.tilesWithPadding(this.numTiles, padding);
const minTilesPx = layout.calculateTilesToPixelsMin(relativeTiles, layout.minVisibleTiles, padding);
const maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, padding);
const tilesWithoutPadding = Math.min(relativeTiles, layout.visibleTiles);
const tilesPx = layout.calculateTilesToPixelsMin(relativeTiles, tilesWithoutPadding, padding);
// Now that we know our padding constraints, let's find out if we need to chop off the
// last rendered visible tile so it doesn't collide with the 'show more' button
let visibleUnpaddedTiles = Math.round(layout.visibleTiles - layout.pixelsToTiles(padding));
if (visibleUnpaddedTiles === visibleTiles.length - 1) {
const placeholder = <div className="mx_RoomSublist2_placeholder" key='placeholder' />;
visibleTiles.splice(visibleUnpaddedTiles, 1, placeholder);
}
const dimensions = {
height: tilesPx,
};
content = ( content = (
<React.Fragment>
<Resizable <Resizable
size={dimensions as any} size={{height: this.state.height} as any}
minHeight={minTilesPx} minHeight={minTilesPx}
maxHeight={maxTilesPx} maxHeight={maxTilesPx}
onResizeStart={this.onResizeStart} onResizeStart={this.onResizeStart}
onResizeStop={this.onResizeStop} onResizeStop={this.onResizeStop}
onResize={this.onResize} onResize={this.onResize}
handleWrapperClass="mx_RoomSublist2_resizerHandles" handleWrapperClass={handleWrapperClasses}
handleClasses={{bottom: "mx_RoomSublist2_resizerHandle"}} handleClasses={{bottom: "mx_RoomSublist2_resizerHandle"}}
className="mx_RoomSublist2_resizeBox" className="mx_RoomSublist2_resizeBox"
enable={handles} enable={handles}
> >
<div className="mx_RoomSublist2_tiles">
{visibleTiles} {visibleTiles}
</div>
{showNButton} {showNButton}
</Resizable> </Resizable>
</React.Fragment>
); );
} }

View file

@ -256,7 +256,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
0 0
)); ));
} else { } 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) { if ((ev as React.KeyboardEvent).key === Key.ENTER) {

View file

@ -20,7 +20,8 @@ const TILE_HEIGHT_PX = 44;
// this comes from the CSS where the show more button is // this comes from the CSS where the show more button is
// mathematically this percent of a tile when floating. // mathematically this percent of a tile when floating.
const RESIZER_BOX_FACTOR = 0.78; //const RESIZER_BOX_FACTOR = 0.78;
const RESIZER_BOX_FACTOR = 0;
interface ISerializedListLayout { interface ISerializedListLayout {
numTiles: number; numTiles: number;
@ -109,6 +110,10 @@ export class ListLayout {
return this.tilesToPixels(Math.min(maxTiles, n)) + padding; return this.tilesToPixels(Math.min(maxTiles, n)) + padding;
} }
public tilesWithResizerBoxFactor(n: number): number {
return n + RESIZER_BOX_FACTOR;
}
public tilesWithPadding(n: number, paddingPx: number): number { public tilesWithPadding(n: number, paddingPx: number): number {
return this.pixelsToTiles(this.tilesToPixelsWithPadding(n, paddingPx)); return this.pixelsToTiles(this.tilesToPixelsWithPadding(n, paddingPx));
} }

View file

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

View file

@ -87,12 +87,6 @@ export class Algorithm extends EventEmitter {
return this._stickyRoom ? this._stickyRoom.room : null; 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 { protected get hasFilters(): boolean {
return this.allowedByFilter.size > 0; return this.allowedByFilter.size > 0;
} }
@ -115,7 +109,7 @@ export class Algorithm extends EventEmitter {
* Awaitable version of the sticky room setter. * Awaitable version of the sticky room setter.
* @param val The new room to sticky. * @param val The new room to sticky.
*/ */
public async setStickyRoomAsync(val: Room) { public async setStickyRoom(val: Room) {
await this.updateStickyRoom(val); await this.updateStickyRoom(val);
} }
@ -321,9 +315,11 @@ export class Algorithm extends EventEmitter {
} }
newMap[tagId] = allowedRoomsInThisTag; newMap[tagId] = allowedRoomsInThisTag;
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[DEBUG] ${newMap[tagId].length}/${rooms.length} rooms filtered into ${tagId}`); 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[]>[]); const allowedRooms = Object.values(newMap).reduce((rv, v) => { rv.push(...v); return rv; }, <Room[]>[]);
this.allowedRoomsByFilters = new Set(allowedRooms); this.allowedRoomsByFilters = new Set(allowedRooms);
@ -331,26 +327,13 @@ export class Algorithm extends EventEmitter {
this.emit(LIST_UPDATED_EVENT); 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 { protected recalculateFilteredRoomsForTag(tagId: TagID): void {
if (!this.hasFilters) return; // don't bother doing work if there's nothing to do 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}`); console.log(`Recalculating filtered rooms for ${tagId}`);
}
delete this.filteredRooms[tagId]; delete this.filteredRooms[tagId];
const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone
this.tryInsertStickyRoomToFilterSet(rooms, tagId); this.tryInsertStickyRoomToFilterSet(rooms, tagId);
@ -359,9 +342,11 @@ export class Algorithm extends EventEmitter {
this.filteredRooms[tagId] = filteredRooms; this.filteredRooms[tagId] = filteredRooms;
} }
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`); console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`);
} }
}
protected tryInsertStickyRoomToFilterSet(rooms: Room[], tagId: TagID) { protected tryInsertStickyRoomToFilterSet(rooms: Room[], tagId: TagID) {
if (!this._stickyRoom || !this._stickyRoom.room || this._stickyRoom.tag !== tagId) return; if (!this._stickyRoom || !this._stickyRoom.room || this._stickyRoom.tag !== tagId) return;
@ -399,8 +384,10 @@ export class Algorithm extends EventEmitter {
} }
if (!this._cachedStickyRooms || !updatedTag) { if (!this._cachedStickyRooms || !updatedTag) {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`Generating clone of cached rooms for sticky room handling`); console.log(`Generating clone of cached rooms for sticky room handling`);
}
const stickiedTagMap: ITagMap = {}; const stickiedTagMap: ITagMap = {};
for (const tagId of Object.keys(this.cachedRooms)) { for (const tagId of Object.keys(this.cachedRooms)) {
stickiedTagMap[tagId] = this.cachedRooms[tagId].map(r => r); // shallow clone stickiedTagMap[tagId] = this.cachedRooms[tagId].map(r => r); // shallow clone
@ -411,8 +398,10 @@ export class Algorithm extends EventEmitter {
if (updatedTag) { if (updatedTag) {
// Update the tag indicated by the caller, if possible. This is mostly to ensure // Update the tag indicated by the caller, if possible. This is mostly to ensure
// our cache is up to date. // our cache is up to date.
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`Replacing cached sticky rooms for ${updatedTag}`); console.log(`Replacing cached sticky rooms for ${updatedTag}`);
}
this._cachedStickyRooms[updatedTag] = this.cachedRooms[updatedTag].map(r => r); // shallow clone this._cachedStickyRooms[updatedTag] = this.cachedRooms[updatedTag].map(r => r); // shallow clone
} }
@ -421,8 +410,10 @@ export class Algorithm extends EventEmitter {
// we might have updated from the cache is also our sticky room. // we might have updated from the cache is also our sticky room.
const sticky = this._stickyRoom; const sticky = this._stickyRoom;
if (!updatedTag || updatedTag === sticky.tag) { if (!updatedTag || updatedTag === sticky.tag) {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // 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}`); 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); this._cachedStickyRooms[sticky.tag].splice(sticky.position, 0, sticky.room);
} }
@ -647,8 +638,10 @@ export class Algorithm extends EventEmitter {
* processing. * processing.
*/ */
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> { 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 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`Handle room update for ${room.roomId} called with cause ${cause}`); 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"); 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 // Note: check the isSticky against the room ID just in case the reference is wrong
@ -705,16 +698,20 @@ export class Algorithm extends EventEmitter {
const diff = arrayDiff(oldTags, newTags); const diff = arrayDiff(oldTags, newTags);
if (diff.removed.length > 0 || diff.added.length > 0) { if (diff.removed.length > 0 || diff.added.length > 0) {
for (const rmTag of diff.removed) { for (const rmTag of diff.removed) {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`Removing ${room.roomId} from ${rmTag}`); console.log(`Removing ${room.roomId} from ${rmTag}`);
}
const algorithm: OrderingAlgorithm = this.algorithms[rmTag]; const algorithm: OrderingAlgorithm = this.algorithms[rmTag];
if (!algorithm) throw new Error(`No algorithm for ${rmTag}`); if (!algorithm) throw new Error(`No algorithm for ${rmTag}`);
await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved); await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
this.cachedRooms[rmTag] = algorithm.orderedRooms; this.cachedRooms[rmTag] = algorithm.orderedRooms;
} }
for (const addTag of diff.added) { for (const addTag of diff.added) {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`Adding ${room.roomId} to ${addTag}`); console.log(`Adding ${room.roomId} to ${addTag}`);
}
const algorithm: OrderingAlgorithm = this.algorithms[addTag]; const algorithm: OrderingAlgorithm = this.algorithms[addTag];
if (!algorithm) throw new Error(`No algorithm for ${addTag}`); if (!algorithm) throw new Error(`No algorithm for ${addTag}`);
await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom); await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom);
@ -724,13 +721,17 @@ export class Algorithm extends EventEmitter {
// Update the tag map so we don't regen it in a moment // Update the tag map so we don't regen it in a moment
this.roomIdsToTags[room.roomId] = newTags; this.roomIdsToTags[room.roomId] = newTags;
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // 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`); console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`);
}
cause = RoomUpdateCause.Timeline; cause = RoomUpdateCause.Timeline;
didTagChange = true; didTagChange = true;
} else { } else {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // 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; cause = RoomUpdateCause.Timeline;
} }
@ -746,7 +747,7 @@ export class Algorithm extends EventEmitter {
}; };
} else { } else {
// We have to clear the lock as the sticky room change will trigger updates. // We have to clear the lock as the sticky room change will trigger updates.
await this.setStickyRoomAsync(room); await this.setStickyRoom(room);
} }
} }
} }
@ -756,20 +757,27 @@ export class Algorithm extends EventEmitter {
// as the sticky room relies on this. // as the sticky room relies on this.
if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) { if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) {
if (this.stickyRoom === room) { if (this.stickyRoom === room) {
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`); console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`);
}
return false; return false;
} }
} }
if (!this.roomIdsToTags[room.roomId]) { if (!this.roomIdsToTags[room.roomId]) {
if (CAUSES_REQUIRING_ROOM.includes(cause)) { 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`); console.warn(`Skipping tag update for ${room.roomId} because we don't know about the room`);
}
return false; return false;
} }
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`); console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`);
}
// Get the tags for the room and populate the cache // Get the tags for the room and populate the cache
const roomTags = this.getTagsForRoom(room).filter(t => !isNullOrUndefined(this.cachedRooms[t])); const roomTags = this.getTagsForRoom(room).filter(t => !isNullOrUndefined(this.cachedRooms[t]));
@ -780,12 +788,16 @@ export class Algorithm extends EventEmitter {
this.roomIdsToTags[room.roomId] = roomTags; this.roomIdsToTags[room.roomId] = roomTags;
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags); 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 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`); console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`);
}
const tags = this.roomIdsToTags[room.roomId]; const tags = this.roomIdsToTags[room.roomId];
if (!tags) { if (!tags) {
@ -807,8 +819,10 @@ export class Algorithm extends EventEmitter {
changed = true; changed = true;
} }
if (!window.mx_QuietRoomListLogging) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`); console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`);
}
return changed; return changed;
} }
} }

View file

@ -87,9 +87,6 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) { public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
super(tagId, initialSortingAlgorithm); 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 // noinspection JSMethodCanBeStatic

View file

@ -28,9 +28,6 @@ export class NaturalAlgorithm extends OrderingAlgorithm {
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) { public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
super(tagId, initialSortingAlgorithm); 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> { public async setRooms(rooms: Room[]): Promise<any> {

View file

@ -52,8 +52,6 @@ export class CommunityFilterCondition extends EventEmitter implements IFilterCon
const beforeRoomIds = this.roomIds; const beforeRoomIds = this.roomIds;
this.roomIds = (await GroupStore.getGroupRooms(this.community.groupId)).map(r => r.roomId); this.roomIds = (await GroupStore.getGroupRooms(this.community.groupId)).map(r => r.roomId);
if (arrayHasDiff(beforeRoomIds, this.roomIds)) { 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); this.emit(FILTER_CHANGED);
} }
}; };

View file

@ -41,8 +41,6 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
public set search(val: string) { public set search(val: string) {
this._search = val; 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); 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();
}
}
}