changeset 97:5d2289127bb7

menu (upload): better menu using dedicated widget: upload menu now use a decicated widget instead of context menu. The menu take half the size of the main window, and show each upload option as an icon. Use can select upload or P2P sending, and a short text message explains how the file will be transmitted.
author Goffi <goffi@goffi.org>
date Thu, 29 Dec 2016 23:47:07 +0100
parents 641678ddc26c
children 4d8c122b86a6
files src/cagou/core/cagou_main.py src/cagou/core/constants.py src/cagou/core/menu.py src/cagou/kv/menu.kv src/cagou/plugins/plugin_transfer_android_gallery.py src/cagou/plugins/plugin_transfer_android_photo.py src/cagou/plugins/plugin_transfer_android_video.py src/cagou/plugins/plugin_transfer_file.kv src/cagou/plugins/plugin_transfer_file.py src/cagou/plugins/plugin_transfer_voice.kv src/cagou/plugins/plugin_transfer_voice.py src/cagou/plugins/plugin_upload_android_gallery.py src/cagou/plugins/plugin_upload_android_photo.py src/cagou/plugins/plugin_upload_android_video.py src/cagou/plugins/plugin_upload_file.kv src/cagou/plugins/plugin_upload_file.py src/cagou/plugins/plugin_upload_voice.kv src/cagou/plugins/plugin_upload_voice.py src/cagou/plugins/plugin_wid_chat.kv src/cagou/plugins/plugin_wid_chat.py
diffstat 20 files changed, 606 insertions(+), 525 deletions(-) [+]
line wrap: on
line diff
--- a/src/cagou/core/cagou_main.py	Thu Dec 29 23:47:04 2016 +0100
+++ b/src/cagou/core/cagou_main.py	Thu Dec 29 23:47:07 2016 +0100
@@ -295,7 +295,7 @@
         self.app.default_avatar = os.path.join(self.media_dir, "misc/default_avatar.png")
         self.app.icon = os.path.join(self.media_dir, "icons/muchoslava/png/cagou_profil_bleu_96.png")
         self._plg_wids = []  # main widgets plugins
-        self._plg_wids_upload = []  # upload widgets plugins
+        self._plg_wids_transfer = []  # transfer widgets plugins
         self._import_plugins()
         self._visible_widgets = {}  # visible widgets by classes
 
@@ -357,13 +357,13 @@
         main_cls = plugin_info['main']
         return self.widgets.getOrCreateWidget(main_cls, target, on_new_widget=None, profiles=iter(self.profiles))
 
-    def _defaultFactoryUpload(self, plugin_info, callback, cancel_cb, profiles):
-        """default factory used to create upload widgets instances
+    def _defaultFactoryTransfer(self, plugin_info, callback, cancel_cb, profiles):
+        """default factory used to create transfer widgets instances
 
         @param plugin_info(dict): plugin datas
-        @param callback(callable): method to call with path to file to upload
-        @param cancel_cb(callable): call when upload is cancelled
-            upload widget must be used as first argument
+        @param callback(callable): method to call with path to file to transfer
+        @param cancel_cb(callable): call when transfer is cancelled
+            transfer widget must be used as first argument
         @param profiles(iterable): list of profiles
             None if not specified
         """
@@ -387,7 +387,7 @@
         plug_lst = [os.path.splitext(p)[0] for p in map(os.path.basename, glob.glob(os.path.join(plugins_path, plugin_glob)))]
 
         imported_names_main = set()  # used to avoid loading 2 times plugin with same import name
-        imported_names_upload = set()
+        imported_names_transfer = set()
         for plug in plug_lst:
             plugin_path = 'cagou.plugins.' + plug
 
@@ -402,9 +402,9 @@
             if plugin_type == C.PLUG_TYPE_WID:
                 imported_names = imported_names_main
                 default_factory = self._defaultFactoryMain
-            elif plugin_type == C.PLUG_TYPE_UPLOAD:
-                imported_names = imported_names_upload
-                default_factory = self._defaultFactoryUpload
+            elif plugin_type == C.PLUG_TYPE_TRANSFER:
+                imported_names = imported_names_transfer
+                default_factory = self._defaultFactoryTransfer
             else:
                 log.error(u"unknown plugin type {type_} for plugin {file_}, skipping".format(
                     type_ = plugin_type,
@@ -484,7 +484,7 @@
 
         # we want widgets sorted by names
         self._plg_wids.sort(key=lambda p: p['name'].lower())
-        self._plg_wids_upload.sort(key=lambda p: p['name'].lower())
+        self._plg_wids_transfer.sort(key=lambda p: p['name'].lower())
 
         if self.default_wid is None:
             # we have no selector widget, we use the first widget as default
@@ -493,8 +493,8 @@
     def _getPluginsSet(self, type_):
         if type_ == C.PLUG_TYPE_WID:
             return self._plg_wids
-        elif type_ == C.PLUG_TYPE_UPLOAD:
-            return self._plg_wids_upload
+        elif type_ == C.PLUG_TYPE_TRANSFER:
+            return self._plg_wids_transfer
         else:
             raise KeyError(u"{} plugin type is unknown".format(type_))
 
--- a/src/cagou/core/constants.py	Thu Dec 29 23:47:04 2016 +0100
+++ b/src/cagou/core/constants.py	Thu Dec 29 23:47:07 2016 +0100
@@ -29,4 +29,7 @@
     DEFAULT_WIDGET_ICON = u'{media}/misc/black.png'
 
     PLUG_TYPE_WID = u'wid'
-    PLUG_TYPE_UPLOAD = u'upload'
+    PLUG_TYPE_TRANSFER = u'transfer'
+
+    TRANSFER_UPLOAD = u"upload"
+    TRANSFER_SEND = u"send"
--- a/src/cagou/core/menu.py	Thu Dec 29 23:47:04 2016 +0100
+++ b/src/cagou/core/menu.py	Thu Dec 29 23:47:07 2016 +0100
@@ -159,55 +159,78 @@
         about.open()
 
 
-class UploadMenu(contextmenu.ContextMenu):
-    """upload menu which handle display and callbacks"""
-    # callback will be called with path to file to upload
+class TransferItem(BoxLayout):
+    plug_info = properties.DictProperty()
+
+    def on_touch_up(self, touch):
+        if not self.collide_point(*touch.pos):
+            return super(TransferItem, self).on_touch_up(touch)
+        else:
+            transfer_menu = self.parent
+            while not isinstance(transfer_menu, TransferMenu):
+                transfer_menu = transfer_menu.parent
+            transfer_menu.do_callback(self.plug_info)
+            return True
+
+
+class TransferMenu(BoxLayout):
+    """transfer menu which handle display and callbacks"""
+    # callback will be called with path to file to transfer
     callback = properties.ObjectProperty()
     # cancel callback need to remove the widget for UI
     # will be called with the widget to remove as argument
     cancel_cb = properties.ObjectProperty()
-    # profiles if set will be sent to upload widget, may be used to get specific files
+    # profiles if set will be sent to transfer widget, may be used to get specific files
     profiles = properties.ObjectProperty()
+    transfer_txt = _(u"Beware! The file will be sent to your server and stay unencrypted there\nServer admin(s) can see the file, and they choose how, when and if it will be deleted")
+    send_txt = _(u"The file will be sent unencrypted directly to your contact (without transiting by the server), except in some cases")
+    items_layout = properties.ObjectProperty()
 
     def __init__(self, **kwargs):
-        super(UploadMenu, self).__init__(**kwargs)
+        super(TransferMenu, self).__init__(**kwargs)
         if self.cancel_cb is None:
-            self.cancel_cb = self.onUploadCancelled
+            self.cancel_cb = self.onTransferCancelled
         if self.profiles is None:
             self.profiles = iter(G.host.profiles)
-        for plug_info in G.host.getPluggedWidgets(type_=C.PLUG_TYPE_UPLOAD):
-            item = contextmenu.ContextMenuTextItem(
-                text = plug_info['name'],
-                on_release = lambda dummy, plug_info=plug_info: self.do_callback(plug_info)
+        for plug_info in G.host.getPluggedWidgets(type_=C.PLUG_TYPE_TRANSFER):
+            item = TransferItem(
+                plug_info = plug_info
                 )
-            self.add_widget(item)
+            self.items_layout.add_widget(item)
 
     def show(self, caller_wid=None):
-        if caller_wid is not None:
-            pos = caller_wid.x, caller_wid.top + self.get_height()
+        self.visible = True
+        G.host.app.root.add_widget(self)
+
+    def on_touch_down(self, touch):
+        # we remove the menu if we click outside
+        # else we want to handle the event, but not
+        # transmit it to parents
+        if not self.collide_point(*touch.pos):
+            self.parent.remove_widget(self)
         else:
-            pos = G.host.app.root_window.mouse_pos
-        super(UploadMenu, self).show(*pos)
+            return super(TransferMenu, self).on_touch_down(touch)
+        return True
 
     def _closeUI(self, wid):
         G.host.closeUI()
 
-    def onUploadCancelled(self, wid, cleaning_cb=None):
+    def onTransferCancelled(self, wid, cleaning_cb=None):
         self._closeUI(wid)
         if cleaning_cb is not None:
             cleaning_cb()
 
     def do_callback(self, plug_info):
-        self.hide()
+        self.parent.remove_widget(self)
         if self.callback is None:
-            log.warning(u"UploadMenu callback is not set")
+            log.warning(u"TransferMenu callback is not set")
         else:
             wid = None
             external = plug_info.get('external', False)
-            def onUploadCb(file_path, cleaning_cb=None):
+            def onTransferCb(file_path, cleaning_cb=None):
                 if not external:
                     self._closeUI(wid)
                 self.callback(file_path, cleaning_cb)
-            wid = plug_info['factory'](plug_info, onUploadCb, self.cancel_cb, self.profiles)
+            wid = plug_info['factory'](plug_info, onTransferCb, self.cancel_cb, self.profiles)
             if not external:
                 G.host.showExtraUI(wid)
--- a/src/cagou/kv/menu.kv	Thu Dec 29 23:47:04 2016 +0100
+++ b/src/cagou/kv/menu.kv	Thu Dec 29 23:47:07 2016 +0100
@@ -14,6 +14,7 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+#:import _ sat.core.i18n._
 
 <AboutContent>:
     text_size: self.size
@@ -34,5 +35,55 @@
 <MainMenu>:
     cancel_handler_widget: self.parent
 
-<UploadMenu>:
-    cancel_handler_widget: self.parent if self.parent else self.orig_parent
+<TransferMenu>:
+    items_layout: items_layout
+    orientation: "vertical"
+    pos_hint: {"top": 0.5}
+    size_hint: 1, 0.5
+    canvas.before:
+        Color:
+            rgba: 0, 0, 0, 1
+        Rectangle:
+            pos: self.pos
+            size: self.size
+    BoxLayout:
+        size_hint: 1, None
+        height: dp(50)
+        ToggleButton:
+            id: upload_btn
+            text: _(u"upload")
+            group: "transfer"
+            state: "down"
+        ToggleButton:
+            id: send_btn
+            text: _(u"send")
+            group: "transfer"
+    Label:
+        size_hint: 1, 0.3
+        text: root.transfer_txt if upload_btn.state == 'down' else root.send_txt
+        text_size: self.size
+        halign: 'center'
+        valign: 'top'
+    ScrollView:
+        do_scroll_x: False
+        StackLayout:
+            size_hint: 1, None
+            padding: 20, 0
+            spacing: 15, 5
+            id: items_layout
+
+<TransferItem>:
+    orientation: "vertical"
+    size_hint: None, None
+    size: dp(50), dp(90)
+    IconButton:
+        source: root.plug_info['icon_medium']
+        allow_stretch: True
+        size_hint: 1, None
+        height: dp(50)
+    Label:
+        text: root.plug_info['name']
+        text_size: self.size
+        halign: "center"
+        valign: "top"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/cagou/plugins/plugin_transfer_android_gallery.py	Thu Dec 29 23:47:07 2016 +0100
@@ -0,0 +1,95 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from sat.core import log as logging
+log = logging.getLogger(__name__)
+from sat.core.i18n import _
+import sys
+import tempfile
+import os
+import os.path
+if sys.platform=="android":
+    from jnius import autoclass
+    from android import activity, mActivity
+
+    Intent = autoclass('android.content.Intent')
+    OpenableColumns = autoclass('android.provider.OpenableColumns')
+    PHOTO_GALLERY = 1
+    RESULT_OK = -1
+
+
+
+PLUGIN_INFO = {
+    "name": _(u"photo"),
+    "main": "AndroidGallery",
+    "platforms": ('android',),
+    "external": True,
+    "description": _(u"upload a photo from photo gallery"),
+    "icon_medium": u"{media}/icons/muchoslava/png/gallery_50.png",
+}
+
+
+class AndroidGallery(object):
+
+    def __init__(self, callback, cancel_cb):
+        self.callback = callback
+        self.cancel_cb = cancel_cb
+        activity.bind(on_activity_result=self.on_activity_result)
+        intent = Intent()
+        intent.setType('image/*')
+        intent.setAction(Intent.ACTION_GET_CONTENT)
+        mActivity.startActivityForResult(intent, PHOTO_GALLERY);
+
+    def on_activity_result(self, requestCode, resultCode, data):
+        # TODO: move file dump to a thread or use async callbacks during file writting
+        if requestCode == PHOTO_GALLERY and resultCode == RESULT_OK:
+            if data is None:
+                log.warning(u"No data found in activity result")
+                self.cancel_cb(self, None)
+                return
+            uri = data.getData()
+
+            # we get filename in the way explained at https://developer.android.com/training/secure-file-sharing/retrieve-info.html
+            cursor = mActivity.getContentResolver().query(uri, None, None, None, None )
+            name_idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+            cursor.moveToFirst()
+            filename = cursor.getString(name_idx)
+
+            # we save data in a temporary file that we send to callback
+            # the file will be removed once upload is done (or if an error happens)
+            input_stream = mActivity.getContentResolver().openInputStream(uri)
+            tmp_dir = tempfile.mkdtemp()
+            tmp_file = os.path.join(tmp_dir, filename)
+            def cleaning():
+                os.unlink(tmp_file)
+                os.rmdir(tmp_dir)
+                log.debug(u'temporary file cleaned')
+            buff = bytearray(4096)
+            with open(tmp_file, 'wb') as f:
+                while True:
+                    ret = input_stream.read(buff, 0, 4096)
+                    if ret != -1:
+                        f.write(buff)
+                    else:
+                        break
+            input_stream.close()
+            self.callback(tmp_file, cleaning)
+        else:
+            self.cancel_cb(self, None)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/cagou/plugins/plugin_transfer_android_photo.py	Thu Dec 29 23:47:07 2016 +0100
@@ -0,0 +1,63 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from sat.core import log as logging
+log = logging.getLogger(__name__)
+from sat.core.i18n import _
+import sys
+import os
+import os.path
+import time
+if sys.platform == "android":
+    from plyer import camera
+    from jnius import autoclass
+    Environment = autoclass('android.os.Environment')
+else:
+    import tempfile
+
+
+PLUGIN_INFO = {
+    "name": _(u"take photo"),
+    "main": "AndroidPhoto",
+    "platforms": ('android',),
+    "external": True,
+    "description": _(u"upload a photo from photo application"),
+    "icon_medium": u"{media}/icons/muchoslava/png/camera_off_50.png",
+}
+
+
+class AndroidPhoto(object):
+
+    def __init__(self, callback, cancel_cb):
+        self.callback = callback
+        self.cancel_cb = cancel_cb
+        filename = time.strftime("%Y-%m-%d_%H:%M:%S.jpg", time.gmtime())
+        tmp_dir = self.getTmpDir()
+        tmp_file = os.path.join(tmp_dir, filename)
+        log.debug(u"Picture will be saved to {}".format(tmp_file))
+        camera.take_picture(tmp_file, self.callback)
+        # we don't delete the file, as it is nice to keep it locally
+
+    def getTmpDir(self):
+        if sys.platform == "android":
+            dcim_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath()
+            return dcim_path
+        else:
+            return tempfile.mkdtemp()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/cagou/plugins/plugin_transfer_android_video.py	Thu Dec 29 23:47:07 2016 +0100
@@ -0,0 +1,63 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from sat.core import log as logging
+log = logging.getLogger(__name__)
+from sat.core.i18n import _
+import sys
+import os
+import os.path
+import time
+if sys.platform == "android":
+    from plyer import camera
+    from jnius import autoclass
+    Environment = autoclass('android.os.Environment')
+else:
+    import tempfile
+
+
+PLUGIN_INFO = {
+    "name": _(u"take video"),
+    "main": "AndroidVideo",
+    "platforms": ('android',),
+    "external": True,
+    "description": _(u"upload a video from video application"),
+    "icon_medium": u"{media}/icons/muchoslava/png/film_camera_off_50.png",
+}
+
+
+class AndroidVideo(object):
+
+    def __init__(self, callback, cancel_cb):
+        self.callback = callback
+        self.cancel_cb = cancel_cb
+        filename = time.strftime("%Y-%m-%d_%H:%M:%S.mpg", time.gmtime())
+        tmp_dir = self.getTmpDir()
+        tmp_file = os.path.join(tmp_dir, filename)
+        log.debug(u"Video will be saved to {}".format(tmp_file))
+        camera.take_video(tmp_file, self.callback)
+        # we don't delete the file, as it is nice to keep it locally
+
+    def getTmpDir(self):
+        if sys.platform == "android":
+            dcim_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath()
+            return dcim_path
+        else:
+            return tempfile.mkdtemp()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/cagou/plugins/plugin_transfer_file.kv	Thu Dec 29 23:47:07 2016 +0100
@@ -0,0 +1,35 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+#:import expanduser os.path.expanduser
+#:import platform kivy.utils.platform
+
+
+<FileTransmitter>:
+    orientation: "vertical"
+    FileChooserListView:
+        id: filechooser
+        rootpath: "/" if platform == 'android' else expanduser('~')
+    Button:
+        text: "choose"
+        size_hint: 1, None
+        height: dp(50)
+        on_release: root.onTransmitOK(filechooser)
+    Button:
+        text: "cancel"
+        size_hint: 1, None
+        height: dp(50)
+        on_release: root.cancel_cb(root)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/cagou/plugins/plugin_transfer_file.py	Thu Dec 29 23:47:07 2016 +0100
@@ -0,0 +1,43 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from sat.core import log as logging
+log = logging.getLogger(__name__)
+from sat.core.i18n import _
+from kivy.uix.boxlayout import BoxLayout
+from kivy import properties
+
+
+PLUGIN_INFO = {
+    "name": _(u"file"),
+    "main": "FileTransmitter",
+    "description": _(u"transmit a local file"),
+    "icon_medium": u"{media}/icons/muchoslava/png/fichier_50.png",
+}
+
+
+class FileTransmitter(BoxLayout):
+    callback = properties.ObjectProperty()
+    cancel_cb = properties.ObjectProperty()
+
+    def onTransmitOK(self, filechooser):
+        if filechooser.selection:
+            file_path = filechooser.selection[0]
+            self.callback(file_path)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/cagou/plugins/plugin_transfer_voice.kv	Thu Dec 29 23:47:07 2016 +0100
@@ -0,0 +1,71 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+#:import _ sat.core.i18n._
+#:import IconButton cagou.core.common.IconButton
+
+
+<VoiceRecorder>:
+    orientation: "vertical"
+    counter: counter
+    Label:
+        size_hint: 1, 0.4
+        text_size: self.size
+        halign: 'center'
+        valign: 'top'
+        text: _(u"Push the microphone button to start the record, then push it again to stop it.\nWhen you are satisfied, click on the transmit button")
+    Label:
+        id: counter
+        size_hint: 1, None
+        height: dp(60)
+        bold: True
+        font_size: sp(40)
+        text_size: self.size
+        text: u"{}:{:02}".format(root.time/60, root.time%60)
+        halign: 'center'
+        valign: 'middle'
+    BoxLayout:
+        size_hint: 1, None
+        height: dp(60)
+        Widget
+        IconButton:
+            source: app.expand("{media}/icons/muchoslava/png/") + ("micro_on_50.png" if root.recording else "micro_off_50.png")
+            allow_stretch: True
+            size_hint: None, None
+            size: dp(60), dp(60)
+            on_release: root.switchRecording()
+        IconButton:
+            opacity: 0 if root.recording or not root.time and not root.playing else 1
+            source: app.expand("{media}/icons/muchoslava/png/") + ("stop_50.png" if root.playing else "play_50.png")
+            allow_stretch: True
+            size_hint: None, None
+            size: dp(60), dp(60)
+            on_release: root.playRecord()
+        Widget
+    Widget:
+        size_hint: 1, None
+        height: dp(50)
+    Button:
+        text: _("transmit")
+        size_hint: 1, None
+        height: dp(50)
+        on_release: root.callback(root.audio.file_path)
+    Button:
+        text: _("cancel")
+        size_hint: 1, None
+        height: dp(50)
+        on_release: root.cancel_cb(root)
+    Widget
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/cagou/plugins/plugin_transfer_voice.py	Thu Dec 29 23:47:07 2016 +0100
@@ -0,0 +1,111 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from sat.core import log as logging
+log = logging.getLogger(__name__)
+from sat.core.i18n import _
+from kivy.uix.boxlayout import BoxLayout
+import sys
+import time
+from kivy.clock import Clock
+from kivy import properties
+if sys.platform == "android":
+    from plyer import audio
+
+
+PLUGIN_INFO = {
+    "name": _(u"voice"),
+    "main": "VoiceRecorder",
+    "platforms": ["android"],
+    "description": _(u"transmit a voice record"),
+    "icon_medium": u"{media}/icons/muchoslava/png/micro_off_50.png",
+}
+
+
+class VoiceRecorder(BoxLayout):
+    callback = properties.ObjectProperty()
+    cancel_cb = properties.ObjectProperty()
+    recording = properties.BooleanProperty(False)
+    playing = properties.BooleanProperty(False)
+    time = properties.NumericProperty(0)
+
+    def __init__(self, **kwargs):
+        super(VoiceRecorder, self).__init__(**kwargs)
+        self._started_at = None
+        self._counter_timer = None
+        self._play_timer = None
+        self.record_time = None
+        self.audio = audio
+        self.audio.file_path = "/sdcard/cagou_record.3gp"
+
+    def _updateTimer(self, dt):
+        self.time = int(time.time() - self._started_at)
+
+    def switchRecording(self):
+        if self.playing:
+            self._stopPlaying()
+        if self.recording:
+            try:
+                audio.stop()
+            except Exception as e:
+                # an exception can happen if record is pressed
+                # repeatedly in a short time (not a normal use)
+                log.warning(u"Exception on stop: {}".format(e))
+            self._counter_timer.cancel()
+            self.time = self.time + 1
+        else:
+            audio.start()
+            self._started_at = time.time()
+            self.time = 0
+            self._counter_timer = Clock.schedule_interval(self._updateTimer, 1)
+
+        self.recording = not self.recording
+
+    def _stopPlaying(self, dummy=None):
+        if self.record_time is None:
+            log.error("_stopPlaying should no be called when record_time is None")
+            return
+        audio.stop()
+        self.playing = False
+        self.time = self.record_time
+        if self._counter_timer is not None:
+            self._counter_timer.cancel()
+
+    def playRecord(self):
+        if self.recording:
+            return
+        if self.playing:
+            self._stopPlaying()
+        else:
+            try:
+                audio.play()
+            except Exception as e:
+                # an exception can happen in the same situation
+                # as for audio.stop() above (i.e. bad record)
+                log.warning(u"Exception on play: {}".format(e))
+                self.time = 0
+                return
+
+            self.playing = True
+            self.record_time = self.time
+            Clock.schedule_once(self._stopPlaying, self.time + 1)
+            self._started_at = time.time()
+            self.time = 0
+            self._counter_timer =  Clock.schedule_interval(self._updateTimer, 0.5)
--- a/src/cagou/plugins/plugin_upload_android_gallery.py	Thu Dec 29 23:47:04 2016 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,94 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
-# Copyright (C) 2016 Jérôme Poisson (goffi@goffi.org)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from sat.core.i18n import _
-import sys
-import tempfile
-import os
-import os.path
-if sys.platform=="android":
-    from jnius import autoclass
-    from android import activity, mActivity
-
-    Intent = autoclass('android.content.Intent')
-    OpenableColumns = autoclass('android.provider.OpenableColumns')
-    PHOTO_GALLERY = 1
-    RESULT_OK = -1
-
-
-
-PLUGIN_INFO = {
-    "name": _(u"photo"),
-    "main": "AndroidGallery",
-    "platforms": ('android',),
-    "external": True,
-    "description": _(u"upload a photo from photo gallery"),
-}
-
-
-class AndroidGallery(object):
-
-    def __init__(self, callback, cancel_cb):
-        self.callback = callback
-        self.cancel_cb = cancel_cb
-        activity.bind(on_activity_result=self.on_activity_result)
-        intent = Intent()
-        intent.setType('image/*')
-        intent.setAction(Intent.ACTION_GET_CONTENT)
-        mActivity.startActivityForResult(intent, PHOTO_GALLERY);
-
-    def on_activity_result(self, requestCode, resultCode, data):
-        # TODO: move file dump to a thread or use async callbacks during file writting
-        if requestCode == PHOTO_GALLERY and resultCode == RESULT_OK:
-            if data is None:
-                log.warning(u"No data found in activity result")
-                self.cancel_cb(self, None)
-                return
-            uri = data.getData()
-
-            # we get filename in the way explained at https://developer.android.com/training/secure-file-sharing/retrieve-info.html
-            cursor = mActivity.getContentResolver().query(uri, None, None, None, None )
-            name_idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
-            cursor.moveToFirst()
-            filename = cursor.getString(name_idx)
-
-            # we save data in a temporary file that we send to callback
-            # the file will be removed once upload is done (or if an error happens)
-            input_stream = mActivity.getContentResolver().openInputStream(uri)
-            tmp_dir = tempfile.mkdtemp()
-            tmp_file = os.path.join(tmp_dir, filename)
-            def cleaning():
-                os.unlink(tmp_file)
-                os.rmdir(tmp_dir)
-                log.debug(u'temporary file cleaned')
-            buff = bytearray(4096)
-            with open(tmp_file, 'wb') as f:
-                while True:
-                    ret = input_stream.read(buff, 0, 4096)
-                    if ret != -1:
-                        f.write(buff)
-                    else:
-                        break
-            input_stream.close()
-            self.callback(tmp_file, cleaning)
-        else:
-            self.cancel_cb(self, None)
--- a/src/cagou/plugins/plugin_upload_android_photo.py	Thu Dec 29 23:47:04 2016 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
-# Copyright (C) 2016 Jérôme Poisson (goffi@goffi.org)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from sat.core.i18n import _
-import sys
-import os
-import os.path
-import time
-if sys.platform == "android":
-    from plyer import camera
-    from jnius import autoclass
-    Environment = autoclass('android.os.Environment')
-else:
-    import tempfile
-
-
-PLUGIN_INFO = {
-    "name": _(u"take photo"),
-    "main": "AndroidPhoto",
-    "platforms": ('android',),
-    "external": True,
-    "description": _(u"upload a photo from photo application"),
-}
-
-
-class AndroidPhoto(object):
-
-    def __init__(self, callback, cancel_cb):
-        self.callback = callback
-        self.cancel_cb = cancel_cb
-        filename = time.strftime("%Y-%m-%d_%H:%M:%S.jpg", time.gmtime())
-        tmp_dir = self.getTmpDir()
-        tmp_file = os.path.join(tmp_dir, filename)
-        log.debug(u"Picture will be saved to {}".format(tmp_file))
-        camera.take_picture(tmp_file, self.callback)
-        # we don't delete the file, as it is nice to keep it locally
-
-    def getTmpDir(self):
-        if sys.platform == "android":
-            dcim_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath()
-            return dcim_path
-        else:
-            return tempfile.mkdtemp()
--- a/src/cagou/plugins/plugin_upload_android_video.py	Thu Dec 29 23:47:04 2016 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
-# Copyright (C) 2016 Jérôme Poisson (goffi@goffi.org)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from sat.core.i18n import _
-import sys
-import os
-import os.path
-import time
-if sys.platform == "android":
-    from plyer import camera
-    from jnius import autoclass
-    Environment = autoclass('android.os.Environment')
-else:
-    import tempfile
-
-
-PLUGIN_INFO = {
-    "name": _(u"take video"),
-    "main": "AndroidVideo",
-    "platforms": ('android',),
-    "external": True,
-    "description": _(u"upload a video from video application"),
-}
-
-
-class AndroidVideo(object):
-
-    def __init__(self, callback, cancel_cb):
-        self.callback = callback
-        self.cancel_cb = cancel_cb
-        filename = time.strftime("%Y-%m-%d_%H:%M:%S.mpg", time.gmtime())
-        tmp_dir = self.getTmpDir()
-        tmp_file = os.path.join(tmp_dir, filename)
-        log.debug(u"Video will be saved to {}".format(tmp_file))
-        camera.take_video(tmp_file, self.callback)
-        # we don't delete the file, as it is nice to keep it locally
-
-    def getTmpDir(self):
-        if sys.platform == "android":
-            dcim_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath()
-            return dcim_path
-        else:
-            return tempfile.mkdtemp()
--- a/src/cagou/plugins/plugin_upload_file.kv	Thu Dec 29 23:47:04 2016 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
-# Copyright (C) 2016 Jérôme Poisson (goffi@goffi.org)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-#:import expanduser os.path.expanduser
-#:import platform kivy.utils.platform
-
-
-<FileUploader>:
-    orientation: "vertical"
-    FileChooserListView:
-        id: filechooser
-        rootpath: "/" if platform == 'android' else expanduser('~')
-    Button:
-        text: "upload"
-        size_hint: 1, None
-        height: dp(50)
-        on_release: root.onUploadOK(filechooser)
-    Button:
-        text: "cancel"
-        size_hint: 1, None
-        height: dp(50)
-        on_release: root.cancel_cb(root)
--- a/src/cagou/plugins/plugin_upload_file.py	Thu Dec 29 23:47:04 2016 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
-# Copyright (C) 2016 Jérôme Poisson (goffi@goffi.org)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from sat.core.i18n import _
-from kivy.uix.boxlayout import BoxLayout
-from kivy import properties
-
-
-PLUGIN_INFO = {
-    "name": _(u"file"),
-    "main": "FileUploader",
-    "description": _(u"upload a local file"),
-}
-
-
-class FileUploader(BoxLayout):
-    callback = properties.ObjectProperty()
-    cancel_cb = properties.ObjectProperty()
-
-    def onUploadOK(self, filechooser):
-        if filechooser.selection:
-            file_path = filechooser.selection[0]
-            self.callback(file_path)
--- a/src/cagou/plugins/plugin_upload_voice.kv	Thu Dec 29 23:47:04 2016 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
-# Copyright (C) 2016 Jérôme Poisson (goffi@goffi.org)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-#:import _ sat.core.i18n._
-#:import IconButton cagou.core.common.IconButton
-
-
-<VoiceRecorder>:
-    orientation: "vertical"
-    counter: counter
-    Label:
-        size_hint: 1, 0.4
-        text_size: self.size
-        halign: 'center'
-        valign: 'top'
-        text: _(u"Push the microphone button to start the record, then push it again to stop it.\nWhen you are satisfied, click on the upload button")
-    Label:
-        id: counter
-        size_hint: 1, None
-        height: dp(60)
-        bold: True
-        font_size: sp(40)
-        text_size: self.size
-        text: u"{}:{:02}".format(root.time/60, root.time%60)
-        halign: 'center'
-        valign: 'middle'
-    BoxLayout:
-        size_hint: 1, None
-        height: dp(60)
-        Widget
-        IconButton:
-            source: app.expand("{media}/icons/muchoslava/png/") + ("micro_on_50.png" if root.recording else "micro_off_50.png")
-            allow_stretch: True
-            size_hint: None, None
-            size: dp(60), dp(60)
-            on_release: root.switchRecording()
-        IconButton:
-            opacity: 0 if root.recording or not root.time and not root.playing else 1
-            source: app.expand("{media}/icons/muchoslava/png/") + ("stop_50.png" if root.playing else "play_50.png")
-            allow_stretch: True
-            size_hint: None, None
-            size: dp(60), dp(60)
-            on_release: root.playRecord()
-        Widget
-    Widget:
-        size_hint: 1, None
-        height: dp(50)
-    Button:
-        text: "upload"
-        size_hint: 1, None
-        height: dp(50)
-        on_release: root.callback(root.audio.file_path)
-    Button:
-        text: "cancel"
-        size_hint: 1, None
-        height: dp(50)
-        on_release: root.cancel_cb(root)
-    Widget
--- a/src/cagou/plugins/plugin_upload_voice.py	Thu Dec 29 23:47:04 2016 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,110 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
-# Copyright (C) 2016 Jérôme Poisson (goffi@goffi.org)
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-
-from sat.core import log as logging
-log = logging.getLogger(__name__)
-from sat.core.i18n import _
-from kivy.uix.boxlayout import BoxLayout
-import sys
-import time
-from kivy.clock import Clock
-from kivy import properties
-if sys.platform == "android":
-    from plyer import audio
-
-
-PLUGIN_INFO = {
-    "name": _(u"voice"),
-    "main": "VoiceRecorder",
-    "platforms": ["android"],
-    "description": _(u"upload a voice record"),
-}
-
-
-class VoiceRecorder(BoxLayout):
-    callback = properties.ObjectProperty()
-    cancel_cb = properties.ObjectProperty()
-    recording = properties.BooleanProperty(False)
-    playing = properties.BooleanProperty(False)
-    time = properties.NumericProperty(0)
-
-    def __init__(self, **kwargs):
-        super(VoiceRecorder, self).__init__(**kwargs)
-        self._started_at = None
-        self._counter_timer = None
-        self._play_timer = None
-        self.record_time = None
-        self.audio = audio
-        self.audio.file_path = "/sdcard/cagou_record.3gp"
-
-    def _updateTimer(self, dt):
-        self.time = int(time.time() - self._started_at)
-
-    def switchRecording(self):
-        if self.playing:
-            self._stopPlaying()
-        if self.recording:
-            try:
-                audio.stop()
-            except Exception as e:
-                # an exception can happen if record is pressed
-                # repeatedly in a short time (not a normal use)
-                log.warning(u"Exception on stop: {}".format(e))
-            self._counter_timer.cancel()
-            self.time = self.time + 1
-        else:
-            audio.start()
-            self._started_at = time.time()
-            self.time = 0
-            self._counter_timer = Clock.schedule_interval(self._updateTimer, 1)
-
-        self.recording = not self.recording
-
-    def _stopPlaying(self, dummy=None):
-        if self.record_time is None:
-            log.error("_stopPlaying should no be called when record_time is None")
-            return
-        audio.stop()
-        self.playing = False
-        self.time = self.record_time
-        if self._counter_timer is not None:
-            self._counter_timer.cancel()
-
-    def playRecord(self):
-        if self.recording:
-            return
-        if self.playing:
-            self._stopPlaying()
-        else:
-            try:
-                audio.play()
-            except Exception as e:
-                # an exception can happen in the same situation
-                # as for audio.stop() above (i.e. bad record)
-                log.warning(u"Exception on play: {}".format(e))
-                self.time = 0
-                return
-
-            self.playing = True
-            self.record_time = self.time
-            Clock.schedule_once(self._stopPlaying, self.time + 1)
-            self._started_at = time.time()
-            self.time = 0
-            self._counter_timer =  Clock.schedule_interval(self._updateTimer, 0.5)
--- a/src/cagou/plugins/plugin_wid_chat.kv	Thu Dec 29 23:47:04 2016 +0100
+++ b/src/cagou/plugins/plugin_wid_chat.kv	Thu Dec 29 23:47:07 2016 +0100
@@ -14,6 +14,8 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+#:import TransferMenu cagou.core.menu.TransferMenu
+
 
 <SimpleXHTMLWidgetEscapedText>:
     size_hint: None, None
@@ -100,12 +102,9 @@
             hint_text: "Enter your message here"
             on_text_validate: root.onSend(args[0])
         IconButton
-            # upload button
+            # transfer button
             source: app.expand("{media}/icons/tango/actions/32/list-add.png")
             allow_stretch: True
             size_hint: None, 1
             width: max(self.texture_size[0], dp(40))
-            on_release: upload_menu.show(self)
-            UploadMenu:
-                id: upload_menu
-                callback: root.onUploadOK
+            on_release: TransferMenu(callback=root.onTransferOK).show(self)
--- a/src/cagou/plugins/plugin_wid_chat.py	Thu Dec 29 23:47:04 2016 +0100
+++ b/src/cagou/plugins/plugin_wid_chat.py	Thu Dec 29 23:47:07 2016 +0100
@@ -613,10 +613,10 @@
             if cleaning_cb is not None:
                 cleaning_cb()
         # TODO: display message to user
-        log.warning(u"Can't upload file: {}".format(err_msg))
+        log.warning(u"Can't transfer file: {}".format(err_msg))
 
-    def fileUploadDone(self, metadata, profile):
-        log.debug("file uploaded: {}".format(metadata))
+    def fileTransferDone(self, metadata, profile):
+        log.debug("file transfered: {}".format(metadata))
         G.host.messageSend(
             self.target,
             {'': metadata['url']},
@@ -624,23 +624,23 @@
             profile_key=profile
             )
 
-    def fileUploadCb(self, progress_data, cleaning_cb):
+    def fileTransferCb(self, progress_data, cleaning_cb):
         try:
             progress_id = progress_data['progress']
         except KeyError:
             xmlui = progress_data['xmlui']
             G.host.showUI(xmlui)
         else:
-            self._waiting_pids[progress_id] = (self.fileUploadDone, cleaning_cb)
+            self._waiting_pids[progress_id] = (self.fileTransferDone, cleaning_cb)
 
-    def onUploadOK(self, file_path, cleaning_cb):
-        G.host.bridge.fileUpload(
+    def onTransferOK(self, file_path, cleaning_cb):
+        G.host.bridge.fileTransfer(
             file_path,
             "",
             "",
             {"ignore_tls_errors": C.BOOL_TRUE},  # FIXME: should not be the default
             self.profile,
-            callback = lambda progress_data: self.fileUploadCb(progress_data, cleaning_cb)
+            callback = lambda progress_data: self.fileTransferCb(progress_data, cleaning_cb)
             )
 
     def _mucJoinCb(self, joined_data):