# HG changeset patch # User Goffi # Date 1582468743 -3600 # Node ID 7c6149c249c18d8a3a9b532edb9948d9d679beef # Parent b018386653c27839193eb0940ca0e1498c39ad6b 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. diff -r b018386653c2 -r 7c6149c249c1 cagou/plugins/plugin_wid_chat.kv --- 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 @@ : 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) : @@ -117,7 +136,38 @@ bold: True if root.mess_type == "info" else False +: + SymbolButton: + opacity: 0 if root.sending else 1 + size_hint: None, 1 + symbol: "cancel-circled" + on_press: root.parent.remove_widget(root) + + +: + 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 + + : + 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 diff -r b018386653c2 -r 7c6149c249c1 cagou/plugins/plugin_wid_chat.py --- 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'] = "".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")