diff --git a/src/components/views/rooms/WhoIsTypingTile.tsx b/src/components/views/rooms/WhoIsTypingTile.tsx index 1b5636cd15..2bc6337945 100644 --- a/src/components/views/rooms/WhoIsTypingTile.tsx +++ b/src/components/views/rooms/WhoIsTypingTile.tsx @@ -17,7 +17,6 @@ limitations under the License. import React from "react"; import { Room, RoomEvent, RoomMember, RoomMemberEvent, MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { compare } from "matrix-js-sdk/src/utils"; import * as WhoIsTyping from "../../../WhoIsTyping"; import Timer from "../../../utils/Timer"; @@ -208,7 +207,8 @@ export default class WhoIsTypingTile extends React.Component { // sort them so the typing members don't change order when // moved to delayedStopTypingTimers - usersTyping.sort((a, b) => compare(a.name, b.name)); + const collator = new Intl.Collator(); + usersTyping.sort((a, b) => collator.compare(a.name, b.name)); const typingString = WhoIsTyping.whoIsTypingString(usersTyping, this.props.whoIsTypingLimit); if (!typingString) { diff --git a/src/components/views/settings/PowerLevelSelector.tsx b/src/components/views/settings/PowerLevelSelector.tsx index 5d823c885d..dcb1590c07 100644 --- a/src/components/views/settings/PowerLevelSelector.tsx +++ b/src/components/views/settings/PowerLevelSelector.tsx @@ -18,7 +18,6 @@ import React, { useState, JSX, PropsWithChildren } from "react"; import { Button } from "@vector-im/compound-web"; -import { compare } from "matrix-js-sdk/src/utils"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import PowerSelector from "../elements/PowerSelector"; @@ -78,9 +77,11 @@ export function PowerLevelSelector({ currentPowerLevel && currentPowerLevel.value !== userLevels[currentPowerLevel?.userId], ); + const collator = new Intl.Collator(); + // We sort the users by power level, then we filter them const users = Object.keys(userLevels) - .sort((userA, userB) => sortUser(userA, userB, userLevels)) + .sort((userA, userB) => sortUser(collator, userA, userB, userLevels)) .filter(filter); // No user to display, we return the children into fragment to convert it to JSX.Element type @@ -136,7 +137,14 @@ export function PowerLevelSelector({ * @param userB * @param userLevels */ -function sortUser(userA: string, userB: string, userLevels: PowerLevelSelectorProps["userLevels"]): number { +function sortUser( + collator: Intl.Collator, + userA: string, + userB: string, + userLevels: PowerLevelSelectorProps["userLevels"], +): number { const powerLevelDiff = userLevels[userA] - userLevels[userB]; - return powerLevelDiff !== 0 ? powerLevelDiff : compare(userA.toLocaleLowerCase(), userB.toLocaleLowerCase()); + return powerLevelDiff !== 0 + ? powerLevelDiff + : collator.compare(userA.toLocaleLowerCase(), userB.toLocaleLowerCase()); } diff --git a/src/integrations/IntegrationManagers.ts b/src/integrations/IntegrationManagers.ts index 0ec7d99185..5ccf6a6e0c 100644 --- a/src/integrations/IntegrationManagers.ts +++ b/src/integrations/IntegrationManagers.ts @@ -16,7 +16,6 @@ limitations under the License. import { logger } from "matrix-js-sdk/src/logger"; import { ClientEvent, IClientWellKnown, MatrixClient } from "matrix-js-sdk/src/matrix"; -import { compare } from "matrix-js-sdk/src/utils"; import type { MatrixEvent } from "matrix-js-sdk/src/matrix"; import SdkConfig from "../SdkConfig"; @@ -145,6 +144,7 @@ export class IntegrationManagers { } public getOrderedManagers(): IntegrationManagerInstance[] { + const collator = new Intl.Collator(); const ordered: IntegrationManagerInstance[] = []; for (const kind of KIND_PREFERENCE) { const managers = this.managers.filter((m) => m.kind === kind); @@ -152,7 +152,7 @@ export class IntegrationManagers { if (kind === Kind.Account) { // Order by state_keys (IDs) - managers.sort((a, b) => compare(a.id ?? "", b.id ?? "")); + managers.sort((a, b) => collator.compare(a.id ?? "", b.id ?? "")); } ordered.push(...managers); diff --git a/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts index 9b2363214a..4421c23c60 100644 --- a/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts +++ b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts @@ -15,7 +15,6 @@ limitations under the License. */ import { Room } from "matrix-js-sdk/src/matrix"; -import { compare } from "matrix-js-sdk/src/utils"; import { TagID } from "../../models"; import { IAlgorithm } from "./IAlgorithm"; @@ -25,8 +24,9 @@ import { IAlgorithm } from "./IAlgorithm"; */ export class AlphabeticAlgorithm implements IAlgorithm { public sortRooms(rooms: Room[], tagId: TagID): Room[] { + const collator = new Intl.Collator(); return rooms.sort((a, b) => { - return compare(a.name, b.name); + return collator.compare(a.name, b.name); }); } } diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts index eef5d84d0d..6b5766ebc9 100644 --- a/src/stores/widgets/WidgetLayoutStore.ts +++ b/src/stores/widgets/WidgetLayoutStore.ts @@ -16,7 +16,7 @@ import { Room, RoomStateEvent, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { Optional } from "matrix-events-sdk"; -import { compare, MapWithDefault, recursiveMapToObject } from "matrix-js-sdk/src/utils"; +import { MapWithDefault, recursiveMapToObject } from "matrix-js-sdk/src/utils"; import { IWidget } from "matrix-widget-api"; import SettingsStore from "../../settings/SettingsStore"; @@ -200,6 +200,8 @@ export class WidgetLayoutStore extends ReadyWatchingStore { const runoff = topWidgets.slice(MAX_PINNED); rightWidgets.push(...runoff); + const collator = new Intl.Collator(); + // Order the widgets in the top container, putting autopinned Jitsi widgets first // unless they have a specific order in mind topWidgets.sort((a, b) => { @@ -219,7 +221,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { if (orderA === orderB) { // We just need a tiebreak - return compare(a.id, b.id); + return collator.compare(a.id, b.id); } return orderA - orderB; diff --git a/src/theme.ts b/src/theme.ts index 3245d72b76..9cff3f95be 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { compare } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "./languageHandler"; @@ -113,7 +112,10 @@ export function getOrderedThemes(): ITheme[] { .map((p) => ({ id: p[0], name: p[1] })) // convert pairs to objects for code readability .filter((p) => !isHighContrastTheme(p.id)); const builtInThemes = themes.filter((p) => !p.id.startsWith("custom-")); - const customThemes = themes.filter((p) => !builtInThemes.includes(p)).sort((a, b) => compare(a.name, b.name)); + const collator = new Intl.Collator(); + const customThemes = themes + .filter((p) => !builtInThemes.includes(p)) + .sort((a, b) => collator.compare(a.name, b.name)); return [...builtInThemes, ...customThemes]; } diff --git a/test/components/views/rooms/MemberList-test.tsx b/test/components/views/rooms/MemberList-test.tsx index 648fa71230..87806e5a85 100644 --- a/test/components/views/rooms/MemberList-test.tsx +++ b/test/components/views/rooms/MemberList-test.tsx @@ -19,7 +19,6 @@ import React from "react"; import { act, fireEvent, render, RenderResult, screen } from "@testing-library/react"; import { Room, MatrixClient, RoomState, RoomMember, User, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; -import { compare } from "matrix-js-sdk/src/utils"; import { mocked, MockedObject } from "jest-mock"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; @@ -145,7 +144,8 @@ describe("MemberList", () => { if (!groupChange) { const nameA = memberA.name[0] === "@" ? memberA.name.slice(1) : memberA.name; const nameB = memberB.name[0] === "@" ? memberB.name.slice(1) : memberB.name; - const nameCompare = compare(nameB, nameA); + const collator = new Intl.Collator(); + const nameCompare = collator.compare(nameB, nameA); console.log("Comparing name"); expect(nameCompare).toBeGreaterThanOrEqual(0); } else { diff --git a/test/integrations/IntegrationManagers-test.ts b/test/integrations/IntegrationManagers-test.ts new file mode 100644 index 0000000000..db95ab435b --- /dev/null +++ b/test/integrations/IntegrationManagers-test.ts @@ -0,0 +1,70 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { mocked } from "jest-mock"; + +import { IntegrationManagers } from "../../src/integrations/IntegrationManagers"; +import { stubClient } from "../test-utils"; + +describe("IntegrationManagers", () => { + let client: MatrixClient; + let intMgrs: IntegrationManagers; + + beforeEach(() => { + client = stubClient(); + mocked(client).getAccountData.mockReturnValue({ + getContent: jest.fn().mockReturnValue({ + foo: { + id: "foo", + content: { + type: "m.integration_manager", + url: "http://foo/ui", + data: { + api_url: "http://foo/api", + }, + }, + }, + bar: { + id: "bar", + content: { + type: "m.integration_manager", + url: "http://bar/ui", + data: { + api_url: "http://bar/api", + }, + }, + }, + }), + } as unknown as MatrixEvent); + + intMgrs = new IntegrationManagers(); + intMgrs.startWatching(); + }); + + afterEach(() => { + intMgrs.stopWatching(); + }); + + describe("getOrderedManagers", () => { + it("should return integration managers in alphabetical order", () => { + const orderedManagers = intMgrs.getOrderedManagers(); + + expect(orderedManagers[0].id).toBe("bar"); + expect(orderedManagers[1].id).toBe("foo"); + }); + }); +}); diff --git a/test/stores/WidgetLayoutStore-test.ts b/test/stores/WidgetLayoutStore-test.ts index 17fbf9f2fa..ee4ebcc6d0 100644 --- a/test/stores/WidgetLayoutStore-test.ts +++ b/test/stores/WidgetLayoutStore-test.ts @@ -25,25 +25,29 @@ import SettingsStore from "../../src/settings/SettingsStore"; // setup test env values const roomId = "!room:server"; -const mockRoom = { - roomId: roomId, - currentState: { - getStateEvents: (_l, _x) => { - return { - getId: () => "$layoutEventId", - getContent: () => null, - }; - }, - }, -}; describe("WidgetLayoutStore", () => { let client: MatrixClient; let store: WidgetLayoutStore; let roomUpdateListener: (event: string) => void; let mockApps: IApp[]; + let mockRoom: Room; + let layoutEventContent: Record | null; beforeEach(() => { + layoutEventContent = null; + mockRoom = { + roomId: roomId, + currentState: { + getStateEvents: (_l, _x) => { + return { + getId: () => "$layoutEventId", + getContent: () => layoutEventContent, + }; + }, + }, + }; + mockApps = [ { roomId: roomId, id: "1" }, { roomId: roomId, id: "2" }, @@ -87,6 +91,22 @@ describe("WidgetLayoutStore", () => { expect(store.getContainerHeight(mockRoom, Container.Top)).toBeNull(); }); + it("ordering of top container widgets should be consistent even if no index specified", async () => { + layoutEventContent = { + widgets: { + "1": { + container: "top", + }, + "2": { + container: "top", + }, + }, + }; + + store.recalculateRoom(mockRoom); + expect(store.getContainerWidgets(mockRoom, Container.Top)).toStrictEqual([mockApps[0], mockApps[1]]); + }); + it("add three widgets to top container", async () => { store.recalculateRoom(mockRoom); store.moveToContainer(mockRoom, mockApps[0], Container.Top); diff --git a/test/theme-test.ts b/test/theme-test.ts index fdba6d0d18..b375a75301 100644 --- a/test/theme-test.ts +++ b/test/theme-test.ts @@ -15,9 +15,13 @@ limitations under the License. */ import SettingsStore from "../src/settings/SettingsStore"; -import { enumerateThemes, setTheme } from "../src/theme"; +import { enumerateThemes, getOrderedThemes, setTheme } from "../src/theme"; describe("theme", () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + describe("setTheme", () => { let lightTheme: HTMLStyleElement; let darkTheme: HTMLStyleElement; @@ -48,7 +52,6 @@ describe("theme", () => { }); afterEach(() => { - jest.restoreAllMocks(); jest.useRealTimers(); }); @@ -162,4 +165,16 @@ describe("theme", () => { }); }); }); + + describe("getOrderedThemes", () => { + it("should return a list of themes in the correct order", () => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue([{ name: "Zebra Striped" }, { name: "Apple Green" }]); + expect(getOrderedThemes()).toEqual([ + { id: "light", name: "Light" }, + { id: "dark", name: "Dark" }, + { id: "custom-Apple Green", name: "Apple Green" }, + { id: "custom-Zebra Striped", name: "Zebra Striped" }, + ]); + }); + }); });