source: Klonkt/src/views/pages/prutter-conversation.ejs@ cbd1761

main
Last change on this file since cbd1761 was 79bba6e, checked in by roboburr <roboburr@…>, 3 months ago

Audio never jumps + hub topnav + Prutter chat mobile-first

  • Global link boost in shell.ejs: all internal navigation links go through htmx into #pcms-main (session/auth/downloads/assets excluded), so the audio player keeps playing everywhere — including clicks within Admin. Back/forward re-fetches (historyCacheSize=0) so the OOB header comes along.
  • Hub-home: the mini topnav bar now shows on the hub landing (no large profile header — the hero is the header); duplicate .hub-nav removed.
  • Prutter conversation: full-height mobile-first chat (bodyClass on-chat → no artist profile header). Page pinned fixed between topnav and the bottom bars; only the thread scrolls. Compact header + clean composer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@…>

  • Property mode set to 100644
File size: 8.1 KB
Line 
1<div class="prutter-conv-page" data-conversation-id="<%= conversation.id %>" data-me="<%= user.id %>">
2 <header class="prutter-conv-header">
3 <a class="prutter-back" href="<%= siteUrlBase %>/prutter" aria-label="Terug naar inbox" title="Inbox">
4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
5 </a>
6 <div class="prutter-conv-avatar">
7 <% if (other && other.avatar_url) { %>
8 <img src="<%= other.avatar_url %>" alt="">
9 <% } else { %>
10 <span><%= (other && other.username || '?').charAt(0).toUpperCase() %></span>
11 <% } %>
12 </div>
13 <div class="prutter-conv-id">
14 <span class="prutter-conv-name"><%= other ? other.username : 'Onbekend' %></span>
15 <% if (other) { %>
16 <a class="prutter-conv-sub" href="/users/<%= encodeURIComponent(other.username) %>">Bekijk profiel</a>
17 <% } %>
18 </div>
19 </header>
20
21 <ol class="prutter-thread" id="prutter-thread">
22 <% messages.forEach(function(m) { %>
23 <li class="prutter-msg <%= m.author_id === user.id ? 'prutter-msg--mine' : 'prutter-msg--theirs' %>" data-msg-id="<%= m.id %>">
24 <div class="prutter-msg-bubble"><%= m.content %></div>
25 <div class="prutter-msg-time" title="<%= m.created_at %>"><%= formatDateTime(m.created_at) %></div>
26 </li>
27 <% }); %>
28 </ol>
29
30 <form class="prutter-composer"
31 method="post"
32 action="<%= siteUrlBase %>/prutter/<%= conversation.id %>/send"
33 hx-post="<%= siteUrlBase %>/prutter/<%= conversation.id %>/send"
34 hx-target="#prutter-thread"
35 hx-swap="beforeend"
36 hx-on::after-request="this.querySelector('textarea').value=''; window.__prutterScroll && window.__prutterScroll();">
37 <textarea name="content" rows="1" maxlength="2000" placeholder="Bericht…" required></textarea>
38 <button type="submit" class="btn btn-primary">Stuur</button>
39 </form>
40</div>
41
42<script>
43(function() {
44 // Auto-scroll to bottom on load and after appends.
45 var thread = document.getElementById('prutter-thread');
46 function scrollToBottom() { thread.scrollTop = thread.scrollHeight; }
47 scrollToBottom();
48 window.__prutterScroll = scrollToBottom;
49
50 // Live updates via WebSocket.
51 // Browser auto-resolves protocol/host; same origin as the page.
52 try {
53 var page = document.querySelector('.prutter-conv-page');
54 var convId = page.dataset.conversationId;
55 var me = page.dataset.me;
56 var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
57 var ws = new WebSocket(proto + '//' + location.host + '/ws/prutter');
58
59 ws.addEventListener('message', function(ev) {
60 try {
61 var data = JSON.parse(ev.data);
62 if (data.type !== 'new_message' || data.conversationId !== convId) return;
63 var m = data.message;
64 // Don't double-render our own messages (already added via HTMX response)
65 if (m.author_id === me) return;
66
67 var li = document.createElement('li');
68 li.className = 'prutter-msg prutter-msg--theirs';
69 li.dataset.msgId = m.id;
70 var bubble = document.createElement('div');
71 bubble.className = 'prutter-msg-bubble';
72 bubble.textContent = m.content; // text content = automatic escaping
73 var time = document.createElement('div');
74 time.className = 'prutter-msg-time';
75 time.textContent = new Date(m.created_at).toLocaleString();
76 li.appendChild(bubble);
77 li.appendChild(time);
78 thread.appendChild(li);
79 scrollToBottom();
80 } catch (e) { /* ignore malformed */ }
81 });
82 } catch (e) {
83 console.warn('Prutter WS unavailable:', e);
84 }
85})();
86</script>
87
88<style>
89/* ── Full-height chat-layout (mobile-first) ──────────────────────────────
90 De pagina is FIXED gepind tussen de topnav (desktop) en de onderste balken
91 (bottom-tab + audiospeler), zodat ALLEEN de thread scrollt — niet de hele
92 pagina. De offsets volgen de body-classes has-bottom-tab/has-audio-player,
93 dus de composer valt nooit achter een balk. */
94.prutter-conv-page {
95 position: fixed;
96 left: 0; right: 0;
97 top: 0; /* mobiel: masthead is verborgen → vanaf de top */
98 bottom: 0;
99 max-width: 760px; margin: 0 auto;
100 display: flex; flex-direction: column;
101 padding: 0.5rem 0.75rem;
102 z-index: 1;
103}
104/* Desktop: de sticky masthead staat erboven → begin eronder. */
105@media (min-width: 768px) {
106 .prutter-conv-page { top: 64px; padding: 0.75rem 1rem; }
107}
108/* Onderste vrije ruimte = de balken die de viewport-bodem overlappen. */
109@media (max-width: 767px) {
110 body.has-bottom-tab .prutter-conv-page { bottom: calc(56px + env(safe-area-inset-bottom, 0)); }
111}
112body.has-audio-player .prutter-conv-page { bottom: calc(76px + env(safe-area-inset-bottom, 0)); }
113@media (max-width: 767px) {
114 body.has-bottom-tab.has-audio-player .prutter-conv-page { bottom: calc(56px + 76px + env(safe-area-inset-bottom, 0)); }
115}
116
117/* Compacte chat-kop: terug-pijl + avatar + naam op één regel. */
118.prutter-conv-header {
119 flex: 0 0 auto;
120 display: flex; align-items: center; gap: 0.65rem;
121 padding-bottom: 0.6rem; margin-bottom: 0.5rem;
122 border-bottom: 1px solid var(--rule);
123}
124.prutter-back {
125 flex: 0 0 auto;
126 display: inline-flex; align-items: center; justify-content: center;
127 width: 40px; height: 40px; border-radius: 8px;
128 color: var(--ink); text-decoration: none;
129}
130.prutter-back:hover { color: var(--accent); background: var(--paper-2); }
131.prutter-back svg { width: 20px; height: 20px; }
132.prutter-conv-avatar {
133 flex: 0 0 auto;
134 width: 40px; height: 40px; border-radius: 50%;
135 overflow: hidden; background: var(--paper-2);
136 display: inline-flex; align-items: center; justify-content: center;
137 color: var(--accent); font-weight: 700;
138}
139.prutter-conv-avatar img { width: 100%; height: 100%; object-fit: cover; }
140.prutter-conv-id { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
141.prutter-conv-name {
142 font-family: var(--font-display, serif); font-size: 1.15rem; color: var(--ink);
143 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
144}
145.prutter-conv-sub { font-size: 0.78rem; color: var(--ink-muted, var(--ink-soft)); text-decoration: none; }
146.prutter-conv-sub:hover { color: var(--accent); }
147
148/* De ENIGE scroller. min-height:0 is cruciaal in een flex-kolom, anders
149 groeit de lijst i.p.v. te scrollen. */
150.prutter-thread {
151 flex: 1 1 auto; min-height: 0;
152 list-style: none; margin: 0;
153 display: flex; flex-direction: column; gap: 0.4rem;
154 padding: 0.25rem 0.1rem 0.5rem;
155 overflow-y: auto; overscroll-behavior: contain;
156 -webkit-overflow-scrolling: touch;
157}
158
159.prutter-msg { display: flex; flex-direction: column; max-width: 82%; }
160.prutter-msg-bubble {
161 padding: 0.55rem 0.85rem;
162 border-radius: 14px;
163 white-space: pre-wrap;
164 word-wrap: break-word;
165 line-height: 1.4;
166 font-size: 0.95rem;
167}
168.prutter-msg-time {
169 font-size: 0.7rem;
170 color: var(--ink-muted, var(--ink-soft));
171 margin-top: 0.15rem;
172}
173
174.prutter-msg--theirs { align-self: flex-start; }
175.prutter-msg--theirs .prutter-msg-bubble {
176 background: var(--paper);
177 border: 1px solid var(--rule);
178 border-bottom-left-radius: 4px;
179}
180.prutter-msg--theirs .prutter-msg-time { padding-left: 0.5rem; }
181
182.prutter-msg--mine { align-self: flex-end; align-items: flex-end; }
183.prutter-msg--mine .prutter-msg-bubble {
184 background: var(--accent);
185 color: white;
186 border-bottom-right-radius: 4px;
187}
188.prutter-msg--mine .prutter-msg-time { padding-right: 0.5rem; }
189
190.prutter-composer {
191 flex: 0 0 auto;
192 display: flex; gap: 0.5rem; align-items: flex-end;
193 padding-top: 0.6rem; margin-top: 0.25rem;
194 border-top: 1px solid var(--rule);
195}
196.prutter-composer textarea {
197 flex: 1;
198 padding: 0.65rem 0.85rem;
199 border: 1px solid var(--rule);
200 border-radius: 12px;
201 background: var(--paper);
202 color: var(--ink);
203 font-family: inherit;
204 font-size: 1rem; /* ≥16px → iOS zoomt niet in bij focus */
205 line-height: 1.35;
206 resize: none;
207 min-height: 44px; max-height: 40vh;
208}
209.prutter-composer .btn { flex: 0 0 auto; min-height: 44px; }
210</style>
Note: See TracBrowser for help on using the repository browser.