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