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()