When developing a React and Firebase powered realtime chat app, a goal was to stretch the incoming messages so that each message is displayed 500ms after another exept the first message, which is displayed instantly. This ensueres readability as it flattens out peaks of incoming messages.
const SLOMO_DELAY = 500
const INITIAL_STATE = {
shouldScroll: true,
editChat: false,
showModal: false,
bufferReset: false,
messages: null,
slowMo: false,
nextTimestamp: 0,
receivedMessages: 0,
}
componentWillReceiveProps(nextProps) {
if (_.size(nextProps.messages) === 0) {
// reset state
this.setState(INITIAL_STATE)
}
const messages = _.map(nextProps.messages, (m, key) => {
m['key'] = key
return m
})
const { receivedMessages } = this.state
const diff = _.size(messages) - receivedMessages
this.setState({
receivedMessages: _.size(messages),
})
if (true) {
this.setState({
slowMo: true
})
}
// deleted messages
if (diff < 0) {
this.setState({
messages
})
return
}
// slomo
if (this.state.slowMo && diff) {
let { nextTimestamp } = this.state
const now = moment.now()
const dt = now - nextTimestamp < 0 ? nextTimestamp - now : 0
const totalDelay = diff * SLOMO_DELAY_CONST
nextTimestamp = dt === 0 ? now + totalDelay : nextTimestamp + totalDelay
_.times(diff, i => {
const next = _.take(messages, _.size(messages) - diff + i + 1)
if (dt === 0) {
this.setState({
messages: next
})
}
} else {
setTimeout(() => {
this.setState({
messages: next
})
}, dt + SLOMO_DELAY_CONST * (i + 1))
}
})
this.setState({
nextTimestamp
})
return
}
if (!this.state.slowMo) {
this.setState({
messages
})
}
}
Starting with variables:
const diff = _.size(messages) - receivedMessages // number of incoming messages
// timestamp when the last cued message will be displayed
let { nextTimestamp } = this.state
// time now
const now = moment.now()
// time difference between now and the last cued message if any, else time difference is 0
const dt = now - nextTimestamp < 0 ? nextTimestamp - now : 0
// total of this batch is the number of incoming messages times our slomo constant
const totalDelay = diff * SLOMO_DELAY_CONST
// if there are no cued messages, time difference will be 0 and we just add the total delay to now.
// else we want to add it to the current total delay:
nextTimestamp = dt === 0 ? now + totalDelay : nextTimestamp + totalDelay
Now that we got our variables straight, we can iterate over the number of incoming messages and set a timeout for each message before updating the state with the new message appended:
_.times(diff, i => { // iterating over number of incoming messages
// get the next slice of array conainting first new message
const next = _.take(messages, _.size(messages) - diff + i + 1)
// if we have no cued messages, just update the state
if (dt === 0) {
this.setState({
messages: next
})
}
}
// else, update the next state with the according timeout
else {
setTimeout(() => {
this.setState({
messages: next
})
}, dt + SLOMO_DELAY_CONST * (i + 1)) // dt = current cued duration, i = current iteration
}
})
this.setState({
nextTimestamp // update timetamp for next batch
})
This is the first working version and surely can use some optimization but that's it for now :)
I also found another way: You could append all new messages with a "hidden" state option i.e. { ..., hidden: true}
and run the delay algorithm against the hidden states and update them when timer is done. This would reduce memory as we wouldn't need to park the incoming messages in our timeout function.