Skip to content

Commit a689a11

Browse files
committed
feat: realtime user co on app
1 parent bf96f2f commit a689a11

8 files changed

Lines changed: 220 additions & 19 deletions

File tree

DESCRIPTION

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ License: MIT + file LICENSE
99
Imports:
1010
bslib,
1111
config,
12+
dplyr,
1213
golem,
13-
pkgload,
14+
httr,
15+
jsonlite,
1416
reactable,
1517
shiny,
1618
whereami

NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ export(run_app)
44
import(shiny)
55
importFrom(bslib,bs_theme)
66
importFrom(bslib,page)
7+
importFrom(dplyr,filter)
8+
importFrom(dplyr,pull)
9+
importFrom(dplyr,tibble)
710
importFrom(golem,activate_js)
811
importFrom(golem,add_resource_path)
912
importFrom(golem,bundle_resources)

R/app_server.R

Lines changed: 169 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,180 @@
33
#' @param input,output,session Internal parameters for {shiny}.
44
#' DO NOT REMOVE.
55
#' @import shiny
6+
#' @importFrom dplyr filter pull tibble
67
#' @noRd
78
app_server <- function(input, output, session) {
89

9-
rv <- reactiveValues()
10+
rv <- reactiveValues(
11+
supabase_url = Sys.getenv("shinyrealtime_supabase_url"),
12+
supabase_key = Sys.getenv("shinyrealtime_supabase_key")
13+
)
14+
15+
observeEvent(TRUE, once = TRUE, {
16+
cat_where(whereami())
1017

11-
observeEvent(TRUE, {
12-
rv$data <- data.frame(
13-
name = c("Alice", "Bob", "Charlie"),
14-
age = c(25, 30, 35)
18+
session$sendCustomMessage(
19+
type = "supabaseConfig",
20+
list(
21+
url = rv$supabase_url,
22+
key = rv$supabase_key
23+
)
1524
)
25+
26+
if (getOption("golem.app.prod")) {
27+
28+
rv$connected_user <- session$user
29+
30+
body <- jsonlite::toJSON(
31+
list(
32+
name = rv$connected_user,
33+
is_connected = TRUE,
34+
is_first_visit = FALSE
35+
),
36+
auto_unbox = TRUE
37+
)
38+
39+
response <- httr::PATCH(
40+
url = paste0(rv$supabase_url, "/rest/v1/users?", "name=eq.", rv$connected_user),
41+
httr::add_headers(
42+
"apikey" = rv$supabase_key,
43+
"Content-Type" = "application/json",
44+
"Prefer" = "return=representation"
45+
),
46+
body = body,
47+
encode = "json"
48+
)
49+
50+
} else {
51+
rv$connected_user <- Sys.getenv("USER")
52+
}
53+
54+
if (getOption("golem.app.prod")) {
55+
56+
response <- httr::GET(
57+
url = paste0(rv$supabase_url, "/rest/v1/users"),
58+
httr::add_headers(
59+
"apikey" = rv$supabase_key,
60+
"Content-Type" = "application/json",
61+
"Prefer" = "return=representation"
62+
),
63+
body = body,
64+
encode = "json"
65+
)
66+
67+
if (httr::status_code(response) == 200) {
68+
rv$users <- jsonlite::fromJSON(rawToChar(response$content))
69+
}
70+
71+
} else {
72+
rv$users <- data.frame(
73+
id = c(1, 2, 3),
74+
name = c("user1", "user2", "user3"),
75+
is_connected = c(FALSE, FALSE, FALSE),
76+
is_first_visit = c(FALSE, FALSE, FALSE)
77+
)
78+
}
79+
})
80+
81+
observeEvent(input$user_update, {
82+
cat_where(whereami())
83+
84+
if (getOption("golem.app.prod")) {
85+
86+
new <- tibble(
87+
id = input$user_update$new$id,
88+
is_connected = input$user_update$new$is_connected,
89+
)
90+
91+
rv$users <- dplyr::rows_update(
92+
rv$users,
93+
new,
94+
by = "id"
95+
)
96+
97+
} else {
98+
rv$users <- data.frame(
99+
id = c(1, 2, 3),
100+
name = c("user1", "user2", "user3"),
101+
is_connected = c(TRUE, FALSE, FALSE),
102+
is_first_visit = c(FALSE, FALSE, FALSE)
103+
)
104+
}
105+
})
106+
107+
108+
session$onSessionEnded(function() {
109+
cat_where(whereami())
110+
111+
if (getOption("golem.app.prod")) {
112+
113+
body <- jsonlite::toJSON(
114+
list(
115+
is_connected = FALSE
116+
),
117+
auto_unbox = TRUE
118+
)
119+
120+
response <- httr::PATCH(
121+
url = paste0(rv$supabase_url, "/rest/v1/users?", "name=eq.", rv$connected_user),
122+
httr::add_headers(
123+
"apikey" = rv$supabase_key,
124+
"Content-Type" = "application/json",
125+
"Prefer" = "return=representation"
126+
),
127+
body = body,
128+
encode = "json"
129+
)
130+
131+
} else {
132+
rv$users <- data.frame(
133+
id = c(1, 2, 3),
134+
name = c("user1", "user2", "user3"),
135+
is_connected = c(FALSE, FALSE, FALSE),
136+
is_first_visit = c(FALSE, FALSE, FALSE)
137+
)
138+
}
139+
})
140+
141+
observeEvent(input$user_inactive, {
142+
cat_where(whereami())
143+
144+
showModal(modalDialog(
145+
title = "You are inactive",
146+
"You've been inactive for a while. Please refresh the page to continue.",
147+
easyClose = FALSE,
148+
footer = NULL
149+
))
150+
151+
if (getOption("golem.app.prod")) {
152+
153+
body <- jsonlite::toJSON(
154+
list(
155+
is_connected = FALSE
156+
),
157+
auto_unbox = TRUE
158+
)
159+
160+
response <- httr::PATCH(
161+
url = paste0(rv$supabase_url, "/rest/v1/users?", "name=eq.", rv$connected_user),
162+
httr::add_headers(
163+
"apikey" = rv$supabase_key,
164+
"Content-Type" = "application/json",
165+
"Prefer" = "return=representation"
166+
),
167+
body = body,
168+
encode = "json"
169+
)
170+
171+
} else {
172+
rv$users <- data.frame(
173+
id = c(1, 2, 3),
174+
name = c("user1", "user2", "user3"),
175+
is_connected = c(FALSE, FALSE, FALSE),
176+
is_first_visit = c(FALSE, FALSE, FALSE)
177+
)
178+
}
179+
16180
})
17181

18182
mod_navbar_server(

R/app_ui.R

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@
88
#' @noRd
99
app_ui <- function(request) {
1010
tagList(
11-
# Leave this function for adding external resources
1211
golem_add_external_resources(),
13-
# Your application UI logic
1412
page(
1513
theme = bs_theme(
1614
primary = "#f1f4fa",
@@ -38,11 +36,12 @@ golem_add_external_resources <- function() {
3836

3937
tags$head(
4038
favicon(),
39+
tags$script(
40+
src = "https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"
41+
),
4142
bundle_resources(
4243
path = app_sys("app/www"),
4344
app_title = "shinyrealtime"
4445
)
45-
# Add here other external resources
46-
# for example, you can add shinyalert::useShinyalert()
4746
)
4847
}

R/mod_main.R

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@ mod_main_ui <- function(id) {
1515
class = "mt-5 mb-3",
1616
div(
1717
class = "container",
18-
mod_action_button_ui(
18+
div(
19+
class = "mb-3",
20+
mod_action_button_ui(
1921
id = ns("action_button")
22+
)
23+
),
24+
reactableOutput(
25+
outputId = ns("table")
2026
),
21-
reactableOutput(ns("table")),
2227
mod_offcanvas_ui(
23-
id = ns("offcanvas")
28+
id = ns("offcanvas")
2429
)
2530
)
2631
)
@@ -46,7 +51,7 @@ mod_main_server <- function(id, rv) {
4651

4752
output$table <- renderReactable({
4853
reactable::reactable(
49-
data = rv$data
54+
data = head(iris)
5055
)
5156
})
5257

R/mod_navbar.R

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,10 @@ mod_navbar_server <- function(id, rv) {
3636
moduleServer(id, function(input, output, session) {
3737
ns <- session$ns
3838

39-
observeEvent(TRUE, {
40-
message("Connected user:")
41-
message(Sys.getenv("USER"))
42-
message(session$user)
43-
rv$connected_users <- c("Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace", "Heidi", "Ivan", "Judy")
39+
observeEvent(rv$users, {
40+
rv$connected_users <- rv$users |>
41+
filter(is_connected == TRUE) |>
42+
pull(name)
4443
})
4544

4645
output$avatars <- renderUI({
@@ -53,6 +52,7 @@ mod_navbar_server <- function(id, rv) {
5352

5453
span(
5554
class = "avatar",
55+
title = user,
5656
style = paste0(
5757
"background-color: ", random_color, ";"
5858
),

inst/app/www/realtime.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Shiny.addCustomMessageHandler("supabaseConfig", function(config) {
2+
console.log("🔐 Supabase config reçue via Shiny");
3+
console.log("🔗 Connexion à Supabase WebSocket...");
4+
5+
const supabase = window.supabase.createClient(config.url, config.key);
6+
7+
supabase
8+
.channel('public:users')
9+
.on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'users' }, payload => {
10+
console.log('💬 Event realtime:', payload);
11+
Shiny.setInputValue("user_update", payload, { priority: "event" });
12+
})
13+
.subscribe();
14+
});
15+
16+
let timeout;
17+
function resetTimer() {
18+
clearTimeout(timeout);
19+
timeout = setTimeout(function() {
20+
Shiny.setInputValue("user_inactive", true);
21+
}, 6000);
22+
}
23+
24+
document.addEventListener("mousemove", resetTimer);
25+
document.addEventListener("keydown", resetTimer);
26+
document.addEventListener("click", resetTimer);
27+
resetTimer();

inst/app/www/shinyrealtime.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
border-radius: 50%;
2323
font-weight: bold;
2424
color: #fff;
25+
cursor: help;
2526
}
2627
}
2728
}

0 commit comments

Comments
 (0)