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

 Conflicts:
	package.json
	yarn.lock
This commit is contained in:
Michael Telatynski 2021-06-14 21:39:06 +01:00
commit 66fce64ee0
141 changed files with 3216 additions and 2016 deletions

View file

@ -24,13 +24,16 @@ import { HostSignupStore } from "../../stores/HostSignupStore";
import SdkConfig from "../../SdkConfig";
import {replaceableComponent} from "../../utils/replaceableComponent";
interface IProps {}
interface IProps {
onClick?(): void;
}
interface IState {}
@replaceableComponent("structures.HostSignupAction")
export default class HostSignupAction extends React.PureComponent<IProps, IState> {
private openDialog = async () => {
this.props.onClick?.();
await HostSignupStore.instance.setHostSignupActive(true);
}

View file

@ -439,6 +439,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
onBlur={this.onBlur}
isMinimized={this.props.isMinimized}
activeSpace={this.state.activeSpace}
onResize={this.refreshStickyHeaders}
onListCollapse={this.refreshStickyHeaders}
/>;

View file

@ -355,7 +355,7 @@ class LoggedInView extends React.Component<IProps, IState> {
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
for (const eventId of pinnedEventIds) {
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
const event = timeline.getEvents().find(ev => ev.getId() === eventId);
if (event) events.push(event);
}

View file

@ -378,7 +378,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.onLoggedIn();
}
const promisesList = [this.firstSyncPromise.promise];
const promisesList: Promise<any>[] = [this.firstSyncPromise.promise];
if (cryptoEnabled) {
// wait for the client to finish downloading cross-signing keys for us so we
// know whether or not we have keys set up on this account

View file

@ -19,17 +19,17 @@ limitations under the License.
import React, {createRef} from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import shouldHideEvent from '../../shouldHideEvent';
import {wantsDateSeparator} from '../../DateUtils';
import * as sdk from '../../index';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import SettingsStore from '../../settings/SettingsStore';
import RoomContext from "../../contexts/RoomContext";
import {Layout, LayoutPropType} from "../../settings/Layout";
import {_t} from "../../languageHandler";
import {haveTileForEvent} from "../views/rooms/EventTile";
import {textForEvent} from "../../TextForEvent";
import {hasText} from "../../TextForEvent";
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
import DMRoomMap from "../../utils/DMRoomMap";
import NewRoomIntro from "../views/rooms/NewRoomIntro";
@ -121,6 +121,9 @@ export default class MessagePanel extends React.Component {
// callback which is called when the panel is scrolled.
onScroll: PropTypes.func,
// callback which is called when the user interacts with the room timeline
onUserScroll: PropTypes.func,
// callback which is called when more content is needed.
onFillRequest: PropTypes.func,
@ -149,6 +152,8 @@ export default class MessagePanel extends React.Component {
enableFlair: PropTypes.bool,
};
static contextType = RoomContext;
constructor(props) {
super(props);
@ -378,7 +383,7 @@ export default class MessagePanel extends React.Component {
// Always show highlighted event
if (this.props.highlightedEventId === mxEv.getId()) return true;
return !shouldHideEvent(mxEv);
return !shouldHideEvent(mxEv, this.context);
}
_readMarkerForEvent(eventId, isLastEvent) {
@ -613,10 +618,6 @@ export default class MessagePanel extends React.Component {
const eventId = mxEv.getId();
const highlight = (eventId === this.props.highlightedEventId);
// we can't use local echoes as scroll tokens, because their event IDs change.
// Local echos have a send "status".
const scrollToken = mxEv.status ? undefined : eventId;
const readReceipts = this._readReceiptsByEvent[eventId];
let isLastSuccessful = false;
@ -645,39 +646,36 @@ export default class MessagePanel extends React.Component {
// use txnId as key if available so that we don't remount during sending
ret.push(
<li
key={mxEv.getTxnId() || eventId}
ref={this._collectEventNode.bind(this, eventId)}
data-scroll-tokens={scrollToken}
>
<TileErrorBoundary mxEvent={mxEv}>
<EventTile
mxEvent={mxEv}
continuation={continuation}
isRedacted={mxEv.isRedacted()}
replacingEventId={mxEv.replacingEventId()}
editState={isEditing && this.props.editState}
onHeightChanged={this._onHeightChanged}
readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap}
showUrlPreview={this.props.showUrlPreview}
checkUnmounting={this._isUnmounting}
eventSendStatus={mxEv.getAssociatedStatus()}
tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour}
permalinkCreator={this.props.permalinkCreator}
last={last}
lastInSection={willWantDateSeparator}
lastSuccessful={isLastSuccessful}
isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions}
layout={this.props.layout}
enableFlair={this.props.enableFlair}
showReadReceipts={this.props.showReadReceipts}
/>
</TileErrorBoundary>
</li>,
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
<EventTile
as="li"
ref={this._collectEventNode.bind(this, eventId)}
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
mxEvent={mxEv}
continuation={continuation}
isRedacted={mxEv.isRedacted()}
replacingEventId={mxEv.replacingEventId()}
editState={isEditing && this.props.editState}
onHeightChanged={this._onHeightChanged}
readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap}
showUrlPreview={this.props.showUrlPreview}
checkUnmounting={this._isUnmounting}
eventSendStatus={mxEv.getAssociatedStatus()}
tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour}
permalinkCreator={this.props.permalinkCreator}
last={last}
lastInSection={willWantDateSeparator}
lastSuccessful={isLastSuccessful}
isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions}
layout={this.props.layout}
enableFlair={this.props.enableFlair}
showReadReceipts={this.props.showReadReceipts}
/>
</TileErrorBoundary>,
);
return ret;
@ -779,7 +777,7 @@ export default class MessagePanel extends React.Component {
}
_collectEventNode = (eventId, node) => {
this.eventNodes[eventId] = node;
this.eventNodes[eventId] = node?.ref?.current;
}
// once dynamic content in the events load, make the scrollPanel check the
@ -853,13 +851,6 @@ export default class MessagePanel extends React.Component {
const style = this.props.hidden ? { display: 'none' } : {};
const className = classNames(
this.props.className,
{
"mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps,
},
);
let whoIsTyping;
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
whoIsTyping = (<WhoIsTypingTile
@ -883,8 +874,9 @@ export default class MessagePanel extends React.Component {
<ErrorBoundary>
<ScrollPanel
ref={this._scrollPanel}
className={className}
className={this.props.className}
onScroll={this.props.onScroll}
onUserScroll={this.props.onUserScroll}
onResize={this.onResize}
onFillRequest={this.props.onFillRequest}
onUnfillRequest={this.props.onUnfillRequest}
@ -1175,11 +1167,8 @@ class MemberGrouper {
add(ev) {
if (ev.getType() === 'm.room.member') {
// We'll just double check that it's worth our time to do so, through an
// ugly hack. If textForEvent returns something, we should group it for
// rendering but if it doesn't then we'll exclude it.
const renderText = textForEvent(ev);
if (!renderText || renderText.trim().length === 0) return; // quietly ignore
// We can ignore any events that don't actually have a message to display
if (!hasText(ev)) return;
}
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
ev.getId(),

View file

@ -1,7 +1,5 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2016, 2019, 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.
@ -16,29 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from "prop-types";
import React from "react";
import { _t } from '../../languageHandler';
import {MatrixClientPeg} from "../../MatrixClientPeg";
import * as sdk from "../../index";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import BaseCard from "../views/right_panel/BaseCard";
import {replaceableComponent} from "../../utils/replaceableComponent";
import { replaceableComponent } from "../../utils/replaceableComponent";
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
interface IProps {
onClose(): void;
}
/*
* Component which shows the global notification list using a TimelinePanel
*/
@replaceableComponent("structures.NotificationPanel")
class NotificationPanel extends React.Component {
static propTypes = {
onClose: PropTypes.func.isRequired,
};
export default class NotificationPanel extends React.PureComponent<IProps> {
render() {
// wrap a TimelinePanel with the jump-to-event bits turned off.
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
const Loader = sdk.getComponent("elements.Spinner");
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
<h2>{_t('Youre all caught up')}</h2>
<p>{_t('You have no visible notifications.')}</p>
@ -47,6 +41,7 @@ class NotificationPanel extends React.Component {
let content;
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
if (timelineSet) {
// wrap a TimelinePanel with the jump-to-event bits turned off.
content = (
<TimelinePanel
manageReadReceipts={false}
@ -55,11 +50,12 @@ class NotificationPanel extends React.Component {
showUrlPreview={false}
tileShape="notif"
empty={emptyState}
alwaysShowTimestamps={true}
/>
);
} else {
console.error("No notifTimelineSet available!");
content = <Loader />;
content = <Spinner />;
}
return <BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
@ -67,5 +63,3 @@ class NotificationPanel extends React.Component {
</BaseCard>;
}
}
export default NotificationPanel;

View file

@ -1,6 +1,6 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2015 - 2020 The Matrix.org Foundation C.I.C.
Copyright 2015 - 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.
@ -16,70 +16,92 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {Room} from "matrix-js-sdk/src/models/room";
import { Room } from "matrix-js-sdk/src/models/room";
import { User } from "matrix-js-sdk/src/models/user";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import RateLimitedFunc from '../../ratelimitedfunc';
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStore from '../../stores/GroupStore';
import {
RightPanelPhases,
RIGHT_PANEL_PHASES_NO_ARGS,
RIGHT_PANEL_SPACE_PHASES,
RightPanelPhases,
} from "../../stores/RightPanelStorePhases";
import RightPanelStore from "../../stores/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import {Action} from "../../dispatcher/actions";
import { Action } from "../../dispatcher/actions";
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
import WidgetCard from "../views/right_panel/WidgetCard";
import {replaceableComponent} from "../../utils/replaceableComponent";
import { replaceableComponent } from "../../utils/replaceableComponent";
import SettingsStore from "../../settings/SettingsStore";
import { ActionPayload } from "../../dispatcher/payloads";
import MemberList from "../views/rooms/MemberList";
import GroupMemberList from "../views/groups/GroupMemberList";
import GroupRoomList from "../views/groups/GroupRoomList";
import GroupRoomInfo from "../views/groups/GroupRoomInfo";
import UserInfo from "../views/right_panel/UserInfo";
import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo";
import FilePanel from "./FilePanel";
import NotificationPanel from "./NotificationPanel";
import ResizeNotifier from "../../utils/ResizeNotifier";
import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
interface IProps {
room?: Room; // if showing panels for a given room, this is set
groupId?: string; // if showing panels for a given group, this is set
user?: User; // used if we know the user ahead of opening the panel
resizeNotifier: ResizeNotifier;
}
interface IState {
phase: RightPanelPhases;
isUserPrivilegedInGroup?: boolean;
member?: RoomMember;
verificationRequest?: VerificationRequest;
verificationRequestPromise?: Promise<VerificationRequest>;
space?: Room;
widgetId?: string;
groupRoomId?: string;
groupId?: string;
event: MatrixEvent;
}
@replaceableComponent("structures.RightPanel")
export default class RightPanel extends React.Component {
static get propTypes() {
return {
room: PropTypes.instanceOf(Room), // if showing panels for a given room, this is set
groupId: PropTypes.string, // if showing panels for a given group, this is set
user: PropTypes.object, // used if we know the user ahead of opening the panel
};
}
export default class RightPanel extends React.Component<IProps, IState> {
static contextType = MatrixClientContext;
private readonly delayedUpdate: RateLimitedFunc;
private dispatcherRef: string;
constructor(props, context) {
super(props, context);
this.state = {
...RightPanelStore.getSharedInstance().roomPanelPhaseParams,
phase: this._getPhaseFromProps(),
phase: this.getPhaseFromProps(),
isUserPrivilegedInGroup: null,
member: this._getUserForPanel(),
member: this.getUserForPanel(),
};
this.onAction = this.onAction.bind(this);
this.onRoomStateMember = this.onRoomStateMember.bind(this);
this.onGroupStoreUpdated = this.onGroupStoreUpdated.bind(this);
this.onInviteToGroupButtonClick = this.onInviteToGroupButtonClick.bind(this);
this.onAddRoomToGroupButtonClick = this.onAddRoomToGroupButtonClick.bind(this);
this._delayedUpdate = new RateLimitedFunc(() => {
this.delayedUpdate = new RateLimitedFunc(() => {
this.forceUpdate();
}, 500);
}
// Helper function to split out the logic for _getPhaseFromProps() and the constructor
// Helper function to split out the logic for getPhaseFromProps() and the constructor
// as both are called at the same time in the constructor.
_getUserForPanel() {
private getUserForPanel() {
if (this.state && this.state.member) return this.state.member;
const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams;
return this.props.user || lastParams['member'];
}
// gets the current phase from the props and also maybe the store
_getPhaseFromProps() {
private getPhaseFromProps() {
const rps = RightPanelStore.getSharedInstance();
const userForPanel = this._getUserForPanel();
const userForPanel = this.getUserForPanel();
if (this.props.groupId) {
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList});
@ -118,7 +140,7 @@ export default class RightPanel extends React.Component {
this.dispatcherRef = dis.register(this.onAction);
const cli = this.context;
cli.on("RoomState.members", this.onRoomStateMember);
this._initGroupStore(this.props.groupId);
this.initGroupStore(this.props.groupId);
}
componentWillUnmount() {
@ -126,61 +148,47 @@ export default class RightPanel extends React.Component {
if (this.context) {
this.context.removeListener("RoomState.members", this.onRoomStateMember);
}
this._unregisterGroupStore(this.props.groupId);
this.unregisterGroupStore();
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
if (newProps.groupId !== this.props.groupId) {
this._unregisterGroupStore(this.props.groupId);
this._initGroupStore(newProps.groupId);
this.unregisterGroupStore();
this.initGroupStore(newProps.groupId);
}
}
_initGroupStore(groupId) {
private initGroupStore(groupId: string) {
if (!groupId) return;
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
}
_unregisterGroupStore() {
private unregisterGroupStore() {
GroupStore.unregisterListener(this.onGroupStoreUpdated);
}
onGroupStoreUpdated() {
private onGroupStoreUpdated = () => {
this.setState({
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
});
}
};
onInviteToGroupButtonClick() {
showGroupInviteDialog(this.props.groupId).then(() => {
this.setState({
phase: RightPanelPhases.GroupMemberList,
});
});
}
onAddRoomToGroupButtonClick() {
showGroupAddRoomDialog(this.props.groupId).then(() => {
this.forceUpdate();
});
}
onRoomStateMember(ev, state, member) {
private onRoomStateMember = (ev: MatrixEvent, _, member: RoomMember) => {
if (!this.props.room || member.roomId !== this.props.room.roomId) {
return;
}
// redraw the badge on the membership list
if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) {
this._delayedUpdate();
this.delayedUpdate();
} else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId &&
member.userId === this.state.member.userId) {
// refresh the member info (e.g. new power level)
this._delayedUpdate();
this.delayedUpdate();
}
}
};
onAction(payload) {
private onAction = (payload: ActionPayload) => {
if (payload.action === Action.AfterRightPanelPhaseChange) {
this.setState({
phase: payload.phase,
@ -194,9 +202,9 @@ export default class RightPanel extends React.Component {
space: payload.space,
});
}
}
};
onClose = () => {
private onClose = () => {
// XXX: There are three different ways of 'closing' this panel depending on what state
// things are in... this knows far more than it should do about the state of the rest
// of the app and is generally a bit silly.
@ -224,16 +232,6 @@ export default class RightPanel extends React.Component {
};
render() {
const MemberList = sdk.getComponent('rooms.MemberList');
const UserInfo = sdk.getComponent('right_panel.UserInfo');
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
const FilePanel = sdk.getComponent('structures.FilePanel');
const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
let panel = <div />;
const roomId = this.props.room ? this.props.room.roomId : undefined;
@ -285,6 +283,7 @@ export default class RightPanel extends React.Component {
user={this.state.member}
groupId={this.props.groupId}
key={this.state.member.userId}
phase={this.state.phase}
onClose={this.onClose} />;
break;
@ -299,6 +298,12 @@ export default class RightPanel extends React.Component {
panel = <NotificationPanel onClose={this.onClose} />;
break;
case RightPanelPhases.PinnedMessages:
if (SettingsStore.getValue("feature_pinning")) {
panel = <PinnedMessagesCard room={this.props.room} onClose={this.onClose} />;
}
break;
case RightPanelPhases.FilePanel:
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
break;

View file

@ -46,7 +46,7 @@ import RoomViewStore from '../../stores/RoomViewStore';
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
import WidgetEchoStore from '../../stores/WidgetEchoStore';
import SettingsStore from "../../settings/SettingsStore";
import {Layout} from "../../settings/Layout";
import { Layout } from "../../settings/Layout";
import AccessibleButton from "../views/elements/AccessibleButton";
import RightPanelStore from "../../stores/RightPanelStore";
import { haveTileForEvent } from "../views/rooms/EventTile";
@ -54,16 +54,13 @@ import RoomContext from "../../contexts/RoomContext";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
import { Action } from "../../dispatcher/actions";
import { SettingLevel } from "../../settings/SettingLevel";
import { IMatrixClientCreds } from "../../MatrixClientPeg";
import ScrollPanel from "./ScrollPanel";
import TimelinePanel from "./TimelinePanel";
import ErrorBoundary from "../views/elements/ErrorBoundary";
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
import ForwardMessage from "../views/rooms/ForwardMessage";
import SearchBar from "../views/rooms/SearchBar";
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
import AuxPanel from "../views/rooms/AuxPanel";
import RoomHeader from "../views/rooms/RoomHeader";
import { XOR } from "../../@types/common";
@ -82,7 +79,8 @@ import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager';
import { objectHasDiff } from "../../utils/objects";
import SpaceRoomView from "./SpaceRoomView";
import { IOpts } from "../../createRoom";
import {replaceableComponent} from "../../utils/replaceableComponent";
import { replaceableComponent } from "../../utils/replaceableComponent";
import { omit } from 'lodash';
import UIStore from "../../stores/UIStore";
const DEBUG = false;
@ -137,7 +135,6 @@ export interface IState {
// Whether to highlight the event scrolled to
isInitialEventHighlighted?: boolean;
replyToEvent?: MatrixEvent;
forwardingEvent?: MatrixEvent;
numUnreadMessages: number;
draggingFile: boolean;
searching: boolean;
@ -156,8 +153,6 @@ export interface IState {
canPeek: boolean;
showApps: boolean;
isPeeking: boolean;
showingPinned: boolean;
showReadReceipts: boolean;
showRightPanel: boolean;
// error object, as from the matrix client/server API
// If we failed to load information about the room,
@ -176,6 +171,7 @@ export interface IState {
statusBarVisible: boolean;
// We load this later by asking the js-sdk to suggest a version for us.
// This object is the result of Room#getRecommendedVersion()
upgradeRecommendation?: {
version: string;
needsUpgrade: boolean;
@ -184,6 +180,12 @@ export interface IState {
canReact: boolean;
canReply: boolean;
layout: Layout;
lowBandwidth: boolean;
showReadReceipts: boolean;
showRedactions: boolean;
showJoinLeaves: boolean;
showAvatarChanges: boolean;
showDisplaynameChanges: boolean;
matrixClientIsReady: boolean;
showUrlPreview?: boolean;
e2eStatus?: E2EStatus;
@ -201,8 +203,7 @@ export default class RoomView extends React.Component<IProps, IState> {
private readonly dispatcherRef: string;
private readonly roomStoreToken: EventSubscription;
private readonly rightPanelStoreToken: EventSubscription;
private readonly showReadReceiptsWatchRef: string;
private readonly layoutWatcherRef: string;
private settingWatchers: string[];
private unmounted = false;
private permalinkCreators: Record<string, RoomPermalinkCreator> = {};
@ -233,8 +234,6 @@ export default class RoomView extends React.Component<IProps, IState> {
canPeek: false,
showApps: false,
isPeeking: false,
showingPinned: false,
showReadReceipts: true,
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
joining: false,
atEndOfLiveTimeline: true,
@ -244,6 +243,12 @@ export default class RoomView extends React.Component<IProps, IState> {
canReact: false,
canReply: false,
layout: SettingsStore.getValue("layout"),
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
showReadReceipts: true,
showRedactions: true,
showJoinLeaves: true,
showAvatarChanges: true,
showDisplaynameChanges: true,
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
dragCounter: 0,
};
@ -270,9 +275,14 @@ export default class RoomView extends React.Component<IProps, IState> {
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
this.showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null,
this.onReadReceiptsChange);
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, this.onLayoutChange);
this.settingWatchers = [
SettingsStore.watchSetting("layout", null, () =>
this.setState({ layout: SettingsStore.getValue("layout") }),
),
SettingsStore.watchSetting("lowBandwidth", null, () =>
this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
),
];
}
private onWidgetStoreUpdate = () => {
@ -325,14 +335,45 @@ export default class RoomView extends React.Component<IProps, IState> {
initialEventId: RoomViewStore.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
replyToEvent: RoomViewStore.getQuotingEvent(),
forwardingEvent: RoomViewStore.getForwardingEvent(),
// we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
showRedactions: SettingsStore.getValue("showRedactions", roomId),
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
};
// Add watchers for each of the settings we just looked up
this.settingWatchers = this.settingWatchers.concat([
SettingsStore.watchSetting("showReadReceipts", null, () =>
this.setState({
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
}),
),
SettingsStore.watchSetting("showRedactions", null, () =>
this.setState({
showRedactions: SettingsStore.getValue("showRedactions", roomId),
}),
),
SettingsStore.watchSetting("showJoinLeaves", null, () =>
this.setState({
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
}),
),
SettingsStore.watchSetting("showAvatarChanges", null, () =>
this.setState({
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
}),
),
SettingsStore.watchSetting("showDisplaynameChanges", null, () =>
this.setState({
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
}),
),
]);
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
// Stop peeking because we have joined this room now
this.context.stopPeeking();
@ -529,7 +570,20 @@ export default class RoomView extends React.Component<IProps, IState> {
}
shouldComponentUpdate(nextProps, nextState) {
return (objectHasDiff(this.props, nextProps) || objectHasDiff(this.state, nextState));
const hasPropsDiff = objectHasDiff(this.props, nextProps);
// React only shallow comparison and we only want to trigger
// a component re-render if a room requires an upgrade
const newUpgradeRecommendation = nextState.upgradeRecommendation || {}
const state = omit(this.state, ['upgradeRecommendation']);
const newState = omit(nextState, ['upgradeRecommendation'])
const hasStateDiff =
objectHasDiff(state, newState) ||
(newUpgradeRecommendation.needsUpgrade === true)
return hasPropsDiff || hasStateDiff;
}
componentDidUpdate() {
@ -628,10 +682,6 @@ export default class RoomView extends React.Component<IProps, IState> {
);
}
if (this.showReadReceiptsWatchRef) {
SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef);
}
// cancel any pending calls to the rate_limited_funcs
this.updateRoomMembers.cancelPendingCall();
@ -639,7 +689,20 @@ export default class RoomView extends React.Component<IProps, IState> {
// console.log("Tinter.tint from RoomView.unmount");
// Tinter.tint(); // reset colourscheme
SettingsStore.unwatchSetting(this.layoutWatcherRef);
for (const watcher of this.settingWatchers) {
SettingsStore.unwatchSetting(watcher);
}
}
private onUserScroll = () => {
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
dis.dispatch({
action: 'view_room',
room_id: this.state.room.roomId,
event_id: this.state.initialEventId,
highlighted: false,
});
}
}
private onLayoutChange = () => {
@ -798,7 +861,7 @@ export default class RoomView extends React.Component<IProps, IState> {
// update unread count when scrolled up
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
// no change
} else if (!shouldHideEvent(ev)) {
} else if (!shouldHideEvent(ev, this.state)) {
this.setState((state, props) => {
return {numUnreadMessages: state.numUnreadMessages + 1};
});
@ -812,7 +875,7 @@ export default class RoomView extends React.Component<IProps, IState> {
};
private onEvent = (ev) => {
if (ev.isBeingDecrypted() || ev.isDecryptionFailure() || ev.shouldAttemptDecryption()) return;
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
this.handleEffects(ev);
};
@ -1377,13 +1440,6 @@ export default class RoomView extends React.Component<IProps, IState> {
return ret;
}
private onPinnedClick = () => {
const nowShowingPinned = !this.state.showingPinned;
const roomId = this.state.room.roomId;
this.setState({showingPinned: nowShowingPinned, searching: false});
SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned);
};
private onCallPlaced = (type: PlaceCallType) => {
dis.dispatch({
action: 'place_call',
@ -1396,18 +1452,6 @@ export default class RoomView extends React.Component<IProps, IState> {
dis.dispatch({ action: "open_room_settings" });
};
private onCancelClick = () => {
console.log("updateTint from onCancelClick");
this.updateTint();
if (this.state.forwardingEvent) {
dis.dispatch({
action: 'forward_event',
event: null,
});
}
dis.fire(Action.FocusComposer);
};
private onAppsClick = () => {
dis.dispatch({
action: "appsDrawer",
@ -1500,7 +1544,6 @@ export default class RoomView extends React.Component<IProps, IState> {
private onSearchClick = () => {
this.setState({
searching: !this.state.searching,
showingPinned: false,
});
};
@ -1513,8 +1556,19 @@ export default class RoomView extends React.Component<IProps, IState> {
// jump down to the bottom of this room, where new events are arriving
private jumpToLiveTimeline = () => {
this.messagePanel.jumpToLiveTimeline();
dis.fire(Action.FocusComposer);
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
// If we were viewing a highlighted event, firing view_room without
// an event will take care of both clearing the URL fragment and
// jumping to the bottom
dis.dispatch({
action: 'view_room',
room_id: this.state.room.roomId,
});
} else {
// Otherwise we have to jump manually
this.messagePanel.jumpToLiveTimeline();
dis.fire(Action.FocusComposer);
}
};
// jump up to wherever our read marker is
@ -1615,6 +1669,24 @@ export default class RoomView extends React.Component<IProps, IState> {
});
};
/**
* called by the parent component when PageUp/Down/etc is pressed.
*
* We pass it down to the scroll panel.
*/
private handleScrollKey = ev => {
let panel;
if (this.searchResultsPanel.current) {
panel = this.searchResultsPanel.current;
} else if (this.messagePanel) {
panel = this.messagePanel;
}
if (panel) {
panel.handleScrollKey(ev);
}
};
/**
* get any current call for this room
*/
@ -1813,11 +1885,7 @@ export default class RoomView extends React.Component<IProps, IState> {
let aux = null;
let previewBar;
let hideCancel = false;
if (this.state.forwardingEvent) {
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
} else if (this.state.searching) {
hideCancel = true; // has own cancel
if (this.state.searching) {
aux = <SearchBar
searchInProgress={this.state.searchInProgress}
onCancelClick={this.onCancelSearchClick}
@ -1826,10 +1894,6 @@ export default class RoomView extends React.Component<IProps, IState> {
/>;
} else if (showRoomUpgradeBar) {
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
hideCancel = true;
} else if (this.state.showingPinned) {
hideCancel = true; // has own cancel
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
} else if (myMembership !== "join") {
// We do have a room object for this room, but we're not currently in it.
// We may have a 3rd party invite to it.
@ -1838,7 +1902,6 @@ export default class RoomView extends React.Component<IProps, IState> {
inviterName = this.props.oobData.inviterName;
}
const invitedEmail = this.props.threepidInvite?.toEmail;
hideCancel = true;
previewBar = (
<RoomPreviewBar
onJoinClick={this.onJoinButtonClicked}
@ -1956,11 +2019,8 @@ export default class RoomView extends React.Component<IProps, IState> {
hideMessagePanel = true;
}
const shouldHighlight = this.state.isInitialEventHighlighted;
let highlightedEventId = null;
if (this.state.forwardingEvent) {
highlightedEventId = this.state.forwardingEvent.getId();
} else if (shouldHighlight) {
if (this.state.isInitialEventHighlighted) {
highlightedEventId = this.state.initialEventId;
}
@ -1985,6 +2045,7 @@ export default class RoomView extends React.Component<IProps, IState> {
eventId={this.state.initialEventId}
eventPixelOffset={this.state.initialEventPixelOffset}
onScroll={this.onMessageListScroll}
onUserScroll={this.onUserScroll}
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
showUrlPreview = {this.state.showUrlPreview}
className={messagePanelClassNames}
@ -2011,6 +2072,7 @@ export default class RoomView extends React.Component<IProps, IState> {
highlight={this.state.room.getUnreadNotificationCount('highlight') > 0}
numUnreadMessages={this.state.numUnreadMessages}
onScrollToBottomClick={this.jumpToLiveTimeline}
roomId={this.state.roomId}
/>);
}
@ -2047,8 +2109,6 @@ export default class RoomView extends React.Component<IProps, IState> {
inRoom={myMembership === 'join'}
onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick}
onPinnedClick={this.onPinnedClick}
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
e2eStatus={this.state.e2eStatus}

View file

@ -133,6 +133,10 @@ export default class ScrollPanel extends React.Component {
*/
onScroll: PropTypes.func,
/* onUserScroll: callback which is called when the user interacts with the room timeline
*/
onUserScroll: PropTypes.func,
/* className: classnames to add to the top-level div
*/
className: PropTypes.string,
@ -535,21 +539,29 @@ export default class ScrollPanel extends React.Component {
* @param {object} ev the keyboard event
*/
handleScrollKey = ev => {
let isScrolling = false;
const roomAction = getKeyBindingsManager().getRoomAction(ev);
switch (roomAction) {
case RoomAction.ScrollUp:
this.scrollRelative(-1);
isScrolling = true;
break;
case RoomAction.RoomScrollDown:
this.scrollRelative(1);
isScrolling = true;
break;
case RoomAction.JumpToFirstMessage:
this.scrollToTop();
isScrolling = true;
break;
case RoomAction.JumpToLatestMessage:
this.scrollToBottom();
isScrolling = true;
break;
}
if (isScrolling && this.props.onUserScroll) {
this.props.onUserScroll(ev);
}
};
/* Scroll the panel to bring the DOM node with the scroll token
@ -888,9 +900,8 @@ export default class ScrollPanel extends React.Component {
<AutoHideScrollbar
wrappedRef={this._collectScroll}
onScroll={this.onScroll}
className={`mx_ScrollPanel ${this.props.className}`}
style={this.props.style}
>
onWheel={this.props.onUserScroll}
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
{ this.props.fixedChildren }
<div className="mx_RoomView_messageListWrapper">
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">

View file

@ -28,7 +28,7 @@ import RoomTopic from "../views/elements/RoomTopic";
import InlineSpinner from "../views/elements/InlineSpinner";
import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
import {useRoomMembers} from "../../hooks/useRoomMembers";
import createRoom, {IOpts, Preset} from "../../createRoom";
import createRoom, {IOpts} from "../../createRoom";
import Field from "../views/elements/Field";
import {useEventEmitter} from "../../hooks/useEventEmitter";
import withValidation from "../views/elements/Validation";
@ -65,6 +65,7 @@ import dis from "../../dispatcher/dispatcher";
import Modal from "../../Modal";
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
import SdkConfig from "../../SdkConfig";
import { Preset } from "matrix-js-sdk/src/@types/partials";
interface IProps {
space: Room;
@ -587,6 +588,10 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
<h3>{ _t("Me and my teammates") }</h3>
<div>{ _t("A private space for you and your teammates") }</div>
</AccessibleButton>
<div className="mx_SpaceRoomView_betaWarning">
<h3>{ _t("Teammates might not be able to view or join any private rooms you make.") }</h3>
<p>{ _t("We're working on this as part of the beta, but just want to let you know.") }</p>
</div>
<SpaceFeedbackPrompt />
</div>;
};

View file

@ -26,6 +26,7 @@ import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline";
import {TimelineWindow} from "matrix-js-sdk/src/timeline-window";
import { _t } from '../../languageHandler';
import {MatrixClientPeg} from "../../MatrixClientPeg";
import RoomContext from "../../contexts/RoomContext";
import UserActivity from "../../UserActivity";
import Modal from "../../Modal";
import dis from "../../dispatcher/dispatcher";
@ -36,7 +37,6 @@ import shouldHideEvent from '../../shouldHideEvent';
import EditorStateTransfer from '../../utils/EditorStateTransfer';
import {haveTileForEvent} from "../views/rooms/EventTile";
import {UIFeature} from "../../settings/UIFeature";
import {objectHasDiff} from "../../utils/objects";
import {replaceableComponent} from "../../utils/replaceableComponent";
import { arrayFastClone } from "../../utils/arrays";
@ -94,6 +94,9 @@ class TimelinePanel extends React.Component {
// callback which is called when the panel is scrolled.
onScroll: PropTypes.func,
// callback which is called when the user interacts with the room timeline
onUserScroll: PropTypes.func,
// callback which is called when the read-up-to mark is updated.
onReadMarkerUpdated: PropTypes.func,
@ -118,8 +121,13 @@ class TimelinePanel extends React.Component {
// which layout to use
layout: LayoutPropType,
// whether to always show timestamps for an event
alwaysShowTimestamps: PropTypes.bool,
}
static contextType = RoomContext;
// a map from room id to read marker event timestamp
static roomReadMarkerTsMap = {};
@ -258,37 +266,15 @@ class TimelinePanel extends React.Component {
console.warn("Replacing timelineSet on a TimelinePanel - confusion may ensue");
}
if (newProps.eventId != this.props.eventId) {
const differentEventId = newProps.eventId != this.props.eventId;
const differentHighlightedEventId = newProps.highlightedEventId != this.props.highlightedEventId;
if (differentEventId || differentHighlightedEventId) {
console.log("TimelinePanel switching to eventId " + newProps.eventId +
" (was " + this.props.eventId + ")");
return this._initTimeline(newProps);
}
}
shouldComponentUpdate(nextProps, nextState) {
if (objectHasDiff(this.props, nextProps)) {
if (DEBUG) {
console.group("Timeline.shouldComponentUpdate: props change");
console.log("props before:", this.props);
console.log("props after:", nextProps);
console.groupEnd();
}
return true;
}
if (objectHasDiff(this.state, nextState)) {
if (DEBUG) {
console.group("Timeline.shouldComponentUpdate: state change");
console.log("state before:", this.state);
console.log("state after:", nextState);
console.groupEnd();
}
return true;
}
return false;
}
componentWillUnmount() {
// set a boolean to say we've been unmounted, which any pending
// promises can use to throw away their results.
@ -1305,7 +1291,7 @@ class TimelinePanel extends React.Component {
const shouldIgnore = !!ev.status || // local echo
(ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message
const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev);
const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev, this.context);
if (isWithoutTile || !node) {
// don't start counting if the event should be ignored,
@ -1456,10 +1442,11 @@ class TimelinePanel extends React.Component {
ourUserId={MatrixClientPeg.get().credentials.userId}
stickyBottom={stickyBottom}
onScroll={this.onMessageListScroll}
onUserScroll={this.props.onUserScroll}
onFillRequest={this.onMessageListFillRequest}
onUnfillRequest={this.onMessageListUnfillRequest}
isTwelveHour={this.state.isTwelveHour}
alwaysShowTimestamps={this.state.alwaysShowTimestamps}
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.alwaysShowTimestamps}
className={this.props.className}
tileShape={this.props.tileShape}
resizeNotifier={this.props.resizeNotifier}

View file

@ -55,6 +55,7 @@ export default class ToastContainer extends React.Component<{}, IState> {
const totalCount = this.state.toasts.length;
const isStacked = totalCount > 1;
let toast;
let containerClasses;
if (totalCount !== 0) {
const topToast = this.state.toasts[0];
const {title, icon, key, component, className, props} = topToast;
@ -79,16 +80,17 @@ export default class ToastContainer extends React.Component<{}, IState> {
</div>
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
</div>);
containerClasses = classNames("mx_ToastContainer", {
"mx_ToastContainer_stacked": isStacked,
});
}
const containerClasses = classNames("mx_ToastContainer", {
"mx_ToastContainer_stacked": isStacked,
});
return (
<div className={containerClasses} role="alert">
{toast}
</div>
);
return toast
? (
<div className={containerClasses} role="alert">
{toast}
</div>
)
: null;
}
}

View file

@ -366,9 +366,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
const mxDomain = MatrixClientPeg.get().getDomain();
const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`)));
if (!hostSignupConfig.domains || validDomains.length > 0) {
topSection = <div onClick={this.onCloseMenu}>
<HostSignupAction />
</div>;
topSection = <HostSignupAction onClick={this.onCloseMenu} />;
}
}
}

View file

@ -61,7 +61,7 @@ interface IProps {
is_url?: string;
session_id: string;
/* eslint-enable camelcase */
}): void;
}): string;
// registration shouldn't know or care how login is done.
onLoginClick(): void;
onServerConfigChange(config: ValidatedServerConfig): void;