Mercurial > libervia-desktop-kivy
comparison 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 |
comparison
equal
deleted
inserted
replaced
411:b018386653c2 | 412:7c6149c249c1 |
---|---|
16 # You should have received a copy of the GNU Affero General Public License | 16 # You should have received a copy of the GNU Affero General Public License |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 17 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
18 | 18 |
19 | 19 |
20 from functools import partial | 20 from functools import partial |
21 import mimetypes | 21 from pathlib import Path |
22 import sys | 22 import sys |
23 import uuid | |
23 from kivy.uix.boxlayout import BoxLayout | 24 from kivy.uix.boxlayout import BoxLayout |
24 from kivy.uix.textinput import TextInput | 25 from kivy.uix.textinput import TextInput |
25 from kivy.uix.screenmanager import Screen, NoTransition | 26 from kivy.uix.screenmanager import Screen, NoTransition |
26 from kivy.uix import screenmanager | 27 from kivy.uix import screenmanager |
27 from kivy.uix.behaviors import ButtonBehavior | 28 from kivy.uix.behaviors import ButtonBehavior |
74 COLOR_ENCRYPTED_TRUSTED = (0.29,0.87,0.0,1) | 75 COLOR_ENCRYPTED_TRUSTED = (0.29,0.87,0.0,1) |
75 | 76 |
76 # below this limit, new messages will be prepended | 77 # below this limit, new messages will be prepended |
77 INFINITE_SCROLL_LIMIT = dp(600) | 78 INFINITE_SCROLL_LIMIT = dp(600) |
78 | 79 |
80 # File sending progress | |
81 PROGRESS_UPDATE = 0.2 # number of seconds before next progress update | |
82 | |
79 | 83 |
80 # FIXME: a ScrollLayout was supposed to be used here, but due | 84 # FIXME: a ScrollLayout was supposed to be used here, but due |
81 # to https://github.com/kivy/kivy/issues/6745, a StackLayout is used for now | 85 # to https://github.com/kivy/kivy/issues/6745, a StackLayout is used for now |
82 class AttachmentsLayout(StackLayout): | 86 class AttachmentsLayout(StackLayout): |
87 """Layout for attachments in a received message""" | |
88 padding = properties.VariableListProperty([dp(5), dp(5), 0, dp(5)]) | |
89 attachments = properties.ObjectProperty() | |
90 | |
91 | |
92 class AttachmentsToSend(BoxLayout): | |
93 """Layout for attachments to be sent with current message""" | |
83 attachments = properties.ObjectProperty() | 94 attachments = properties.ObjectProperty() |
84 | 95 |
85 | 96 |
86 class AttachmentItem(BoxLayout): | 97 class AttachmentItem(BoxLayout): |
87 data = properties.DictProperty() | 98 data = properties.DictProperty() |
99 progress = properties.NumericProperty(0) | |
88 | 100 |
89 def get_symbol(self, data): | 101 def get_symbol(self, data): |
90 media_type = data.get('media_type', '') | 102 media_type = data.get('media_type', '') |
91 main_type = media_type.split('/', 1)[0] | 103 main_type = media_type.split('/', 1)[0] |
92 if main_type == 'image': | 104 if main_type == 'image': |
101 def on_press(self): | 113 def on_press(self): |
102 url = self.data.get('url') | 114 url = self.data.get('url') |
103 if url: | 115 if url: |
104 G.local_platform.open_url(url, self) | 116 G.local_platform.open_url(url, self) |
105 else: | 117 else: |
106 log.warning("can't find URL in {self.data}") | 118 log.warning(f"can't find URL in {self.data}") |
119 | |
120 | |
121 class AttachmentToSendItem(AttachmentItem): | |
122 # True when the item is being sent | |
123 sending = properties.BooleanProperty(False) | |
107 | 124 |
108 | 125 |
109 class MessAvatar(ButtonBehavior, Image): | 126 class MessAvatar(ButtonBehavior, Image): |
110 pass | 127 pass |
111 | 128 |
439 | 456 |
440 class Chat(quick_chat.QuickChat, cagou_widget.CagouWidget): | 457 class Chat(quick_chat.QuickChat, cagou_widget.CagouWidget): |
441 message_input = properties.ObjectProperty() | 458 message_input = properties.ObjectProperty() |
442 messages_widget = properties.ObjectProperty() | 459 messages_widget = properties.ObjectProperty() |
443 history_scroll = properties.ObjectProperty() | 460 history_scroll = properties.ObjectProperty() |
461 attachments_to_send = properties.ObjectProperty() | |
444 use_header_input = True | 462 use_header_input = True |
445 global_screen_manager = True | 463 global_screen_manager = True |
446 collection_carousel = True | 464 collection_carousel = True |
447 | 465 |
448 def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None, | 466 def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None, |
720 profile=self.profile | 738 profile=self.profile |
721 ) | 739 ) |
722 | 740 |
723 # message input | 741 # message input |
724 | 742 |
743 def _attachmentProgressCb(self, item, metadata, profile): | |
744 item.parent.remove_widget(item) | |
745 log.info(f"item {item.data.get('path')} uploaded successfully") | |
746 | |
747 def _attachmentProgressEb(self, item, err_msg, profile): | |
748 item.parent.remove_widget(item) | |
749 path = item.data.get('path') | |
750 msg = _("item {path} could not be uploaded: {err_msg}").format( | |
751 path=path, err_msg=err_msg) | |
752 G.host.addNote(_("can't upload file"), msg, C.XMLUI_DATA_LVL_WARNING) | |
753 log.warning(msg) | |
754 | |
755 def _progressGetCb(self, item, metadata): | |
756 try: | |
757 position = int(metadata["position"]) | |
758 size = int(metadata["size"]) | |
759 except KeyError: | |
760 # we got empty metadata, the progression is either not yet started or | |
761 # finished | |
762 if item.progress: | |
763 # if progress is already started, receiving empty metadata means | |
764 # that progression is finished | |
765 item.progress = 100 | |
766 return | |
767 else: | |
768 item.progress = position/size*100 | |
769 | |
770 if item.parent is not None: | |
771 # the item is not yet fully received, we reschedule an update | |
772 Clock.schedule_once( | |
773 partial(self._attachmentProgressUpdate, item), | |
774 PROGRESS_UPDATE) | |
775 | |
776 def _attachmentProgressUpdate(self, item, __): | |
777 G.host.bridge.progressGet( | |
778 item.data["progress_id"], | |
779 self.profile, | |
780 callback=partial(self._progressGetCb, item), | |
781 errback=G.host.errback, | |
782 ) | |
783 | |
725 def addNick(self, nick): | 784 def addNick(self, nick): |
726 """Add a nickname to message_input if suitable""" | 785 """Add a nickname to message_input if suitable""" |
727 if (self.type == C.CHAT_GROUP and not self.message_input.text.startswith(nick)): | 786 if (self.type == C.CHAT_GROUP and not self.message_input.text.startswith(nick)): |
728 self.message_input.text = f'{nick}: {self.message_input.text}' | 787 self.message_input.text = f'{nick}: {self.message_input.text}' |
729 | 788 |
730 def onSend(self, input_widget): | 789 def onSend(self, input_widget): |
790 extra = {} | |
791 for item in self.attachments_to_send.attachments.children: | |
792 if item.sending: | |
793 # the item is already being sent | |
794 continue | |
795 item.sending = True | |
796 progress_id = item.data["progress_id"] = str(uuid.uuid4()) | |
797 attachments = extra.setdefault(C.MESS_KEY_ATTACHMENTS, []) | |
798 attachment = { | |
799 "path": str(item.data["path"]), | |
800 "progress_id": progress_id, | |
801 } | |
802 attachments.append(attachment) | |
803 | |
804 Clock.schedule_once( | |
805 partial(self._attachmentProgressUpdate, item), | |
806 PROGRESS_UPDATE) | |
807 | |
808 G.host.registerProgressCbs( | |
809 progress_id, | |
810 callback=partial(self._attachmentProgressCb, item), | |
811 errback=partial(self._attachmentProgressEb, item) | |
812 ) | |
813 | |
814 | |
731 G.host.messageSend( | 815 G.host.messageSend( |
732 self.target, | 816 self.target, |
733 {'': input_widget.text}, # TODO: handle language | 817 # TODO: handle language |
734 mess_type = (C.MESS_TYPE_GROUPCHAT | 818 {'': input_widget.text}, |
735 if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT), # TODO: put this in QuickChat | 819 # TODO: put this in QuickChat |
820 mess_type= | |
821 C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, | |
822 extra=extra, | |
736 profile_key=self.profile | 823 profile_key=self.profile |
737 ) | 824 ) |
738 input_widget.text = '' | 825 input_widget.text = '' |
739 | 826 |
740 def fileTransferEb(self, err_msg, cleaning_cb, profile): | 827 def addAttachment(self, file_path): |
741 if cleaning_cb is not None: | 828 file_path = Path(file_path) |
742 cleaning_cb() | 829 data = { |
743 msg = _("can't transfer file: {reason}").format(reason=err_msg) | 830 "path": file_path, |
744 log.warning(msg) | 831 "name": file_path.name, |
745 G.host.addNote(_("File transfer error"), | 832 } |
746 msg, | 833 self.attachments_to_send.attachments.add_widget( |
747 level=C.XMLUI_DATA_LVL_WARNING) | 834 AttachmentToSendItem(data=data) |
748 | 835 ) |
749 def fileTransferCb(self, metadata, cleaning_cb, profile): | |
750 log.debug("file transfered: {}".format(metadata)) | |
751 extra = {} | |
752 | |
753 # FIXME: Q&D way of getting file type, upload plugins shouls give it | |
754 mime_type = mimetypes.guess_type(metadata['url'])[0] | |
755 if mime_type is not None: | |
756 if mime_type.split('/')[0] == 'image': | |
757 # we generate url ourselves, so this formatting is safe | |
758 extra['xhtml'] = "<img src='{url}' />".format(**metadata) | |
759 | |
760 G.host.messageSend( | |
761 self.target, | |
762 {'': metadata['url']}, | |
763 mess_type = (C.MESS_TYPE_GROUPCHAT | |
764 if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT), | |
765 extra = extra, | |
766 profile_key=profile | |
767 ) | |
768 | |
769 if cleaning_cb is not None: | |
770 cleaning_cb() | |
771 | |
772 | 836 |
773 def transferFile(self, file_path, transfer_type=C.TRANSFER_UPLOAD, cleaning_cb=None): | 837 def transferFile(self, file_path, transfer_type=C.TRANSFER_UPLOAD, cleaning_cb=None): |
838 # FIXME: cleaning_cb is not managed | |
774 if transfer_type == C.TRANSFER_UPLOAD: | 839 if transfer_type == C.TRANSFER_UPLOAD: |
775 options = { | 840 self.addAttachment(file_path) |
776 "ignore_tls_errors": not G.host.tls_validation, | |
777 } | |
778 if self.encrypted: | |
779 options['encryption'] = C.ENC_AES_GCM | |
780 G.host.bridge.fileUpload( | |
781 str(file_path), | |
782 "", | |
783 "", | |
784 data_format.serialise(options), | |
785 self.profile, | |
786 callback = partial( | |
787 G.host.actionManager, | |
788 progress_cb = partial(self.fileTransferCb, cleaning_cb=cleaning_cb), | |
789 progress_eb = partial(self.fileTransferEb, cleaning_cb=cleaning_cb), | |
790 profile = self.profile, | |
791 ), | |
792 errback = partial(G.host.errback, | |
793 message=_("can't upload file: {msg}")) | |
794 ) | |
795 elif transfer_type == C.TRANSFER_SEND: | 841 elif transfer_type == C.TRANSFER_SEND: |
796 if self.type == C.CHAT_GROUP: | 842 if self.type == C.CHAT_GROUP: |
797 log.warning("P2P transfer is not possible for group chat") | 843 log.warning("P2P transfer is not possible for group chat") |
798 # TODO: show an error dialog to user, or better hide the send button for | 844 # TODO: show an error dialog to user, or better hide the send button for |
799 # MUC | 845 # MUC |