Implement MSC3575: Sliding Sync (#8328)
* Add labs flag for sliding sync; add sliding_sync_proxy_url to config.json * Disable the labs toggle if sliding_sync_proxy_url is not set * Do validation checks on the sliding sync proxy URL before enabling it in Labs * Enable sliding sync and add SlidingSyncManager * Get room subscriptions working * Hijack renderSublists in sliding sync mode * Add support for sorting alphabetically/recency and room name filters * Filter out tombstoned rooms; start adding show more logic list ranges update but the UI doesn't * update the UI when the list is updated * bugfix: make sure the list sorts numerically * Get invites transitioning correctly * Force enable sliding sync and labs for now * Linting * Disable spotlight search * Initial cypress plugins for Sliding Sync Proxy * Use --rm when running Synapse in Docker for Cypress tests * Update src/MatrixClientPeg.ts Co-authored-by: Travis Ralston <travisr@matrix.org> * Update src/components/views/rooms/RoomSublist.tsx Co-authored-by: Travis Ralston <travisr@matrix.org> * Update src/settings/controllers/SlidingSyncController.ts Co-authored-by: Travis Ralston <travisr@matrix.org> * Update src/components/views/rooms/RoomSublist.tsx Co-authored-by: Travis Ralston <travisr@matrix.org> * WIP add room searching to spotlight search * Only read sliding sync results when there is a result, else use the local cache * Use feature_sliding_sync not slidingSync * Some review comments * More review comments * Use RoomViewStore to set room subscriptions * Comment why any * Update src/components/views/rooms/RoomSublist.tsx Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> * Fix cypress docker abstraction * Iterate sliding sync proxy support * Stash mostly functional test * Update sliding sync proxy image * i18n * Add support for spaces; use list ID -> index mappings - Mappings are more reusable and easier to understand than racing for index positions. - Register for all spaces immediately on startup. * When the active space is updated, update the list registration * Set spaces filter in the correct place * Skeleton placeholder whilst loading the space * Filter out spaces from the room list * Use the new txn_id promises * Ensure we actually resolve list registrations * Fix matrix-org/sliding-sync#30: don't show tombstoned search results * Remove unused imports * Add SYNCV3_SECRET to proxy to ensure it starts up; correct aliases for SS test * Add another basic sliding sync e2e test * Unbreak netlify * Add more logging for debugging duplicate rooms * If sliding sync is enabled, always use the rooms result even if it's empty * Drop-in copy of RoomListStore for sliding sync * Remove conditionals from RoomListStore - we have SlidingRoomListStore now * WIP SlidingRoomListStore * Add most sliding sync logic to SlidingRoomListStore Still lots of logic in RoomSublist. Broken things: - Join count is wrong completely. - No skeleton placeholder when switching spaces. * Migrate joined count to SS RLS * Reinstate the skeleton UI when the list is loading * linting * Add support for sticky rooms based on the currently active room * Add a bunch of passing SS E2E tests; some WIP * Unbreak build from git merge * Suppress unread indicators in sliding sync mode * Add regression test for https://github.com/matrix-org/sliding-sync/issues/28 * Add invite test flows; show the invite list The refactor to SS RLS removed the invite list entirely. * Remove show more click as it wasn't the bug * Linting and i18n * only enable SS by default on netlify * Jest fixes; merge conflict fixes; remove debug logging; use right sort enum values * Actually fix jest tests * Add support for favourites and low priority * Bump sliding sync version * Update sliding sync labs to be user configurable * delint * To disable SS or change proxy URL the user has to log out * Review comments * Linting * Apply suggestions from code review Co-authored-by: Travis Ralston <travisr@matrix.org> * Update src/stores/room-list/SlidingRoomListStore.ts Co-authored-by: Travis Ralston <travisr@matrix.org> * Review comments * Add issue link for TODO markers * Linting * Apply suggestions from code review Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> * More review comments * More review comments * stricter types Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Travis Ralston <travisr@matrix.org>
This commit is contained in:
parent
5bdae150fa
commit
a215027c6b
25 changed files with 1632 additions and 51 deletions
130
src/components/views/dialogs/SlidingSyncOptionsDialog.tsx
Normal file
130
src/components/views/dialogs/SlidingSyncOptionsDialog.tsx
Normal file
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/matrix';
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import TextInputDialog from "./TextInputDialog";
|
||||
import withValidation from "../elements/Validation";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
|
||||
/**
|
||||
* Check that the server natively supports sliding sync.
|
||||
* @param cli The MatrixClient of the logged in user.
|
||||
* @throws if the proxy server is unreachable or not configured to the given homeserver
|
||||
*/
|
||||
async function syncHealthCheck(cli: MatrixClient): Promise<void> {
|
||||
const controller = new AbortController();
|
||||
const id = setTimeout(() => controller.abort(), 10 * 1000); // 10s
|
||||
const url = cli.http.getUrl("/sync", {}, "/_matrix/client/unstable/org.matrix.msc3575");
|
||||
const res = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
method: "POST",
|
||||
});
|
||||
clearTimeout(id);
|
||||
if (res.status != 200) {
|
||||
throw new Error(`syncHealthCheck: server returned HTTP ${res.status}`);
|
||||
}
|
||||
logger.info("server natively support sliding sync OK");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the proxy url is in fact a sliding sync proxy endpoint and it is up.
|
||||
* @param endpoint The proxy endpoint url
|
||||
* @param hsUrl The homeserver url of the logged in user.
|
||||
* @throws if the proxy server is unreachable or not configured to the given homeserver
|
||||
*/
|
||||
async function proxyHealthCheck(endpoint: string, hsUrl?: string): Promise<void> {
|
||||
const controller = new AbortController();
|
||||
const id = setTimeout(() => controller.abort(), 10 * 1000); // 10s
|
||||
const res = await fetch(endpoint + "/client/server.json", {
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(id);
|
||||
if (res.status != 200) {
|
||||
throw new Error(`proxyHealthCheck: proxy server returned HTTP ${res.status}`);
|
||||
}
|
||||
const body = await res.json();
|
||||
if (body.server !== hsUrl) {
|
||||
throw new Error(`proxyHealthCheck: client using ${hsUrl} but server is as ${body.server}`);
|
||||
}
|
||||
logger.info("sliding sync proxy is OK");
|
||||
}
|
||||
|
||||
export const SlidingSyncOptionsDialog: React.FC<IDialogProps> = ({ onFinished }) => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const currentProxy = SettingsStore.getValue("feature_sliding_sync_proxy_url");
|
||||
const hasNativeSupport = useAsyncMemo(() => syncHealthCheck(cli).then(() => true, () => false), [], null);
|
||||
|
||||
let nativeSupport: string;
|
||||
if (hasNativeSupport === null) {
|
||||
nativeSupport = _t("Checking...");
|
||||
} else {
|
||||
nativeSupport = hasNativeSupport
|
||||
? _t("Your server has native support")
|
||||
: _t("Your server lacks native support");
|
||||
}
|
||||
|
||||
const validProxy = withValidation<undefined, { error?: Error }>({
|
||||
async deriveData({ value }): Promise<{ error?: Error }> {
|
||||
try {
|
||||
await proxyHealthCheck(value, MatrixClientPeg.get().baseUrl);
|
||||
return {};
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: async ({ value }) => !!value || hasNativeSupport,
|
||||
invalid: () => _t("Your server lacks native support, you must specify a proxy"),
|
||||
}, {
|
||||
key: "working",
|
||||
final: true,
|
||||
test: async (_, { error }) => !error,
|
||||
valid: () => _t("Looks good"),
|
||||
invalid: ({ error }) => error?.message,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return <TextInputDialog
|
||||
title={_t("Sliding Sync configuration")}
|
||||
description={<div>
|
||||
<div><b>{ _t("To disable you will need to log out and back in, use with caution!") }</b></div>
|
||||
{ nativeSupport }
|
||||
</div>}
|
||||
placeholder={hasNativeSupport ? _t('Proxy URL (optional)') : _t('Proxy URL')}
|
||||
value={currentProxy}
|
||||
button={_t("Enable")}
|
||||
validator={validProxy}
|
||||
onFinished={(enable: boolean, proxyUrl: string) => {
|
||||
if (enable) {
|
||||
SettingsStore.setValue("feature_sliding_sync_proxy_url", null, SettingLevel.DEVICE, proxyUrl);
|
||||
onFinished(true);
|
||||
} else {
|
||||
onFinished(false);
|
||||
}
|
||||
}}
|
||||
/>;
|
||||
};
|
|
@ -32,7 +32,7 @@ interface IProps extends IDialogProps {
|
|||
busyMessage?: string; // pass _td string
|
||||
focus?: boolean;
|
||||
hasCancel?: boolean;
|
||||
validator?: (fieldState: IFieldState) => IValidationResult; // result of withValidation
|
||||
validator?: (fieldState: IFieldState) => Promise<IValidationResult>; // result of withValidation
|
||||
fixedWidth?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -92,6 +92,7 @@ import { RoomResultContextMenus } from "./RoomResultContextMenus";
|
|||
import { RoomContextDetails } from "../../rooms/RoomContextDetails";
|
||||
import { TooltipOption } from "./TooltipOption";
|
||||
import { isLocalRoom } from "../../../../utils/localRoom/isLocalRoom";
|
||||
import { useSlidingSyncRoomSearch } from "../../../../hooks/useSlidingSyncRoomSearch";
|
||||
|
||||
const MAX_RECENT_SEARCHES = 10;
|
||||
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
|
||||
|
@ -242,7 +243,7 @@ export const useWebSearchMetrics = (numResults: number, queryLength: number, via
|
|||
}, [numResults, queryLength, viaSpotlight]);
|
||||
};
|
||||
|
||||
const findVisibleRooms = (cli: MatrixClient) => {
|
||||
const findVisibleRooms = (cli: MatrixClient): Room[] => {
|
||||
return cli.getVisibleRooms().filter(room => {
|
||||
// Do not show local rooms
|
||||
if (isLocalRoom(room)) return false;
|
||||
|
@ -340,25 +341,43 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
searchProfileInfo,
|
||||
searchParams,
|
||||
);
|
||||
const isSlidingSyncEnabled = SettingsStore.getValue("feature_sliding_sync");
|
||||
let {
|
||||
loading: slidingSyncRoomSearchLoading,
|
||||
rooms: slidingSyncRooms,
|
||||
search: searchRoomsServerside,
|
||||
} = useSlidingSyncRoomSearch();
|
||||
useDebouncedCallback(isSlidingSyncEnabled, searchRoomsServerside, searchParams);
|
||||
if (!isSlidingSyncEnabled) {
|
||||
slidingSyncRoomSearchLoading = false;
|
||||
}
|
||||
|
||||
const possibleResults = useMemo<Result[]>(
|
||||
() => {
|
||||
const roomResults = findVisibleRooms(cli).map(toRoomResult);
|
||||
// If we already have a DM with the user we're looking for, we will
|
||||
// show that DM instead of the user themselves
|
||||
const alreadyAddedUserIds = roomResults.reduce((userIds, result) => {
|
||||
const userId = DMRoomMap.shared().getUserIdForRoomId(result.room.roomId);
|
||||
if (!userId) return userIds;
|
||||
if (result.room.getJoinedMemberCount() > 2) return userIds;
|
||||
userIds.add(userId);
|
||||
return userIds;
|
||||
}, new Set<string>());
|
||||
const userResults = [];
|
||||
for (const user of [...findVisibleRoomMembers(cli), ...users]) {
|
||||
// Make sure we don't have any user more than once
|
||||
if (alreadyAddedUserIds.has(user.userId)) continue;
|
||||
alreadyAddedUserIds.add(user.userId);
|
||||
const userResults: IMemberResult[] = [];
|
||||
let roomResults: IRoomResult[];
|
||||
let alreadyAddedUserIds: Set<string>;
|
||||
if (isSlidingSyncEnabled) {
|
||||
// use the rooms sliding sync returned as the server has already worked it out for us
|
||||
roomResults = slidingSyncRooms.map(toRoomResult);
|
||||
} else {
|
||||
roomResults = findVisibleRooms(cli).map(toRoomResult);
|
||||
// If we already have a DM with the user we're looking for, we will
|
||||
// show that DM instead of the user themselves
|
||||
alreadyAddedUserIds = roomResults.reduce((userIds, result) => {
|
||||
const userId = DMRoomMap.shared().getUserIdForRoomId(result.room.roomId);
|
||||
if (!userId) return userIds;
|
||||
if (result.room.getJoinedMemberCount() > 2) return userIds;
|
||||
userIds.add(userId);
|
||||
return userIds;
|
||||
}, new Set<string>());
|
||||
for (const user of [...findVisibleRoomMembers(cli), ...users]) {
|
||||
// Make sure we don't have any user more than once
|
||||
if (alreadyAddedUserIds.has(user.userId)) continue;
|
||||
alreadyAddedUserIds.add(user.userId);
|
||||
|
||||
userResults.push(toMemberResult(user));
|
||||
userResults.push(toMemberResult(user));
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
|
@ -382,7 +401,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
...publicRooms.map(toPublicRoomResult),
|
||||
].filter(result => filter === null || result.filter.includes(filter));
|
||||
},
|
||||
[cli, users, profile, publicRooms, filter],
|
||||
[cli, users, profile, publicRooms, slidingSyncRooms, isSlidingSyncEnabled, filter],
|
||||
);
|
||||
|
||||
const results = useMemo<Record<Section, Result[]>>(() => {
|
||||
|
@ -401,10 +420,13 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
|
||||
possibleResults.forEach(entry => {
|
||||
if (isRoomResult(entry)) {
|
||||
if (!entry.room.normalizedName?.includes(normalizedQuery) &&
|
||||
!entry.room.getCanonicalAlias()?.toLowerCase().includes(lcQuery) &&
|
||||
!entry.query?.some(q => q.includes(lcQuery))
|
||||
) return; // bail, does not match query
|
||||
// sliding sync gives the correct rooms in the list so we don't need to filter
|
||||
if (!isSlidingSyncEnabled) {
|
||||
if (!entry.room.normalizedName?.includes(normalizedQuery) &&
|
||||
!entry.room.getCanonicalAlias()?.toLowerCase().includes(lcQuery) &&
|
||||
!entry.query?.some(q => q.includes(lcQuery))
|
||||
) return; // bail, does not match query
|
||||
}
|
||||
} else if (isMemberResult(entry)) {
|
||||
if (!entry.query?.some(q => q.includes(lcQuery))) return; // bail, does not match query
|
||||
} else if (isPublicRoomResult(entry)) {
|
||||
|
@ -455,7 +477,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
}
|
||||
|
||||
return results;
|
||||
}, [trimmedQuery, filter, cli, possibleResults, memberComparator]);
|
||||
}, [trimmedQuery, filter, cli, possibleResults, isSlidingSyncEnabled, memberComparator]);
|
||||
|
||||
const numResults = sum(Object.values(results).map(it => it.length));
|
||||
useWebSearchMetrics(numResults, query.length, true);
|
||||
|
@ -1208,7 +1230,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
aria-label={_t("Search")}
|
||||
aria-describedby="mx_SpotlightDialog_keyboardPrompt"
|
||||
/>
|
||||
{ (publicRoomsLoading || peopleLoading || profileLoading) && (
|
||||
{ (publicRoomsLoading || peopleLoading || profileLoading || slidingSyncRoomSearchLoading) && (
|
||||
<Spinner w={24} h={24} />
|
||||
) }
|
||||
</div>
|
||||
|
|
|
@ -16,9 +16,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from 'classnames';
|
||||
import { Dispatcher } from "flux";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { Enable, Resizable } from "re-resizable";
|
||||
import { Direction } from "re-resizable/lib/resizer";
|
||||
import * as React from "react";
|
||||
|
@ -39,7 +39,7 @@ import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorith
|
|||
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT, LISTS_LOADING_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||
import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
|
||||
import { objectExcluding, objectHasDiff } from "../../../utils/objects";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
|
@ -52,6 +52,8 @@ import ContextMenu, {
|
|||
import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import ExtraTile from "./ExtraTile";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { SlidingSyncManager } from "../../../SlidingSyncManager";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
import RoomTile from "./RoomTile";
|
||||
|
||||
|
@ -98,6 +100,7 @@ interface IState {
|
|||
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
|
||||
height: number;
|
||||
rooms: Room[];
|
||||
roomsLoading: boolean;
|
||||
}
|
||||
|
||||
export default class RoomSublist extends React.Component<IProps, IState> {
|
||||
|
@ -109,8 +112,12 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
private heightAtStart: number;
|
||||
private notificationState: ListNotificationState;
|
||||
|
||||
private slidingSyncMode: boolean;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
// when this setting is toggled it restarts the app so it's safe to not watch this.
|
||||
this.slidingSyncMode = SettingsStore.getValue("feature_sliding_sync");
|
||||
|
||||
this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId);
|
||||
this.heightAtStart = 0;
|
||||
|
@ -121,6 +128,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
isExpanded: !this.layout.isCollapsed,
|
||||
height: 0, // to be fixed in a moment, we need `rooms` to calculate this.
|
||||
rooms: arrayFastClone(RoomListStore.instance.orderedLists[this.props.tagId] || []),
|
||||
roomsLoading: false,
|
||||
};
|
||||
// Why Object.assign() and not this.state.height? Because TypeScript says no.
|
||||
this.state = Object.assign(this.state, { height: this.calculateInitialHeight() });
|
||||
|
@ -167,6 +175,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private get numVisibleTiles(): number {
|
||||
if (this.slidingSyncMode) {
|
||||
return this.state.rooms.length;
|
||||
}
|
||||
const nVisible = Math.ceil(this.layout.visibleTiles);
|
||||
return Math.min(nVisible, this.numTiles);
|
||||
}
|
||||
|
@ -239,6 +250,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
public componentDidMount() {
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||
RoomListStore.instance.on(LISTS_LOADING_EVENT, this.onListsLoading);
|
||||
|
||||
// Using the passive option to not block the main thread
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||
this.tilesRef.current?.addEventListener("scroll", this.onScrollPrevent, { passive: true });
|
||||
|
@ -247,9 +260,19 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
public componentWillUnmount() {
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||
RoomListStore.instance.off(LISTS_LOADING_EVENT, this.onListsLoading);
|
||||
this.tilesRef.current?.removeEventListener("scroll", this.onScrollPrevent);
|
||||
}
|
||||
|
||||
private onListsLoading = (tagId: TagID, isLoading: boolean) => {
|
||||
if (this.props.tagId !== tagId) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
roomsLoading: isLoading,
|
||||
});
|
||||
};
|
||||
|
||||
private onListsUpdated = () => {
|
||||
const stateUpdates: IState & any = {}; // &any is to avoid a cast on the initializer
|
||||
|
||||
|
@ -315,7 +338,16 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
this.setState({ isResizing: false, height: newHeight });
|
||||
};
|
||||
|
||||
private onShowAllClick = () => {
|
||||
private onShowAllClick = async () => {
|
||||
if (this.slidingSyncMode) {
|
||||
const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(this.props.tagId);
|
||||
const count = RoomListStore.instance.getCount(this.props.tagId);
|
||||
await SlidingSyncManager.instance.ensureListRegistered(slidingSyncIndex, {
|
||||
ranges: [
|
||||
[0, count],
|
||||
],
|
||||
});
|
||||
}
|
||||
// read number of visible tiles before we mutate it
|
||||
const numVisibleTiles = this.numVisibleTiles;
|
||||
const newHeight = this.layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
|
||||
|
@ -531,8 +563,17 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
|
||||
let contextMenu = null;
|
||||
if (this.state.contextMenuPosition) {
|
||||
const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
|
||||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
||||
let isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
|
||||
let isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
||||
if (this.slidingSyncMode) {
|
||||
const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(this.props.tagId);
|
||||
const slidingList = SlidingSyncManager.instance.slidingSync.getList(slidingSyncIndex);
|
||||
isAlphabetical = slidingList.sort[0] === "by_name";
|
||||
isUnreadFirst = (
|
||||
slidingList.sort[0] === "by_highlight_count" ||
|
||||
slidingList.sort[0] === "by_notification_count"
|
||||
);
|
||||
}
|
||||
|
||||
// Invites don't get some nonsense options, so only add them if we have to.
|
||||
let otherSections = null;
|
||||
|
@ -717,7 +758,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
});
|
||||
|
||||
let content = null;
|
||||
if (visibleTiles.length > 0 && this.props.forceExpanded) {
|
||||
if (this.state.roomsLoading) {
|
||||
content = <div className="mx_RoomSublist_skeletonUI" />;
|
||||
} else if (visibleTiles.length > 0 && this.props.forceExpanded) {
|
||||
content = <div className="mx_RoomSublist_resizeBox mx_RoomSublist_resizeBox_forceExpanded">
|
||||
<div className="mx_RoomSublist_tiles" ref={this.tilesRef}>
|
||||
{ visibleTiles }
|
||||
|
@ -739,12 +782,18 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
// floats above the resize handle, if we have one present. If the user has all
|
||||
// tiles visible, it becomes 'show less'.
|
||||
let showNButton = null;
|
||||
const hasMoreSlidingSync = (
|
||||
this.slidingSyncMode && (RoomListStore.instance.getCount(this.props.tagId) > this.state.rooms.length)
|
||||
);
|
||||
|
||||
if (maxTilesPx > this.state.height) {
|
||||
if ((maxTilesPx > this.state.height) || hasMoreSlidingSync) {
|
||||
// the height of all the tiles is greater than the section height: we need a 'show more' button
|
||||
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 numMissing = this.numTiles - amountFullyShown;
|
||||
if (this.slidingSyncMode) {
|
||||
numMissing = RoomListStore.instance.getCount(this.props.tagId) - amountFullyShown;
|
||||
}
|
||||
const label = _t("Show %(count)s more", { count: numMissing });
|
||||
let showMoreText = (
|
||||
<span className='mx_RoomSublist_showNButtonText'>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue