Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/17365
Conflicts: src/components/views/dialogs/InviteDialog.tsx
This commit is contained in:
commit
f2643151c6
79 changed files with 3953 additions and 859 deletions
|
@ -212,7 +212,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
|||
autoComplete={true}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<AutoHideScrollbar className="mx_AddExistingToSpace_content" id="mx_AddExistingToSpace">
|
||||
<AutoHideScrollbar className="mx_AddExistingToSpace_content">
|
||||
{ rooms.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
<h3>{ _t("Rooms") }</h3>
|
||||
|
|
|
@ -49,10 +49,11 @@ import {mediaFromMxc} from "../../../customisations/Media";
|
|||
import {getAddressType} from "../../../UserAddress";
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { compare } from '../../../utils/strings';
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {copyPlaintext, selectText} from "../../../utils/strings";
|
||||
import { copyPlaintext, selectText } from "../../../utils/strings";
|
||||
import * as ContextMenu from "../../structures/ContextMenu";
|
||||
import {toRightOf} from "../../structures/ContextMenu";
|
||||
import { toRightOf } from "../../structures/ContextMenu";
|
||||
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
|
@ -587,7 +588,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
members.sort((a, b) => {
|
||||
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;
|
||||
|
|
|
@ -73,9 +73,13 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
|||
const promises = [];
|
||||
|
||||
if (avatarChanged) {
|
||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
|
||||
url: await cli.uploadContent(newAvatar),
|
||||
}, ""));
|
||||
if (newAvatar) {
|
||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
|
||||
url: await cli.uploadContent(newAvatar),
|
||||
}, ""));
|
||||
} else {
|
||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, ""));
|
||||
}
|
||||
}
|
||||
|
||||
if (nameChanged) {
|
||||
|
|
|
@ -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(
|
||||
<MenuItemRadio
|
||||
|
|
|
@ -73,7 +73,7 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
|
|||
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
|
||||
label={tooltip || title}
|
||||
yOffset={yOffset}
|
||||
/> : <div />;
|
||||
/> : null;
|
||||
return (
|
||||
<AccessibleButton
|
||||
{...props}
|
||||
|
|
|
@ -116,7 +116,7 @@ export default class Flair extends React.Component {
|
|||
|
||||
render() {
|
||||
if (this.state.profiles.length === 0) {
|
||||
return <span className="mx_Flair" />;
|
||||
return null;
|
||||
}
|
||||
const avatars = this.state.profiles.map((profile, index) => {
|
||||
return <FlairAvatar key={index} groupProfile={profile} />;
|
||||
|
|
|
@ -214,7 +214,7 @@ export default class ReplyThread extends React.Component {
|
|||
|
||||
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout) {
|
||||
if (!ReplyThread.getParentEventId(parentEv)) {
|
||||
return <div className="mx_ReplyThread_wrapper_empty" />;
|
||||
return null;
|
||||
}
|
||||
return <ReplyThread
|
||||
parentEv={parentEv}
|
||||
|
@ -269,36 +269,27 @@ export default class ReplyThread extends React.Component {
|
|||
const {parentEv} = this.props;
|
||||
// at time of making this component we checked that props.parentEv has a parentEventId
|
||||
const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv));
|
||||
|
||||
if (this.unmounted) return;
|
||||
|
||||
if (ev) {
|
||||
const loadedEv = await this.getNextEvent(ev);
|
||||
this.setState({
|
||||
events: [ev],
|
||||
}, this.loadNextEvent);
|
||||
loadedEv,
|
||||
loading: false,
|
||||
});
|
||||
} else {
|
||||
this.setState({err: true});
|
||||
}
|
||||
}
|
||||
|
||||
async loadNextEvent() {
|
||||
if (this.unmounted) return;
|
||||
const ev = this.state.events[0];
|
||||
const inReplyToEventId = ReplyThread.getParentEventId(ev);
|
||||
|
||||
if (!inReplyToEventId) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const loadedEv = await this.getEvent(inReplyToEventId);
|
||||
if (this.unmounted) return;
|
||||
|
||||
if (loadedEv) {
|
||||
this.setState({loadedEv});
|
||||
} else {
|
||||
this.setState({err: true});
|
||||
async getNextEvent(ev) {
|
||||
try {
|
||||
const inReplyToEventId = ReplyThread.getParentEventId(ev);
|
||||
return await this.getEvent(inReplyToEventId);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -326,13 +317,18 @@ export default class ReplyThread extends React.Component {
|
|||
this.initialize();
|
||||
}
|
||||
|
||||
onQuoteClick() {
|
||||
async onQuoteClick() {
|
||||
const events = [this.state.loadedEv, ...this.state.events];
|
||||
|
||||
let loadedEv = null;
|
||||
if (events.length > 0) {
|
||||
loadedEv = await this.getNextEvent(events[0]);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loadedEv: null,
|
||||
loadedEv,
|
||||
events,
|
||||
}, this.loadNextEvent);
|
||||
});
|
||||
|
||||
dis.fire(Action.FocusComposer);
|
||||
}
|
||||
|
|
|
@ -112,7 +112,7 @@ interface IProps {
|
|||
const MAX_PER_ROW = 6;
|
||||
|
||||
const SSOButtons: React.FC<IProps> = ({matrixClient, flow, loginType, fragmentAfterLogin, primary}) => {
|
||||
const providers = flow["org.matrix.msc2858.identity_providers"] || [];
|
||||
const providers = flow.identity_providers || [];
|
||||
if (providers.length < 2) {
|
||||
return <div className="mx_SSOButtons">
|
||||
<SSOButton
|
||||
|
|
|
@ -70,7 +70,10 @@ export default class Tooltip extends React.Component<IProps> {
|
|||
this.tooltipContainer = document.createElement("div");
|
||||
this.tooltipContainer.className = "mx_Tooltip_wrapper";
|
||||
document.body.appendChild(this.tooltipContainer);
|
||||
window.addEventListener('scroll', this.renderTooltip, true);
|
||||
window.addEventListener('scroll', this.renderTooltip, {
|
||||
passive: true,
|
||||
capture: true,
|
||||
});
|
||||
|
||||
this.parent = ReactDOM.findDOMNode(this).parentNode as Element;
|
||||
|
||||
|
@ -85,7 +88,9 @@ export default class Tooltip extends React.Component<IProps> {
|
|||
public componentWillUnmount() {
|
||||
ReactDOM.unmountComponentAtNode(this.tooltipContainer);
|
||||
document.body.removeChild(this.tooltipContainer);
|
||||
window.removeEventListener('scroll', this.renderTooltip, true);
|
||||
window.removeEventListener('scroll', this.renderTooltip, {
|
||||
capture: true,
|
||||
});
|
||||
}
|
||||
|
||||
private updatePosition(style: CSSProperties) {
|
||||
|
|
|
@ -31,21 +31,23 @@ export default class SenderProfile extends React.Component {
|
|||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
state = {
|
||||
userGroups: null,
|
||||
relatedGroups: [],
|
||||
};
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const senderId = this.props.mxEvent.getSender();
|
||||
|
||||
this.state = {
|
||||
userGroups: FlairStore.cachedPublicisedGroups(senderId) || [],
|
||||
relatedGroups: [],
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
this.unmounted = false;
|
||||
this._updateRelatedGroups();
|
||||
|
||||
FlairStore.getPublicisedGroupsCached(
|
||||
this.context, this.props.mxEvent.getSender(),
|
||||
).then((userGroups) => {
|
||||
if (this.unmounted) return;
|
||||
this.setState({userGroups});
|
||||
});
|
||||
if (this.state.userGroups.length === 0) {
|
||||
this.getPublicisedGroups();
|
||||
}
|
||||
|
||||
|
||||
this.context.on('RoomState.events', this.onRoomStateEvents);
|
||||
}
|
||||
|
@ -55,6 +57,15 @@ export default class SenderProfile extends React.Component {
|
|||
this.context.removeListener('RoomState.events', this.onRoomStateEvents);
|
||||
}
|
||||
|
||||
async getPublicisedGroups() {
|
||||
if (!this.unmounted) {
|
||||
const userGroups = await FlairStore.getPublicisedGroupsCached(
|
||||
this.context, this.props.mxEvent.getSender(),
|
||||
);
|
||||
this.setState({userGroups});
|
||||
}
|
||||
}
|
||||
|
||||
onRoomStateEvents = event => {
|
||||
if (event.getType() === 'm.room.related_groups' &&
|
||||
event.getRoomId() === this.props.mxEvent.getRoomId()
|
||||
|
@ -93,10 +104,10 @@ export default class SenderProfile extends React.Component {
|
|||
const {msgtype} = mxEvent.getContent();
|
||||
|
||||
if (msgtype === 'm.emote') {
|
||||
return <span />; // emote message must include the name so don't duplicate it
|
||||
return null; // emote message must include the name so don't duplicate it
|
||||
}
|
||||
|
||||
let flair = <div />;
|
||||
let flair = null;
|
||||
if (this.props.enableFlair) {
|
||||
const displayedGroups = this._getDisplayedGroups(
|
||||
this.state.userGroups, this.state.relatedGroups,
|
||||
|
@ -110,19 +121,12 @@ export default class SenderProfile extends React.Component {
|
|||
|
||||
const nameElem = name || '';
|
||||
|
||||
// Name + flair
|
||||
const nameFlair = <span>
|
||||
<span className={`mx_SenderProfile_name ${colorClass}`}>
|
||||
{ nameElem }
|
||||
</span>
|
||||
{ flair }
|
||||
</span>;
|
||||
|
||||
return (
|
||||
<div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
|
||||
<div className="mx_SenderProfile_hover">
|
||||
{ nameFlair }
|
||||
</div>
|
||||
<div className="mx_SenderProfile mx_SenderProfile_hover" dir="auto" onClick={this.props.onClick}>
|
||||
<span className={`mx_SenderProfile_name ${colorClass}`}>
|
||||
{ nameElem }
|
||||
</span>
|
||||
{ flair }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -278,15 +278,15 @@ export default class TextualBody extends React.Component {
|
|||
// pass only the first child which is the event tile otherwise this recurses on edited events
|
||||
let links = this.findLinks([this._content.current]);
|
||||
if (links.length) {
|
||||
// de-dup the links (but preserve ordering)
|
||||
const seen = new Set();
|
||||
links = links.filter((link) => {
|
||||
if (seen.has(link)) return false;
|
||||
seen.add(link);
|
||||
return true;
|
||||
});
|
||||
// de-duplicate the links after stripping hashes as they don't affect the preview
|
||||
// using a set here maintains the order
|
||||
links = Array.from(new Set(links.map(link => {
|
||||
const url = new URL(link);
|
||||
url.hash = "";
|
||||
return url.toString();
|
||||
})));
|
||||
|
||||
this.setState({ links: links });
|
||||
this.setState({ links });
|
||||
|
||||
// lazy-load the hidden state of the preview widget from localstorage
|
||||
if (global.localStorage) {
|
||||
|
|
|
@ -168,6 +168,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
if (nextEvent) {
|
||||
dis.dispatch({action: 'edit_event', event: nextEvent});
|
||||
} else {
|
||||
this._clearStoredEditorState();
|
||||
dis.dispatch({action: 'edit_event', event: null});
|
||||
dis.fire(Action.FocusComposer);
|
||||
}
|
||||
|
|
|
@ -277,6 +277,12 @@ interface IProps {
|
|||
|
||||
// Helper to build permalinks for the room
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
|
||||
// Symbol of the root node
|
||||
as?: string
|
||||
|
||||
// whether or not to always show timestamps
|
||||
alwaysShowTimestamps?: boolean
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -291,12 +297,15 @@ interface IState {
|
|||
previouslyRequestedKeys: boolean;
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions: Relations;
|
||||
|
||||
hover: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.EventTile")
|
||||
export default class EventTile extends React.Component<IProps, IState> {
|
||||
private suppressReadReceiptAnimation: boolean;
|
||||
private isListeningForReceipts: boolean;
|
||||
private ref: React.RefObject<unknown>;
|
||||
private tile = React.createRef();
|
||||
private replyThread = React.createRef();
|
||||
|
||||
|
@ -322,6 +331,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
previouslyRequestedKeys: false,
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions: this.getReactions(),
|
||||
|
||||
hover: false,
|
||||
};
|
||||
|
||||
// don't do RR animations until we are mounted
|
||||
|
@ -333,6 +344,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
// to determine if we've already subscribed and use a combination of other flags to find
|
||||
// out if we should even be subscribed at all.
|
||||
this.isListeningForReceipts = false;
|
||||
|
||||
this.ref = React.createRef();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -640,6 +653,11 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
let left = 0;
|
||||
|
||||
const receipts = this.props.readReceipts || [];
|
||||
|
||||
if (receipts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let i = 0; i < receipts.length; ++i) {
|
||||
const receipt = receipts[i];
|
||||
|
||||
|
@ -690,10 +708,14 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
return <span className="mx_EventTile_readAvatars">
|
||||
{ remText }
|
||||
{ avatars }
|
||||
</span>;
|
||||
return (
|
||||
<div className="mx_EventTile_msgOption">
|
||||
<span className="mx_EventTile_readAvatars">
|
||||
{ remText }
|
||||
{ avatars }
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
onSenderProfileClick = event => {
|
||||
|
@ -953,7 +975,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
onFocusChange={this.onActionBarFocusChange}
|
||||
/> : undefined;
|
||||
|
||||
const timestamp = this.props.mxEvent.getTs() ?
|
||||
const showTimestamp = this.props.mxEvent.getTs() && (this.props.alwaysShowTimestamps || this.state.hover);
|
||||
const timestamp = showTimestamp ?
|
||||
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
||||
|
||||
const keyRequestHelpText =
|
||||
|
@ -1016,11 +1039,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
let msgOption;
|
||||
if (this.props.showReadReceipts) {
|
||||
const readAvatars = this.getReadAvatars();
|
||||
msgOption = (
|
||||
<div className="mx_EventTile_msgOption">
|
||||
{ readAvatars }
|
||||
</div>
|
||||
);
|
||||
msgOption = readAvatars;
|
||||
}
|
||||
|
||||
switch (this.props.tileShape) {
|
||||
|
@ -1124,11 +1143,20 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
|
||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
||||
return (
|
||||
<div className={classes} tabIndex={-1} aria-live={ariaLive} aria-atomic="true">
|
||||
{ ircTimestamp }
|
||||
{ sender }
|
||||
{ ircPadlock }
|
||||
<div className="mx_EventTile_line">
|
||||
React.createElement(this.props.as || "div", {
|
||||
"ref": this.ref,
|
||||
"className": classes,
|
||||
"tabIndex": -1,
|
||||
"aria-live": ariaLive,
|
||||
"aria-atomic": "true",
|
||||
"data-scroll-tokens": this.props["data-scroll-tokens"],
|
||||
"onMouseEnter": () => this.setState({ hover: true }),
|
||||
"onMouseLeave": () => this.setState({ hover: false }),
|
||||
}, [
|
||||
ircTimestamp,
|
||||
sender,
|
||||
ircPadlock,
|
||||
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
||||
{ groupTimestamp }
|
||||
{ groupPadlock }
|
||||
{ thread }
|
||||
|
@ -1145,16 +1173,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
{ keyRequestInfo }
|
||||
{ reactionsRow }
|
||||
{ actionBar }
|
||||
</div>
|
||||
{msgOption}
|
||||
{
|
||||
// The avatar goes after the event tile as it's absolutely positioned to be over the
|
||||
// event tile line, so needs to be later in the DOM so it appears on top (this avoids
|
||||
// the need for further z-indexing chaos)
|
||||
}
|
||||
{ avatar }
|
||||
</div>
|
||||
);
|
||||
</div>,
|
||||
msgOption,
|
||||
avatar,
|
||||
|
||||
])
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -238,6 +238,8 @@ export default class MemberList extends React.Component {
|
|||
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!
|
||||
// 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'
|
||||
);
|
||||
});
|
||||
const language = SettingsStore.getValue("language");
|
||||
this.collator = new Intl.Collator(language, { sensitivity: 'base', usePunctuation: true });
|
||||
filteredAndSortedMembers.sort(this.memberSort);
|
||||
return filteredAndSortedMembers;
|
||||
}
|
||||
|
@ -351,13 +355,7 @@ export default class MemberList extends React.Component {
|
|||
}
|
||||
|
||||
// Fourth by name (alphabetical)
|
||||
const nameA = (memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name).replace(SORT_REGEX, "");
|
||||
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",
|
||||
});
|
||||
return this.collator.compare(memberA.sortName, memberB.sortName);
|
||||
};
|
||||
|
||||
onSearchQueryChanged = searchQuery => {
|
||||
|
@ -422,7 +420,7 @@ export default class MemberList extends React.Component {
|
|||
} else {
|
||||
// Is a 3pid invite
|
||||
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) {
|
||||
invitedHeader = <h2>{ _t("Invited") }</h2>;
|
||||
invitedSection = <TruncatedList className="mx_MemberList_section mx_MemberList_invited" truncateAt={this.state.truncateAtInvited}
|
||||
createOverflowElement={this._createOverflowTileInvited}
|
||||
getChildren={this._getChildrenInvited}
|
||||
getChildCount={this._getChildCountInvited}
|
||||
/>;
|
||||
createOverflowElement={this._createOverflowTileInvited}
|
||||
getChildren={this._getChildrenInvited}
|
||||
getChildCount={this._getChildCountInvited}
|
||||
/>;
|
||||
}
|
||||
|
||||
const footer = (
|
||||
|
@ -520,9 +518,9 @@ export default class MemberList extends React.Component {
|
|||
>
|
||||
<div className="mx_MemberList_wrapper">
|
||||
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
|
||||
createOverflowElement={this._createOverflowTileJoined}
|
||||
getChildren={this._getChildrenJoined}
|
||||
getChildCount={this._getChildCountJoined} />
|
||||
createOverflowElement={this._createOverflowTileJoined}
|
||||
getChildren={this._getChildrenJoined}
|
||||
getChildCount={this._getChildCountJoined} />
|
||||
{ invitedHeader }
|
||||
{ invitedSection }
|
||||
</div>
|
||||
|
|
|
@ -55,6 +55,7 @@ interface IProps {
|
|||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
onFocus: (ev: React.FocusEvent) => void;
|
||||
onBlur: (ev: React.FocusEvent) => void;
|
||||
onListCollapse?: (isExpanded: boolean) => void;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
isMinimized: boolean;
|
||||
activeSpace: Room;
|
||||
|
@ -538,6 +539,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
extraTiles={extraTiles}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)}
|
||||
onListCollapse={this.props.onListCollapse}
|
||||
/>
|
||||
});
|
||||
}
|
||||
|
|
|
@ -78,6 +78,7 @@ interface IProps {
|
|||
alwaysVisible?: boolean;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
|
||||
onListCollapse?: (isExpanded: boolean) => void;
|
||||
|
||||
// TODO: Account for https://github.com/vector-im/element-web/issues/14179
|
||||
}
|
||||
|
@ -104,6 +105,7 @@ interface IState {
|
|||
export default class RoomSublist extends React.Component<IProps, IState> {
|
||||
private headerButton = createRef<HTMLDivElement>();
|
||||
private sublistRef = createRef<HTMLDivElement>();
|
||||
private tilesRef = createRef<HTMLDivElement>();
|
||||
private dispatcherRef: string;
|
||||
private layout: ListLayout;
|
||||
private heightAtStart: number;
|
||||
|
@ -245,11 +247,15 @@ 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);
|
||||
// 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 });
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||
this.tilesRef.current?.removeEventListener("scroll", this.onScrollPrevent);
|
||||
}
|
||||
|
||||
private onListsUpdated = () => {
|
||||
|
@ -472,6 +478,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
private toggleCollapsed = () => {
|
||||
this.layout.isCollapsed = this.state.isExpanded;
|
||||
this.setState({isExpanded: !this.layout.isCollapsed});
|
||||
if (this.props.onListCollapse) {
|
||||
this.props.onListCollapse(!this.layout.isCollapsed)
|
||||
}
|
||||
};
|
||||
|
||||
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
|
||||
|
@ -751,7 +760,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
private onScrollPrevent(e: React.UIEvent<HTMLDivElement>) {
|
||||
private onScrollPrevent(e: Event) {
|
||||
// the RoomTile calls scrollIntoView and the browser may scroll a div we do not wish to be scrollable
|
||||
// this fixes https://github.com/vector-im/element-web/issues/14413
|
||||
(e.target as HTMLDivElement).scrollTop = 0;
|
||||
|
@ -880,7 +889,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
className="mx_RoomSublist_resizeBox"
|
||||
enable={handles}
|
||||
>
|
||||
<div className="mx_RoomSublist_tiles" onScroll={this.onScrollPrevent}>
|
||||
<div className="mx_RoomSublist_tiles" ref={this.tilesRef}>
|
||||
{visibleTiles}
|
||||
</div>
|
||||
{showNButton}
|
||||
|
|
|
@ -62,13 +62,11 @@ export default class SimpleRoomHeader extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomHeader" >
|
||||
<div className="mx_RoomHeader_wrapper">
|
||||
<div className="mx_RoomHeader_simpleHeader">
|
||||
{ icon }
|
||||
{ this.props.title }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
<div className="mx_RoomHeader mx_RoomHeader_wrapper" >
|
||||
<div className="mx_RoomHeader_simpleHeader">
|
||||
{ icon }
|
||||
{ this.props.title }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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,14 +208,14 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
|
|||
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,
|
||||
this.props.whoIsTypingLimit,
|
||||
);
|
||||
if (!typingString) {
|
||||
return (<div className="mx_WhoIsTypingTile_empty" />);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -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<IProps> {
|
|||
// 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);
|
||||
|
|
|
@ -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<IProps, I
|
|||
.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 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 (
|
||||
<div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_themeSection">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue