library(shiny)
library(shinyMobile)
library(DBI)
library(RSQLite)
library(dplyr)
library(tibble)
library(shinyjs)
# Connexion et création table si besoin
con <- dbConnect(SQLite(), dbname = "chatcontent.db")
# Création de la table 'products'
dbExecute(con, "
CREATE TABLE IF NOT EXISTS products (
id TEXT PRIMARY KEY
);
")
# Création de la table 'messages' avec conversation_id
dbExecute(con, "
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
product_id TEXT NOT NULL,
conversation_id TEXT NOT NULL,
nickname TEXT NOT NULL,
message TEXT NOT NULL,
date TEXT DEFAULT (datetime('now')),
FOREIGN KEY(product_id) REFERENCES products(id)
);
")
dbDisconnect(con)
# Fonction pour créer l'ID de conversation
create_conversation_id <- function(product_id, person1, person2) {
persons <- sort(c(person1, person2))
paste(product_id, persons[1], persons[2], sep = "_")
}
# Fonction pour extraire les infos d'une conversation_id
parse_conversation_id <- function(conversation_id) {
parts <- strsplit(conversation_id, "_")[[1]]
if(length(parts) >= 3) {
product_id <- parts[1]
person1 <- parts[2]
person2 <- parts[3]
list(product_id = product_id, person1 = person1, person2 = person2)
} else {
NULL
}
}
defautmessages_df <- tibble::tibble(
id = 1:6,
product_id = c("prod001", "prod001", "prod002", "prod002", "prod003", "prod003"),
conversation_id = c(
"prod001_alice_bob", "prod001_alice_bob",
"prod002_charlie_dana", "prod002_charlie_dana",
"prod003_jon_doe", "prod003_jon_doe"
),
nickname = c("alice", "bob", "charlie", "dana", "jon", "doe"),
message = c(
"Salut Bob !",
"Salut Alice, comment ça va ?",
"Hello Dana, dispo pour parler ?",
"Oui, dis-moi tout.",
"Tu as vu le produit ?",
"Oui, ça m'intéresse."
),
date = as.character(Sys.time() - c(3600, 3500, 2000, 1900, 1000, 900))
)
### comment after first run
con <- DBI::dbConnect(RSQLite::SQLite(), dbname = "chatcontent.db")
DBI::dbWriteTable(con, "messages", defautmessages_df, append = TRUE, row.names = FALSE)
DBI::dbDisconnect(con)
###
ui <- f7Page(
title = "Chat Marketplace",
options = list(dark = FALSE, theme = "auto", filled = FALSE, color = "#065961"),
useShinyjs(),
f7TabLayout(
navbar = f7Navbar(),
tags$style(HTML("
.headerfullconversation {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
justify-content: space-between;
align-items: center;
padding: 1rem;
background-color: white;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
#mymessagebar {
display: none;
}
")),
f7Tabs(
id = "tabs",
f7Tab(
tabName = "Conversations",
icon = f7Icon("chat_bubble_2"),
active = TRUE,
div(id = "chatlist-content",
uiOutput("conversations_list"),
),
div(id = "detailconversation-content",
uiOutput("FullChatdetail")
)
),
f7Tab(
tabName = "Tab2",
icon = f7Icon("plus"),
tagList(
sliderInput("bins", "Nombre de classes", 5, 50, 20),
plotOutput("hist")
)
)
)
)
)
server <- function(input, output, session) {
user <- reactiveValues(
nickname = NULL,
product_id = NULL,
conversation_id = NULL,
other_person = NULL,
userconversations = NULL
)
current_conversation <- reactiveVal("")
last_message_id <- reactiveVal(0)
conversations <- reactiveVal(data.frame())
firstLoad <- reactiveVal(TRUE)
load_conversations <- function() {
con <- dbConnect(SQLite(), dbname = "chatcontent.db")
query <- "
SELECT
conversation_id,
product_id,
MAX(date) as last_message_date,
COUNT(*) as message_count,
GROUP_CONCAT(DISTINCT nickname) as participants
FROM messages
GROUP BY conversation_id
ORDER BY last_message_date DESC
"
data <- dbGetQuery(con, query)
dbDisconnect(con)
conversations(data)
}
observe({
load_conversations()
})
observe({
invalidateLater(5000, session)
load_conversations()
})
output$conversations_list <- renderUI({
conv_data <- conversations()
if (nrow(conv_data) == 0) {
return(f7Block("Aucune conversation pour le moment"))
}
conv_items <- lapply(1:nrow(conv_data), function(i) {
row <- conv_data[i, ]
conv_info <- parse_conversation_id(row$conversation_id)
if (!is.null(conv_info)) {
title <- paste("Produit:", conv_info$product_id)
subtitle <- paste("Entre:", conv_info$person1, "et", conv_info$person2)
footer <- paste(row$message_count, "messages -",
format(as.POSIXct(row$last_message_date), "%d/%m %H:%M"))
div(
class = "conversation-item",
style = "padding: 10px; border-bottom: 1px solid #ddd; cursor: pointer;",
onclick = paste0("Shiny.setInputValue('select_conversation', '",
row$conversation_id, "', {priority: 'event'});"),
div(style = "font-weight: bold; font-size: 16px;", title),
div(style = "color: #666; font-size: 14px; margin-top: 2px;", subtitle),
div(style = "color: #999; font-size: 12px; margin-top: 4px;", footer)
)
}
})
})
observeEvent(input$select_conversation, {
user$nickname <- sample(strsplit(input$select_conversation, "_")[[1]][2:3], 1)
user$product_id <- strsplit(input$select_conversation, "_")[[1]][1]
user$conversation_id <- input$select_conversation
shinyjs::runjs("document.getElementById('detailconversation-content').style.display = 'block';")
shinyjs::runjs("document.querySelector('navbar').style.zIndex = '0';")
runjs("
const style = document.createElement('style');
style.innerHTML = `
.navbar, .navbars {
z-index: 0 !important;
}
.navbar {
z-index: 0 !important;
height: 0px;
}
.toolbar {
z-index: 0 !important;
height: 0px;
}
.messagebar {
position: fixed;
bottom: 0;
left: 0;
z-index: 1000 !important;
}
#mymessagebar {
display: flex !important;
}
`;
document.head.appendChild(style);
")
shinyjs::runjs("document.getElementById('chatlist-content').style.display = 'none';")
shinyjs::runjs("document.getElementById('headerfullconversation').style.display = 'flex';")
})
observeEvent(input$back_to_list, {
updateTextInput(session, "conversation_id", value = NULL)
current_conversation("")
last_message_id(0)
firstLoad(TRUE)
user$nickname <- NULL
user$product_id <- NULL
user$conversation_id <- NULL
user$other_person <- NULL
shinyjs::runjs("document.getElementById('chatlist-content').style.display = 'flex';")
shinyjs::runjs("document.getElementById('detailconversation-content').style.display = 'none';")
runjs("
const style = document.createElement('style');
style.innerHTML = `
.recalculating{
opacity: 1;
}
.toolbar {
z-index: 600 !important;
height: 4rem;
}
`;
document.head.appendChild(style);
")
})
output$FullChatdetail <- renderUI({
div(
div( class = "headerfullconversation", id ="headerfullconversation",
f7Button("back_to_list", "← Retour aux conversations"),
),
f7Messages(id = paste0("mymessages", input$select_conversation, sep = "_"), title = ""),
f7MessageBar(inputId = "mymessagebar", placeholder = "Message")
)
})
load_all_messages <- function() {
req(user$conversation_id)
con <- dbConnect(SQLite(), dbname = "chatcontent.db")
data <- dbReadTable(con, "messages") %>%
filter(conversation_id == user$conversation_id) %>%
arrange(id)
dbDisconnect(con)
if (nrow(data) > 0) {
last_message_id(max(data$id))
msgs <- lapply(seq_len(nrow(data)), function(i) {
row <- data[i, ]
f7Message(
text = row$message,
name = row$nickname,
type = if (row$nickname == user$nickname) "sent" else "received",
header = row$date
)
})
updateF7Messages(id = paste0("mymessages", input$select_conversation, sep = "_"), msgs)
}
}
load_new_messages <- function() {
req(user$conversation_id)
con <- dbConnect(SQLite(), dbname = "chatcontent.db")
new_data <- dbReadTable(con, "messages") %>%
filter(conversation_id == user$conversation_id, id > last_message_id()) %>%
arrange(id)
dbDisconnect(con)
if (nrow(new_data) > 0) {
last_message_id(max(new_data$id))
new_msgs <- lapply(seq_len(nrow(new_data)), function(i) {
row <- new_data[i, ]
f7Message(
text = row$message,
name = row$nickname,
type = if (row$nickname == user$nickname) "sent" else "received",
header = row$date
)
})
updateF7Messages(
id = paste0("mymessages", input$select_conversation, sep = "_"),
new_msgs,
showTyping = FALSE
)
}
}
observe({
req(user$nickname, user$product_id, user$conversation_id, firstLoad())
load_all_messages()
firstLoad(FALSE)
})
observe({
req(user$nickname, user$product_id, user$conversation_id, !firstLoad())
invalidateLater(2000, session)
load_new_messages()
})
observeEvent(input[["mymessagebar-send"]], {
req(user$nickname, user$product_id, user$conversation_id)
new_msg <- tibble(
product_id = user$product_id,
conversation_id = user$conversation_id,
nickname = user$nickname,
message = input$mymessagebar,
date = as.character(Sys.time())
)
con <- dbConnect(SQLite(), dbname = "chatcontent.db")
dbAppendTable(con, "messages", new_msg)
# Récupérer l'ID du message qui vient d'être inséré
new_id <- dbGetQuery(con, "SELECT last_insert_rowid() as id")$id
dbDisconnect(con)
# Mettre à jour last_message_id avec le nouveau message
last_message_id(new_id)
f7_msg <- f7Message(
text = input$mymessagebar,
name = user$nickname,
type = "sent",
header = Sys.time()
)
updateF7Messages(
id = paste0("mymessages", input$select_conversation, sep = "_"),
list(f7_msg),
showTyping = FALSE
)
})
output$hist <- renderPlot({
hist(rnorm(100), breaks = input$bins, col = "skyblue", border = "white")
})
}
shinyApp(ui, server)
Hello,
First of all, I would like to express my sincere gratitude for this excellent package. ShinyMobile has been incredibly useful for many projects i build.
I have developed an R Shiny application with ShinyMobile, in which I integrate a chat-like messaging system. The system works perfectly when the application uses a simple layout with
f7SingleLayout().However, when I migrate to a structure with
f7TabLayout(), the behavior becomes unstable.✅ Expected behavior
❌ Observed behavior
During initial loading, everything works as expected:
After switching tabs, then returning to the Conversations tab:
Thank you in advance for your help 🙏
Reproducible example