Site icon R-bloggers

Shiny chat in few lines of code

[This article was first published on Appsilon Data Science Blog, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Intro

We live in a society heavily reliant upon communication, networking and information exchange. Think about all the chat apps that have recently popped out: WhatsApp, Messenger, Skype, Viber, Slack (not to mention Snapchat or Telegram). They have engrained themselves into our daily lives. You’d be hard pressed to find someone who doesn’t use at least one of these apps on a regular basis; with some using all of them! This leads to the obvious conclusion that written communication has become an integral part of our lives.

You may be thinking that, from a software developers perspective, creating communication apps is difficult, tedious and time consuming. I’m going to convince you otherwise.

We’re going to build a shiny chat app in no more than 15 minutes and less than 100 lines of R code!

App’s architecture

Let’s start with our chat app design. The focus is going to be basic functionality. All we need for a functioning chat app is a message window (1) to store previous comments, a text input (2) and a send button (3). Let’s also add another text input (4) to identify our users with some nicknames.

Having laid out out the desired functionality, the question is where to store our data? A text file could be fine, but periodical reading and writing would become unbearable for larger apps. Note that our problem is asynchronous – we want to send a text message only when the user presses a button. The message should then immediately appear on the other user’s screen. You may have heard of the RethinkDB open-source database, which is based on JSONs. Its asynchronous nature makes it perfect for realtime apps. Feel free to visit their documentation website if you want to learn more about RethinkDB.

For the purpose of this development let’s assume that our database will include records containing: user, text and time fields.

Another more important problem is which framework should we choose to build the app? Although there are several obvious solutions, we are going to take a closer look at R and Shiny. With shiny.collections package it will be as easy as making pie chart in ggplot.

Can’t wait to see how that’s possible? Let’s move on to the code!

Gimme some code

Chat UI

At the very beginning, let’s create an R script chat.R, which will contain our chat code. As I mentioned above, we will use shiny and shiny.collections packages. They should be attached first.

library(shiny)
library(shiny.collections)

You can install the latest version of shiny.collections via devtools.

devtools::install_github("Appsilon/shiny.collections")

We might need some other libraries but can add them later.

As you probably know, each Shiny App consists of ui, which contains the app’s layout and server for the app’s logic.

First, we implement the ui part of the app, following the mock-up above.

ui <- shinyUI(fluidPage(
  titlePanel("Chat app (shiny.collections demo)"),
  div(textInput("username_field", "Username", width = "200px")),
  uiOutput("chatbox"),
  div(style = "display:inline-block",
  textInput("message_field", "Your message", width = "500px")),
  div(style = "display:inline-block",
  actionButton("send", "Send"))
))

Create an empty server if you want to see the current state of your app:

server <- shinyServer( function(input, output, session) {})

and add shinyApp creator at the end of the file.

shinyApp(ui = ui, server = server)

After running it you should see something like this:

App’s logic

Great! Now that we have the chat design, we can start thinking of adding some functionality. We should ensure that our database is up and running before engaging the items we’ve created so far. If you have RethinkDB properly installed, using it is a piece of cake. Just type in your shell:

$ rethinkdb

If the final line of your shell’s output looks similar to the one below, you are ready to use shiny.collections.

Server ready, "user_QTG8WL_n0v" 67f1e11d-0acb-4fe9-ac1c-811b9bbecb66

The first step is to connect a database. Add the following line just before the server definition.

connection <- shiny.collections::connect()

(I am using double colon notation here to avoid ambiguity about which functionality comes from standard shiny and which from shiny.collections package).

By default shiny.collections connects to a database named “shiny”, or creates it if it’s not present.

The initial content in our server will be:

chat <- shiny.collections::collection("chat", connection)

It makes a collection from a table named chat. It will be empty until we fill it in.

It’s high time to focus on UI elements. Let’s work from top to bottom. The first component is the username field (4). It should definitely not be empty because we need to identify our users somehow. Let’s fill it with some random values from function get_random_username to start.

get_random_username <- function() {
  paste0("User", round(runif(1, 10000, 99999)))
}

(You can add all function definitons before ui and server in your script).

The following command added to server will update username_field with the output of the function while we initialize the chat.

updateTextInput(session, "username_field",
  value = get_random_username()
)

Now, we can add some action to the send button:

observeEvent(input$send, {
  # ...
})

First thing to do is to form a data structure to send to our database an R list new_message:

new_message <- list(user = input$username_field,
                    text = input$message_field,
                    time = Sys.time())

Then we use insert function from shiny.collections:

shiny.collections::insert(chat, new_message)

and clear text input.

updateTextInput(session, "message_field", value = "")

Once everything is put together, it should look like this:

observeEvent(input$send, {
  new_message <- list(user = input$username_field,
                      text = input$message_field,
                      time = Sys.time())
  shiny.collections::insert(chat, new_message)
  updateTextInput(session, "message_field", value = "")
})

The data from database will be displayed in chatbox (UI item (1)).

output$chatbox <- renderUI({
  if (!is_empty(chat$collection)) {
    render_msg_divs(chat$collection)
  } else {
    tags$span("Empty chat")
  }
})

The observeEvent function allows us to observe chat$collection data and render div with messages only when it changes. There are two conditions here: empty collection and collection with content. Test it with the following logical statement !is_empty(chat$collection). It employs is_empty function from the purrr package (so don’t forget to add library(purrr) at the beginning of your R script). When it’s empty, your app should simply return a span with an “Empty chat” notification. If not, the chat should display messages with neat formatting. The render_msg_divs function is responsible for that:

render_msg_divs <- function(collection) {
  div(class = "ui very relaxed list",
  collection %>%
    arrange(time) %>%
    by_row(~ div(class = "item",
      a(class = "header", .$user),
      div(class = "description", .$text)
    )) %>% {.$.out}
)
}

I must admit that there is some magic going on there. We use dplyr and purrrlyr to make the code tidy. Our goal is to create a div with messages listed one after another. Using div(class = "ui very relaxed list", ...) is an easy way to achieve this.

As our collection comes from unstructured noSQL database we first need to sort all items by time collection %>% arrange(time). Next, for each row we create div named “item” with “header” being username (field user from our chat$collection) and “description” being content of the user’s message (field text).

In order to apply it to every row, we use by_row function from purrrlyr package. The function returns a data.frame with an additional column named .out with results of the operation applied to respective row. With syntax %>% {.$.out} we extract only results of that operation as a list.

To sum up, whole shiny server code should look like this:

server <- shinyServer(function(input, output, session) {
  chat <- shiny.collections::collection("chat", connection)

  updateTextInput(session, "username_field",
                  value = get_random_username()
                  )

  observeEvent(input$send, {
    new_message <- list(user = input$username_field,
                        text = input$message_field,
                        time = Sys.time())
    shiny.collections::insert(chat, new_message)
    updateTextInput(session, "message_field", value = "")
  })

  output$chatbox <- renderUI({
    if (!is_empty(chat$collection)) {
      render_msg_divs(chat$collection)
    } else {
      tags$span("Empty chat")
    }
  })
})

We’re pretty much ready to run our chat but should consider introducing some amendments beforehand.

Improving the view

According to our mock-up with design, the box with messages should be fixed height rectangle but it’s currently just a list. We can fix this by adding this CSS code:

#chatbox {
  padding: .5em;
  border: 1px solid #777;
  height: 300px;
  overflow-y: scroll;
}

To define our customized style in our shiny app we need to add tags$head() at the beginning of our fluidPage definition in UI. Then we insert CSS code. This can be done in two ways: by using HTML command from shiny:

tags$style(HTML("#CSS code"))

or adding a path to CSS file.

tags$link(rel = "stylesheet", type = "text/css", href = "file_with_style.css")

Commonly, for shiny apps, all external files are stored in a www/ folder.

We haven’t finished yet. Our chatbox div has the proper size now, but it is unable to automatically scroll down after receiving a new message. Another issue is that the user is forced to press the send button every time he or she finishes typing a new message. Most modern chat apps allow you to proceed after pressing ENTER. There’s no need for our app to be worse in that regard. We can easily introduce those two functionalities with the following JavaScript code (taken from ShinyChat example: look at Not less important section).

You can add that script to UI’s head with tags$script command:

tags$script(src = "script.js")

Launching the app

Awesome! You’re ready to launch the chat. If you followed the previous steps carefully you should see a view like this:

Now it’s your turn! Have fun and try to upgrade the chat by adding new functionalities or changing the style.

Not less important

You can find the entire code in chat.R script published in shiny.collections repository in examples folder.

The main inspiration for this demo came from a ShinyChat example from Shiny Gallery. I decided to follow this idea to show that with shiny.collections you can make it faster and easier (130 vs 60 of lines of pure R code).

To leave a comment for the author, please follow the link and comment on their blog: Appsilon Data Science Blog.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.