Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/6606

 Conflicts:
	src/components/structures/RoomView.tsx
	src/components/views/right_panel/UserInfo.tsx
This commit is contained in:
Michael Telatynski 2021-06-03 08:42:05 +01:00
commit 4f50727010
101 changed files with 4823 additions and 1420 deletions

View file

@ -36,6 +36,7 @@ import {Container, WidgetLayoutStore} from "../../../stores/widgets/WidgetLayout
import {clamp, percentageOf, percentageWithin} from "../../../utils/numbers";
import {useStateCallback} from "../../../hooks/useStateCallback";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import UIStore from "../../../stores/UIStore";
@replaceableComponent("views.rooms.AppsDrawer")
export default class AppsDrawer extends React.Component {
@ -290,7 +291,7 @@ const PersistentVResizer = ({
// Arbitrary defaults to avoid NaN problems. 100 px or 3/4 of the visible window.
if (!minHeight) minHeight = 100;
if (!maxHeight) maxHeight = (window.innerHeight / 4) * 3;
if (!maxHeight) maxHeight = (UIStore.instance.windowHeight / 4) * 3;
// Convert from percentage to height. Note that the default height is 280px.
if (defaultHeight) {

View file

@ -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);
}

View file

@ -790,13 +790,6 @@ export default class EventTile extends React.Component<IProps, IState> {
return null;
}
const eventId = this.props.mxEvent.getId();
if (!eventId) {
// XXX: Temporary diagnostic logging for https://github.com/vector-im/element-web/issues/11120
console.error("EventTile attempted to get relations for an event without an ID");
// Use event's special `toJSON` method to log key data.
console.log(JSON.stringify(this.props.mxEvent, null, 4));
console.trace("Stacktrace for https://github.com/vector-im/element-web/issues/11120");
}
return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
};

View file

@ -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>

View file

@ -55,7 +55,7 @@ interface IProps {
onKeyDown: (ev: React.KeyboardEvent) => void;
onFocus: (ev: React.FocusEvent) => void;
onBlur: (ev: React.FocusEvent) => void;
onResize: () => void;
onListCollapse?: (isExpanded: boolean) => void;
resizeNotifier: ResizeNotifier;
isMinimized: boolean;
activeSpace: Room;
@ -404,9 +404,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
const newSublists = objectWithOnly(newLists, newListIds);
const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v));
this.setState({sublists, isNameFiltering}, () => {
this.props.onResize();
});
this.setState({sublists, isNameFiltering});
}
};
@ -537,11 +535,11 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
addRoomContextMenu={aesthetics.addRoomContextMenu}
isMinimized={this.props.isMinimized}
onResize={this.props.onResize}
showSkeleton={showSkeleton}
extraTiles={extraTiles}
resizeNotifier={this.props.resizeNotifier}
alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)}
onListCollapse={this.props.onListCollapse}
/>
});
}

View file

@ -14,14 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useState} from "react";
import React, {useEffect, useState} from "react";
import { _t } from "../../../languageHandler";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import SpaceStore from "../../../stores/SpaceStore";
const RoomListNumResults: React.FC = () => {
interface IProps {
onVisibilityChange?: () => void
}
const RoomListNumResults: React.FC<IProps> = ({ onVisibilityChange }) => {
const [count, setCount] = useState<number>(null);
useEventEmitter(RoomListStore.instance, LISTS_UPDATE_EVENT, () => {
if (RoomListStore.instance.getFirstNameFilterCondition()) {
@ -32,6 +36,12 @@ const RoomListNumResults: React.FC = () => {
}
});
useEffect(() => {
if (onVisibilityChange) {
onVisibilityChange();
}
}, [count, onVisibilityChange]);
if (typeof count !== "number") return null;
return <div className="mx_LeftPanel_roomListFilterCount">

View file

@ -74,11 +74,11 @@ interface IProps {
addRoomLabel: string;
isMinimized: boolean;
tagId: TagID;
onResize: () => void;
showSkeleton?: boolean;
alwaysVisible?: boolean;
resizeNotifier: ResizeNotifier;
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
onListCollapse?: (isExpanded: boolean) => void;
// TODO: Account for https://github.com/vector-im/element-web/issues/14179
}
@ -105,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;
@ -246,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 = () => {
@ -473,7 +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});
setImmediate(() => this.props.onResize()); // needs to happen when the DOM is updated
if (this.props.onListCollapse) {
this.props.onListCollapse(!this.layout.isCollapsed)
}
};
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
@ -530,7 +537,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
tiles.push(<RoomTile
room={room}
key={`room-${room.roomId}`}
resizeNotifier={this.props.resizeNotifier}
showMessagePreview={this.layout.showPreviews}
isMinimized={this.props.isMinimized}
tag={this.props.tagId}
@ -754,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;
@ -883,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}

View file

@ -53,14 +53,12 @@ import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/Community
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { getUnsentMessages } from "../../structures/RoomStatusBar";
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
interface IProps {
room: Room;
showMessagePreview: boolean;
isMinimized: boolean;
tag: TagID;
resizeNotifier: ResizeNotifier;
}
type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;
@ -106,9 +104,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
this.roomProps = EchoChamber.forRoom(this.props.room);
if (this.props.resizeNotifier) {
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
}
}
private countUnsentEvents(): number {
@ -123,12 +118,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.forceUpdate(); // notification state changed - update
};
private onResize = () => {
if (this.showMessagePreview && !this.state.messagePreview) {
this.generatePreview();
}
};
private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
if (!room?.roomId === this.props.room.roomId) return;
this.setState({hasUnsentEvents: this.countUnsentEvents() > 0});
@ -148,7 +137,9 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
}
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>) {
if (prevProps.showMessagePreview !== this.props.showMessagePreview && this.showMessagePreview) {
const showMessageChanged = prevProps.showMessagePreview !== this.props.showMessagePreview;
const minimizedChanged = prevProps.isMinimized !== this.props.isMinimized;
if (showMessageChanged || minimizedChanged) {
this.generatePreview();
}
if (prevProps.room?.roomId !== this.props.room?.roomId) {
@ -208,9 +199,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
);
this.props.room.off("Room.name", this.onRoomNameUpdate);
}
if (this.props.resizeNotifier) {
this.props.resizeNotifier.off("middlePanelResized", this.onResize);
}
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
defaultDispatcher.unregister(this.dispatcherRef);
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);

View file

@ -40,7 +40,7 @@ const STICKERPICKER_Z_INDEX = 3500;
const PERSISTED_ELEMENT_KEY = "stickerPicker";
@replaceableComponent("views.rooms.Stickerpicker")
export default class Stickerpicker extends React.Component {
export default class Stickerpicker extends React.PureComponent {
static currentWidget;
constructor(props) {
@ -341,21 +341,27 @@ export default class Stickerpicker extends React.Component {
* @param {Event} ev Event that triggered the function call
*/
_onHideStickersClick(ev) {
this.setState({showStickers: false});
if (this.state.showStickers) {
this.setState({showStickers: false});
}
}
/**
* Called when the window is resized
*/
_onResize() {
this.setState({showStickers: false});
if (this.state.showStickers) {
this.setState({showStickers: false});
}
}
/**
* The stickers picker was hidden
*/
_onFinished() {
this.setState({showStickers: false});
if (this.state.showStickers) {
this.setState({showStickers: false});
}
}
/**

View file

@ -16,36 +16,45 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import Room from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import * as WhoIsTyping from '../../../WhoIsTyping';
import Timer from '../../../utils/Timer';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import MemberAvatar from '../avatars/MemberAvatar';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { compare } from "../../../utils/strings";
interface IProps {
// the room this statusbar is representing.
room: Room;
onShown?: () => void;
onHidden?: () => void;
// Number of names to display in typing indication. E.g. set to 3, will
// result in "X, Y, Z and 100 others are typing."
whoIsTypingLimit: number;
}
interface IState {
usersTyping: RoomMember[];
// a map with userid => Timer to delay
// hiding the "x is typing" message for a
// user so hiding it can coincide
// with the sent message by the other side
// resulting in less timeline jumpiness
delayedStopTypingTimers: Record<string, Timer>;
}
@replaceableComponent("views.rooms.WhoIsTypingTile")
export default class WhoIsTypingTile extends React.Component {
static propTypes = {
// the room this statusbar is representing.
room: PropTypes.object.isRequired,
onShown: PropTypes.func,
onHidden: PropTypes.func,
// Number of names to display in typing indication. E.g. set to 3, will
// result in "X, Y, Z and 100 others are typing."
whoIsTypingLimit: PropTypes.number,
};
export default class WhoIsTypingTile extends React.Component<IProps, IState> {
static defaultProps = {
whoIsTypingLimit: 3,
};
state = {
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
// a map with userid => Timer to delay
// hiding the "x is typing" message for a
// user so hiding it can coincide
// with the sent message by the other side
// resulting in less timeline jumpiness
delayedStopTypingTimers: {},
};
@ -71,37 +80,39 @@ export default class WhoIsTypingTile extends React.Component {
client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
client.removeListener("Room.timeline", this.onRoomTimeline);
}
Object.values(this.state.delayedStopTypingTimers).forEach((t) => t.abort());
Object.values(this.state.delayedStopTypingTimers).forEach((t) => (t as Timer).abort());
}
_isVisible(state) {
private _isVisible(state: IState): boolean {
return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0;
}
isVisible = () => {
public isVisible = (): boolean => {
return this._isVisible(this.state);
};
onRoomTimeline = (event, room) => {
private onRoomTimeline = (event: MatrixEvent, room: Room): void => {
if (room?.roomId === this.props.room?.roomId) {
const userId = event.getSender();
// remove user from usersTyping
const usersTyping = this.state.usersTyping.filter((m) => m.userId !== userId);
this.setState({usersTyping});
if (usersTyping.length !== this.state.usersTyping.length) {
this.setState({usersTyping});
}
// abort timer if any
this._abortUserTimer(userId);
this.abortUserTimer(userId);
}
};
onRoomMemberTyping = (ev, member) => {
private onRoomMemberTyping = (): void => {
const usersTyping = WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room);
this.setState({
delayedStopTypingTimers: this._updateDelayedStopTypingTimers(usersTyping),
delayedStopTypingTimers: this.updateDelayedStopTypingTimers(usersTyping),
usersTyping,
});
};
_updateDelayedStopTypingTimers(usersTyping) {
private updateDelayedStopTypingTimers(usersTyping: RoomMember[]): Record<string, Timer> {
const usersThatStoppedTyping = this.state.usersTyping.filter((a) => {
return !usersTyping.some((b) => a.userId === b.userId);
});
@ -129,7 +140,7 @@ export default class WhoIsTypingTile extends React.Component {
delayedStopTypingTimers[m.userId] = timer;
timer.start();
timer.finished().then(
() => this._removeUserTimer(m.userId), // on elapsed
() => this.removeUserTimer(m.userId), // on elapsed
() => {/* aborted */},
);
}
@ -139,15 +150,15 @@ export default class WhoIsTypingTile extends React.Component {
return delayedStopTypingTimers;
}
_abortUserTimer(userId) {
private abortUserTimer(userId: string): void {
const timer = this.state.delayedStopTypingTimers[userId];
if (timer) {
timer.abort();
this._removeUserTimer(userId);
this.removeUserTimer(userId);
}
}
_removeUserTimer(userId) {
private removeUserTimer(userId: string): void {
const timer = this.state.delayedStopTypingTimers[userId];
if (timer) {
const delayedStopTypingTimers = Object.assign({}, this.state.delayedStopTypingTimers);
@ -156,7 +167,7 @@ export default class WhoIsTypingTile extends React.Component {
}
}
_renderTypingIndicatorAvatars(users, limit) {
private renderTypingIndicatorAvatars(users: RoomMember[], limit: number): JSX.Element[] {
let othersCount = 0;
if (users.length > limit) {
othersCount = users.length - limit + 1;
@ -197,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,
@ -210,7 +221,7 @@ export default class WhoIsTypingTile extends React.Component {
return (
<li className="mx_WhoIsTypingTile" aria-atomic="true">
<div className="mx_WhoIsTypingTile_avatars">
{ this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
{ this.renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
</div>
<div className="mx_WhoIsTypingTile_label">
{ typingString }