Mercurial > libervia-desktop-kivy
diff cagou/plugins/plugin_wid_chat.py @ 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 |
line wrap: on
line diff
--- 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")