Repository navigation
chats 0.6.0
The messaging page is two panes, LinkedIn-style: on a wide screen
/messages is the conversation list beside the open conversation, and
/messages/:id is that same page with its conversation open. Phones keep
the separate pages. No migration; config.inbox_layout = :stacked keeps
the old pages everywhere.
Added
- Two panes (
config.inbox_layout = :split, the default). From 700px
wide (the width at which the dock turns into a phone sheet) the list sits
on the left in a<nav>named "Messages",--chats-list-width(320px)
wide, and the open conversation on the right in a
turbo_frame_tag "chats_conversation"; with none open, the pane says
"Select a conversation" (chats.inbox.select_hint), and with no
conversations at all it holds the inbox's empty state and the
inbox_emptyslot (rendered once, in the pane, no longer in the list
too). Both panes are as tall as--chats-split-height(the screen,
100dvh, by default; a host with its own header above the page sets what
is left,calc(100dvh - 4rem), which a phone's list page also takes as
its least height) and scroll on their own; the page is as
wide as the list plus a thread page (--chats-max-width).
Below 700px it is one column, exactly the separate pages:/messagesis
the list,/messages/:idthe thread with its back link. One media query
decides, so the layout holds before JavaScript and without it. - A row opens its conversation in the pane. Each row is a real link to
/messages/:idaimed at the frame (data-turbo-frame); the frame is
data-turbo-action="advance", so the address bar and history follow, and
the server answers the frame with the pane alone (newpane.html.erb: the
header overchats/conversations/_thread, no list, no layout). A reload, a
deep link or back/forward renders the whole page, the row current
(.chats-row--current,aria-current="page"). The newchats--split
controller (controllers/chats/split_controller, pinned like the others)
enables the frame only on a wide screen; on a phone, before it connects
and without JavaScript, the same link opens the page. It also skips the
hover prefetch of a row while the frame is disabled (Turbo would ask for
the pane alone and then render it as the page), marks the row the pane
opened current at once, and takes the viewer into it, as LinkedIn does:
the focus goes to its composer, inside the pane a screen reader names
after the conversation; on a touch screen (pointer: coarse), and in a
locked conversation, to the pane's title (tabindex="-1"), so no
keyboard rises over a conversation not read yet. - A filtered list stays filtered. A row of a search's results or of a
stack (?with=) links to/messages/:id?q=…/?with=…: the address
bar names the list the page shows, so the list's refreshes, which
re-request it, keep it, and the pane's back link (phones) returns to it. - The open conversation survives the list's refreshes. The inbox's live
updates are Turbo 8 morphs of the whole page; the pane's thread sits in a
data-turbo-permanentelement keyed by the conversation and the viewer
(chats_conversation_pane_<id>_<viewer key>), so a refresh re-renders the
rows and leaves the draft, the scroll position, the read state and the
stream subscription alone, while another conversation (another id)
replaces it. The pane's header (avatar, name, verified badge, subject, the
⋯ menu) stays outside it, so a mute or a renamed group still shows. The
list and the pane each catch up on missed broadcasts once: a page refresh
for the list, the thread catch-up for the conversation. config.inbox_layout,:splitor:stacked, checked at boot (a
wrong value fails the initializer), in the generated initializer and the
README.
Changed
- When
/messages/:idreads. Opening a conversation still reads it: a
row clicked (the pane's frame), a link from elsewhere, a reload, a page
loaded without JavaScript. A Turbo visit of the two-pane page does not
read on the server, because the inbox's own refresh is one: it may arrive
in a background tab, and a read there said "Seen" while nobody looked and
refreshed the inbox again, in a loop. The pane rendersread-pending
instead and its thread reads once it is on screen, as a dock window does.
A prefetched thread page (:stacked), which nothing used to read when
Turbo showed it, now reads itself the same way. - The inbox's search form has no
action, so it searches whatever page
it is on (an HTML form without one submits to the page's URL): after a row
opened a conversation and moved the address bar to/messages/:id, a
search keeps that conversation open (/messages/:id?q=…) instead of
going back to/messages. - The dock drops a window whose conversation a frame of the page loads
(turbo:frame-load), as it did after a visit: one copy of a thread's DOM
ids per page, withchats_dock everywhere: truetoo. - Templates. Hosts with ejected views are unaffected: an app whose own
indexorshowcame from arails generate chats:viewsbefore 0.6
(no_conversation_paneof its own) keeps its separate pages, rendered
and read exactly as before; re-eject to get the panes. The bundled
templates that changed:chats/conversations/index.html.erb: the two-pane page (the list in
nav.chats-split__list, thechats_conversationframe), rendered for
/messages/:idtoo; the list alone under:stacked. The search form is
a bare<form>with noaction.chats/conversations/show.html.erb: the thread page, now only under
:stacked(or for views ejected before 0.6); its header moved to the new
chats/conversations/_thread_header.html.erb, shared with the pane.- new
chats/conversations/_conversation_pane.html.erb(the pane:
section.chats-split__pane, labelled by itsh2title, the header, and
the permanent.chats-split__threadaround the thread),
chats/conversations/_empty_pane.html.erb(.chats-split__empty,
--inboxfor an empty inbox) andchats/conversations/pane.html.erb
(the frame's answer). chats/conversations/_thread.html.erb:pane:(rooted at
.chats-thread--pane, inside the page's.chatsshell) and
read_pending:locals.chats/conversations/_conversation_row.html.erband_group.html.erb:
withpane: truethe link carries
data-turbo-frame="chats_conversation"(a stack only when it holds one
conversation) and the list'sfilter:(q,with) in its URL, and
withcurrent:the current row its class andaria-current="page";
the<li>'s classes come fromclass_names(no trailing space).chats/conversations/_thread_header.html.erbtakesback_path:(the
pane's keeps the list's filter) and gives a titled pane's heading
tabindex="-1".chats/dock/_dock.html.erb: one more action,
turbo:frame-load@document->chats--dock#pageFrameLoaded.chats.css: thechats-split,chats-split__list,
chats-split__conversation,chats-split__pane,chats-split__thread,
chats-split__emptyandchats-thread--panerules and
--chats-list-width;--chats-split-heightis read with a100dvh
fallback and never declared by the gem, so a host's declaration wins
wherever it is. The existing class names are unchanged, so a theme on
.chats,.chats-rowor.chats-threadkeeps applying.