Skip to content

Commit 4635ba0

Browse files
committed
chore: shoutbox improvements
1 parent 35610f8 commit 4635ba0

File tree

4 files changed

+92
-51
lines changed

4 files changed

+92
-51
lines changed

crates/rostra-web-ui/src/layout.rs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -286,19 +286,36 @@ pub(crate) fn render_html_footer() -> Markup {
286286
const tpl = document.createElement('template');
287287
tpl.innerHTML = event.data;
288288
289-
// Morph each element in the fragment into the DOM
290-
tpl.content.querySelectorAll('[id]').forEach((element) => {
291-
const target = document.getElementById(element.id);
292-
if (target && Alpine.morph) {
293-
Alpine.morph(target, element);
289+
const focus = (el) => {
290+
const target = el?.matches?.('[x-autofocus]') ? el : el?.querySelector?.('[x-autofocus]');
291+
if (target) target.scrollIntoView({ block: 'nearest' });
292+
};
293+
294+
// Process elements with IDs - morph or merge based on x-merge attribute
295+
tpl.content.querySelectorAll('[id]').forEach((content) => {
296+
const target = document.getElementById(content.id);
297+
if (!target) return;
298+
299+
const merge = content.getAttribute('x-merge');
300+
if (merge === 'append') {
301+
target.append(...content.childNodes);
302+
Alpine.initTree(target.lastElementChild);
303+
focus(target.lastElementChild);
304+
} else if (merge === 'prepend') {
305+
target.prepend(...content.childNodes);
306+
Alpine.initTree(target.firstElementChild);
307+
focus(target.firstElementChild);
308+
} else {
309+
Alpine.morph(target, content);
310+
focus(target);
294311
}
295312
});
296313
297-
// Initialize any new elements (for x-init to fire $dispatch)
298-
tpl.content.querySelectorAll('[x-init]').forEach((element) => {
299-
document.body.appendChild(element);
300-
Alpine.initTree(element);
301-
element.remove();
314+
// Run standalone x-init elements (for $dispatch etc.)
315+
tpl.content.querySelectorAll('[x-init]').forEach((el) => {
316+
document.body.appendChild(el);
317+
Alpine.initTree(el);
318+
el.remove();
302319
});
303320
};
304321

crates/rostra-web-ui/src/routes/post.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,11 @@ pub async fn get_single_post(
110110
}
111111

112112
// Default behavior: render full timeline page
113-
let navbar = state.timeline_common_navbar(&session).await?;
113+
let navbar = state
114+
.timeline_common_navbar()
115+
.session(&session)
116+
.call()
117+
.await?;
114118
Ok(Maud(
115119
state
116120
.render_timeline_page(

crates/rostra-web-ui/src/routes/shoutbox.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ pub async fn get_shoutbox(
113113

114114
// WebSocket URL for live updates (start with 0 counts, shoutbox is current page
115115
// so 0)
116-
let ws_url = "websocket('/updates?followees=0&network=0&notifications=0&shoutbox=0')";
116+
let ws_url =
117+
"websocket('/updates?followees=0&network=0&notifications=0&shoutbox=0&on_shoutbox=true')";
117118
let badge_counts = "badgeCounts({ followees: 0, network: 0, notifications: 0, shoutbox: 0 })";
118119

119120
// Render the shoutbox content with chat-like layout
@@ -189,7 +190,7 @@ pub async fn get_shoutbox(
189190
form ."o-shoutbox__form"
190191
action="/shoutbox/post"
191192
method="post"
192-
x-target="shoutbox-posts ajax-scripts"
193+
"x-target.nofocus"="shoutbox-posts ajax-scripts"
193194
"@ajax:before"=(form_ajax.before)
194195
"@ajax:after"=(form_ajax.after)
195196
"@ajax:success"="setTimeout(() => { const el = document.getElementById('shoutbox-messages'); el.scrollTop = el.scrollHeight; }, 50)"
@@ -224,8 +225,13 @@ pub async fn get_shoutbox(
224225
}
225226
};
226227

227-
// Full page layout
228-
let navbar = state.timeline_common_navbar(&session).await?;
228+
// Full page layout (hide new post form since shoutbox has its own input)
229+
let navbar = state
230+
.timeline_common_navbar()
231+
.session(&session)
232+
.hide_new_post_form(true)
233+
.call()
234+
.await?;
229235
let page_layout = state.render_page_layout(navbar, shoutbox_content);
230236

231237
let content = html! {
@@ -286,8 +292,10 @@ pub async fn post_shoutbox(
286292
div id="ajax-scripts" {
287293
script {
288294
(PreEscaped(r#"
289-
const input = document.getElementById('shoutbox-input');
290-
if (input) input.value = '';
295+
(function() {
296+
const input = document.getElementById('shoutbox-input');
297+
if (input) input.value = '';
298+
})()
291299
"#))
292300
}
293301
}

crates/rostra-web-ui/src/routes/timeline.rs

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ pub async fn get_followees(
5454
form.event_id
5555
.map(|event_id| TimelineCursor::ByEventTime(EventPaginationCursor { ts, event_id }))
5656
});
57-
let navbar = state.timeline_common_navbar(&session).await?;
57+
let navbar = state
58+
.timeline_common_navbar()
59+
.session(&session)
60+
.call()
61+
.await?;
5862
Ok(Maud(
5963
state
6064
.render_timeline_page(
@@ -80,7 +84,11 @@ pub async fn get_network(
8084
form.event_id
8185
.map(|event_id| TimelineCursor::ByEventTime(EventPaginationCursor { ts, event_id }))
8286
});
83-
let navbar = state.timeline_common_navbar(&session).await?;
87+
let navbar = state
88+
.timeline_common_navbar()
89+
.session(&session)
90+
.call()
91+
.await?;
8492
Ok(Maud(
8593
state
8694
.render_timeline_page(
@@ -106,7 +114,11 @@ pub async fn get_notifications(
106114
form.seq
107115
.map(|seq| TimelineCursor::ByReceivedTime(ReceivedAtPaginationCursor { ts, seq }))
108116
});
109-
let navbar = state.timeline_common_navbar(&session).await?;
117+
let navbar = state
118+
.timeline_common_navbar()
119+
.session(&session)
120+
.call()
121+
.await?;
110122
Ok(Maud(
111123
state
112124
.render_timeline_page(
@@ -127,6 +139,8 @@ pub struct UpdatesQuery {
127139
pub network: Option<usize>,
128140
pub notifications: Option<usize>,
129141
pub shoutbox: Option<usize>,
142+
/// If true, we're on the shoutbox page - skip shoutbox counter updates
143+
pub on_shoutbox: Option<bool>,
130144
}
131145

132146
pub async fn get_updates(
@@ -141,9 +155,10 @@ pub async fn get_updates(
141155
notifications: query.notifications.unwrap_or(0),
142156
shoutbox: query.shoutbox.unwrap_or(0),
143157
};
158+
let on_shoutbox = query.on_shoutbox.unwrap_or(false);
144159
ws.on_upgrade(move |ws| async move {
145160
let _ = state
146-
.handle_get_updates(ws, &session, pending)
161+
.handle_get_updates(ws, &session, pending, on_shoutbox)
147162
.await
148163
.inspect_err(|err| {
149164
debug!(target: LOG_TARGET, err=%err.fmt_compact(), "WS handler failed");
@@ -165,9 +180,14 @@ pub async fn get_post_replies(
165180

166181
#[bon::bon]
167182
impl UiState {
183+
#[builder]
168184
pub(crate) async fn timeline_common_navbar(
169185
&self,
170186
session: &UserSession,
187+
/// If true, the new post form is hidden (e.g., on shoutbox page which
188+
/// has its own input)
189+
#[builder(default)]
190+
hide_new_post_form: bool,
171191
) -> RequestResult<Markup> {
172192
let client = self.client(session.id()).await?;
173193
let client_ref = client.client_ref()?;
@@ -182,7 +202,9 @@ impl UiState {
182202
(self.render_self_profile_summary(session, ro_mode).await?)
183203
}
184204

185-
(self.new_post_form(None, ro_mode, Some(user_id)))
205+
@if !hide_new_post_form {
206+
(self.new_post_form(None, ro_mode, Some(user_id)))
207+
}
186208
}
187209
})
188210
}
@@ -192,6 +214,7 @@ impl UiState {
192214
mut ws: WebSocket,
193215
user: &UserSession,
194216
initial_pending: PendingCounts,
217+
on_shoutbox: bool,
195218
) -> RequestResult<()> {
196219
let client = self.client(user.id()).await?;
197220
let client_ref = client.client_ref()?;
@@ -261,17 +284,20 @@ impl UiState {
261284
}
262285
};
263286
let author = event_content.event.event.author;
264-
// Count shoutbox posts from others
265-
if author != self_id {
287+
288+
// Only count shoutbox posts if not on shoutbox page
289+
if !on_shoutbox && author != self_id {
266290
shoutbox_count += 1;
267291
}
268292

269-
// Send the rendered shout for live updates
270-
let shout_html = self
271-
.render_shoutbox_post_live(&client_ref, self_id, author, &shoutbox_content)
272-
.await
273-
.into_string();
274-
let _ = ws.send(shout_html.into()).await;
293+
// Send the rendered shout for live updates (only if on shoutbox page)
294+
if on_shoutbox {
295+
let shout_html = self
296+
.render_shoutbox_post_live(&client_ref, self_id, author, &shoutbox_content)
297+
.await
298+
.into_string();
299+
let _ = ws.send(shout_html.into()).await;
300+
}
275301
}
276302
}
277303

@@ -305,7 +331,7 @@ impl UiState {
305331
}
306332

307333
/// Render a shoutbox post for live WebSocket updates.
308-
/// Returns HTML that will be inserted into the shoutbox via x-init.
334+
/// Returns HTML that will be appended to #shoutbox-posts via x-merge.
309335
async fn render_shoutbox_post_live(
310336
&self,
311337
client: &ClientRef<'_>,
@@ -327,27 +353,13 @@ impl UiState {
327353
.map(|c| format!(r#"{{"ts":{},"seq":{}}}"#, u64::from(c.ts), c.seq))
328354
.unwrap_or_default();
329355

330-
// The WebSocket handler appends [x-init] elements to body, calls initTree, then
331-
// removes them. We use x-data + x-init to ensure Alpine context is
332-
// available.
356+
// WebSocket handler supports x-merge="append" for appending children to target
333357
html! {
334-
div
335-
x-data
336-
x-init=(format!(r#"
337-
const container = document.getElementById('shoutbox-posts');
338-
if (container) {{
339-
const post = $el.querySelector('.o-shoutbox__post');
340-
if (post) {{
341-
container.appendChild(post);
342-
const messages = document.getElementById('shoutbox-messages');
343-
if (messages) messages.scrollTop = messages.scrollHeight;
344-
// Update last-seen cookie since user is viewing shoutbox
345-
document.cookie = '{cookie_name}=' + encodeURIComponent('{cookie_value}') + '; path=/; max-age=31536000';
346-
}}
347-
}}
348-
"#))
349-
{
350-
div ."o-shoutbox__post -new" {
358+
div id="shoutbox-posts" x-merge="append" {
359+
div ."o-shoutbox__post -new"
360+
x-autofocus
361+
x-init=(format!(r#"document.cookie = '{cookie_name}=' + encodeURIComponent('{cookie_value}') + '; path=/; max-age=31536000';"#))
362+
{
351363
img ."o-shoutbox__avatar u-userImage"
352364
src=(self.avatar_url(author))
353365
alt="Avatar"

0 commit comments

Comments
 (0)