Skip to content

Instantly share code, notes, and snippets.

@escherlies
Last active May 5, 2018 19:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save escherlies/f4af7d64da16004c3ec5a06e073c704b to your computer and use it in GitHub Desktop.
Save escherlies/f4af7d64da16004c3ec5a06e073c704b to your computer and use it in GitHub Desktop.
Algorithm for Showing a constant stream of incoming messages, buffered and display with a delay to optimise reading.

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.

First, the Complete Snipped:

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
      })
    }

  }

Second, Let's Take a Closer Look:

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment