Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2021-06-04 08:19:29 +02:00
commit 1d3bf91e83
No known key found for this signature in database
GPG key ID: 9760693FDD98A790
165 changed files with 6632 additions and 2938 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

@ -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();
}
/**
@ -631,7 +644,7 @@ export default class EventTile extends React.Component<IProps, IState> {
// return early if there are no read receipts
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
return (<span className="mx_EventTile_readAvatars" />);
return null;
}
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
@ -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 => {
@ -790,13 +812,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");
};
@ -960,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 =
@ -1023,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) {
@ -1131,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 }
@ -1152,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,
])
)
}
}
}

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

@ -1,111 +0,0 @@
/*
Copyright 2017 Travis Ralston
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 React from "react";
import PropTypes from 'prop-types';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
import AccessibleButton from "../elements/AccessibleButton";
import MessageEvent from "../messages/MessageEvent";
import MemberAvatar from "../avatars/MemberAvatar";
import { _t } from '../../../languageHandler';
import {formatFullDate} from '../../../DateUtils';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.rooms.PinnedEventTile")
export default class PinnedEventTile extends React.Component {
static propTypes = {
mxRoom: PropTypes.object.isRequired,
mxEvent: PropTypes.object.isRequired,
onUnpinned: PropTypes.func,
};
onTileClicked = () => {
dis.dispatch({
action: 'view_room',
event_id: this.props.mxEvent.getId(),
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),
});
};
onUnpinClicked = () => {
const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
// Nothing to do: already unpinned
if (this.props.onUnpinned) this.props.onUnpinned();
} else {
const pinned = pinnedEvents.getContent().pinned;
const index = pinned.indexOf(this.props.mxEvent.getId());
if (index !== -1) {
pinned.splice(index, 1);
MatrixClientPeg.get().sendStateEvent(this.props.mxRoom.roomId, 'm.room.pinned_events', {pinned}, '')
.then(() => {
if (this.props.onUnpinned) this.props.onUnpinned();
});
} else if (this.props.onUnpinned) this.props.onUnpinned();
}
};
_canUnpin() {
return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get());
}
render() {
const sender = this.props.mxEvent.getSender();
// Get the latest sender profile rather than historical
const senderProfile = this.props.mxRoom.getMember(sender);
const avatarSize = 40;
let unpinButton = null;
if (this._canUnpin()) {
unpinButton = (
<AccessibleButton onClick={this.onUnpinClicked} className="mx_PinnedEventTile_unpinButton">
<img src={require("../../../../res/img/cancel-red.svg")} width="8" height="8" alt={_t('Unpin Message')} title={_t('Unpin Message')} />
</AccessibleButton>
);
}
return (
<div className="mx_PinnedEventTile">
<div className="mx_PinnedEventTile_actions">
<AccessibleButton className="mx_PinnedEventTile_gotoButton mx_textButton" onClick={this.onTileClicked}>
{ _t("Jump to message") }
</AccessibleButton>
{ unpinButton }
</div>
<span className="mx_PinnedEventTile_senderAvatar">
<MemberAvatar member={senderProfile} width={avatarSize} height={avatarSize} fallbackUserId={sender} />
</span>
<span className="mx_PinnedEventTile_sender">
{ senderProfile ? senderProfile.name : sender }
</span>
<span className="mx_PinnedEventTile_timestamp">
{ formatFullDate(new Date(this.props.mxEvent.getTs())) }
</span>
<div className="mx_PinnedEventTile_message">
<MessageEvent
mxEvent={this.props.mxEvent}
className="mx_PinnedEventTile_body"
maxImageHeight={150}
onHeightChanged={() => {}} // we need to give this, apparently
/>
</div>
</div>
);
}
}

View file

@ -0,0 +1,104 @@
/*
Copyright 2017 Travis Ralston
Copyright 2021 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 React from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import dis from "../../../dispatcher/dispatcher";
import AccessibleButton from "../elements/AccessibleButton";
import MessageEvent from "../messages/MessageEvent";
import MemberAvatar from "../avatars/MemberAvatar";
import { _t } from '../../../languageHandler';
import { formatDate } from '../../../DateUtils';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
interface IProps {
room: Room;
event: MatrixEvent;
onUnpinClicked?(): void;
}
const AVATAR_SIZE = 24;
@replaceableComponent("views.rooms.PinnedEventTile")
export default class PinnedEventTile extends React.Component<IProps> {
public static contextType = MatrixClientContext;
private onTileClicked = () => {
dis.dispatch({
action: 'view_room',
event_id: this.props.event.getId(),
highlighted: true,
room_id: this.props.event.getRoomId(),
});
};
render() {
const sender = this.props.event.getSender();
const senderProfile = this.props.room.getMember(sender);
let unpinButton = null;
if (this.props.onUnpinClicked) {
unpinButton = (
<AccessibleTooltipButton
onClick={this.props.onUnpinClicked}
className="mx_PinnedEventTile_unpinButton"
title={_t("Unpin")}
/>
);
}
return <div className="mx_PinnedEventTile">
<MemberAvatar
className="mx_PinnedEventTile_senderAvatar"
member={senderProfile}
width={AVATAR_SIZE}
height={AVATAR_SIZE}
fallbackUserId={sender}
/>
<span className={"mx_PinnedEventTile_sender " + getUserNameColorClass(sender)}>
{ senderProfile?.name || sender }
</span>
{ unpinButton }
<div className="mx_PinnedEventTile_message">
<MessageEvent
mxEvent={this.props.event}
className="mx_PinnedEventTile_body"
maxImageHeight={150}
onHeightChanged={() => {}} // we need to give this, apparently
/>
</div>
<div className="mx_PinnedEventTile_footer">
<span className="mx_PinnedEventTile_timestamp">
{ formatDate(new Date(this.props.event.getTs())) }
</span>
<AccessibleButton onClick={this.onTileClicked} kind="link">
{ _t("View message") }
</AccessibleButton>
</div>
</div>;
}
}

View file

@ -1,145 +0,0 @@
/*
Copyright 2017 Travis Ralston
Copyright 2019 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 React from "react";
import PropTypes from 'prop-types';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import AccessibleButton from "../elements/AccessibleButton";
import PinnedEventTile from "./PinnedEventTile";
import { _t } from '../../../languageHandler';
import PinningUtils from "../../../utils/PinningUtils";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.rooms.PinnedEventsPanel")
export default class PinnedEventsPanel extends React.Component {
static propTypes = {
// The Room from the js-sdk we're going to show pinned events for
room: PropTypes.object.isRequired,
onCancelClick: PropTypes.func,
};
state = {
loading: true,
};
componentDidMount() {
this._updatePinnedMessages();
MatrixClientPeg.get().on("RoomState.events", this._onStateEvent);
}
componentWillUnmount() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent);
}
}
_onStateEvent = ev => {
if (ev.getRoomId() === this.props.room.roomId && ev.getType() === "m.room.pinned_events") {
this._updatePinnedMessages();
}
};
_updatePinnedMessages = () => {
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
this.setState({ loading: false, pinned: [] });
} else {
const promises = [];
const cli = MatrixClientPeg.get();
pinnedEvents.getContent().pinned.map((eventId) => {
promises.push(cli.getEventTimeline(this.props.room.getUnfilteredTimelineSet(), eventId, 0).then(
(timeline) => {
const event = timeline.getEvents().find((e) => e.getId() === eventId);
return {eventId, timeline, event};
}).catch((err) => {
console.error("Error looking up pinned event " + eventId + " in room " + this.props.room.roomId);
console.error(err);
return null; // return lack of context to avoid unhandled errors
}));
});
Promise.all(promises).then((contexts) => {
// Filter out the messages before we try to render them
const pinned = contexts.filter((context) => PinningUtils.isPinnable(context.event));
this.setState({ loading: false, pinned });
});
}
this._updateReadState();
};
_updateReadState() {
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents) return; // nothing to read
let readStateEvents = [];
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
if (readPinsEvent && readPinsEvent.getContent()) {
readStateEvents = readPinsEvent.getContent().event_ids || [];
}
if (!readStateEvents.includes(pinnedEvents.getId())) {
readStateEvents.push(pinnedEvents.getId());
// Only keep the last 10 event IDs to avoid infinite growth
readStateEvents = readStateEvents.reverse().splice(0, 10).reverse();
MatrixClientPeg.get().setRoomAccountData(this.props.room.roomId, "im.vector.room.read_pins", {
event_ids: readStateEvents,
});
}
}
_getPinnedTiles() {
if (this.state.pinned.length === 0) {
return (<div>{ _t("No pinned messages.") }</div>);
}
return this.state.pinned.map((context) => {
return (
<PinnedEventTile
key={context.event.getId()}
mxRoom={this.props.room}
mxEvent={context.event}
onUnpinned={this._updatePinnedMessages}
/>
);
});
}
render() {
let tiles = <div>{ _t("Loading...") }</div>;
if (this.state && !this.state.loading) {
tiles = this._getPinnedTiles();
}
return (
<div className="mx_PinnedEventsPanel">
<div className="mx_PinnedEventsPanel_body">
<AccessibleButton className="mx_PinnedEventsPanel_cancel" onClick={this.props.onCancelClick}>
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18" />
</AccessibleButton>
<h3 className="mx_PinnedEventsPanel_header">{ _t("Pinned Messages") }</h3>
{ tiles }
</div>
</div>
);
}
}

View file

@ -19,10 +19,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import RateLimitedFunc from '../../../ratelimitedfunc';
import {CancelButton} from './SimpleRoomHeader';
import { CancelButton } from './SimpleRoomHeader';
import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
import E2EIcon from './E2EIcon';
@ -42,7 +42,6 @@ export default class RoomHeader extends React.Component {
oobData: PropTypes.object,
inRoom: PropTypes.bool,
onSettingsClick: PropTypes.func,
onPinnedClick: PropTypes.func,
onSearchClick: PropTypes.func,
onLeaveClick: PropTypes.func,
onCancelClick: PropTypes.func,
@ -61,14 +60,12 @@ export default class RoomHeader extends React.Component {
componentDidMount() {
const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this._onRoomStateEvents);
cli.on("Room.accountData", this._onRoomAccountData);
}
componentWillUnmount() {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.events", this._onRoomStateEvents);
cli.removeListener("Room.accountData", this._onRoomAccountData);
}
}
@ -81,44 +78,11 @@ export default class RoomHeader extends React.Component {
this._rateLimitedUpdate();
};
_onRoomAccountData = (event, room) => {
if (!this.props.room || room.roomId !== this.props.room.roomId) return;
if (event.getType() !== "im.vector.room.read_pins") return;
this._rateLimitedUpdate();
};
_rateLimitedUpdate = new RateLimitedFunc(function() {
/* eslint-disable babel/no-invalid-this */
this.forceUpdate();
}, 500);
_hasUnreadPins() {
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
if (!currentPinEvent) return false;
if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) {
return false; // no pins == nothing to read
}
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
if (readPinsEvent && readPinsEvent.getContent()) {
const readStateEvents = readPinsEvent.getContent().event_ids || [];
if (readStateEvents) {
return !readStateEvents.includes(currentPinEvent.getId());
}
}
// There's pins, and we haven't read any of them
return true;
}
_hasPins() {
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
if (!currentPinEvent) return false;
return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0);
}
_displayInfoDialogAboutScreensharing() {
Modal.createDialog(InfoDialog, {
title: _t("Screensharing has changed"),
@ -130,7 +94,6 @@ export default class RoomHeader extends React.Component {
render() {
let searchStatus = null;
let cancelButton = null;
let pinnedEventsButton = null;
if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
@ -191,24 +154,6 @@ export default class RoomHeader extends React.Component {
/>;
}
if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) {
let pinsIndicator = null;
if (this._hasUnreadPins()) {
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />);
} else if (this._hasPins()) {
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator" />);
}
pinnedEventsButton =
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_pinnedButton"
onClick={this.props.onPinnedClick}
title={_t("Pinned Messages")}
>
{ pinsIndicator }
</AccessibleTooltipButton>;
}
let forgetButton;
if (this.props.onForgetClick) {
forgetButton =
@ -258,7 +203,6 @@ export default class RoomHeader extends React.Component {
<div className="mx_RoomHeader_buttons">
{ videoCallButton }
{ voiceCallButton }
{ pinnedEventsButton }
{ forgetButton }
{ appsButton }
{ searchButton }
@ -275,7 +219,7 @@ export default class RoomHeader extends React.Component {
{ topicElement }
{ cancelButton }
{ rightRow }
<RoomHeaderButtons />
<RoomHeaderButtons room={this.props.room} />
</div>
</div>
);

View file

@ -46,17 +46,16 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con
import AccessibleButton from "../elements/AccessibleButton";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import CallHandler from "../../../CallHandler";
import SpaceStore, {SUGGESTED_ROOMS} from "../../../stores/SpaceStore";
import SpaceStore, {ISuggestedRoom, SUGGESTED_ROOMS} from "../../../stores/SpaceStore";
import {showAddExistingRooms, showCreateNewRoom, showSpaceInvite} from "../../../utils/space";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import RoomAvatar from "../avatars/RoomAvatar";
import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory";
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;
@ -66,7 +65,7 @@ interface IState {
sublists: ITagMap;
isNameFiltering: boolean;
currentRoomId?: string;
suggestedRooms: ISpaceSummaryRoom[];
suggestedRooms: ISuggestedRoom[];
}
const TAG_ORDER: TagID[] = [
@ -363,7 +362,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
return room;
};
private updateSuggestedRooms = (suggestedRooms: ISpaceSummaryRoom[]) => {
private updateSuggestedRooms = (suggestedRooms: ISuggestedRoom[]) => {
this.setState({ suggestedRooms });
};
@ -405,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});
}
};
@ -443,7 +440,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
const viewRoom = () => {
defaultDispatcher.dispatch({
action: "view_room",
room_alias: room.canonical_alias || room.aliases?.[0],
room_id: room.room_id,
via_servers: room.viaServers,
oobData: {
avatarUrl: room.avatar_url,
name,
@ -536,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

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

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,20 +208,20 @@ 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,
this.props.whoIsTypingLimit,
);
if (!typingString) {
return (<div className="mx_WhoIsTypingTile_empty" />);
return null;
}
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 }