diff 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
line wrap: on
line diff
--- a/cagou/plugins/plugin_wid_chat.py	Wed Feb 26 16:47:39 2020 +0100
+++ b/cagou/plugins/plugin_wid_chat.py	Wed Feb 26 22:07:15 2020 +0100
@@ -23,8 +23,10 @@
 import uuid
 from urllib.parse import urlparse
 from kivy.uix.boxlayout import BoxLayout
+from kivy.uix.gridlayout import GridLayout
+from kivy.uix.screenmanager import Screen, NoTransition
 from kivy.uix.textinput import TextInput
-from kivy.uix.screenmanager import Screen, NoTransition
+from kivy.uix.label import Label
 from kivy.uix import screenmanager
 from kivy.uix.behaviors import ButtonBehavior
 from kivy.metrics import sp, dp
@@ -44,8 +46,8 @@
 from ..core.constants import Const as C
 from ..core import cagou_widget
 from ..core import xmlui
-from ..core.image import Image
-from ..core.common import SymbolButton, JidButton, ContactButton
+from ..core.image import Image, AsyncImage
+from ..core.common import Symbol, SymbolButton, JidButton, ContactButton
 from ..core.behaviors import FilterBehavior
 from ..core import menu
 from ..core.common_widgets import ImagesGallery
@@ -130,6 +132,70 @@
         G.host.showExtraUI(gallery)
 
 
+class AttachmentImagesCollectionItem(ButtonBehavior, GridLayout):
+    attachments = properties.ListProperty([])
+    chat = properties.ObjectProperty()
+    mess_data = properties.ObjectProperty()
+
+    def _setDecryptedPath(self, attachment, wid, path):
+        attachment['path'] = path
+        if wid is not None:
+            wid.source = path
+
+    def on_kv_post(self, __):
+        attachments = self.attachments
+        self.clear_widgets()
+        for idx, attachment in enumerate(attachments):
+            url = attachment['url']
+            to_decrypt = url.startswith("aesgcm:")
+
+            if idx < 3 or len(attachments) <= 4:
+                if ((self.mess_data.own_mess
+                     or self.chat.contact_list.isInRoster(self.mess_data.from_jid))):
+                    wid_kwargs = {
+                        "size_hint": (1, 1),
+                    }
+                    if not to_decrypt:
+                        wid_kwargs["source"] = url
+                    wid = AsyncImage(**wid_kwargs)
+                else:
+                    # we don't download automatically the image if the contact is not
+                    # in roster, to avoid leaking the ip
+                    wid = Symbol(symbol="file-image")
+                self.add_widget(wid)
+            else:
+                wid = None
+
+            if to_decrypt:
+                # the file needs to be decrypted, it will be done by downloadURL
+                # and the widget source and attachment path will then be completed
+                del attachment['url']
+                G.host.downloadURL(
+                    url,
+                    callback=partial(self._setDecryptedPath, attachment, wid),
+                    dest=C.FILE_DEST_CACHE,
+                    profile=self.chat.profile,
+                )
+
+        if len(attachments) > 4:
+            counter = Label(
+                bold=True,
+                text=f"+{len(attachments) - 3}",
+            )
+            self.add_widget(counter)
+
+    def on_press(self):
+        sources = []
+        for attachment in self.attachments:
+            source = attachment.get('url', attachment.get('path'))
+            if not source:
+                log.warning(f"no source for {attachment}")
+            else:
+                sources.append(source)
+        gallery = ImagesGallery(sources=sources)
+        G.host.showExtraUI(gallery)
+
+
 class AttachmentToSendItem(AttachmentItem):
     # True when the item is being sent
     sending = properties.BooleanProperty(False)
@@ -209,9 +275,9 @@
             status = update_dict['status']
             self.delivery.text =  '\u2714' if status == 'delivered' else ''
 
-    def _setPath(self, item, path):
+    def _setPath(self, data, path):
         """Set path of decrypted file to an item"""
-        item.data['path'] = path
+        data['path'] = path
 
     def add_attachments(self):
         """Add attachments layout + attachments item"""
@@ -221,17 +287,37 @@
         root_layout = AttachmentsLayout()
         self.right_part.add_widget(root_layout)
         layout = root_layout.attachments
+
+        image_attachments = []
+        other_attachments = []
+        # we first separate images and other attachments, so we know if we need
+        # to use an image collection
         for attachment in attachments:
             media_type = attachment.get(C.MESS_KEY_MEDIA_TYPE, '')
             main_type = media_type.split('/', 1)[0]
-            if ((main_type == 'image'
-                 # GIF images are really badly handled by Kivy, the
-                 # memory consumption explode, and the images frequencies
-                 # are not handled correctly, thus we can't display them
-                 # and user will have to open the item.
-                 and media_type != "image/gif"
-                 and (self.mess_data.own_mess or self.chat.contact_list.isInRoster(
-                    self.mess_data.from_jid)))):
+            # GIF images are really badly handled by Kivy, the memory
+            # consumption explode, and the images frequencies are not handled
+            # correctly, thus we can't display them and we consider them as
+            # other attachment, so user can open the item with appropriate
+            # software.
+            if main_type == 'image' and media_type != "image/gif":
+                image_attachments.append(attachment)
+            else:
+                other_attachments.append(attachment)
+
+        if len(image_attachments) > 1:
+            collection = AttachmentImagesCollectionItem(
+                attachments=image_attachments,
+                chat=self.chat,
+                mess_data=self.mess_data,
+            )
+            layout.add_widget(collection)
+        elif image_attachments:
+            attachment = image_attachments[0]
+            # to avoid leaking IP address, we only display image if the contact is in
+            # roster
+            if ((self.mess_data.own_mess
+                 or self.chat.contact_list.isInRoster(self.mess_data.from_jid))):
                 url = urlparse(attachment['url'])
                 if url.scheme == "aesgcm":
                     # we remove the URL now, we'll replace it by
@@ -240,7 +326,7 @@
                     item = AttachmentImageItem(data=attachment)
                     G.host.downloadURL(
                         url.geturl(),
-                        callback=partial(self._setPath, item),
+                        callback=partial(self._setPath, item.data),
                         dest=C.FILE_DEST_CACHE,
                         profile=self.chat.profile,
                     )
@@ -248,6 +334,11 @@
                     item = AttachmentImageItem(data=attachment)
             else:
                 item = AttachmentItem(data=attachment)
+
+            layout.add_widget(item)
+
+        for attachment in other_attachments:
+            item = AttachmentItem(data=attachment)
             layout.add_widget(item)