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

main
Last change on this file since 5a394e5 was 4885eab, checked in by roboburr <roboburr@…>, 3 months ago

feat(i18n phase 6): remaining public + other pages translated (NL/EN/DE)

post-edit, account, newsletter, download, press kit /pers, fan-gate, link-in-bio,
my-site, password-reset (2), artists directory, auth-login Google error map,
user/hub-home/circle-feed+post/viewer-blocked/home/favourites/changelog/404/
type/tag/archive/prutter (inbox+conversation). embed-player skipped (no t
in that standalone route). 1026 view-keys cross-checked → 0 missing.

Co-Authored-By: Claude <noreply@…>

  • Property mode set to 100644
File size: 9.9 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="<%= t('prcv.back_aria') %>" title="<%= t('prcv.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 : t('prcv.unknown') %></span>
15 <% if (other) { %>
16 <a class="prutter-conv-sub" href="/users/<%= encodeURIComponent(other.username) %>"><%= t('prcv.view_profile') %></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" id="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 <textarea name="content" rows="1" maxlength="2000" placeholder="<%= t('prcv.placeholder') %>" required></textarea>
37 <button type="submit" class="btn btn-primary"><%= t('prcv.send') %></button>
38 </form>
39</div>
40
41<script>
42(function() {
43 // Auto-scroll to bottom on load and after appends.
44 var thread = document.getElementById('prutter-thread');
45 function scrollToBottom() { thread.scrollTop = thread.scrollHeight; }
46 scrollToBottom();
47 window.__prutterScroll = scrollToBottom;
48
49 // Live updates via WebSocket.
50 // Browser auto-resolves protocol/host; same origin as the page.
51 try {
52 var page = document.querySelector('.prutter-conv-page');
53 var convId = page.dataset.conversationId;
54 var me = page.dataset.me;
55 var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
56 var ws = new WebSocket(proto + '//' + location.host + '/ws/prutter');
57
58 ws.addEventListener('message', function(ev) {
59 try {
60 var data = JSON.parse(ev.data);
61 if (data.type !== 'new_message' || data.conversationId !== convId) return;
62 var m = data.message;
63 // Don't double-render our own messages (already added via HTMX response)
64 if (m.author_id === me) return;
65
66 var li = document.createElement('li');
67 li.className = 'prutter-msg prutter-msg--theirs';
68 li.dataset.msgId = m.id;
69 var bubble = document.createElement('div');
70 bubble.className = 'prutter-msg-bubble';
71 bubble.textContent = m.content; // text content = automatic escaping
72 var time = document.createElement('div');
73 time.className = 'prutter-msg-time';
74 time.textContent = new Date(m.created_at).toLocaleString();
75 li.appendChild(bubble);
76 li.appendChild(time);
77 thread.appendChild(li);
78 scrollToBottom();
79 } catch (e) { /* ignore malformed */ }
80 });
81 } catch (e) {
82 console.warn('Prutter WS unavailable:', e);
83 }
84
85 // ── Composer: invoer-gedrag + auto-groei + wissen na versturen ──────────
86 var form = document.getElementById('prutter-composer');
87 var ta = form ? form.querySelector('textarea') : null;
88 if (form && ta) {
89 var MAXH = function () { return Math.round(window.innerHeight * 0.4); };
90 function autoGrow() {
91 ta.style.height = 'auto';
92 ta.style.height = Math.min(ta.scrollHeight, MAXH()) + 'px';
93 }
94 function isTouch() {
95 return window.matchMedia && window.matchMedia('(pointer: coarse)').matches;
96 }
97 function submit() {
98 if (!ta.value.trim()) return;
99 if (form.requestSubmit) form.requestSubmit();
100 else form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
101 }
102
103 ta.addEventListener('input', autoGrow);
104 ta.addEventListener('keydown', function (e) {
105 if (e.key !== 'Enter' || e.isComposing) return; // IME-invoer met rust laten
106 // Touch (mobiel/tablet): Enter = nieuwe regel, versturen via de Stuur-knop.
107 if (isTouch()) return;
108 // Desktop: Shift+Enter = nieuwe regel, gewone Enter = versturen.
109 if (e.shiftKey) return;
110 e.preventDefault();
111 submit();
112 });
113
114 // Wissen + terug naar één regel ná een geslaagde verzending.
115 form.addEventListener('htmx:afterRequest', function (e) {
116 if (e.detail && e.detail.successful) {
117 ta.value = '';
118 autoGrow();
119 scrollToBottom();
120 ta.focus();
121 }
122 });
123
124 autoGrow();
125 }
126})();
127</script>
128
129<style>
130/* De chat is position:fixed (los uit de flow) → de site-footer (met copyright,
131 versie én de "Installeer app"-knop) schemerde erdoorheen in het lege
132 berichtgebied. In de chat hoort die footer niet thuis: verbergen. */
133body:has(.prutter-conv-page) .site-footer { display: none; }
134
135/* ── Full-height chat-layout (mobile-first) ──────────────────────────────
136 De pagina is FIXED gepind tussen de topnav (desktop) en de onderste balken
137 (bottom-tab + audiospeler), zodat ALLEEN de thread scrollt — niet de hele
138 pagina. De offsets volgen de body-classes has-bottom-tab/has-audio-player,
139 dus de composer valt nooit achter een balk. */
140.prutter-conv-page {
141 position: fixed;
142 left: 0; right: 0;
143 top: 0; /* mobiel: masthead is verborgen → vanaf de top */
144 bottom: 0;
145 max-width: 760px; margin: 0 auto;
146 display: flex; flex-direction: column;
147 padding: 0.5rem 0.75rem;
148 z-index: 1;
149}
150/* Desktop: de sticky masthead staat erboven → begin eronder. */
151@media (min-width: 768px) {
152 .prutter-conv-page { top: 64px; padding: 0.75rem 1rem; }
153}
154/* Onderste vrije ruimte = de balken die de viewport-bodem overlappen. */
155@media (max-width: 767px) {
156 body.has-bottom-tab .prutter-conv-page { bottom: calc(56px + env(safe-area-inset-bottom, 0)); }
157}
158body.has-audio-player .prutter-conv-page { bottom: calc(76px + env(safe-area-inset-bottom, 0)); }
159@media (max-width: 767px) {
160 body.has-bottom-tab.has-audio-player .prutter-conv-page { bottom: calc(56px + 76px + env(safe-area-inset-bottom, 0)); }
161}
162
163/* Compacte chat-kop: terug-pijl + avatar + naam op één regel. */
164.prutter-conv-header {
165 flex: 0 0 auto;
166 display: flex; align-items: center; gap: 0.65rem;
167 padding-bottom: 0.6rem; margin-bottom: 0.5rem;
168 border-bottom: 1px solid var(--rule);
169}
170.prutter-back {
171 flex: 0 0 auto;
172 display: inline-flex; align-items: center; justify-content: center;
173 width: 40px; height: 40px; border-radius: 8px;
174 color: var(--ink); text-decoration: none;
175}
176.prutter-back:hover { color: var(--accent); background: var(--paper-2); }
177.prutter-back svg { width: 20px; height: 20px; }
178.prutter-conv-avatar {
179 flex: 0 0 auto;
180 width: 40px; height: 40px; border-radius: 50%;
181 overflow: hidden; background: var(--paper-2);
182 display: inline-flex; align-items: center; justify-content: center;
183 color: var(--accent); font-weight: 700;
184}
185.prutter-conv-avatar img { width: 100%; height: 100%; object-fit: cover; }
186.prutter-conv-id { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
187.prutter-conv-name {
188 font-family: var(--font-display, serif); font-size: 1.15rem; color: var(--ink);
189 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
190}
191.prutter-conv-sub { font-size: 0.78rem; color: var(--ink-muted, var(--ink-soft)); text-decoration: none; }
192.prutter-conv-sub:hover { color: var(--accent); }
193
194/* De ENIGE scroller. min-height:0 is cruciaal in een flex-kolom, anders
195 groeit de lijst i.p.v. te scrollen. */
196.prutter-thread {
197 flex: 1 1 auto; min-height: 0;
198 list-style: none; margin: 0;
199 display: flex; flex-direction: column; gap: 0.4rem;
200 padding: 0.25rem 0.1rem 0.5rem;
201 overflow-y: auto; overscroll-behavior: contain;
202 -webkit-overflow-scrolling: touch;
203}
204
205.prutter-msg { display: flex; flex-direction: column; max-width: 82%; }
206.prutter-msg-bubble {
207 padding: 0.55rem 0.85rem;
208 border-radius: 14px;
209 white-space: pre-wrap;
210 word-wrap: break-word;
211 line-height: 1.4;
212 font-size: 0.95rem;
213}
214.prutter-msg-time {
215 font-size: 0.7rem;
216 color: var(--ink-muted, var(--ink-soft));
217 margin-top: 0.15rem;
218}
219
220.prutter-msg--theirs { align-self: flex-start; }
221.prutter-msg--theirs .prutter-msg-bubble {
222 background: var(--paper);
223 border: 1px solid var(--rule);
224 border-bottom-left-radius: 4px;
225}
226.prutter-msg--theirs .prutter-msg-time { padding-left: 0.5rem; }
227
228.prutter-msg--mine { align-self: flex-end; align-items: flex-end; }
229.prutter-msg--mine .prutter-msg-bubble {
230 background: var(--accent);
231 color: white;
232 border-bottom-right-radius: 4px;
233}
234.prutter-msg--mine .prutter-msg-time { padding-right: 0.5rem; }
235
236.prutter-composer {
237 flex: 0 0 auto;
238 display: flex; gap: 0.5rem; align-items: flex-end;
239 padding-top: 0.6rem; margin-top: 0.25rem;
240 border-top: 1px solid var(--rule);
241}
242.prutter-composer textarea {
243 flex: 1;
244 padding: 0.65rem 0.85rem;
245 border: 1px solid var(--rule);
246 border-radius: 12px;
247 background: var(--paper);
248 color: var(--ink);
249 font-family: inherit;
250 font-size: 1rem; /* ≥16px → iOS zoomt niet in bij focus */
251 line-height: 1.35;
252 resize: none;
253 min-height: 44px; max-height: 40vh;
254 overflow-y: auto; /* scrollt intern zodra de auto-groei de max raakt */
255}
256.prutter-composer .btn { flex: 0 0 auto; min-height: 44px; }
257</style>
Note: See TracBrowser for help on using the repository browser.