changeset 412:7c6149c249c1

chat: attachment sending: - files to send are not sent directly anymore, but added to attachment, and linked to the message when it is sent, this is more user friendly and avoid the accidental sending of wrong file - user can remove the attachment before sending the message, using the "close" symbol - new "Chat.addAtachment" method - upload progress is shown on the AttachmentItem thanks to the "progress" property - AttachmentItem stays in the attachments layout until uploaded or an error happens. Messages can still be sent while the item is being uploaded.
author Goffi <goffi@goffi.org>
date Sun, 23 Feb 2020 15:39:03 +0100
parents b018386653c2
children c466678c57b2
files cagou/plugins/plugin_wid_chat.kv cagou/plugins/plugin_wid_chat.py
diffstat 2 files changed, 156 insertions(+), 58 deletions(-) [+]
line wrap: on
line diff
--- a/cagou/plugins/plugin_wid_chat.kv	Sat Feb 22 18:34:09 2020 +0100
+++ b/cagou/plugins/plugin_wid_chat.kv	Sun Feb 23 15:39:03 2020 +0100
@@ -16,6 +16,7 @@
 
 #:import _ sat.core.i18n._
 #:import C cagou.core.constants.Const
+#:import G cagou.G
 #:import escape kivy.utils.escape_markup
 #:import SimpleXHTMLWidget cagou.core.simple_xhtml.SimpleXHTMLWidget
 #:import DelayedBoxLayout cagou.core.common_widgets.DelayedBoxLayout
@@ -29,9 +30,26 @@
 <AttachmentItem>:
     size_hint: None, None
     size: self.minimum_width, dp(50)
+    canvas.before:
+        Color:
+            rgb: app.c_prim_dark
+        RoundedRectangle:
+            pos: self.pos
+            size: self.size
+        Color:
+            rgb: 1, 1, 1, 1
+        RoundedRectangle:
+            pos: self.x + dp(1), self.y + dp(1)
+            size: self.width - dp(2), self.height - dp(2)
+        Color:
+            rgb: app.c_sec_light
+        RoundedRectangle:
+            pos: self.x + dp(1), self.y + dp(1)
+            size: (self.width - dp(2)) * root.progress / 100, self.height - dp(2)
     SymbolLabel:
         symbol: root.get_symbol(root.data)
-        text: root.data.get('name', '')
+        color: 0, 0, 0, 1
+        text: root.data.get('name', _('unnamed'))
         bold: False
         on_press: root.on_press()
 
@@ -40,6 +58,7 @@
     attachments: self
     size_hint: 1, None
     height: self.minimum_height
+    spacing: dp(5)
 
 
 <MessAvatar>:
@@ -117,7 +136,38 @@
             bold: True if root.mess_type == "info" else False
 
 
+<AttachmentToSendItem>:
+    SymbolButton:
+        opacity: 0 if root.sending else 1
+        size_hint: None, 1
+        symbol: "cancel-circled"
+        on_press: root.parent.remove_widget(root)
+
+
+<AttachmentsToSend>:
+    attachments: attachments_layout.attachments
+    orientation: "vertical"
+    size_hint: 1, None
+    height: self.minimum_height if self.attachments.children else 0
+    opacity: 1 if self.attachments.children else 0
+    padding: [app.MARGIN_LEFT, dp(5), app.MARGIN_RIGHT, dp(5)]
+    Label:
+        size_hint: 1, None
+        size: self.texture_size
+        text: _("attachments:")
+        bold: True
+    AttachmentsLayout:
+        id: attachments_layout
+        canvas.before:
+            Color:
+                rgba: app.c_prim
+            Rectangle:
+                pos: self.pos
+                size: self.size
+
+
 <Chat>:
+    attachments_to_send: attachments_to_send
     message_input: message_input
     messages_widget: messages_widget
     history_scroll: history_scroll
@@ -136,6 +186,8 @@
             spacing: dp(10)
             height: self.minimum_height
             orientation: 'vertical'
+    AttachmentsToSend:
+        id: attachments_to_send
     MessageInputBox:
         size_hint: 1, None
         height: self.minimum_height
--- a/cagou/plugins/plugin_wid_chat.py	Sat Feb 22 18:34:09 2020 +0100
+++ b/cagou/plugins/plugin_wid_chat.py	Sun Feb 23 15:39:03 2020 +0100
@@ -18,8 +18,9 @@
 
 
 from functools import partial
-import mimetypes
+from pathlib import Path
 import sys
+import uuid
 from kivy.uix.boxlayout import BoxLayout
 from kivy.uix.textinput import TextInput
 from kivy.uix.screenmanager import Screen, NoTransition
@@ -76,15 +77,26 @@
 # below this limit, new messages will be prepended
 INFINITE_SCROLL_LIMIT = dp(600)
 
+# File sending progress
+PROGRESS_UPDATE = 0.2 # number of seconds before next progress update
+
 
 # FIXME: a ScrollLayout was supposed to be used here, but due
 #   to https://github.com/kivy/kivy/issues/6745, a StackLayout is used for now
 class AttachmentsLayout(StackLayout):
+    """Layout for attachments in a received message"""
+    padding = properties.VariableListProperty([dp(5), dp(5), 0, dp(5)])
+    attachments = properties.ObjectProperty()
+
+
+class AttachmentsToSend(BoxLayout):
+    """Layout for attachments to be sent with current message"""
     attachments = properties.ObjectProperty()
 
 
 class AttachmentItem(BoxLayout):
     data = properties.DictProperty()
+    progress = properties.NumericProperty(0)
 
     def get_symbol(self, data):
         media_type = data.get('media_type', '')
@@ -103,7 +115,12 @@
         if url:
             G.local_platform.open_url(url, self)
         else:
-            log.warning("can't find URL in {self.data}")
+            log.warning(f"can't find URL in {self.data}")
+
+
+class AttachmentToSendItem(AttachmentItem):
+    # True when the item is being sent
+    sending = properties.BooleanProperty(False)
 
 
 class MessAvatar(ButtonBehavior, Image):
@@ -441,6 +458,7 @@
     message_input = properties.ObjectProperty()
     messages_widget = properties.ObjectProperty()
     history_scroll = properties.ObjectProperty()
+    attachments_to_send = properties.ObjectProperty()
     use_header_input = True
     global_screen_manager = True
     collection_carousel = True
@@ -722,76 +740,104 @@
 
     # message input
 
+    def _attachmentProgressCb(self, item, metadata, profile):
+        item.parent.remove_widget(item)
+        log.info(f"item {item.data.get('path')} uploaded successfully")
+
+    def _attachmentProgressEb(self, item, err_msg, profile):
+        item.parent.remove_widget(item)
+        path = item.data.get('path')
+        msg = _("item {path} could not be uploaded: {err_msg}").format(
+            path=path, err_msg=err_msg)
+        G.host.addNote(_("can't upload file"), msg, C.XMLUI_DATA_LVL_WARNING)
+        log.warning(msg)
+
+    def _progressGetCb(self, item, metadata):
+        try:
+            position = int(metadata["position"])
+            size = int(metadata["size"])
+        except KeyError:
+            # we got empty metadata, the progression is either not yet started or
+            # finished
+            if item.progress:
+                # if progress is already started, receiving empty metadata means
+                # that progression is finished
+                item.progress = 100
+                return
+        else:
+            item.progress = position/size*100
+
+        if item.parent is not None:
+            # the item is not yet fully received, we reschedule an update
+            Clock.schedule_once(
+                partial(self._attachmentProgressUpdate, item),
+                PROGRESS_UPDATE)
+
+    def _attachmentProgressUpdate(self, item, __):
+        G.host.bridge.progressGet(
+            item.data["progress_id"],
+            self.profile,
+            callback=partial(self._progressGetCb, item),
+            errback=G.host.errback,
+        )
+
     def addNick(self, nick):
         """Add a nickname to message_input if suitable"""
         if (self.type == C.CHAT_GROUP and not self.message_input.text.startswith(nick)):
             self.message_input.text = f'{nick}: {self.message_input.text}'
 
     def onSend(self, input_widget):
+        extra = {}
+        for item in self.attachments_to_send.attachments.children:
+            if item.sending:
+                # the item is already being sent
+                continue
+            item.sending = True
+            progress_id = item.data["progress_id"] = str(uuid.uuid4())
+            attachments = extra.setdefault(C.MESS_KEY_ATTACHMENTS, [])
+            attachment = {
+                "path": str(item.data["path"]),
+                "progress_id": progress_id,
+            }
+            attachments.append(attachment)
+
+            Clock.schedule_once(
+                partial(self._attachmentProgressUpdate, item),
+                PROGRESS_UPDATE)
+
+            G.host.registerProgressCbs(
+                progress_id,
+                callback=partial(self._attachmentProgressCb, item),
+                errback=partial(self._attachmentProgressEb, item)
+            )
+
+
         G.host.messageSend(
             self.target,
-            {'': input_widget.text}, # TODO: handle language
-            mess_type = (C.MESS_TYPE_GROUPCHAT
-                if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT), # TODO: put this in QuickChat
+            # TODO: handle language
+            {'': input_widget.text},
+            # TODO: put this in QuickChat
+            mess_type=
+                C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT,
+            extra=extra,
             profile_key=self.profile
             )
         input_widget.text = ''
 
-    def fileTransferEb(self, err_msg, cleaning_cb, profile):
-        if cleaning_cb is not None:
-            cleaning_cb()
-        msg = _("can't transfer file: {reason}").format(reason=err_msg)
-        log.warning(msg)
-        G.host.addNote(_("File transfer error"),
-                       msg,
-                       level=C.XMLUI_DATA_LVL_WARNING)
-
-    def fileTransferCb(self, metadata, cleaning_cb, profile):
-        log.debug("file transfered: {}".format(metadata))
-        extra = {}
-
-        # FIXME: Q&D way of getting file type, upload plugins shouls give it
-        mime_type = mimetypes.guess_type(metadata['url'])[0]
-        if mime_type is not None:
-            if mime_type.split('/')[0] == 'image':
-                # we generate url ourselves, so this formatting is safe
-                extra['xhtml'] = "<img src='{url}' />".format(**metadata)
-
-        G.host.messageSend(
-            self.target,
-            {'': metadata['url']},
-            mess_type = (C.MESS_TYPE_GROUPCHAT
-                if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT),
-            extra = extra,
-            profile_key=profile
-            )
-
-        if cleaning_cb is not None:
-            cleaning_cb()
-
+    def addAttachment(self, file_path):
+        file_path = Path(file_path)
+        data = {
+            "path": file_path,
+            "name": file_path.name,
+        }
+        self.attachments_to_send.attachments.add_widget(
+            AttachmentToSendItem(data=data)
+        )
 
     def transferFile(self, file_path, transfer_type=C.TRANSFER_UPLOAD, cleaning_cb=None):
+        # FIXME: cleaning_cb is not managed
         if transfer_type == C.TRANSFER_UPLOAD:
-            options = {
-                "ignore_tls_errors": not G.host.tls_validation,
-            }
-            if self.encrypted:
-                options['encryption'] = C.ENC_AES_GCM
-            G.host.bridge.fileUpload(
-                str(file_path),
-                "",
-                "",
-                data_format.serialise(options),
-                self.profile,
-                callback = partial(
-                    G.host.actionManager,
-                    progress_cb = partial(self.fileTransferCb, cleaning_cb=cleaning_cb),
-                    progress_eb = partial(self.fileTransferEb, cleaning_cb=cleaning_cb),
-                    profile = self.profile,
-                    ),
-                errback = partial(G.host.errback,
-                                  message=_("can't upload file: {msg}"))
-            )
+            self.addAttachment(file_path)
         elif transfer_type == C.TRANSFER_SEND:
             if self.type == C.CHAT_GROUP:
                 log.warning("P2P transfer is not possible for group chat")