Mercurial > libervia-desktop-kivy
comparison cagou/plugins/plugin_wid_chat.py @ 426:d3a6ae859556
chat: image attachments collection, first draft:
when more than one image is attached in a message, they are collected and a dedicated
attachment item is shown. Opening this item will launch the carousel with all collected
images.
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 26 Feb 2020 22:07:15 +0100 |
parents | 13884aac1220 |
children | 36c3f1c02d33 |
comparison
equal
deleted
inserted
replaced
425:13884aac1220 | 426:d3a6ae859556 |
---|---|
21 from pathlib import Path | 21 from pathlib import Path |
22 import sys | 22 import sys |
23 import uuid | 23 import uuid |
24 from urllib.parse import urlparse | 24 from urllib.parse import urlparse |
25 from kivy.uix.boxlayout import BoxLayout | 25 from kivy.uix.boxlayout import BoxLayout |
26 from kivy.uix.gridlayout import GridLayout | |
27 from kivy.uix.screenmanager import Screen, NoTransition | |
26 from kivy.uix.textinput import TextInput | 28 from kivy.uix.textinput import TextInput |
27 from kivy.uix.screenmanager import Screen, NoTransition | 29 from kivy.uix.label import Label |
28 from kivy.uix import screenmanager | 30 from kivy.uix import screenmanager |
29 from kivy.uix.behaviors import ButtonBehavior | 31 from kivy.uix.behaviors import ButtonBehavior |
30 from kivy.metrics import sp, dp | 32 from kivy.metrics import sp, dp |
31 from kivy.clock import Clock | 33 from kivy.clock import Clock |
32 from kivy import properties | 34 from kivy import properties |
42 from sat_frontends.tools import jid | 44 from sat_frontends.tools import jid |
43 from cagou import G | 45 from cagou import G |
44 from ..core.constants import Const as C | 46 from ..core.constants import Const as C |
45 from ..core import cagou_widget | 47 from ..core import cagou_widget |
46 from ..core import xmlui | 48 from ..core import xmlui |
47 from ..core.image import Image | 49 from ..core.image import Image, AsyncImage |
48 from ..core.common import SymbolButton, JidButton, ContactButton | 50 from ..core.common import Symbol, SymbolButton, JidButton, ContactButton |
49 from ..core.behaviors import FilterBehavior | 51 from ..core.behaviors import FilterBehavior |
50 from ..core import menu | 52 from ..core import menu |
51 from ..core.common_widgets import ImagesGallery | 53 from ..core.common_widgets import ImagesGallery |
52 | 54 |
53 log = logging.getLogger(__name__) | 55 log = logging.getLogger(__name__) |
125 class AttachmentImageItem(ButtonBehavior, BaseAttachmentItem): | 127 class AttachmentImageItem(ButtonBehavior, BaseAttachmentItem): |
126 image = properties.ObjectProperty() | 128 image = properties.ObjectProperty() |
127 | 129 |
128 def on_press(self): | 130 def on_press(self): |
129 gallery = ImagesGallery(sources=[self.image.source]) | 131 gallery = ImagesGallery(sources=[self.image.source]) |
132 G.host.showExtraUI(gallery) | |
133 | |
134 | |
135 class AttachmentImagesCollectionItem(ButtonBehavior, GridLayout): | |
136 attachments = properties.ListProperty([]) | |
137 chat = properties.ObjectProperty() | |
138 mess_data = properties.ObjectProperty() | |
139 | |
140 def _setDecryptedPath(self, attachment, wid, path): | |
141 attachment['path'] = path | |
142 if wid is not None: | |
143 wid.source = path | |
144 | |
145 def on_kv_post(self, __): | |
146 attachments = self.attachments | |
147 self.clear_widgets() | |
148 for idx, attachment in enumerate(attachments): | |
149 url = attachment['url'] | |
150 to_decrypt = url.startswith("aesgcm:") | |
151 | |
152 if idx < 3 or len(attachments) <= 4: | |
153 if ((self.mess_data.own_mess | |
154 or self.chat.contact_list.isInRoster(self.mess_data.from_jid))): | |
155 wid_kwargs = { | |
156 "size_hint": (1, 1), | |
157 } | |
158 if not to_decrypt: | |
159 wid_kwargs["source"] = url | |
160 wid = AsyncImage(**wid_kwargs) | |
161 else: | |
162 # we don't download automatically the image if the contact is not | |
163 # in roster, to avoid leaking the ip | |
164 wid = Symbol(symbol="file-image") | |
165 self.add_widget(wid) | |
166 else: | |
167 wid = None | |
168 | |
169 if to_decrypt: | |
170 # the file needs to be decrypted, it will be done by downloadURL | |
171 # and the widget source and attachment path will then be completed | |
172 del attachment['url'] | |
173 G.host.downloadURL( | |
174 url, | |
175 callback=partial(self._setDecryptedPath, attachment, wid), | |
176 dest=C.FILE_DEST_CACHE, | |
177 profile=self.chat.profile, | |
178 ) | |
179 | |
180 if len(attachments) > 4: | |
181 counter = Label( | |
182 bold=True, | |
183 text=f"+{len(attachments) - 3}", | |
184 ) | |
185 self.add_widget(counter) | |
186 | |
187 def on_press(self): | |
188 sources = [] | |
189 for attachment in self.attachments: | |
190 source = attachment.get('url', attachment.get('path')) | |
191 if not source: | |
192 log.warning(f"no source for {attachment}") | |
193 else: | |
194 sources.append(source) | |
195 gallery = ImagesGallery(sources=sources) | |
130 G.host.showExtraUI(gallery) | 196 G.host.showExtraUI(gallery) |
131 | 197 |
132 | 198 |
133 class AttachmentToSendItem(AttachmentItem): | 199 class AttachmentToSendItem(AttachmentItem): |
134 # True when the item is being sent | 200 # True when the item is being sent |
207 self.avatar.source = update_dict['avatar'] | 273 self.avatar.source = update_dict['avatar'] |
208 if 'status' in update_dict: | 274 if 'status' in update_dict: |
209 status = update_dict['status'] | 275 status = update_dict['status'] |
210 self.delivery.text = '\u2714' if status == 'delivered' else '' | 276 self.delivery.text = '\u2714' if status == 'delivered' else '' |
211 | 277 |
212 def _setPath(self, item, path): | 278 def _setPath(self, data, path): |
213 """Set path of decrypted file to an item""" | 279 """Set path of decrypted file to an item""" |
214 item.data['path'] = path | 280 data['path'] = path |
215 | 281 |
216 def add_attachments(self): | 282 def add_attachments(self): |
217 """Add attachments layout + attachments item""" | 283 """Add attachments layout + attachments item""" |
218 attachments = self.mess_data.attachments | 284 attachments = self.mess_data.attachments |
219 if not attachments: | 285 if not attachments: |
220 return | 286 return |
221 root_layout = AttachmentsLayout() | 287 root_layout = AttachmentsLayout() |
222 self.right_part.add_widget(root_layout) | 288 self.right_part.add_widget(root_layout) |
223 layout = root_layout.attachments | 289 layout = root_layout.attachments |
290 | |
291 image_attachments = [] | |
292 other_attachments = [] | |
293 # we first separate images and other attachments, so we know if we need | |
294 # to use an image collection | |
224 for attachment in attachments: | 295 for attachment in attachments: |
225 media_type = attachment.get(C.MESS_KEY_MEDIA_TYPE, '') | 296 media_type = attachment.get(C.MESS_KEY_MEDIA_TYPE, '') |
226 main_type = media_type.split('/', 1)[0] | 297 main_type = media_type.split('/', 1)[0] |
227 if ((main_type == 'image' | 298 # GIF images are really badly handled by Kivy, the memory |
228 # GIF images are really badly handled by Kivy, the | 299 # consumption explode, and the images frequencies are not handled |
229 # memory consumption explode, and the images frequencies | 300 # correctly, thus we can't display them and we consider them as |
230 # are not handled correctly, thus we can't display them | 301 # other attachment, so user can open the item with appropriate |
231 # and user will have to open the item. | 302 # software. |
232 and media_type != "image/gif" | 303 if main_type == 'image' and media_type != "image/gif": |
233 and (self.mess_data.own_mess or self.chat.contact_list.isInRoster( | 304 image_attachments.append(attachment) |
234 self.mess_data.from_jid)))): | 305 else: |
306 other_attachments.append(attachment) | |
307 | |
308 if len(image_attachments) > 1: | |
309 collection = AttachmentImagesCollectionItem( | |
310 attachments=image_attachments, | |
311 chat=self.chat, | |
312 mess_data=self.mess_data, | |
313 ) | |
314 layout.add_widget(collection) | |
315 elif image_attachments: | |
316 attachment = image_attachments[0] | |
317 # to avoid leaking IP address, we only display image if the contact is in | |
318 # roster | |
319 if ((self.mess_data.own_mess | |
320 or self.chat.contact_list.isInRoster(self.mess_data.from_jid))): | |
235 url = urlparse(attachment['url']) | 321 url = urlparse(attachment['url']) |
236 if url.scheme == "aesgcm": | 322 if url.scheme == "aesgcm": |
237 # we remove the URL now, we'll replace it by | 323 # we remove the URL now, we'll replace it by |
238 # the local decrypted version | 324 # the local decrypted version |
239 del attachment['url'] | 325 del attachment['url'] |
240 item = AttachmentImageItem(data=attachment) | 326 item = AttachmentImageItem(data=attachment) |
241 G.host.downloadURL( | 327 G.host.downloadURL( |
242 url.geturl(), | 328 url.geturl(), |
243 callback=partial(self._setPath, item), | 329 callback=partial(self._setPath, item.data), |
244 dest=C.FILE_DEST_CACHE, | 330 dest=C.FILE_DEST_CACHE, |
245 profile=self.chat.profile, | 331 profile=self.chat.profile, |
246 ) | 332 ) |
247 else: | 333 else: |
248 item = AttachmentImageItem(data=attachment) | 334 item = AttachmentImageItem(data=attachment) |
249 else: | 335 else: |
250 item = AttachmentItem(data=attachment) | 336 item = AttachmentItem(data=attachment) |
337 | |
338 layout.add_widget(item) | |
339 | |
340 for attachment in other_attachments: | |
341 item = AttachmentItem(data=attachment) | |
251 layout.add_widget(item) | 342 layout.add_widget(item) |
252 | 343 |
253 | 344 |
254 class MessageInputBox(BoxLayout): | 345 class MessageInputBox(BoxLayout): |
255 message_input = properties.ObjectProperty() | 346 message_input = properties.ObjectProperty() |