chat-fe: add unread and day indicators

This commit is contained in:
Liam Fitzgerald 2020-04-19 02:23:32 +10:00
parent 18878727b0
commit f10bf3c894
4 changed files with 127 additions and 34 deletions

View File

@ -169,6 +169,10 @@ h2 {
border-radius: 100%; border-radius: 100%;
} }
.green3 {
color: #7ea899;
}
/* responsive */ /* responsive */
@media all and (max-width: 34.375em) { @media all and (max-width: 34.375em) {

View File

@ -120,7 +120,9 @@ class UrbitApi {
} }
}; };
this.action("chat-hook", "json", data); this.action("chat-hook", "json", data).then(() => {
this.chatRead(path);
})
data.message.envelope.author = data.message.envelope.author.substr(1); data.message.envelope.author = data.message.envelope.author.substr(1);
this.addPendingMessage(data.message); this.addPendingMessage(data.message);
} }

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment';
import { Route, Link } from "react-router-dom"; import { Route, Link } from "react-router-dom";
import { store } from "/store"; import { store } from "/store";
@ -10,6 +11,7 @@ import { Message } from '/components/lib/message';
import { SidebarSwitcher } from '/components/lib/icons/icon-sidebar-switch.js'; import { SidebarSwitcher } from '/components/lib/icons/icon-sidebar-switch.js';
import { ChatTabBar } from '/components/lib/chat-tabbar'; import { ChatTabBar } from '/components/lib/chat-tabbar';
import { ChatInput } from '/components/lib/chat-input'; import { ChatInput } from '/components/lib/chat-input';
import { UnreadNotice } from '/components/lib/unread-notice';
import { deSig } from '/lib/util'; import { deSig } from '/lib/util';
@ -24,25 +26,26 @@ export class ChatScreen extends Component {
this.hasAskedForMessages = false; this.hasAskedForMessages = false;
this.onScroll = this.onScroll.bind(this); this.onScroll = this.onScroll.bind(this);
this.updateReadInterval = setInterval( this.unreadMarker = null;
this.updateReadNumber.bind(this),
1000 moment.updateLocale('en', {
); calendar: {
sameDay: '[Today]',
nextDay: '[Tomorrow]',
nextWeek: 'dddd',
lastDay: '[Yesterday]',
lastWeek: '[Last] dddd',
sameElse: 'DD/MM/YYYY'
}
});
} }
componentDidMount() { componentDidMount() {
this.updateReadNumber();
this.askForMessages(); this.askForMessages();
} }
componentWillUnmount() {
if (this.updateReadInterval) {
clearInterval(this.updateReadInterval);
this.updateReadInterval = null;
}
}
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
const { props, state } = this; const { props, state } = this;
@ -55,18 +58,11 @@ export class ChatScreen extends Component {
if (props.envelopes.length < 100) { if (props.envelopes.length < 100) {
this.askForMessages(); this.askForMessages();
} }
clearInterval(this.updateReadInterval);
this.setState( this.setState(
{ scrollLocked: false }, { scrollLocked: false },
() => { () => {
this.scrollToBottom(); this.scrollToBottom();
this.updateReadInterval = setInterval(
this.updateReadNumber.bind(this),
1000
);
this.updateReadNumber();
} }
); );
} else if (props.chatInitialized && } else if (props.chatInitialized &&
@ -80,14 +76,8 @@ export class ChatScreen extends Component {
this.hasAskedForMessages = false; this.hasAskedForMessages = false;
} }
} }
updateReadNumber() {
const { props, state } = this;
if (props.read < props.length) {
props.api.chat.read(props.station);
}
}
askForMessages() { askForMessages() {
const { props, state } = this; const { props, state } = this;
@ -170,6 +160,12 @@ export class ChatScreen extends Component {
} else { } else {
console.log("Your browser is not supported."); console.log("Your browser is not supported.");
} }
if(!!this.unreadMarker &&
e.target.scrollHeight - e.target.scrollTop - (e.target.clientHeight * 1.5) + this.unreadMarker.offsetTop > 50 ) {
this.props.api.chat.read(this.props.station);
}
} }
render() { render() {
@ -189,6 +185,11 @@ export class ChatScreen extends Component {
pendingMessages.map(function(value) { pendingMessages.map(function(value) {
return (value.pending = true); return (value.pending = true);
}); });
const unread = props.length - props.read;
const unreadMsg = unread > 0 && messages[unread - 1];
let messageElements = pendingMessages.concat(messages).map((msg, i) => { let messageElements = pendingMessages.concat(messages).map((msg, i) => {
// Render sigil if previous message is not by the same sender // Render sigil if previous message is not by the same sender
@ -200,8 +201,13 @@ export class ChatScreen extends Component {
let paddingBot = let paddingBot =
_.get(messages[i - 1], aut) !== _.get(messages[i - 1], aut) !==
_.get(msg, aut, msg.author); _.get(msg, aut, msg.author);
let when = ['when'];
let dayBreak =
moment(_.get(messages[i+1], when)).format('YYYY.MM.DD') !==
moment(_.get(messages[i], when)).format('YYYY.MM.DD')
return ( const messageElem = (
<Message <Message
key={msg.uid} key={msg.uid}
msg={msg} msg={msg}
@ -212,6 +218,39 @@ export class ChatScreen extends Component {
pending={!!msg.pending} pending={!!msg.pending}
/> />
); );
if(unread > 0 && i === unread) {
return (
<>
{messageElem}
<div key={'unreads'+ msg.uid} ref={ref => (this.unreadMarker = ref)} className="mv2 green2 flex items-center f9">
<hr className="ma0 w2 b--green2 bt-0" />
<p className="mh4">
New messages below
</p>
<hr className="ma0 flex-grow-1 b--green2 bt-0" />
{ dayBreak && (
<p className="gray2 mh4">
{moment(_.get(messages[i], when)).calendar()}
</p>
)}
<hr style={{ width: 'calc(50% - 48px)' }} className="b--green2 ma0 bt-0"/>
</div>
</>
);
} else if(dayBreak) {
return (
<>
{messageElem}
<div key={'daybreak' + msg.uid} className="pv3 gray2 b--gray2 flex items-center justify-center f9 ">
<p>
{moment(_.get(messages[i], when)).calendar()}
</p>
</div>
</>
);
} else {
return messageElem;
}
}); });
let group = Array.from(props.permission.who.values()); let group = Array.from(props.permission.who.values());
@ -229,11 +268,12 @@ export class ChatScreen extends Component {
? props.association.metadata.title ? props.association.metadata.title
: props.station.substr(1); : props.station.substr(1);
} }
return ( return (
<div <div
key={props.station} key={props.station}
className="h-100 w-100 overflow-hidden flex flex-column"> className="h-100 w-100 overflow-hidden flex flex-column relative">
<div <div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8" className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: "1rem" }}> style={{ height: "1rem" }}>
@ -265,6 +305,13 @@ export class ChatScreen extends Component {
api={props.api} api={props.api}
/> />
</div> </div>
{ !!unreadMsg && (
<UnreadNotice
unread={unread}
unreadMsg={unreadMsg}
onRead={() => props.api.chat.read(props.station)}
/>
) }
<div <div
className="overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse" className="overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
style={{ height: "100%", resize: "vertical" }} style={{ height: "100%", resize: "vertical" }}

View File

@ -0,0 +1,40 @@
import React, { Component } from "react";
import classnames from "classnames";
import moment from "moment";
export class UnreadNotice extends Component {
render() {
let { unread, unreadMsg, onRead } = this.props;
let when = moment.unix(unreadMsg.when / 10000);
let datestamp = moment.unix(unreadMsg.when / 1000).format("YYYY.M.D");
let timestamp = moment.unix(unreadMsg.when / 1000).format("HH:mm");
if (datestamp === moment().format("YYYY.M.D")) {
datestamp = null;
}
return (
<div
style={{ left: "0px", top: "48px" }}
className="pa4 w-100 absolute z-1"
>
<div className="ba b--green2 green2 bg-white bg-gray0-d flex items-center pa2 f9 justify-between br1">
<p className="lh-copy db">
{unread} new messages since{" "}
{datestamp && (
<>
<span className="green3">~{datestamp}</span> at{" "}
</>
)}
<span className="green3">{timestamp}</span>
</p>
<div onClick={onRead} className="inter b--green2 pointer">
Mark as Read
</div>
</div>
</div>
);
}
}