Add date markers

This commit is contained in:
Ken-Håvard Lieng 2018-12-14 14:24:23 +01:00
parent 34d89c75b2
commit 50d735aaa3
7 changed files with 218 additions and 92 deletions

File diff suppressed because one or more lines are too long

View File

@ -595,8 +595,6 @@ input.chat-title {
top: 50px; top: 50px;
bottom: 50px; bottom: 50px;
right: 0; right: 0;
z-index: 1;
overflow: hidden;
} }
.chat-channel .messagebox { .chat-channel .messagebox {
@ -615,6 +613,24 @@ input.chat-title {
overflow-y: scroll !important; overflow-y: scroll !important;
} }
.messagebox-topdate-container {
position: absolute;
text-align: center;
left: 0;
height: 0;
}
.messagebox-topdate {
position: relative;
top: -12px;
background: #f0f0f0;
color: #999;
border-radius: 50vh;
padding: 0 5px;
font-size: 12px;
z-index: 2;
}
.message { .message {
padding: 4px 15px; padding: 4px 15px;
} }
@ -637,6 +653,18 @@ input.chat-title {
color: #ff6698; color: #ff6698;
} }
.message-date {
text-align: center;
color: #999;
font-size: 12px;
margin-top: 12px;
}
.message-date hr {
border: none;
border-bottom: 1px solid #ddd;
}
.message-time { .message-time {
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
@ -706,7 +734,7 @@ input.message-input-nick.invalid {
width: 200px; width: 200px;
border-left: 1px solid #ddd; border-left: 1px solid #ddd;
background: #f0f0f0; background: #f0f0f0;
z-index: 2; z-index: 1;
transition: transform 0.2s; transition: transform 0.2s;
} }

View File

@ -97,6 +97,7 @@ export default class Chat extends Component {
hasMoreMessages={hasMoreMessages} hasMoreMessages={hasMoreMessages}
messages={messages} messages={messages}
tab={tab} tab={tab}
hideTopDate={search.show}
onAddMore={addFetchedMessages} onAddMore={addFetchedMessages}
onFetchMore={fetchMessages} onFetchMore={fetchMessages}
onNickClick={this.handleNickClick} onNickClick={this.handleNickClick}

View File

@ -7,6 +7,15 @@ const Message = ({ message, coloredNick, style, onNickClick }) => {
[`message-${message.type}`]: message.type [`message-${message.type}`]: message.type
}); });
if (message.type === 'date') {
return (
<div className={className} style={style}>
{message.content}
<hr />
</div>
);
}
style = { style = {
...style, ...style,
paddingLeft: `${window.messageIndent + 15}px`, paddingLeft: `${window.messageIndent + 15}px`,

View File

@ -2,6 +2,7 @@ import React, { PureComponent, createRef } from 'react';
import { VariableSizeList as List } from 'react-window'; import { VariableSizeList as List } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { formatDate, measureScrollBarWidth } from 'utils';
import { getScrollPos, saveScrollPos } from 'utils/scrollPosition'; import { getScrollPos, saveScrollPos } from 'utils/scrollPosition';
import { windowHeight } from 'utils/size'; import { windowHeight } from 'utils/size';
import Message from './Message'; import Message from './Message';
@ -12,7 +13,10 @@ const fetchThreshold = 600;
// this is done to prevent the scroll from jumping all over the place // this is done to prevent the scroll from jumping all over the place
const scrollbackDebounce = 150; const scrollbackDebounce = 150;
const scrollBarWidth = measureScrollBarWidth() + 'px';
export default class MessageBox extends PureComponent { export default class MessageBox extends PureComponent {
state = { topDate: '' };
list = createRef(); list = createRef();
outer = createRef(); outer = createRef();
@ -177,6 +181,17 @@ export default class MessageBox extends PureComponent {
this.bottom = scrollOffset + clientHeight >= scrollHeight - 20; this.bottom = scrollOffset + clientHeight >= scrollHeight - 20;
}; };
handleItemsRendered = ({ visibleStartIndex }) => {
const startIndex = visibleStartIndex === 0 ? 0 : visibleStartIndex - 1;
const firstVisibleMessage = this.props.messages[startIndex];
if (firstVisibleMessage && firstVisibleMessage.date) {
this.setState({ topDate: formatDate(firstVisibleMessage.date) });
} else {
this.setState({ topDate: '' });
}
};
handleMouseDown = () => { handleMouseDown = () => {
this.mouseDown = true; this.mouseDown = true;
}; };
@ -221,12 +236,27 @@ export default class MessageBox extends PureComponent {
}; };
render() { render() {
const { messages, hideTopDate } = this.props;
const { topDate } = this.state;
const dateContainerStyle = {
right: scrollBarWidth
};
return ( return (
<div <div
className="messagebox" className="messagebox"
onMouseDown={this.handleMouseDown} onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp} onMouseUp={this.handleMouseUp}
> >
<div
className="messagebox-topdate-container"
style={dateContainerStyle}
>
{!hideTopDate && topDate && (
<span className="messagebox-topdate">{topDate}</span>
)}
</div>
<AutoSizer> <AutoSizer>
{({ width, height }) => ( {({ width, height }) => (
<List <List
@ -234,12 +264,13 @@ export default class MessageBox extends PureComponent {
outerRef={this.outer} outerRef={this.outer}
width={width} width={width}
height={height} height={height}
itemCount={this.props.messages.length + 2} itemCount={messages.length + 2}
itemKey={this.getItemKey} itemKey={this.getItemKey}
itemSize={this.getRowHeight} itemSize={this.getRowHeight}
estimatedItemSize={32} estimatedItemSize={32}
initialScrollOffset={this.initialScrollTop} initialScrollOffset={this.initialScrollTop}
onScroll={this.handleScroll} onScroll={this.handleScroll}
onItemsRendered={this.handleItemsRendered}
className="messagebox-window" className="messagebox-window"
overscanCount={5} overscanCount={5}
> >

View File

@ -5,7 +5,8 @@ import {
messageHeight, messageHeight,
linkify, linkify,
timestamp, timestamp,
isChannel isChannel,
formatDate
} from 'utils'; } from 'utils';
import createReducer from 'utils/createReducer'; import createReducer from 'utils/createReducer';
import { getApp } from './app'; import { getApp } from './app';
@ -43,22 +44,71 @@ function init(state, server, tab) {
} }
} }
let nextID = 0;
function createDateMessage(date) {
const message = {
id: nextID,
type: 'date',
content: formatDate(date),
height: 40
};
nextID++;
return message;
}
function isSameDay(d1, d2) {
return (
d1.getDate() === d2.getDate() &&
d1.getMonth() === d2.getMonth() &&
d1.getFullYear() === d2.getFullYear()
);
}
function reducerAddMessage(message, server, tab, state, prepend) {
const messages = state[server][tab];
if (messages.length > 0) {
if (prepend) {
const firstMessage = messages[0];
if (firstMessage.date && !isSameDay(firstMessage.date, message.date)) {
messages.unshift(createDateMessage(firstMessage.date));
}
} else {
const lastMessage = messages[messages.length - 1];
if (lastMessage.date && !isSameDay(lastMessage.date, message.date)) {
messages.push(createDateMessage(message.date));
}
}
}
if (prepend) {
messages.unshift(message);
} else {
messages.push(message);
}
}
export default createReducer( export default createReducer(
{}, {},
{ {
[actions.ADD_MESSAGE](state, { server, tab, message }) { [actions.ADD_MESSAGE](state, { server, tab, message }) {
init(state, server, tab); init(state, server, tab);
state[server][tab].push(message); reducerAddMessage(message, server, tab, state);
}, },
[actions.ADD_MESSAGES](state, { server, tab, messages, prepend }) { [actions.ADD_MESSAGES](state, { server, tab, messages, prepend }) {
if (prepend) { if (prepend) {
init(state, server, tab); init(state, server, tab);
state[server][tab].unshift(...messages); for (let i = messages.length - 1; i >= 0; i--) {
reducerAddMessage(messages[i], server, tab, state, true);
}
} else { } else {
messages.forEach(message => { messages.forEach(message => {
init(state, server, message.tab || tab); init(state, server, message.tab || tab);
state[server][message.tab || tab].push(message); reducerAddMessage(message, server, message.tab || tab, state);
}); });
} }
}, },
@ -78,6 +128,10 @@ export default createReducer(
Object.keys(state).forEach(server => Object.keys(state).forEach(server =>
Object.keys(state[server]).forEach(target => Object.keys(state[server]).forEach(target =>
state[server][target].forEach(message => { state[server][target].forEach(message => {
if (message.type === 'date') {
return;
}
message.height = messageHeight( message.height = messageHeight(
message, message,
wrapWidth, wrapWidth,
@ -100,15 +154,15 @@ export default createReducer(
} }
); );
let nextID = 0;
function initMessage(message, tab, state) { function initMessage(message, tab, state) {
if (message.time) { if (message.time) {
message.time = timestamp(new Date(message.time * 1000)); message.date = new Date(message.time * 1000);
} else { } else {
message.time = timestamp(); message.date = new Date();
} }
message.time = timestamp(message.date);
if (!message.id) { if (!message.id) {
message.id = nextID; message.id = nextID;
nextID++; nextID++;

View File

@ -137,6 +137,9 @@ export function timestamp(date = new Date()) {
return `${h}:${m}`; return `${h}:${m}`;
} }
const dateFmt = new Intl.DateTimeFormat(window.navigator.language);
export const formatDate = dateFmt.format;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');