Mercurial > libervia-web
annotate libervia/web/pages/chat/_browser/__init__.py @ 1540:b4342176fa0a
browser (chat): minor reformatting
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 28 Jun 2023 10:06:54 +0200 |
parents | dc81403a5b2f |
children | 3c384b51b2a1 |
rev | line source |
---|---|
1536 | 1 import json |
2 | |
3 import dialog | |
4 import jid | |
5 from bridge import AsyncBridge as Bridge | |
6 from browser import aio, console as log, document, DOMNode, window, bind | |
7 from template import Template, safe | |
8 from file_uploader import FileUploader | |
9 from cache import cache, identities | |
10 | |
11 log.warning = log.warn | |
12 profile = window.profile or "" | |
13 own_jid = jid.JID(window.own_jid) | |
14 target_jid = jid.JID(window.target_jid) | |
15 bridge = Bridge() | |
16 | |
17 # Sensible value to consider that user is at the bottom | |
18 SCROLL_SENSITIVITY = 200 | |
19 | |
20 | |
21 class LiberviaWebChat: | |
22 def __init__(self): | |
23 self.message_tpl = Template("chat/message.html") | |
24 self.messages_elt = document["messages"] | |
25 | |
26 # attachments | |
27 self.file_uploader = FileUploader( | |
28 "", "chat/attachment_preview.html", on_delete_cb=self.on_attachment_delete | |
29 ) | |
30 self.attachments_elt = document["attachments"] | |
31 self.message_input = document["message_input"] | |
32 | |
33 # hide/show attachments | |
34 MutationObserver = window.MutationObserver | |
35 observer = MutationObserver.new(lambda *__: self.update_attachments_visibility()) | |
36 observer.observe(self.attachments_elt, {"childList": True}) | |
37 | |
38 # we want the message scroll to be initially at the bottom | |
39 self.messages_elt.scrollTop = self.messages_elt.scrollHeight | |
40 | |
41 @property | |
42 def is_at_bottom(self): | |
43 return ( | |
44 self.messages_elt.scrollHeight | |
45 - self.messages_elt.scrollTop | |
46 - self.messages_elt.clientHeight | |
47 <= SCROLL_SENSITIVITY | |
48 ) | |
49 | |
50 def send_message(self): | |
51 """Send message currently in input area | |
52 | |
53 The message and corresponding attachment will be sent | |
54 """ | |
55 message = self.message_input.value.rstrip() | |
56 log.info(f"{message=}") | |
57 | |
58 # attachments | |
59 attachments = [] | |
60 for attachment_elt in self.attachments_elt.children: | |
61 file_data = json.loads(attachment_elt.getAttribute("data-file")) | |
62 attachments.append(file_data) | |
63 | |
64 if message or attachments: | |
65 extra = {} | |
66 | |
67 if attachments: | |
68 extra["attachments"] = attachments | |
69 | |
70 # now we send the message | |
71 try: | |
72 aio.run( | |
73 bridge.message_send( | |
74 str(target_jid), {"": message}, {}, "auto", json.dumps(extra) | |
75 ) | |
76 ) | |
77 except Exception as e: | |
78 dialog.notification.show(f"Can't send message: {e}", "error") | |
79 else: | |
80 self.message_input.value = "" | |
81 self.attachments_elt.clear() | |
82 | |
83 def _on_message_new( | |
84 self, | |
85 uid: str, | |
86 timestamp: float, | |
87 from_jid: str, | |
88 to_jid: str, | |
89 message: dict, | |
90 subject: dict, | |
91 mess_type: str, | |
92 extra_s: str, | |
93 profile: str, | |
94 ) -> None: | |
95 if ( | |
96 jid.JID(from_jid).bare == window.target_jid | |
97 or jid.JID(to_jid).bare == window.target_jid | |
98 ): | |
99 aio.run( | |
100 self.on_message_new( | |
101 uid, | |
102 timestamp, | |
103 from_jid, | |
104 to_jid, | |
105 message, | |
106 subject, | |
107 mess_type, | |
108 json.loads(extra_s), | |
109 profile, | |
110 ) | |
111 ) | |
112 | |
113 async def on_message_new( | |
114 self, | |
115 uid: str, | |
116 timestamp: float, | |
117 from_jid: str, | |
118 to_jid: str, | |
119 message_data: dict, | |
120 subject_data: dict, | |
121 mess_type: str, | |
122 extra: dict, | |
123 profile: str, | |
124 ) -> None: | |
125 log.info(f"on_message_new: [{from_jid} -> {to_jid}] {message_data}, {extra=}") | |
126 xhtml_data = extra.get("xhtml") | |
127 if not xhtml_data: | |
128 xhtml = None | |
129 else: | |
130 try: | |
131 xhtml = xhtml_data[""] | |
132 except KeyError: | |
133 xhtml = next(iter(xhtml_data.values())) | |
134 | |
135 await cache.fill_identities([from_jid]) | |
136 | |
137 msg_data = { | |
138 "id": uid, | |
139 "timestamp": timestamp, | |
140 "type": mess_type, | |
141 "from_": from_jid, | |
142 "text": message_data.get("") or next(iter(message_data.values()), ""), | |
143 "subject": subject_data.get("") or next(iter(subject_data.values()), ""), | |
144 "type": mess_type, | |
145 "thread": extra.get("thread"), | |
146 "thread_parent": extra.get("thread_parent"), | |
147 "reeceived": extra.get("received_timestamp") or timestamp, | |
148 "delay_sender": extra.get("delay_sender"), | |
149 "info_type": extra.get("info_type"), | |
150 "html": safe(xhtml) if xhtml else None, | |
151 "encrypted": extra.get("encrypted", False), | |
152 "received": extra.get("received", False), | |
153 "edited": extra.get("edited", False), | |
154 "attachments": extra.get("attachments", []), | |
155 } | |
156 message_elt = self.message_tpl.get_elt( | |
157 { | |
158 "own_jid": own_jid, | |
159 "msg": msg_data, | |
160 "identities": identities, | |
161 } | |
162 ) | |
163 | |
164 # Check if user is viewing older messages or is at the bottom | |
165 is_at_bottom = self.is_at_bottom | |
166 | |
167 self.messages_elt <= message_elt | |
168 self.make_attachments_dynamic(message_elt) | |
169 | |
170 # If user was at the bottom, keep the scroll at the bottom | |
171 if is_at_bottom: | |
172 self.messages_elt.scrollTop = self.messages_elt.scrollHeight | |
173 | |
174 def auto_resize_message_input(self): | |
175 """Resize the message input field according to content.""" | |
176 | |
177 is_at_bottom = self.is_at_bottom | |
178 | |
179 # The textarea's height is first reset to 'auto' to ensure it's not influenced by | |
180 # the previous content. | |
181 self.message_input.style.height = "auto" | |
182 | |
183 # Then the height is set to the scrollHeight of the textarea (which is the height | |
184 # of the content), plus the vertical border, resulting in a textarea that grows as | |
185 # more lines of text are added. | |
186 self.message_input.style.height = f"{self.message_input.scrollHeight + 2}px" | |
187 | |
188 if is_at_bottom: | |
189 # we want the message are to still display the last message | |
190 self.messages_elt.scrollTop = self.messages_elt.scrollHeight | |
191 | |
192 def on_message_keydown(self, evt): | |
193 """Handle the 'keydown' event of the message input field | |
194 | |
195 @param evt: The event object. 'target' refers to the textarea element. | |
196 """ | |
197 if evt.keyCode == 13: # <Enter> key | |
198 if not window.navigator.maxTouchPoints: | |
199 # we have a non touch device, we send message on <Enter> | |
200 if not evt.shiftKey: | |
201 evt.preventDefault() # Prevents line break | |
202 self.send_message() | |
203 | |
204 def update_attachments_visibility(self): | |
205 if len(self.attachments_elt.children): | |
206 self.attachments_elt.classList.remove("is-contracted") | |
207 else: | |
208 self.attachments_elt.classList.add("is-contracted") | |
209 | |
210 def on_file_selected(self, evt): | |
211 """Handle file selection""" | |
212 log.info("file selected") | |
213 files = evt.currentTarget.files | |
214 self.file_uploader.upload_files(files, self.attachments_elt) | |
215 self.message_input.focus() | |
216 | |
217 def on_attachment_delete(self, evt): | |
218 evt.stopPropagation() | |
219 target = evt.currentTarget | |
1540
b4342176fa0a
browser (chat): minor reformatting
Goffi <goffi@goffi.org>
parents:
1536
diff
changeset
|
220 item_elt = DOMNode(target.closest(".attachment-preview")) |
1536 | 221 item_elt.remove() |
222 | |
223 def on_attach_button_click(self, evt): | |
224 document["file_input"].click() | |
225 | |
1540
b4342176fa0a
browser (chat): minor reformatting
Goffi <goffi@goffi.org>
parents:
1536
diff
changeset
|
226 @bind(document["attachments"], "wheel") |
1536 | 227 def wheel_event(evt): |
228 """Make the mouse wheel to act on horizontal scrolling for attachments | |
229 | |
230 Attachments don't have vertical scrolling, thus is makes sense to use the wheel | |
231 for horizontal scrolling | |
232 """ | |
233 if evt.deltaY != 0: | |
1540
b4342176fa0a
browser (chat): minor reformatting
Goffi <goffi@goffi.org>
parents:
1536
diff
changeset
|
234 document["attachments"].scrollLeft += evt.deltaY * 0.8 |
1536 | 235 evt.preventDefault() |
236 | |
1540
b4342176fa0a
browser (chat): minor reformatting
Goffi <goffi@goffi.org>
parents:
1536
diff
changeset
|
237 def make_attachments_dynamic(self, parent_elt=None): |
1536 | 238 """Make attachments dynamically clickable""" |
239 # FIXME: only handle images for now, and display them in a modal | |
240 if parent_elt is None: | |
241 parent_elt = document | |
1540
b4342176fa0a
browser (chat): minor reformatting
Goffi <goffi@goffi.org>
parents:
1536
diff
changeset
|
242 img_elts = parent_elt.select(".message-attachment img") |
1536 | 243 for img_elt in img_elts: |
1540
b4342176fa0a
browser (chat): minor reformatting
Goffi <goffi@goffi.org>
parents:
1536
diff
changeset
|
244 img_elt.bind("click", self.open_modal) |
b4342176fa0a
browser (chat): minor reformatting
Goffi <goffi@goffi.org>
parents:
1536
diff
changeset
|
245 img_elt.style.cursor = "pointer" |
1536 | 246 |
1540
b4342176fa0a
browser (chat): minor reformatting
Goffi <goffi@goffi.org>
parents:
1536
diff
changeset
|
247 close_button = document.select_one(".modal-close") |
b4342176fa0a
browser (chat): minor reformatting
Goffi <goffi@goffi.org>
parents:
1536
diff
changeset
|
248 close_button.bind("click", self.close_modal) |
1536 | 249 |
250 def open_modal(self, evt): | |
1540
b4342176fa0a
browser (chat): minor reformatting
Goffi <goffi@goffi.org>
parents:
1536
diff
changeset
|
251 modal_image = document.select_one("#modal-image") |
1536 | 252 modal_image.src = evt.target.src |
253 modal_image.alt = evt.target.alt | |
1540
b4342176fa0a
browser (chat): minor reformatting
Goffi <goffi@goffi.org>
parents:
1536
diff
changeset
|
254 modal = document.select_one("#modal") |
b4342176fa0a
browser (chat): minor reformatting
Goffi <goffi@goffi.org>
parents:
1536
diff
changeset
|
255 modal.classList.add("is-active") |
1536 | 256 |
257 def close_modal(self, evt): | |
1540
b4342176fa0a
browser (chat): minor reformatting
Goffi <goffi@goffi.org>
parents:
1536
diff
changeset
|
258 modal = document.select_one("#modal") |
b4342176fa0a
browser (chat): minor reformatting
Goffi <goffi@goffi.org>
parents:
1536
diff
changeset
|
259 modal.classList.remove("is-active") |
1536 | 260 |
261 | |
262 libervia_web_chat = LiberviaWebChat() | |
1540
b4342176fa0a
browser (chat): minor reformatting
Goffi <goffi@goffi.org>
parents:
1536
diff
changeset
|
263 |
1536 | 264 document["message_input"].bind( |
265 "input", lambda __: libervia_web_chat.auto_resize_message_input() | |
266 ) | |
267 document["message_input"].bind("keydown", libervia_web_chat.on_message_keydown) | |
268 document["send_button"].bind("click", lambda __: libervia_web_chat.send_message()) | |
1540
b4342176fa0a
browser (chat): minor reformatting
Goffi <goffi@goffi.org>
parents:
1536
diff
changeset
|
269 document["attach_button"].bind("click", libervia_web_chat.on_attach_button_click) |
1536 | 270 document["file_input"].bind("change", libervia_web_chat.on_file_selected) |
271 bridge.register_signal("message_new", libervia_web_chat._on_message_new) | |
1540
b4342176fa0a
browser (chat): minor reformatting
Goffi <goffi@goffi.org>
parents:
1536
diff
changeset
|
272 |
1536 | 273 libervia_web_chat.make_attachments_dynamic() |