2016-04-21 01:03:05 +03:00
|
|
|
/*
|
|
|
|
Copyright 2016 OpenMarket Ltd
|
2019-12-20 03:45:24 +03:00
|
|
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
2016-04-21 01:03:05 +03:00
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2021-06-29 15:11:58 +03:00
|
|
|
import React, { createRef } from 'react';
|
2017-12-26 04:03:18 +03:00
|
|
|
import PropTypes from 'prop-types';
|
2017-06-08 16:08:51 +03:00
|
|
|
import { _t } from '../../../languageHandler';
|
2021-06-29 15:11:58 +03:00
|
|
|
import { formatDate } from '../../../DateUtils';
|
2021-04-14 12:18:45 +03:00
|
|
|
import NodeAnimator from "../../../NodeAnimator";
|
2019-12-21 00:41:07 +03:00
|
|
|
import * as sdk from "../../../index";
|
2021-06-29 15:11:58 +03:00
|
|
|
import { toPx } from "../../../utils/units";
|
|
|
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
2017-05-15 04:55:07 +03:00
|
|
|
|
2021-03-09 06:12:00 +03:00
|
|
|
@replaceableComponent("views.rooms.ReadReceiptMarker")
|
2021-02-27 08:25:50 +03:00
|
|
|
export default class ReadReceiptMarker extends React.PureComponent {
|
2020-08-29 14:14:16 +03:00
|
|
|
static propTypes = {
|
2016-04-21 01:03:05 +03:00
|
|
|
// the RoomMember to show the RR for
|
2018-10-10 17:14:09 +03:00
|
|
|
member: PropTypes.object,
|
|
|
|
// userId to fallback the avatar to
|
|
|
|
// if the member hasn't been loaded yet
|
|
|
|
fallbackUserId: PropTypes.string.isRequired,
|
2016-04-21 01:03:05 +03:00
|
|
|
|
|
|
|
// number of pixels to offset the avatar from the right of its parent;
|
|
|
|
// typically a negative value.
|
2017-12-26 04:03:18 +03:00
|
|
|
leftOffset: PropTypes.number,
|
2016-04-21 01:03:05 +03:00
|
|
|
|
|
|
|
// true to hide the avatar (it will still be animated)
|
2017-12-26 04:03:18 +03:00
|
|
|
hidden: PropTypes.bool,
|
2016-04-21 01:03:05 +03:00
|
|
|
|
|
|
|
// don't animate this RR into position
|
2017-12-26 04:03:18 +03:00
|
|
|
suppressAnimation: PropTypes.bool,
|
2016-04-21 01:03:05 +03:00
|
|
|
|
|
|
|
// an opaque object for storing information about this user's RR in
|
|
|
|
// this room
|
2017-12-26 04:03:18 +03:00
|
|
|
readReceiptInfo: PropTypes.object,
|
2016-04-21 01:03:05 +03:00
|
|
|
|
2016-04-22 19:03:15 +03:00
|
|
|
// A function which is used to check if the parent panel is being
|
|
|
|
// unmounted, to avoid unnecessary work. Should return true if we
|
|
|
|
// are being unmounted.
|
2017-12-26 04:03:18 +03:00
|
|
|
checkUnmounting: PropTypes.func,
|
2016-04-22 19:03:15 +03:00
|
|
|
|
2016-04-21 01:03:05 +03:00
|
|
|
// callback for clicks on this RR
|
2017-12-26 04:03:18 +03:00
|
|
|
onClick: PropTypes.func,
|
2016-12-08 19:23:20 +03:00
|
|
|
|
|
|
|
// Timestamp when the receipt was read
|
2017-12-26 04:03:18 +03:00
|
|
|
timestamp: PropTypes.number,
|
2017-06-22 17:53:58 +03:00
|
|
|
|
|
|
|
// True to show twelve hour format, false otherwise
|
2017-12-26 04:03:18 +03:00
|
|
|
showTwelveHour: PropTypes.bool,
|
2020-08-29 14:14:16 +03:00
|
|
|
};
|
2016-04-21 01:03:05 +03:00
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
static defaultProps = {
|
|
|
|
leftOffset: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
2016-04-21 01:03:05 +03:00
|
|
|
|
2019-12-23 15:24:49 +03:00
|
|
|
this._avatar = createRef();
|
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
this.state = {
|
|
|
|
// if we are going to animate the RR, we don't show it on first render,
|
|
|
|
// and instead just add a placeholder to the DOM; once we've been
|
|
|
|
// mounted, we start an animation which moves the RR from its old
|
|
|
|
// position.
|
|
|
|
suppressDisplay: !this.props.suppressAnimation,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
2016-04-21 01:03:05 +03:00
|
|
|
// before we remove the rr, store its location in the map, so that if
|
|
|
|
// it reappears, it can be animated from the right place.
|
2017-10-11 19:56:17 +03:00
|
|
|
const rrInfo = this.props.readReceiptInfo;
|
2016-04-21 01:03:05 +03:00
|
|
|
if (!rrInfo) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-04-22 19:03:15 +03:00
|
|
|
// checking the DOM properties can force a re-layout, which can be
|
|
|
|
// quite expensive; so if the parent messagepanel is being unmounted,
|
|
|
|
// then don't bother with this.
|
|
|
|
if (this.props.checkUnmounting && this.props.checkUnmounting()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-12-23 15:24:49 +03:00
|
|
|
const avatarNode = this._avatar.current;
|
2016-04-21 01:03:05 +03:00
|
|
|
rrInfo.top = avatarNode.offsetTop;
|
|
|
|
rrInfo.left = avatarNode.offsetLeft;
|
|
|
|
rrInfo.parent = avatarNode.offsetParent;
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
2016-04-21 01:03:05 +03:00
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
componentDidMount() {
|
2016-04-21 01:03:05 +03:00
|
|
|
if (!this.state.suppressDisplay) {
|
|
|
|
// we've already done our display - nothing more to do.
|
|
|
|
return;
|
|
|
|
}
|
2021-04-08 12:36:38 +03:00
|
|
|
this._animateMarker();
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidUpdate(prevProps) {
|
|
|
|
const differentLeftOffset = prevProps.leftOffset !== this.props.leftOffset;
|
|
|
|
const visibilityChanged = prevProps.hidden !== this.props.hidden;
|
|
|
|
if (differentLeftOffset || visibilityChanged) {
|
|
|
|
this._animateMarker();
|
|
|
|
}
|
|
|
|
}
|
2016-04-21 01:03:05 +03:00
|
|
|
|
2021-04-08 12:36:38 +03:00
|
|
|
_animateMarker() {
|
2016-04-21 01:03:05 +03:00
|
|
|
// treat new RRs as though they were off the top of the screen
|
2017-10-11 19:56:17 +03:00
|
|
|
let oldTop = -15;
|
2016-04-21 01:03:05 +03:00
|
|
|
|
2017-10-11 19:56:17 +03:00
|
|
|
const oldInfo = this.props.readReceiptInfo;
|
2016-04-21 01:03:05 +03:00
|
|
|
if (oldInfo && oldInfo.parent) {
|
|
|
|
oldTop = oldInfo.top + oldInfo.parent.getBoundingClientRect().top;
|
|
|
|
}
|
|
|
|
|
2019-12-23 15:24:49 +03:00
|
|
|
const newElement = this._avatar.current;
|
2017-08-30 11:59:02 +03:00
|
|
|
let startTopOffset;
|
|
|
|
if (!newElement.offsetParent) {
|
|
|
|
// this seems to happen sometimes for reasons I don't understand
|
|
|
|
// the docs for `offsetParent` say it may be null if `display` is
|
|
|
|
// `none`, but I can't see why that would happen.
|
|
|
|
console.warn(
|
2018-10-10 17:14:09 +03:00
|
|
|
`ReadReceiptMarker for ${this.props.fallbackUserId} in has no offsetParent`,
|
2017-08-30 11:59:02 +03:00
|
|
|
);
|
|
|
|
startTopOffset = 0;
|
|
|
|
} else {
|
|
|
|
startTopOffset = oldTop - newElement.offsetParent.getBoundingClientRect().top;
|
|
|
|
}
|
2016-04-21 01:03:05 +03:00
|
|
|
|
2017-10-11 19:56:17 +03:00
|
|
|
const startStyles = [];
|
2016-04-21 01:03:05 +03:00
|
|
|
|
|
|
|
if (oldInfo && oldInfo.left) {
|
|
|
|
// start at the old height and in the old h pos
|
|
|
|
startStyles.push({ top: startTopOffset+"px",
|
2020-06-08 16:45:12 +03:00
|
|
|
left: toPx(oldInfo.left) });
|
2016-04-21 01:03:05 +03:00
|
|
|
}
|
|
|
|
|
2021-04-08 11:27:41 +03:00
|
|
|
startStyles.push({ top: startTopOffset+'px', left: '0' });
|
2016-04-21 01:03:05 +03:00
|
|
|
|
|
|
|
this.setState({
|
|
|
|
suppressDisplay: false,
|
|
|
|
startStyles: startStyles,
|
|
|
|
});
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
2016-04-21 01:03:05 +03:00
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
render() {
|
2017-10-11 19:56:17 +03:00
|
|
|
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
2016-04-21 01:03:05 +03:00
|
|
|
if (this.state.suppressDisplay) {
|
2019-12-23 15:54:31 +03:00
|
|
|
return <div ref={this._avatar} />;
|
2016-04-21 01:03:05 +03:00
|
|
|
}
|
|
|
|
|
2017-10-11 19:56:17 +03:00
|
|
|
const style = {
|
2020-06-08 16:45:12 +03:00
|
|
|
left: toPx(this.props.leftOffset),
|
2016-04-21 01:03:05 +03:00
|
|
|
top: '0px',
|
|
|
|
};
|
|
|
|
|
2016-12-09 14:43:23 +03:00
|
|
|
let title;
|
2016-12-08 19:23:20 +03:00
|
|
|
if (this.props.timestamp) {
|
2018-03-23 07:14:29 +03:00
|
|
|
const dateString = formatDate(new Date(this.props.timestamp), this.props.showTwelveHour);
|
2018-10-10 17:14:09 +03:00
|
|
|
if (!this.props.member || this.props.fallbackUserId === this.props.member.rawDisplayName) {
|
2018-03-23 07:14:29 +03:00
|
|
|
title = _t(
|
|
|
|
"Seen by %(userName)s at %(dateTime)s",
|
2021-06-29 15:11:58 +03:00
|
|
|
{ userName: this.props.fallbackUserId,
|
|
|
|
dateTime: dateString },
|
2018-03-23 07:14:29 +03:00
|
|
|
);
|
2018-03-26 15:33:47 +03:00
|
|
|
} else {
|
2018-03-23 07:14:29 +03:00
|
|
|
title = _t(
|
|
|
|
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
|
2021-06-29 15:11:58 +03:00
|
|
|
{ displayName: this.props.member.rawDisplayName,
|
2018-10-10 17:14:09 +03:00
|
|
|
userName: this.props.fallbackUserId,
|
2021-06-29 15:11:58 +03:00
|
|
|
dateTime: dateString },
|
2018-03-23 07:14:29 +03:00
|
|
|
);
|
|
|
|
}
|
2016-12-08 19:23:20 +03:00
|
|
|
}
|
|
|
|
|
2016-04-21 01:03:05 +03:00
|
|
|
return (
|
2021-04-29 20:57:02 +03:00
|
|
|
<NodeAnimator startStyles={this.state.startStyles}>
|
2016-04-21 01:03:05 +03:00
|
|
|
<MemberAvatar
|
|
|
|
member={this.props.member}
|
2018-10-10 17:14:09 +03:00
|
|
|
fallbackUserId={this.props.fallbackUserId}
|
2019-05-28 14:19:35 +03:00
|
|
|
aria-hidden="true"
|
2016-04-21 01:03:05 +03:00
|
|
|
width={14} height={14} resizeMethod="crop"
|
|
|
|
style={style}
|
2016-12-08 19:23:20 +03:00
|
|
|
title={title}
|
2017-02-02 20:36:26 +03:00
|
|
|
onClick={this.props.onClick}
|
2019-12-23 15:24:49 +03:00
|
|
|
inputRef={this._avatar}
|
2016-04-21 01:03:05 +03:00
|
|
|
/>
|
2021-04-14 12:18:45 +03:00
|
|
|
</NodeAnimator>
|
2016-04-21 01:03:05 +03:00
|
|
|
);
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
|
|
|
}
|