Merge pull request #6079 from matrix-org/gsouquet/switch-rooms

This commit is contained in:
Germain 2021-06-03 08:44:01 +01:00 committed by GitHub
commit 7f83590846
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 184 additions and 171 deletions

View file

@ -239,6 +239,7 @@ hr.mx_RoomView_myReadMarker {
position: relative;
top: -1px;
z-index: 1;
will-change: width;
transition: width 400ms easeinsine 1s, opacity 400ms easeinsine 1s;
width: 99%;
opacity: 1;

View file

@ -115,8 +115,7 @@ $irc-line-height: $font-18px;
.mx_EventTile_line {
.mx_EventTile_e2eIcon,
.mx_TextualEvent,
.mx_MTextBody,
.mx_ReplyThread_wrapper_empty {
.mx_MTextBody {
display: inline-block;
}
}
@ -177,8 +176,6 @@ $irc-line-height: $font-18px;
.mx_SenderProfile_hover {
background-color: $primary-bg-color;
overflow: hidden;
> span {
display: flex;
> .mx_SenderProfile_name {
@ -188,7 +185,6 @@ $irc-line-height: $font-18px;
text-align: end;
}
}
}
.mx_SenderProfile:hover {
justify-content: flex-start;

View file

@ -32,14 +32,14 @@ limitations under the License.
// first triggering the enter state with the newest breadcrumb off screen (-40px) then
// sliding it into view.
&.mx_RoomBreadcrumbs-enter {
margin-left: -40px; // 32px for the avatar, 8px for the margin
transform: translateX(-40px); // 32px for the avatar, 8px for the margin
}
&.mx_RoomBreadcrumbs-enter-active {
margin-left: 0;
transform: translateX(0);
// Timing function is as-requested by design.
// NOTE: The transition time MUST match the value passed to CSSTransition!
transition: margin-left 640ms cubic-bezier(0.66, 0.02, 0.36, 1);
transition: transform 640ms cubic-bezier(0.66, 0.02, 0.36, 1);
}
.mx_RoomBreadcrumbs_placeholder {

View file

@ -648,13 +648,12 @@ 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}>
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
<EventTile
as="li"
data-scroll-tokens={scrollToken}
ref={this._collectEventNode.bind(this, eventId)}
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
mxEvent={mxEv}
continuation={continuation}
isRedacted={mxEv.isRedacted()}
@ -679,8 +678,7 @@ export default class MessagePanel extends React.Component {
enableFlair={this.props.enableFlair}
showReadReceipts={this.props.showReadReceipts}
/>
</TileErrorBoundary>
</li>,
</TileErrorBoundary>,
);
return ret;
@ -782,7 +780,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

View file

@ -83,6 +83,7 @@ import { objectHasDiff } from "../../utils/objects";
import SpaceRoomView from "./SpaceRoomView";
import { IOpts } from "../../createRoom";
import {replaceableComponent} from "../../utils/replaceableComponent";
import { omit } from 'lodash';
import UIStore from "../../stores/UIStore";
const DEBUG = false;
@ -176,6 +177,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;
@ -529,7 +531,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() {
@ -823,7 +838,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);
};

View file

@ -36,7 +36,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";
@ -270,30 +269,6 @@ class TimelinePanel extends React.Component {
}
}
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.

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>);
}
const containerClasses = classNames("mx_ToastContainer", {
containerClasses = classNames("mx_ToastContainer", {
"mx_ToastContainer_stacked": isStacked,
});
return (
}
return toast
? (
<div className={containerClasses} role="alert">
{toast}
</div>
);
)
: null;
}
}

View file

@ -73,7 +73,7 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
label={tooltip || title}
yOffset={yOffset}
/> : <div />;
/> : null;
return (
<AccessibleButton
{...props}

View file

@ -116,7 +116,7 @@ export default class Flair extends React.Component {
render() {
if (this.state.profiles.length === 0) {
return <span className="mx_Flair" />;
return null;
}
const avatars = this.state.profiles.map((profile, index) => {
return <FlairAvatar key={index} groupProfile={profile} />;

View file

@ -214,7 +214,7 @@ export default class ReplyThread extends React.Component {
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout) {
if (!ReplyThread.getParentEventId(parentEv)) {
return <div className="mx_ReplyThread_wrapper_empty" />;
return null;
}
return <ReplyThread
parentEv={parentEv}
@ -269,39 +269,30 @@ export default class ReplyThread extends React.Component {
const {parentEv} = this.props;
// at time of making this component we checked that props.parentEv has a parentEventId
const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv));
if (this.unmounted) return;
if (ev) {
const loadedEv = await this.getNextEvent(ev);
this.setState({
events: [ev],
}, this.loadNextEvent);
} else {
this.setState({err: true});
}
}
async loadNextEvent() {
if (this.unmounted) return;
const ev = this.state.events[0];
const inReplyToEventId = ReplyThread.getParentEventId(ev);
if (!inReplyToEventId) {
this.setState({
loadedEv,
loading: false,
});
return;
}
const loadedEv = await this.getEvent(inReplyToEventId);
if (this.unmounted) return;
if (loadedEv) {
this.setState({loadedEv});
} else {
this.setState({err: true});
}
}
async getNextEvent(ev) {
try {
const inReplyToEventId = ReplyThread.getParentEventId(ev);
return await this.getEvent(inReplyToEventId);
} catch (e) {
return null;
}
}
async getEvent(eventId) {
const event = this.room.findEventById(eventId);
if (event) return event;
@ -326,13 +317,18 @@ export default class ReplyThread extends React.Component {
this.initialize();
}
onQuoteClick() {
async onQuoteClick() {
const events = [this.state.loadedEv, ...this.state.events];
let loadedEv = null;
if (events.length > 0) {
loadedEv = await this.getNextEvent(events[0]);
}
this.setState({
loadedEv: null,
loadedEv,
events,
}, this.loadNextEvent);
});
dis.fire(Action.FocusComposer);
}

View file

@ -31,21 +31,23 @@ export default class SenderProfile extends React.Component {
static contextType = MatrixClientContext;
state = {
userGroups: null,
constructor(props) {
super(props);
const senderId = this.props.mxEvent.getSender();
this.state = {
userGroups: FlairStore.cachedPublicisedGroups(senderId) || [],
relatedGroups: [],
};
}
componentDidMount() {
this.unmounted = false;
this._updateRelatedGroups();
FlairStore.getPublicisedGroupsCached(
this.context, this.props.mxEvent.getSender(),
).then((userGroups) => {
if (this.unmounted) return;
this.setState({userGroups});
});
if (this.state.userGroups.length === 0) {
this.getPublicisedGroups();
}
this.context.on('RoomState.events', this.onRoomStateEvents);
}
@ -55,6 +57,15 @@ export default class SenderProfile extends React.Component {
this.context.removeListener('RoomState.events', this.onRoomStateEvents);
}
async getPublicisedGroups() {
if (!this.unmounted) {
const userGroups = await FlairStore.getPublicisedGroupsCached(
this.context, this.props.mxEvent.getSender(),
);
this.setState({userGroups});
}
}
onRoomStateEvents = event => {
if (event.getType() === 'm.room.related_groups' &&
event.getRoomId() === this.props.mxEvent.getRoomId()
@ -93,10 +104,10 @@ export default class SenderProfile extends React.Component {
const {msgtype} = mxEvent.getContent();
if (msgtype === 'm.emote') {
return <span />; // emote message must include the name so don't duplicate it
return null; // emote message must include the name so don't duplicate it
}
let flair = <div />;
let flair = null;
if (this.props.enableFlair) {
const displayedGroups = this._getDisplayedGroups(
this.state.userGroups, this.state.relatedGroups,
@ -110,19 +121,12 @@ export default class SenderProfile extends React.Component {
const nameElem = name || '';
// Name + flair
const nameFlair = <span>
return (
<div className="mx_SenderProfile mx_SenderProfile_hover" dir="auto" onClick={this.props.onClick}>
<span className={`mx_SenderProfile_name ${colorClass}`}>
{ nameElem }
</span>
{ flair }
</span>;
return (
<div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
<div className="mx_SenderProfile_hover">
{ nameFlair }
</div>
</div>
);
}

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();
}
/**
@ -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">
return (
<div className="mx_EventTile_msgOption">
<span className="mx_EventTile_readAvatars">
{ remText }
{ avatars }
</span>;
</span>
</div>
)
}
onSenderProfileClick = event => {
@ -953,7 +975,8 @@ export default class EventTile extends React.Component<IProps, IState> {
onFocusChange={this.onActionBarFocusChange}
/> : undefined;
const timestamp = this.props.mxEvent.getTs() ?
const showTimestamp = this.props.mxEvent.getTs() && (this.props.alwaysShowTimestamps || this.state.hover);
const timestamp = showTimestamp ?
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
const keyRequestHelpText =
@ -1016,11 +1039,7 @@ export default class EventTile extends React.Component<IProps, IState> {
let msgOption;
if (this.props.showReadReceipts) {
const readAvatars = this.getReadAvatars();
msgOption = (
<div className="mx_EventTile_msgOption">
{ readAvatars }
</div>
);
msgOption = readAvatars;
}
switch (this.props.tileShape) {
@ -1124,11 +1143,20 @@ export default class EventTile extends React.Component<IProps, IState> {
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
return (
<div className={classes} tabIndex={-1} aria-live={ariaLive} aria-atomic="true">
{ ircTimestamp }
{ sender }
{ ircPadlock }
<div className="mx_EventTile_line">
React.createElement(this.props.as || "div", {
"ref": this.ref,
"className": classes,
"tabIndex": -1,
"aria-live": ariaLive,
"aria-atomic": "true",
"data-scroll-tokens": this.props["data-scroll-tokens"],
"onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }),
}, [
ircTimestamp,
sender,
ircPadlock,
<div className="mx_EventTile_line" key="mx_EventTile_line">
{ groupTimestamp }
{ groupPadlock }
{ thread }
@ -1145,16 +1173,12 @@ export default class EventTile extends React.Component<IProps, IState> {
{ keyRequestInfo }
{ reactionsRow }
{ actionBar }
</div>
{msgOption}
{
// The avatar goes after the event tile as it's absolutely positioned to be over the
// event tile line, so needs to be later in the DOM so it appears on top (this avoids
// the need for further z-indexing chaos)
}
{ avatar }
</div>
);
</div>,
msgOption,
avatar,
])
)
}
}
}

View file

@ -62,15 +62,13 @@ export default class SimpleRoomHeader extends React.Component {
}
return (
<div className="mx_RoomHeader" >
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader mx_RoomHeader_wrapper" >
<div className="mx_RoomHeader_simpleHeader">
{ icon }
{ this.props.title }
{ cancelButton }
</div>
</div>
</div>
);
}
}

View file

@ -215,7 +215,7 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
this.props.whoIsTypingLimit,
);
if (!typingString) {
return (<div className="mx_WhoIsTypingTile_empty" />);
return null;
}
return (

View file

@ -65,6 +65,10 @@ class FlairStore extends EventEmitter {
delete this._userGroups[userId];
}
cachedPublicisedGroups(userId) {
return this._userGroups[userId];
}
getPublicisedGroupsCached(matrixClient, userId) {
if (this._userGroups[userId]) {
return Promise.resolve(this._userGroups[userId]);

View file

@ -309,7 +309,7 @@ describe('MessagePanel', function() {
const rm = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_RoomView_myReadMarker_container');
// it should follow the <li> which wraps the event tile for event 4
const eventContainer = ReactDOM.findDOMNode(tiles[4]).parentNode;
const eventContainer = ReactDOM.findDOMNode(tiles[4]);
expect(rm.previousSibling).toEqual(eventContainer);
});
@ -365,7 +365,7 @@ describe('MessagePanel', function() {
const tiles = TestUtils.scryRenderedComponentsWithType(
mp, sdk.getComponent('rooms.EventTile'));
const tileContainers = tiles.map(function(t) {
return ReactDOM.findDOMNode(t).parentNode;
return ReactDOM.findDOMNode(t);
});
// find the <li> which wraps the read marker