chat: fixes unread marker behavior

fixes #3495
This commit is contained in:
Tyler Brown Cifu Shuster 2020-09-17 20:13:51 -07:00
parent 6f5663bcd3
commit 80815b9880
2 changed files with 61 additions and 51 deletions

View File

@ -1,6 +1,7 @@
import React, { Component, PureComponent } from "react";
import moment from "moment";
import _ from "lodash";
import { Box } from "@tlon/indigo-react";
import { OverlaySigil } from './overlay-sigil';
import { uxToHex, cite, writeText } from '~/logic/lib/util';
@ -12,14 +13,10 @@ import RemoteContent from '~/views/components/RemoteContent';
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
export const UnreadMarker = React.forwardRef(({ dayBreak, when, style }, ref) => (
<div ref={element => {
setTimeout(() => {
element.style.opacity = '1';
}, 250);
}} className="green2 flex items-center f9 absolute w-100" style={{...style, opacity: '0'}}>
export const UnreadMarker = React.forwardRef(({ dayBreak, when }, ref) => (
<div ref={ref} className="green2 flex items-center f9 absolute w-100 left-0">
<hr className="dn-s ma0 w2 b--green2 bt-0" />
<p className="mh4">New messages below</p>
<p className="mh4" style={{ whiteSpace: 'normal' }}>New messages below</p>
<hr className="ma0 flex-grow-1 b--green2 bt-0" />
{dayBreak
? <p className="gray2 mh4">{moment(when).calendar()}</p>
@ -39,18 +36,19 @@ interface ChatMessageProps {
msg: Envelope | IMessage;
previousMsg: Envelope | IMessage | undefined;
nextMsg: Envelope | IMessage | undefined;
isFirstUnread: boolean;
isLastRead: boolean;
group: Group;
association: Association;
contacts: Contacts;
unreadRef: React.RefObject<HTMLDivElement>;
hideAvatars: boolean;
hideNicknames: boolean;
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
className: string;
className?: string;
isPending: boolean;
style?: any;
scrollWindow: HTMLDivElement;
isLastMessage?: boolean;
unreadMarkerRef: React.RefObject<HTMLDivElement>;
}
export default class ChatMessage extends Component<ChatMessageProps> {
@ -72,11 +70,10 @@ export default class ChatMessage extends Component<ChatMessageProps> {
msg,
previousMsg,
nextMsg,
isFirstUnread,
isLastRead,
group,
association,
contacts,
unreadRef,
hideAvatars,
hideNicknames,
remoteContentPolicy,
@ -84,7 +81,9 @@ export default class ChatMessage extends Component<ChatMessageProps> {
isPending,
style,
measure,
scrollWindow
scrollWindow,
isLastMessage,
unreadMarkerRef
} = this.props;
const renderSigil = Boolean((nextMsg && msg.author !== nextMsg.author) || !nextMsg || msg.number === 1);
@ -92,7 +91,7 @@ export default class ChatMessage extends Component<ChatMessageProps> {
const containerClass = `${renderSigil
? `w-100 flex flex-wrap cf pr3 f7 pt4 pl3 lh-copy`
: `w-100 flex flex-wrap cf pr3 hide-child`} ${isPending ? ' o-40' : ''} ${className}`
: `w-100 flex flex-wrap cf pr3 hide-child`} ${isPending ? 'o-40' : ''} ${isLastMessage ? 'pb3' : ''} ${className}`
const timestamp = moment.unix(msg.when / 1000).format(renderSigil ? 'hh:mm a' : 'hh:mm');
@ -116,15 +115,19 @@ export default class ChatMessage extends Component<ChatMessageProps> {
scrollWindow
};
const unreadContainerStyle = {
height: isLastRead ? '1.66em' : '0',
};
return (
<div ref={this.divRef} className={containerClass} style={style} data-number={msg.number}>
{dayBreak && !isFirstUnread ? <DayBreak when={msg.when} /> : null}
{dayBreak && !isLastRead ? <DayBreak when={msg.when} /> : null}
{renderSigil
? <MessageWithSigil {...messageProps} />
: <MessageWithoutSigil {...messageProps} />}
{isFirstUnread
? <UnreadMarker ref={unreadRef} dayBreak={dayBreak} when={msg.when} style={{ marginTop: (renderSigil ? "-17px" : "-6px") }} />
: null}
<Box fontSize='0' position='relative' width='100%' overflow='hidden' style={unreadContainerStyle}>{isLastRead
? <UnreadMarker dayBreak={dayBreak} when={msg.when} ref={unreadMarkerRef} />
: null}</Box>
</div>
);
}

View File

@ -48,10 +48,12 @@ interface ChatWindowState {
fetchPending: boolean;
idle: boolean;
initialized: boolean;
lastRead: number;
}
export default class ChatWindow extends Component<ChatWindowProps, ChatWindowState> {
private virtualList: VirtualScroller | null;
private unreadMarkerRef: React.RefObject<HTMLDivElement>;
INITIALIZATION_MAX_TIME = 1500;
@ -61,18 +63,20 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
this.state = {
fetchPending: false,
idle: true,
initialized: false
initialized: false,
lastRead: props.unreadCount ? props.mailboxSize - props.unreadCount : Infinity,
};
this.dismissUnread = this.dismissUnread.bind(this);
this.initialIndex = this.initialIndex.bind(this);
this.scrollToUnread = this.scrollToUnread.bind(this);
this.handleWindowBlur = this.handleWindowBlur.bind(this);
this.handleWindowFocus = this.handleWindowFocus.bind(this);
this.stayLockedIfActive = this.stayLockedIfActive.bind(this);
this.firstUnread = this.firstUnread.bind(this);
this.dismissIfLineVisible = this.dismissIfLineVisible.bind(this);
this.lastRead = this.lastRead.bind(this);
this.virtualList = null;
this.unreadMarkerRef = React.createRef();
}
componentDidMount() {
@ -97,14 +101,6 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
this.setState({ idle: false });
}
initialIndex() {
const { mailboxSize, unreadCount } = this.props;
return Math.min(Math.max(mailboxSize - 1 < INITIAL_LOAD
? 0
: this.firstUnread(),
0), mailboxSize);
}
initialFetch() {
const { envelopes, mailboxSize, unreadCount } = this.props;
if (envelopes.length > 0) {
@ -112,17 +108,9 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
this.stayLockedIfActive();
this.fetchMessages(start, start + DEFAULT_BACKLOG_SIZE, true).then(() => {
if (!this.virtualList) return;
const initialIndex = this.initialIndex();
this.virtualList.scrollToData(initialIndex).then(() => {
if (
initialIndex === mailboxSize
|| (this.virtualList && this.virtualList.window && this.virtualList.window.scrollTop === 0)
) {
this.setState({ idle: false });
this.dismissUnread();
}
this.setState({ initialized: true });
});
this.setState({ idle: false });
this.setState({ initialized: true });
this.dismissIfLineVisible();
});
} else {
setTimeout(() => {
@ -195,9 +183,31 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
});
}
firstUnread() {
lastRead() {
const { mailboxSize, unreadCount } = this.props;
return mailboxSize - unreadCount + 1;
return mailboxSize - unreadCount;
}
onScroll({ scrollTop, scrollHeight, windowHeight }) {
if (!this.state.idle && scrollTop > IDLE_THRESHOLD) {
this.setState({ idle: true });
}
this.dismissIfLineVisible();
}
dismissIfLineVisible() {
if (this.props.unreadCount === 0) return;
if (!this.unreadMarkerRef.current || !this.virtualList?.window) return;
const parent = this.unreadMarkerRef.current.parentElement?.parentElement;
if (!parent) return;
const { scrollTop, scrollHeight, offsetHeight } = this.virtualList.window;
if (
(scrollHeight - parent.offsetTop > scrollTop)
&& (scrollHeight - parent.offsetTop < scrollTop + offsetHeight)
) {
this.dismissUnread();
}
}
render() {
@ -220,6 +230,8 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
remoteContentPolicy,
} = this.props;
const unreadMarkerRef = this.unreadMarkerRef;
const messages = new Map();
let lastMessage = 0;
@ -238,7 +250,7 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
lastMessage = envelopes.length + index;
});
const messageProps = { association, group, contacts, hideAvatars, hideNicknames, remoteContentPolicy };
const messageProps = { association, group, contacts, hideAvatars, hideNicknames, remoteContentPolicy, unreadMarkerRef };
return (
<>
@ -258,11 +270,7 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
this.setState({ idle: false });
this.dismissUnread();
}}
onScroll={({ scrollTop }) => {
if (!this.state.idle && scrollTop > IDLE_THRESHOLD) {
this.setState({ idle: true });
}
}}
onScroll={this.onScroll.bind(this)}
data={messages}
size={mailboxSize + stationPendingMessages.length}
renderer={({ index, measure, scrollWindow }) => {
@ -272,15 +280,14 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
return <MessagePlaceholder key={index} height="64px" index={index} />;
}
const isPending: boolean = 'pending' in msg && Boolean(msg.pending);
const isFirstUnread: boolean = Boolean(unreadCount && index === this.firstUnread());
const isLastMessage: boolean = Boolean(index === lastMessage)
const props = { measure, scrollWindow, isPending, isFirstUnread, msg, ...messageProps };
const isLastRead: boolean = Boolean(!isLastMessage && index === this.state.lastRead);
const props = { measure, scrollWindow, isPending, isLastRead, isLastMessage, msg, ...messageProps };
return (
<ChatMessage
key={index}
previousMsg={messages.get(index + 1)}
nextMsg={messages.get(index - 1)}
className={isLastMessage ? 'pb3' : ''}
{...props}
/>
);