Add date markers
This commit is contained in:
parent
34d89c75b2
commit
50d735aaa3
File diff suppressed because one or more lines are too long
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
@ -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`,
|
||||||
|
@ -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}
|
||||||
>
|
>
|
||||||
|
@ -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++;
|
||||||
|
@ -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');
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user