diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index b00b45bf60..b006205f11 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -49,6 +49,7 @@ import {mediaFromMxc} from "../../../customisations/Media"; import {getAddressType} from "../../../UserAddress"; import BaseAvatar from '../avatars/BaseAvatar'; import AccessibleButton from '../elements/AccessibleButton'; +import { compare } from '../../../utils/strings'; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ @@ -578,7 +579,7 @@ export default class InviteDialog extends React.PureComponent { if (a.score === b.score) { if (a.numRooms === b.numRooms) { - return a.member.userId.localeCompare(b.member.userId); + return compare(a.member.userId, b.member.userId); } return b.numRooms - a.numRooms; diff --git a/src/components/views/directory/NetworkDropdown.tsx b/src/components/views/directory/NetworkDropdown.tsx index 08787812f6..c805ee42e7 100644 --- a/src/components/views/directory/NetworkDropdown.tsx +++ b/src/components/views/directory/NetworkDropdown.tsx @@ -39,6 +39,7 @@ import { SettingLevel } from "../../../settings/SettingLevel"; import TextInputDialog from "../dialogs/TextInputDialog"; import QuestionDialog from "../dialogs/QuestionDialog"; import UIStore from "../../../stores/UIStore"; +import { compare } from "../../../utils/strings"; export const ALL_ROOMS = Symbol("ALL_ROOMS"); @@ -187,7 +188,7 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s protocolsList.forEach(({instances=[]}) => { [...instances].sort((b, a) => { - return a.desc.localeCompare(b.desc); + return compare(a.desc, b.desc); }).forEach(({desc, instance_id: instanceId}) => { entries.push( { @@ -422,7 +420,7 @@ export default class MemberList extends React.Component { } else { // Is a 3pid invite return this._onPending3pidInviteClick(m)} />; + onClick={() => this._onPending3pidInviteClick(m)} />; } }); } @@ -484,10 +482,10 @@ export default class MemberList extends React.Component { if (this._getChildCountInvited() > 0) { invitedHeader =

{ _t("Invited") }

; invitedSection = ; + createOverflowElement={this._createOverflowTileInvited} + getChildren={this._getChildrenInvited} + getChildCount={this._getChildCountInvited} + />; } const footer = ( @@ -520,9 +518,9 @@ export default class MemberList extends React.Component { >
+ createOverflowElement={this._createOverflowTileJoined} + getChildren={this._getChildrenJoined} + getChildCount={this._getChildCountJoined} /> { invitedHeader } { invitedSection }
diff --git a/src/components/views/rooms/WhoIsTypingTile.tsx b/src/components/views/rooms/WhoIsTypingTile.tsx index 21afbc30f4..c5d5929546 100644 --- a/src/components/views/rooms/WhoIsTypingTile.tsx +++ b/src/components/views/rooms/WhoIsTypingTile.tsx @@ -25,6 +25,7 @@ import Timer from '../../../utils/Timer'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import MemberAvatar from '../avatars/MemberAvatar'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { compare } from "../../../utils/strings"; interface IProps { // the room this statusbar is representing. @@ -207,7 +208,7 @@ export default class WhoIsTypingTile extends React.Component { usersTyping = usersTyping.concat(stoppedUsersOnTimer); // sort them so the typing members don't change order when // moved to delayedStopTypingTimers - usersTyping.sort((a, b) => a.name.localeCompare(b.name)); + usersTyping.sort((a, b) => compare(a.name, b.name)); const typingString = WhoIsTyping.whoIsTypingString( usersTyping, diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 4fa521f598..19ebe2a77e 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -25,6 +25,7 @@ import {EventType} from "matrix-js-sdk/src/@types/event"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { RoomState } from "matrix-js-sdk/src/models/room-state"; +import { compare } from "../../../../../utils/strings"; const plEventsToLabels = { // These will be translated for us later. @@ -312,7 +313,7 @@ export default class RolesRoomSettingsTab extends React.Component { // comparator for sorting PL users lexicographically on PL descending, MXID ascending. (case-insensitive) const comparator = (a, b) => { const plDiff = userLevels[b.key] - userLevels[a.key]; - return plDiff !== 0 ? plDiff : a.key.toLocaleLowerCase().localeCompare(b.key.toLocaleLowerCase()); + return plDiff !== 0 ? plDiff : compare(a.key.toLocaleLowerCase(), b.key.toLocaleLowerCase()); }; privilegedUsers.sort(comparator); diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index bc40c36bda..9e27ed968e 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -35,9 +35,10 @@ import Field from '../../../elements/Field'; import EventTilePreview from '../../../elements/EventTilePreview'; import StyledRadioGroup from "../../../elements/StyledRadioGroup"; import { SettingLevel } from "../../../../../settings/SettingLevel"; -import {UIFeature} from "../../../../../settings/UIFeature"; -import {Layout} from "../../../../../settings/Layout"; -import {replaceableComponent} from "../../../../../utils/replaceableComponent"; +import { UIFeature } from "../../../../../settings/UIFeature"; +import { Layout } from "../../../../../settings/Layout"; +import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +import { compare } from "../../../../../utils/strings"; interface IProps { } @@ -295,7 +296,7 @@ export default class AppearanceUserSettingsTab extends React.Component ({id: p[0], name: p[1]})); // convert pairs to objects for code readability const builtInThemes = themes.filter(p => !p.id.startsWith("custom-")); const customThemes = themes.filter(p => !builtInThemes.includes(p)) - .sort((a, b) => a.name.localeCompare(b.name)); + .sort((a, b) => compare(a.name, b.name)); const orderedThemes = [...builtInThemes, ...customThemes]; return (
diff --git a/src/integrations/IntegrationManagers.ts b/src/integrations/IntegrationManagers.ts index a29c74c5eb..780f6d4660 100644 --- a/src/integrations/IntegrationManagers.ts +++ b/src/integrations/IntegrationManagers.ts @@ -28,6 +28,7 @@ import WidgetUtils from "../utils/WidgetUtils"; import {MatrixClientPeg} from "../MatrixClientPeg"; import SettingsStore from "../settings/SettingsStore"; import url from 'url'; +import { compare } from "../utils/strings"; const KIND_PREFERENCE = [ // Ordered: first is most preferred, last is least preferred. @@ -152,7 +153,7 @@ export class IntegrationManagers { if (kind === Kind.Account) { // Order by state_keys (IDs) - managers.sort((a, b) => a.id.localeCompare(b.id)); + managers.sort((a, b) => 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 d909fb6288..b016a4256c 100644 --- a/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts +++ b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts @@ -17,6 +17,7 @@ limitations under the License. import { Room } from "matrix-js-sdk/src/models/room"; import { TagID } from "../../models"; import { IAlgorithm } from "./IAlgorithm"; +import { compare } from "../../../../utils/strings"; /** * Sorts rooms according to the browser's determination of alphabetic. @@ -24,7 +25,7 @@ import { IAlgorithm } from "./IAlgorithm"; export class AlphabeticAlgorithm implements IAlgorithm { public async sortRooms(rooms: Room[], tagId: TagID): Promise { return rooms.sort((a, b) => { - return a.name.localeCompare(b.name); + return compare(a.name, b.name); }); } } diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts index e6ef534202..f5734d74c5 100644 --- a/src/stores/widgets/WidgetLayoutStore.ts +++ b/src/stores/widgets/WidgetLayoutStore.ts @@ -25,6 +25,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { SettingLevel } from "../../settings/SettingLevel"; import { arrayFastClone } from "../../utils/arrays"; import { UPDATE_EVENT } from "../AsyncStore"; +import { compare } from "../../utils/strings"; export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout"; @@ -240,7 +241,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore { if (orderA === orderB) { // We just need a tiebreak - return a.id.localeCompare(b.id); + return compare(a.id, b.id); } return orderA - orderB; diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 5856682445..beb9f31ddd 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -73,3 +73,14 @@ export function copyNode(ref: Element): boolean { selectText(ref); return document.execCommand('copy'); } + + +const collator = new Intl.Collator(); +/** + * Performant language-sensitive string comparison + * @param a the first string to compare + * @param b the second string to compare + */ +export function compare(a: string, b: string): number { + return collator.compare(a, b); +} diff --git a/test/components/views/rooms/MemberList-test.js b/test/components/views/rooms/MemberList-test.js index 50b40dea20..28fead770c 100644 --- a/test/components/views/rooms/MemberList-test.js +++ b/test/components/views/rooms/MemberList-test.js @@ -9,6 +9,8 @@ import sdk from '../../../skinned-sdk'; import {Room, RoomMember, User} from 'matrix-js-sdk'; +import { compare } from "../../../../src/utils/strings"; + function generateRoomId() { return '!' + Math.random().toString().slice(2, 10) + ':domain'; } @@ -173,7 +175,7 @@ describe('MemberList', () => { if (!groupChange) { const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name; const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name; - const nameCompare = nameB.localeCompare(nameA); + const nameCompare = compare(nameB, nameA); console.log("Comparing name"); expect(nameCompare).toBeGreaterThanOrEqual(0); } else {