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

main
Last change on this file since 7bc636b was 7bc636b, checked in by Robin <robin@…>, 4 months ago

Initial commit — PrutFolio v1 source (pulled from Hetzner /srv/prutfolio)

  • Property mode set to 100644
File size: 5.7 KB
Line 
1<div class="container prutter-conv-page" data-conversation-id="<%= conversation.id %>" data-me="<%= user.id %>">
2 <header class="prutter-conv-header">
3 <p class="prutter-back"><a href="<%= siteUrlBase %>/prutter">&larr; Inbox</a></p>
4 <div class="prutter-conv-headline">
5 <div class="prutter-conv-avatar prutter-conv-avatar--lg">
6 <% if (other && other.avatar_url) { %>
7 <img src="<%= other.avatar_url %>" alt="">
8 <% } else { %>
9 <span><%= (other && other.username || '?').charAt(0).toUpperCase() %></span>
10 <% } %>
11 </div>
12 <div>
13 <h1><%= other ? other.username : 'Unknown' %></h1>
14 <% if (other) { %>
15 <p class="prutter-conv-sub"><a href="/users/<%= encodeURIComponent(other.username) %>">View profile</a></p>
16 <% } %>
17 </div>
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="2" maxlength="2000" placeholder="Type a message…" required></textarea>
38 <button type="submit" class="btn btn-primary">Send</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.prutter-conv-page { max-width: 720px; margin: 2rem auto; padding: 0 1rem; display: flex; flex-direction: column; gap: 1rem; }
90.prutter-back { margin: 0; }
91.prutter-back a { color: var(--accent); text-decoration: none; }
92
93.prutter-conv-headline {
94 display: flex; align-items: center; gap: 0.85rem;
95 padding-bottom: 1rem; border-bottom: 1px solid var(--rule);
96}
97.prutter-conv-headline h1 {
98 font-family: var(--font-display, serif);
99 font-size: 1.5rem; margin: 0;
100}
101.prutter-conv-sub { margin: 0; font-size: 0.85rem; color: var(--ink-muted, var(--ink-soft)); }
102.prutter-conv-sub a { color: var(--accent); }
103
104.prutter-conv-avatar--lg { width: 56px; height: 56px; }
105.prutter-conv-avatar--lg span { font-size: 1.2rem; }
106
107.prutter-thread {
108 list-style: none; padding: 0; margin: 0;
109 display: flex; flex-direction: column; gap: 0.5rem;
110 background: var(--paper-2);
111 border: 1px solid var(--rule);
112 border-radius: 8px;
113 padding: 1rem;
114 height: 60vh;
115 overflow-y: auto;
116}
117
118.prutter-msg { display: flex; flex-direction: column; max-width: 80%; }
119.prutter-msg-bubble {
120 padding: 0.55rem 0.85rem;
121 border-radius: 14px;
122 white-space: pre-wrap;
123 word-wrap: break-word;
124 line-height: 1.4;
125 font-size: 0.95rem;
126}
127.prutter-msg-time {
128 font-size: 0.7rem;
129 color: var(--ink-muted, var(--ink-soft));
130 margin-top: 0.15rem;
131}
132
133.prutter-msg--theirs { align-self: flex-start; }
134.prutter-msg--theirs .prutter-msg-bubble {
135 background: var(--paper);
136 border: 1px solid var(--rule);
137 border-bottom-left-radius: 4px;
138}
139.prutter-msg--theirs .prutter-msg-time { padding-left: 0.5rem; }
140
141.prutter-msg--mine { align-self: flex-end; align-items: flex-end; }
142.prutter-msg--mine .prutter-msg-bubble {
143 background: var(--accent);
144 color: white;
145 border-bottom-right-radius: 4px;
146}
147.prutter-msg--mine .prutter-msg-time { padding-right: 0.5rem; }
148
149.prutter-composer {
150 display: flex; gap: 0.5rem; align-items: flex-end;
151 padding-bottom: env(safe-area-inset-bottom, 0);
152}
153.prutter-composer textarea {
154 flex: 1;
155 padding: 0.6rem 0.85rem;
156 border: 1px solid var(--rule);
157 border-radius: 8px;
158 background: var(--paper);
159 color: var(--ink);
160 font-family: inherit;
161 font-size: 0.95rem;
162 resize: vertical;
163 min-height: 44px;
164}
165</style>
Note: See TracBrowser for help on using the repository browser.