Merge pull request #6130 from matrix-org/gsouquet/member-list-sort

This commit is contained in:
Germain 2021-06-02 11:40:52 +01:00 committed by GitHub
commit 96f5d3af05
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 46 additions and 27 deletions

View file

@ -49,6 +49,7 @@ import {mediaFromMxc} from "../../../customisations/Media";
import {getAddressType} from "../../../UserAddress"; import {getAddressType} from "../../../UserAddress";
import BaseAvatar from '../avatars/BaseAvatar'; import BaseAvatar from '../avatars/BaseAvatar';
import AccessibleButton from '../elements/AccessibleButton'; 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. // we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@ -578,7 +579,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
members.sort((a, b) => { members.sort((a, b) => {
if (a.score === b.score) { if (a.score === b.score) {
if (a.numRooms === b.numRooms) { 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; return b.numRooms - a.numRooms;

View file

@ -39,6 +39,7 @@ import { SettingLevel } from "../../../settings/SettingLevel";
import TextInputDialog from "../dialogs/TextInputDialog"; import TextInputDialog from "../dialogs/TextInputDialog";
import QuestionDialog from "../dialogs/QuestionDialog"; import QuestionDialog from "../dialogs/QuestionDialog";
import UIStore from "../../../stores/UIStore"; import UIStore from "../../../stores/UIStore";
import { compare } from "../../../utils/strings";
export const ALL_ROOMS = Symbol("ALL_ROOMS"); export const ALL_ROOMS = Symbol("ALL_ROOMS");
@ -187,7 +188,7 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
protocolsList.forEach(({instances=[]}) => { protocolsList.forEach(({instances=[]}) => {
[...instances].sort((b, a) => { [...instances].sort((b, a) => {
return a.desc.localeCompare(b.desc); return compare(a.desc, b.desc);
}).forEach(({desc, instance_id: instanceId}) => { }).forEach(({desc, instance_id: instanceId}) => {
entries.push( entries.push(
<MenuItemRadio <MenuItemRadio

View file

@ -238,6 +238,8 @@ export default class MemberList extends React.Component {
member.user = cli.getUser(member.userId); member.user = cli.getUser(member.userId);
} }
member.sortName = (member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, "");
// XXX: this user may have no lastPresenceTs value! // XXX: this user may have no lastPresenceTs value!
// the right solution here is to fix the race rather than leave it as 0 // the right solution here is to fix the race rather than leave it as 0
}); });
@ -252,6 +254,8 @@ export default class MemberList extends React.Component {
m.membership === 'join' || m.membership === 'invite' m.membership === 'join' || m.membership === 'invite'
); );
}); });
const language = SettingsStore.getValue("language");
this.collator = new Intl.Collator(language, { sensitivity: 'base', usePunctuation: true });
filteredAndSortedMembers.sort(this.memberSort); filteredAndSortedMembers.sort(this.memberSort);
return filteredAndSortedMembers; return filteredAndSortedMembers;
} }
@ -351,13 +355,7 @@ export default class MemberList extends React.Component {
} }
// Fourth by name (alphabetical) // Fourth by name (alphabetical)
const nameA = (memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name).replace(SORT_REGEX, ""); return this.collator.compare(memberA.sortName, memberB.sortName);
const nameB = (memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name).replace(SORT_REGEX, "");
// console.log(`Comparing userA_name=${nameA} against userB_name=${nameB} - returning`);
return nameA.localeCompare(nameB, {
ignorePunctuation: true,
sensitivity: "base",
});
}; };
onSearchQueryChanged = searchQuery => { onSearchQueryChanged = searchQuery => {
@ -422,7 +420,7 @@ export default class MemberList extends React.Component {
} else { } else {
// Is a 3pid invite // Is a 3pid invite
return <EntityTile key={m.getStateKey()} name={m.getContent().display_name} suppressOnHover={true} return <EntityTile key={m.getStateKey()} name={m.getContent().display_name} suppressOnHover={true}
onClick={() => this._onPending3pidInviteClick(m)} />; onClick={() => this._onPending3pidInviteClick(m)} />;
} }
}); });
} }
@ -484,10 +482,10 @@ export default class MemberList extends React.Component {
if (this._getChildCountInvited() > 0) { if (this._getChildCountInvited() > 0) {
invitedHeader = <h2>{ _t("Invited") }</h2>; invitedHeader = <h2>{ _t("Invited") }</h2>;
invitedSection = <TruncatedList className="mx_MemberList_section mx_MemberList_invited" truncateAt={this.state.truncateAtInvited} invitedSection = <TruncatedList className="mx_MemberList_section mx_MemberList_invited" truncateAt={this.state.truncateAtInvited}
createOverflowElement={this._createOverflowTileInvited} createOverflowElement={this._createOverflowTileInvited}
getChildren={this._getChildrenInvited} getChildren={this._getChildrenInvited}
getChildCount={this._getChildCountInvited} getChildCount={this._getChildCountInvited}
/>; />;
} }
const footer = ( const footer = (
@ -520,9 +518,9 @@ export default class MemberList extends React.Component {
> >
<div className="mx_MemberList_wrapper"> <div className="mx_MemberList_wrapper">
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined} <TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
createOverflowElement={this._createOverflowTileJoined} createOverflowElement={this._createOverflowTileJoined}
getChildren={this._getChildrenJoined} getChildren={this._getChildrenJoined}
getChildCount={this._getChildCountJoined} /> getChildCount={this._getChildCountJoined} />
{ invitedHeader } { invitedHeader }
{ invitedSection } { invitedSection }
</div> </div>

View file

@ -25,6 +25,7 @@ import Timer from '../../../utils/Timer';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import MemberAvatar from '../avatars/MemberAvatar'; import MemberAvatar from '../avatars/MemberAvatar';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { compare } from "../../../utils/strings";
interface IProps { interface IProps {
// the room this statusbar is representing. // the room this statusbar is representing.
@ -207,7 +208,7 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
usersTyping = usersTyping.concat(stoppedUsersOnTimer); usersTyping = usersTyping.concat(stoppedUsersOnTimer);
// sort them so the typing members don't change order when // sort them so the typing members don't change order when
// moved to delayedStopTypingTimers // 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( const typingString = WhoIsTyping.whoIsTypingString(
usersTyping, usersTyping,

View file

@ -25,6 +25,7 @@ import {EventType} from "matrix-js-sdk/src/@types/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomState } from "matrix-js-sdk/src/models/room-state"; import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { compare } from "../../../../../utils/strings";
const plEventsToLabels = { const plEventsToLabels = {
// These will be translated for us later. // These will be translated for us later.
@ -312,7 +313,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
// comparator for sorting PL users lexicographically on PL descending, MXID ascending. (case-insensitive) // comparator for sorting PL users lexicographically on PL descending, MXID ascending. (case-insensitive)
const comparator = (a, b) => { const comparator = (a, b) => {
const plDiff = userLevels[b.key] - userLevels[a.key]; 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); privilegedUsers.sort(comparator);

View file

@ -35,9 +35,10 @@ import Field from '../../../elements/Field';
import EventTilePreview from '../../../elements/EventTilePreview'; import EventTilePreview from '../../../elements/EventTilePreview';
import StyledRadioGroup from "../../../elements/StyledRadioGroup"; import StyledRadioGroup from "../../../elements/StyledRadioGroup";
import { SettingLevel } from "../../../../../settings/SettingLevel"; import { SettingLevel } from "../../../../../settings/SettingLevel";
import {UIFeature} from "../../../../../settings/UIFeature"; import { UIFeature } from "../../../../../settings/UIFeature";
import {Layout} from "../../../../../settings/Layout"; import { Layout } from "../../../../../settings/Layout";
import {replaceableComponent} from "../../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import { compare } from "../../../../../utils/strings";
interface IProps { interface IProps {
} }
@ -295,7 +296,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
.map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability .map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-")); const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
const customThemes = themes.filter(p => !builtInThemes.includes(p)) 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]; const orderedThemes = [...builtInThemes, ...customThemes];
return ( return (
<div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_themeSection"> <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_themeSection">

View file

@ -28,6 +28,7 @@ import WidgetUtils from "../utils/WidgetUtils";
import {MatrixClientPeg} from "../MatrixClientPeg"; import {MatrixClientPeg} from "../MatrixClientPeg";
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
import url from 'url'; import url from 'url';
import { compare } from "../utils/strings";
const KIND_PREFERENCE = [ const KIND_PREFERENCE = [
// Ordered: first is most preferred, last is least preferred. // Ordered: first is most preferred, last is least preferred.
@ -152,7 +153,7 @@ export class IntegrationManagers {
if (kind === Kind.Account) { if (kind === Kind.Account) {
// Order by state_keys (IDs) // 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); ordered.push(...managers);

View file

@ -17,6 +17,7 @@ limitations under the License.
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { TagID } from "../../models"; import { TagID } from "../../models";
import { IAlgorithm } from "./IAlgorithm"; import { IAlgorithm } from "./IAlgorithm";
import { compare } from "../../../../utils/strings";
/** /**
* Sorts rooms according to the browser's determination of alphabetic. * Sorts rooms according to the browser's determination of alphabetic.
@ -24,7 +25,7 @@ import { IAlgorithm } from "./IAlgorithm";
export class AlphabeticAlgorithm implements IAlgorithm { export class AlphabeticAlgorithm implements IAlgorithm {
public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> { public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> {
return rooms.sort((a, b) => { return rooms.sort((a, b) => {
return a.name.localeCompare(b.name); return compare(a.name, b.name);
}); });
} }
} }

View file

@ -25,6 +25,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { SettingLevel } from "../../settings/SettingLevel"; import { SettingLevel } from "../../settings/SettingLevel";
import { arrayFastClone } from "../../utils/arrays"; import { arrayFastClone } from "../../utils/arrays";
import { UPDATE_EVENT } from "../AsyncStore"; import { UPDATE_EVENT } from "../AsyncStore";
import { compare } from "../../utils/strings";
export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout"; export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout";
@ -240,7 +241,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
if (orderA === orderB) { if (orderA === orderB) {
// We just need a tiebreak // We just need a tiebreak
return a.id.localeCompare(b.id); return compare(a.id, b.id);
} }
return orderA - orderB; return orderA - orderB;

View file

@ -73,3 +73,14 @@ export function copyNode(ref: Element): boolean {
selectText(ref); selectText(ref);
return document.execCommand('copy'); 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);
}

View file

@ -9,6 +9,8 @@ import sdk from '../../../skinned-sdk';
import {Room, RoomMember, User} from 'matrix-js-sdk'; import {Room, RoomMember, User} from 'matrix-js-sdk';
import { compare } from "../../../../src/utils/strings";
function generateRoomId() { function generateRoomId() {
return '!' + Math.random().toString().slice(2, 10) + ':domain'; return '!' + Math.random().toString().slice(2, 10) + ':domain';
} }
@ -173,7 +175,7 @@ describe('MemberList', () => {
if (!groupChange) { if (!groupChange) {
const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name; const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name;
const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.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"); console.log("Comparing name");
expect(nameCompare).toBeGreaterThanOrEqual(0); expect(nameCompare).toBeGreaterThanOrEqual(0);
} else { } else {