Daniel Brötzmann pushed to branch master at gajim / gajim

Commits:

17 changed files:

Changes:

  • gajim/common/events.py
    ... ... @@ -44,9 +44,9 @@
    44 44
     
    
    45 45
     if typing.TYPE_CHECKING:
    
    46 46
         from gajim.common.client import Client
    
    47
    +    from gajim.common.modules.httpupload import HTTPFileTransfer
    
    47 48
         from gajim.plugins.pluginmanager import PluginManifest
    
    48 49
     
    
    49
    -
    
    50 50
     ChatListEventT = Union[
    
    51 51
         'MessageReceived',
    
    52 52
         'MamMessageReceived',
    
    ... ... @@ -237,6 +237,22 @@ class FileSendError(ApplicationEvent):
    237 237
         error_msg: str = ''
    
    238 238
     
    
    239 239
     
    
    240
    +@dataclass
    
    241
    +class HTTPUploadStarted(ApplicationEvent):
    
    242
    +    name: str = field(init=False, default='http-upload-started')
    
    243
    +    account: str
    
    244
    +    jid: JID
    
    245
    +    transfer: HTTPFileTransfer
    
    246
    +
    
    247
    +
    
    248
    +@dataclass
    
    249
    +class HTTPUploadError(ApplicationEvent):
    
    250
    +    name: str = field(init=False, default='http-upload-error')
    
    251
    +    account: str
    
    252
    +    jid: JID
    
    253
    +    error_msg: str
    
    254
    +
    
    255
    +
    
    240 256
     @dataclass
    
    241 257
     class AccountEnabled(ApplicationEvent):
    
    242 258
         name: str = field(init=False, default='account-enabled')
    

  • gajim/common/modules/httpupload.py
    ... ... @@ -25,6 +25,7 @@
    25 25
     from urllib.parse import urlparse
    
    26 26
     import mimetypes
    
    27 27
     from collections import defaultdict
    
    28
    +from pathlib import Path
    
    28 29
     
    
    29 30
     from nbxmpp.errors import StanzaError
    
    30 31
     from nbxmpp.errors import MalformedStanzaError
    
    ... ... @@ -42,6 +43,8 @@
    42 43
     
    
    43 44
     from gajim.common import app
    
    44 45
     from gajim.common import types
    
    46
    +from gajim.common.events import HTTPUploadError
    
    47
    +from gajim.common.events import HTTPUploadStarted
    
    45 48
     from gajim.common.i18n import _
    
    46 49
     from gajim.common.helpers import get_tls_error_phrase
    
    47 50
     from gajim.common.helpers import get_account_proxy
    
    ... ... @@ -49,6 +52,7 @@
    49 52
     from gajim.common.filetransfer import FileTransfer
    
    50 53
     from gajim.common.modules.base import BaseModule
    
    51 54
     from gajim.common.exceptions import FileError
    
    55
    +from gajim.common.structs import OutgoingMessage
    
    52 56
     
    
    53 57
     
    
    54 58
     class HTTPUpload(BaseModule):
    
    ... ... @@ -105,18 +109,44 @@ def get_running_transfers(self,
    105 109
     
    
    106 110
             return self._running_transfers.get((contact.account, contact.jid))
    
    107 111
     
    
    108
    -    def make_transfer(self,
    
    109
    -                      path: str,
    
    110
    -                      encryption: Optional[str],
    
    111
    -                      contact: types.ContactT,
    
    112
    -                      groupchat: bool = False
    
    113
    -                      ) -> HTTPFileTransfer:
    
    114
    -        if not path or not os.path.exists(path):
    
    112
    +    def send_file(self, contact: types.ChatContactT, path: Path) -> None:
    
    113
    +        encryption = contact.settings.get('encryption') or None
    
    114
    +
    
    115
    +        try:
    
    116
    +            transfer = self._make_transfer(
    
    117
    +                path,
    
    118
    +                encryption,
    
    119
    +                contact)
    
    120
    +        except FileError as error:
    
    121
    +            event = HTTPUploadError(
    
    122
    +                contact.account,
    
    123
    +                contact.jid,
    
    124
    +                _('Could not open file (%s)') % str(error))
    
    125
    +            app.ged.raise_event(event)
    
    126
    +            return
    
    127
    +
    
    128
    +        transfer.connect('cancel', self._on_cancel_upload)
    
    129
    +        transfer.connect('state-changed', self._on_http_upload_state_changed)
    
    130
    +
    
    131
    +        event = HTTPUploadStarted(
    
    132
    +            contact.account,
    
    133
    +            contact.jid,
    
    134
    +            transfer)
    
    135
    +        app.ged.raise_event(event)
    
    136
    +        self._start_transfer(transfer)
    
    137
    +
    
    138
    +    def _make_transfer(self,
    
    139
    +                       path: Path,
    
    140
    +                       encryption: Optional[str],
    
    141
    +                       contact: types.ChatContactT,
    
    142
    +                       ) -> HTTPFileTransfer:
    
    143
    +
    
    144
    +        if not path or not path.exists():
    
    115 145
                 raise FileError(_('Could not access file'))
    
    116 146
     
    
    117 147
             invalid_file = False
    
    118 148
             msg = ''
    
    119
    -        stat = os.stat(path)
    
    149
    +        stat = path.stat()
    
    120 150
     
    
    121 151
             if os.path.isfile(path):
    
    122 152
                 if stat[6] == 0:
    
    ... ... @@ -143,18 +173,43 @@ def make_transfer(self,
    143 173
             self._log.info('Detected MIME type of file: %s', mime)
    
    144 174
     
    
    145 175
             transfer = HTTPFileTransfer(self._account,
    
    146
    -                                    path,
    
    176
    +                                    str(path),
    
    147 177
                                         contact,
    
    148 178
                                         mime,
    
    149 179
                                         encryption,
    
    150
    -                                    groupchat)
    
    180
    +                                    contact.is_groupchat)
    
    151 181
     
    
    152 182
             key = (contact.account, contact.jid)
    
    153 183
             self._running_transfers[key].add(transfer)
    
    154 184
     
    
    155 185
             return transfer
    
    156 186
     
    
    157
    -    def cancel_transfer(self, transfer: HTTPFileTransfer) -> None:
    
    187
    +    def _on_http_upload_state_changed(self,
    
    188
    +                                      transfer: HTTPFileTransfer,
    
    189
    +                                      _signal_name: str,
    
    190
    +                                      state: FTState
    
    191
    +                                      ) -> None:
    
    192
    +
    
    193
    +        if state.is_finished:
    
    194
    +            uri = transfer.get_transformed_uri()
    
    195
    +
    
    196
    +            type_ = 'chat'
    
    197
    +            if transfer.is_groupchat:
    
    198
    +                type_ = 'groupchat'
    
    199
    +
    
    200
    +            message = OutgoingMessage(account=transfer.account,
    
    201
    +                                      contact=transfer.contact,
    
    202
    +                                      message=uri,
    
    203
    +                                      type_=type_,
    
    204
    +                                      oob_url=uri)
    
    205
    +
    
    206
    +            self._client.send_message(message)
    
    207
    +
    
    208
    +    def _on_cancel_upload(self,
    
    209
    +                          transfer: HTTPFileTransfer,
    
    210
    +                          _signal_name: str
    
    211
    +                          ) -> None:
    
    212
    +
    
    158 213
             transfer.set_cancelled()
    
    159 214
     
    
    160 215
             key = (transfer.account, transfer.contact.jid)
    
    ... ... @@ -166,14 +221,14 @@ def cancel_transfer(self, transfer: HTTPFileTransfer) -> None:
    166 221
     
    
    167 222
             self._session.cancel_message(message, Soup.Status.CANCELLED)
    
    168 223
     
    
    169
    -    def start_transfer(self, transfer: HTTPFileTransfer) -> None:
    
    224
    +    def _start_transfer(self, transfer: HTTPFileTransfer) -> None:
    
    170 225
             if transfer.encryption is not None and not transfer.is_encrypted:
    
    171 226
                 transfer.set_encrypting()
    
    172 227
                 plugin = app.plugin_manager.encryption_plugins[transfer.encryption]
    
    173 228
                 if hasattr(plugin, 'encrypt_file'):
    
    174 229
                     plugin.encrypt_file(transfer,
    
    175 230
                                         self._account,
    
    176
    -                                    self.start_transfer)
    
    231
    +                                    self._start_transfer)
    
    177 232
                 else:
    
    178 233
                     transfer.set_error('encryption-not-available')
    
    179 234
     
    

  • gajim/data/gui/file_transfer_send.ui → gajim/data/gui/file_transfer_selector.ui
    1 1
     <?xml version="1.0" encoding="UTF-8"?>
    
    2
    -<!-- Generated with glade 3.38.2 -->
    
    2
    +<!-- Generated with glade 3.40.0 -->
    
    3 3
     <interface>
    
    4 4
       <requires lib="gtk+" version="3.24"/>
    
    5
    -  <object class="GtkStack" id="send_stack">
    
    5
    +  <object class="GtkBox" id="file_box">
    
    6 6
         <property name="visible">True</property>
    
    7 7
         <property name="can-focus">False</property>
    
    8
    -    <property name="transition-type">slide-left-right</property>
    
    8
    +    <property name="spacing">12</property>
    
    9 9
         <child>
    
    10
    -      <object class="GtkBox">
    
    10
    +      <object class="GtkBox" id="preview_image_box">
    
    11 11
             <property name="visible">True</property>
    
    12 12
             <property name="can-focus">False</property>
    
    13
    -        <property name="border-width">18</property>
    
    13
    +        <property name="valign">center</property>
    
    14 14
             <property name="orientation">vertical</property>
    
    15
    -        <property name="spacing">6</property>
    
    16 15
             <child>
    
    17
    -          <object class="GtkLabel">
    
    16
    +          <object class="GtkImage" id="preview_image">
    
    18 17
                 <property name="visible">True</property>
    
    19 18
                 <property name="can-focus">False</property>
    
    20
    -            <property name="label" translatable="yes">Files to send</property>
    
    21
    -            <property name="xalign">0</property>
    
    19
    +            <property name="icon_size">6</property>
    
    22 20
                 <style>
    
    23
    -              <class name="dim-label"/>
    
    21
    +              <class name="preview-image"/>
    
    24 22
                 </style>
    
    25 23
               </object>
    
    26 24
               <packing>
    
    27 25
                 <property name="expand">False</property>
    
    28 26
                 <property name="fill">True</property>
    
    29
    -            <property name="position">1</property>
    
    27
    +            <property name="position">0</property>
    
    28
    +          </packing>
    
    29
    +        </child>
    
    30
    +      </object>
    
    31
    +      <packing>
    
    32
    +        <property name="expand">False</property>
    
    33
    +        <property name="fill">True</property>
    
    34
    +        <property name="position">0</property>
    
    35
    +      </packing>
    
    36
    +    </child>
    
    37
    +    <child>
    
    38
    +      <object class="GtkBox">
    
    39
    +        <property name="visible">True</property>
    
    40
    +        <property name="can-focus">False</property>
    
    41
    +        <property name="orientation">vertical</property>
    
    42
    +        <property name="spacing">3</property>
    
    43
    +        <child>
    
    44
    +          <object class="GtkLabel" id="file_name_label">
    
    45
    +            <property name="visible">True</property>
    
    46
    +            <property name="can-focus">False</property>
    
    47
    +            <property name="ellipsize">end</property>
    
    48
    +            <property name="xalign">0</property>
    
    49
    +          </object>
    
    50
    +          <packing>
    
    51
    +            <property name="expand">False</property>
    
    52
    +            <property name="fill">True</property>
    
    53
    +            <property name="position">0</property>
    
    30 54
               </packing>
    
    31 55
             </child>
    
    32 56
             <child>
    
    33 57
               <object class="GtkBox">
    
    34 58
                 <property name="visible">True</property>
    
    35 59
                 <property name="can-focus">False</property>
    
    36
    -            <property name="hexpand">True</property>
    
    37
    -            <property name="vexpand">True</property>
    
    38
    -            <property name="orientation">vertical</property>
    
    60
    +            <property name="spacing">6</property>
    
    39 61
                 <child>
    
    40
    -              <object class="GtkScrolledWindow">
    
    62
    +              <object class="GtkLabel" id="file_size_label">
    
    41 63
                     <property name="visible">True</property>
    
    42
    -                <property name="can-focus">True</property>
    
    43
    -                <property name="hscrollbar-policy">never</property>
    
    44
    -                <property name="shadow-type">in</property>
    
    45
    -                <child>
    
    46
    -                  <object class="GtkViewport">
    
    47
    -                    <property name="visible">True</property>
    
    48
    -                    <property name="can-focus">False</property>
    
    49
    -                    <child>
    
    50
    -                      <object class="GtkListBox" id="listbox">
    
    51
    -                        <property name="visible">True</property>
    
    52
    -                        <property name="can-focus">False</property>
    
    53
    -                        <property name="vexpand">True</property>
    
    54
    -                        <property name="selection-mode">multiple</property>
    
    55
    -                        <property name="activate-on-single-click">False</property>
    
    56
    -                        <signal name="add" handler="_on_files_changed" swapped="no"/>
    
    57
    -                        <signal name="remove" handler="_on_files_changed" swapped="no"/>
    
    58
    -                      </object>
    
    59
    -                    </child>
    
    60
    -                  </object>
    
    61
    -                </child>
    
    64
    +                <property name="can-focus">False</property>
    
    65
    +                <style>
    
    66
    +                  <class name="dim-label"/>
    
    67
    +                </style>
    
    62 68
                   </object>
    
    63 69
                   <packing>
    
    64
    -                <property name="expand">True</property>
    
    70
    +                <property name="expand">False</property>
    
    65 71
                     <property name="fill">True</property>
    
    66 72
                     <property name="position">0</property>
    
    67 73
                   </packing>
    
    68 74
                 </child>
    
    69 75
                 <child>
    
    70
    -              <object class="GtkToolbar">
    
    71
    -                <property name="visible">True</property>
    
    76
    +              <object class="GtkLabel" id="warning_label">
    
    72 77
                     <property name="can-focus">False</property>
    
    73
    -                <property name="show-arrow">False</property>
    
    74
    -                <property name="icon_size">1</property>
    
    75
    -                <child>
    
    76
    -                  <object class="GtkToolButton">
    
    77
    -                    <property name="visible">True</property>
    
    78
    -                    <property name="can-focus">False</property>
    
    79
    -                    <property name="tooltip-text" translatable="yes">Add Files</property>
    
    80
    -                    <property name="use-underline">True</property>
    
    81
    -                    <property name="icon-name">list-add-symbolic</property>
    
    82
    -                    <signal name="clicked" handler="_select_files" swapped="no"/>
    
    83
    -                  </object>
    
    84
    -                  <packing>
    
    85
    -                    <property name="expand">False</property>
    
    86
    -                    <property name="homogeneous">False</property>
    
    87
    -                  </packing>
    
    88
    -                </child>
    
    89
    -                <child>
    
    90
    -                  <object class="GtkToolButton">
    
    91
    -                    <property name="visible">True</property>
    
    92
    -                    <property name="can-focus">False</property>
    
    93
    -                    <property name="tooltip-text" translatable="yes">Remove Files</property>
    
    94
    -                    <property name="use-underline">True</property>
    
    95
    -                    <property name="icon-name">list-remove-symbolic</property>
    
    96
    -                    <signal name="clicked" handler="_remove_files" swapped="no"/>
    
    97
    -                  </object>
    
    98
    -                  <packing>
    
    99
    -                    <property name="expand">False</property>
    
    100
    -                    <property name="homogeneous">True</property>
    
    101
    -                  </packing>
    
    102
    -                </child>
    
    78
    +                <property name="no-show-all">True</property>
    
    79
    +                <property name="ellipsize">end</property>
    
    80
    +                <property name="xalign">0</property>
    
    103 81
                     <style>
    
    104
    -                  <class name="inline-toolbar"/>
    
    82
    +                  <class name="warning-color"/>
    
    105 83
                     </style>
    
    106 84
                   </object>
    
    107 85
                   <packing>
    
    ... ... @@ -114,89 +92,143 @@
    114 92
               <packing>
    
    115 93
                 <property name="expand">False</property>
    
    116 94
                 <property name="fill">True</property>
    
    117
    -            <property name="position">2</property>
    
    95
    +            <property name="position">1</property>
    
    118 96
               </packing>
    
    119 97
             </child>
    
    98
    +      </object>
    
    99
    +      <packing>
    
    100
    +        <property name="expand">True</property>
    
    101
    +        <property name="fill">True</property>
    
    102
    +        <property name="position">1</property>
    
    103
    +      </packing>
    
    104
    +    </child>
    
    105
    +    <child>
    
    106
    +      <object class="GtkButton" id="remove_file_button">
    
    107
    +        <property name="visible">True</property>
    
    108
    +        <property name="can-focus">True</property>
    
    109
    +        <property name="receives-default">True</property>
    
    110
    +        <property name="tooltip-text" translatable="yes">Remove</property>
    
    111
    +        <property name="valign">center</property>
    
    120 112
             <child>
    
    121
    -          <object class="GtkLabel">
    
    113
    +          <object class="GtkImage">
    
    122 114
                 <property name="visible">True</property>
    
    123 115
                 <property name="can-focus">False</property>
    
    124
    -            <property name="label" translatable="yes">Description</property>
    
    125
    -            <property name="xalign">0</property>
    
    126
    -            <style>
    
    127
    -              <class name="dim-label"/>
    
    128
    -            </style>
    
    116
    +            <property name="icon-name">list-remove-symbolic</property>
    
    129 117
               </object>
    
    130
    -          <packing>
    
    131
    -            <property name="expand">False</property>
    
    132
    -            <property name="fill">True</property>
    
    133
    -            <property name="position">3</property>
    
    134
    -          </packing>
    
    135 118
             </child>
    
    119
    +      </object>
    
    120
    +      <packing>
    
    121
    +        <property name="expand">False</property>
    
    122
    +        <property name="fill">True</property>
    
    123
    +        <property name="position">2</property>
    
    124
    +      </packing>
    
    125
    +    </child>
    
    126
    +  </object>
    
    127
    +  <object class="GtkStack" id="stack">
    
    128
    +    <property name="visible">True</property>
    
    129
    +    <property name="can-focus">False</property>
    
    130
    +    <property name="transition-type">slide-left-right</property>
    
    131
    +    <child>
    
    132
    +      <object class="GtkBox">
    
    133
    +        <property name="visible">True</property>
    
    134
    +        <property name="can-focus">False</property>
    
    135
    +        <property name="orientation">vertical</property>
    
    136
    +        <property name="spacing">12</property>
    
    136 137
             <child>
    
    137 138
               <object class="GtkScrolledWindow">
    
    138
    -            <property name="height-request">70</property>
    
    139
    +            <property name="width-request">500</property>
    
    140
    +            <property name="height-request">350</property>
    
    139 141
                 <property name="visible">True</property>
    
    140 142
                 <property name="can-focus">True</property>
    
    141 143
                 <property name="hscrollbar-policy">never</property>
    
    142 144
                 <property name="shadow-type">in</property>
    
    143 145
                 <child>
    
    144
    -              <object class="GtkTextView" id="description">
    
    146
    +              <object class="GtkViewport">
    
    145 147
                     <property name="visible">True</property>
    
    146
    -                <property name="can-focus">True</property>
    
    147
    -                <property name="wrap-mode">word-char</property>
    
    148
    -                <property name="left-margin">6</property>
    
    149
    -                <property name="right-margin">6</property>
    
    150
    -                <property name="top-margin">6</property>
    
    151
    -                <property name="bottom-margin">6</property>
    
    148
    +                <property name="can-focus">False</property>
    
    149
    +                <child>
    
    150
    +                  <object class="GtkListBox" id="listbox">
    
    151
    +                    <property name="visible">True</property>
    
    152
    +                    <property name="can-focus">False</property>
    
    153
    +                    <property name="selection-mode">none</property>
    
    154
    +                    <property name="activate-on-single-click">False</property>
    
    155
    +                    <signal name="add" handler="_on_files_changed" swapped="no"/>
    
    156
    +                    <signal name="remove" handler="_on_files_changed" swapped="no"/>
    
    157
    +                    <child type="placeholder">
    
    158
    +                      <object class="GtkBox">
    
    159
    +                        <property name="visible">True</property>
    
    160
    +                        <property name="can-focus">False</property>
    
    161
    +                        <property name="halign">center</property>
    
    162
    +                        <property name="valign">center</property>
    
    163
    +                        <property name="orientation">vertical</property>
    
    164
    +                        <property name="spacing">18</property>
    
    165
    +                        <child>
    
    166
    +                          <object class="GtkLabel">
    
    167
    +                            <property name="visible">True</property>
    
    168
    +                            <property name="can-focus">False</property>
    
    169
    +                            <property name="label" translatable="yes">Drop files here</property>
    
    170
    +                            <style>
    
    171
    +                              <class name="dim-label"/>
    
    172
    +                            </style>
    
    173
    +                          </object>
    
    174
    +                          <packing>
    
    175
    +                            <property name="expand">False</property>
    
    176
    +                            <property name="fill">True</property>
    
    177
    +                            <property name="position">0</property>
    
    178
    +                          </packing>
    
    179
    +                        </child>
    
    180
    +                        <style>
    
    181
    +                          <class name="dnd-area"/>
    
    182
    +                        </style>
    
    183
    +                      </object>
    
    184
    +                    </child>
    
    185
    +                    <style>
    
    186
    +                      <class name="gajim-listbox"/>
    
    187
    +                    </style>
    
    188
    +                  </object>
    
    189
    +                </child>
    
    152 190
                   </object>
    
    153 191
                 </child>
    
    154 192
               </object>
    
    155 193
               <packing>
    
    156
    -            <property name="expand">False</property>
    
    194
    +            <property name="expand">True</property>
    
    157 195
                 <property name="fill">True</property>
    
    158
    -            <property name="position">4</property>
    
    196
    +            <property name="position">1</property>
    
    159 197
               </packing>
    
    160 198
             </child>
    
    161 199
             <child>
    
    162
    -          <object class="GtkButton" id="files_send">
    
    163
    -            <property name="label" translatable="yes">_Send Files</property>
    
    200
    +          <object class="GtkButton">
    
    201
    +            <property name="label" translatable="yes">Add Files…</property>
    
    164 202
                 <property name="visible">True</property>
    
    165
    -            <property name="sensitive">False</property>
    
    166 203
                 <property name="can-focus">True</property>
    
    167 204
                 <property name="receives-default">True</property>
    
    168 205
                 <property name="halign">center</property>
    
    169
    -            <property name="use-underline">True</property>
    
    170
    -            <signal name="clicked" handler="_on_send_clicked" swapped="no"/>
    
    171
    -            <style>
    
    172
    -              <class name="suggested-action"/>
    
    173
    -            </style>
    
    206
    +            <signal name="clicked" handler="_on_choose_files_clicked" swapped="no"/>
    
    174 207
               </object>
    
    175 208
               <packing>
    
    176 209
                 <property name="expand">False</property>
    
    177 210
                 <property name="fill">True</property>
    
    178
    -            <property name="position">5</property>
    
    211
    +            <property name="position">2</property>
    
    179 212
               </packing>
    
    180 213
             </child>
    
    181 214
           </object>
    
    182 215
           <packing>
    
    183
    -        <property name="name">file_selection</property>
    
    216
    +        <property name="name">file-selection</property>
    
    184 217
           </packing>
    
    185 218
         </child>
    
    186 219
         <child>
    
    187 220
           <object class="GtkBox" id="resource_box">
    
    188 221
             <property name="visible">True</property>
    
    189 222
             <property name="can-focus">False</property>
    
    190
    -        <property name="border-width">18</property>
    
    191 223
             <property name="orientation">vertical</property>
    
    192
    -        <property name="spacing">6</property>
    
    224
    +        <property name="spacing">18</property>
    
    193 225
             <child>
    
    194 226
               <object class="GtkLabel">
    
    195 227
                 <property name="visible">True</property>
    
    196 228
                 <property name="can-focus">False</property>
    
    197 229
                 <property name="label" translatable="yes">Choose Device</property>
    
    198 230
                 <style>
    
    199
    -              <class name="large-header"/>
    
    231
    +              <class name="bold16"/>
    
    200 232
                 </style>
    
    201 233
               </object>
    
    202 234
               <packing>
    
    ... ... @@ -205,27 +237,6 @@
    205 237
                 <property name="position">0</property>
    
    206 238
               </packing>
    
    207 239
             </child>
    
    208
    -        <child>
    
    209
    -          <object class="GtkButton" id="resource_send">
    
    210
    -            <property name="label" translatable="yes">_Send Files</property>
    
    211
    -            <property name="visible">True</property>
    
    212
    -            <property name="sensitive">False</property>
    
    213
    -            <property name="can-focus">True</property>
    
    214
    -            <property name="receives-default">False</property>
    
    215
    -            <property name="halign">center</property>
    
    216
    -            <property name="use-underline">True</property>
    
    217
    -            <signal name="clicked" handler="_on_send_to_resource" swapped="no"/>
    
    218
    -            <style>
    
    219
    -              <class name="suggested-action"/>
    
    220
    -            </style>
    
    221
    -          </object>
    
    222
    -          <packing>
    
    223
    -            <property name="expand">False</property>
    
    224
    -            <property name="fill">True</property>
    
    225
    -            <property name="pack-type">end</property>
    
    226
    -            <property name="position">1</property>
    
    227
    -          </packing>
    
    228
    -        </child>
    
    229 240
             <child>
    
    230 241
               <object class="GtkLabel" id="resource_instructions">
    
    231 242
                 <property name="visible">True</property>
    
    ... ... @@ -240,7 +251,7 @@
    240 251
               <packing>
    
    241 252
                 <property name="expand">False</property>
    
    242 253
                 <property name="fill">True</property>
    
    243
    -            <property name="position">2</property>
    
    254
    +            <property name="position">1</property>
    
    244 255
               </packing>
    
    245 256
             </child>
    
    246 257
             <child>
    
    ... ... @@ -248,7 +259,7 @@
    248 259
             </child>
    
    249 260
           </object>
    
    250 261
           <packing>
    
    251
    -        <property name="name">resource_selection</property>
    
    262
    +        <property name="name">resource-selection</property>
    
    252 263
             <property name="position">1</property>
    
    253 264
           </packing>
    
    254 265
         </child>
    

  • gajim/data/gui/message_actions_box.ui
    ... ... @@ -117,6 +117,7 @@
    117 117
             <property name="focus-on-click">False</property>
    
    118 118
             <property name="receives-default">True</property>
    
    119 119
             <property name="action-name">win.send-file</property>
    
    120
    +        <property name="action-target">['']</property>
    
    120 121
             <property name="relief">none</property>
    
    121 122
             <child>
    
    122 123
               <object class="GtkImage">
    

  • gajim/data/style/gajim.css
    ... ... @@ -920,9 +920,6 @@ #FeaturesInfoGrid > list > row { padding: 10px 20px 10px 10px; }
    920 920
     #FeaturesInfoGrid > list > label { padding:10px; color: @insensitive_fg_color; font-weight: bold; }
    
    921 921
     #FeaturesInfoGrid > list > row.activatable:active { box-shadow: none; }
    
    922 922
     
    
    923
    -/* SendFileDialog */
    
    924
    -#SendFileDialog grid list > row { padding: 10px 20px 10px 10px; }
    
    925
    -
    
    926 923
     /* AccountsWindow */
    
    927 924
     .accounts-menu {
    
    928 925
         border-right: 1px solid @borders;
    

  • gajim/gtk/builder.pyi
    ... ... @@ -326,13 +326,17 @@ class FileTransferJingleBuilder(Builder):
    326 326
         progress_bar: Gtk.ProgressBar
    
    327 327
     
    
    328 328
     
    
    329
    -class FileTransferSendBuilder(Builder):
    
    330
    -    send_stack: Gtk.Stack
    
    329
    +class FileTransferSelectorBuilder(Builder):
    
    330
    +    file_box: Gtk.Box
    
    331
    +    preview_image_box: Gtk.Box
    
    332
    +    preview_image: Gtk.Image
    
    333
    +    file_name_label: Gtk.Label
    
    334
    +    file_size_label: Gtk.Label
    
    335
    +    warning_label: Gtk.Label
    
    336
    +    remove_file_button: Gtk.Button
    
    337
    +    stack: Gtk.Stack
    
    331 338
         listbox: Gtk.ListBox
    
    332
    -    description: Gtk.TextView
    
    333
    -    files_send: Gtk.Button
    
    334 339
         resource_box: Gtk.Box
    
    335
    -    resource_send: Gtk.Button
    
    336 340
         resource_instructions: Gtk.Label
    
    337 341
     
    
    338 342
     
    
    ... ... @@ -964,7 +968,7 @@ def get_builder(file_name: Literal['file_transfer.ui'], widgets: list[str] = ...
    964 968
     @overload
    
    965 969
     def get_builder(file_name: Literal['file_transfer_jingle.ui'], widgets: list[str] = ...) -> FileTransferJingleBuilder: ...
    
    966 970
     @overload
    
    967
    -def get_builder(file_name: Literal['file_transfer_send.ui'], widgets: list[str] = ...) -> FileTransferSendBuilder: ...
    
    971
    +def get_builder(file_name: Literal['file_transfer_selector.ui'], widgets: list[str] = ...) -> FileTransferSelectorBuilder: ...
    
    968 972
     @overload
    
    969 973
     def get_builder(file_name: Literal['filetransfers.ui'], widgets: list[str] = ...) -> FiletransfersBuilder: ...
    
    970 974
     @overload
    

  • gajim/gtk/chat_function_page.py
    ... ... @@ -17,6 +17,7 @@
    17 17
     from typing import Optional
    
    18 18
     
    
    19 19
     from enum import Enum
    
    20
    +from pathlib import Path
    
    20 21
     
    
    21 22
     from gi.repository import GObject
    
    22 23
     from gi.repository import Gtk
    
    ... ... @@ -33,11 +34,13 @@
    33 34
     from gajim.common.modules.contacts import GroupchatContact
    
    34 35
     
    
    35 36
     from .dataform import DataFormWidget
    
    37
    +from .file_transfer_selector import FileTransferSelector
    
    36 38
     from .groupchat_inviter import GroupChatInviter
    
    37 39
     
    
    38 40
     
    
    39 41
     class FunctionMode(Enum):
    
    40 42
         INVITE = 'invite'
    
    43
    +    SEND_FILE = 'send-file'
    
    41 44
         CHANGE_NICKNAME = 'change-nickname'
    
    42 45
         KICK = 'kick'
    
    43 46
         BAN = 'ban'
    
    ... ... @@ -135,7 +138,8 @@ def _reset(self) -> None:
    135 138
         def set_mode(self,
    
    136 139
                      contact: types.ChatContactT,
    
    137 140
                      mode: FunctionMode,
    
    138
    -                 data: Optional[str] = None
    
    141
    +                 data: Optional[str] = None,
    
    142
    +                 files: Optional[list[str]] = None
    
    139 143
                      ) -> None:
    
    140 144
     
    
    141 145
             self._reset()
    
    ... ... @@ -159,6 +163,15 @@ def set_mode(self,
    159 163
                 self._widget.connect('listbox-changed', self._on_ready)
    
    160 164
                 self._widget.load_contacts()
    
    161 165
     
    
    166
    +        elif mode == FunctionMode.SEND_FILE:
    
    167
    +            self._confirm_button.set_label(_('Send Files'))
    
    168
    +            self._confirm_button.get_style_context().add_class(
    
    169
    +                'suggested-action')
    
    170
    +            self._widget = FileTransferSelector(self._contact, data)
    
    171
    +            self._widget.connect('changed', self._on_ready)
    
    172
    +            if files is not None:
    
    173
    +                self._widget.add_files(files)
    
    174
    +
    
    162 175
             elif mode == FunctionMode.CHANGE_NICKNAME:
    
    163 176
                 self._confirm_button.set_label(_('Change'))
    
    164 177
                 self._confirm_button.get_style_context().add_class(
    
    ... ... @@ -264,6 +277,13 @@ def _on_confirm_clicked(self, _button: Gtk.Button) -> None:
    264 277
                 for jid in invitees:
    
    265 278
                     self._invite(JID.from_string(jid))
    
    266 279
     
    
    280
    +        elif self._mode == FunctionMode.SEND_FILE:
    
    281
    +            assert isinstance(self._widget, FileTransferSelector)
    
    282
    +            if self._widget.transfer_resource_required():
    
    283
    +                return
    
    284
    +
    
    285
    +            self._send_files(self._widget.get_catalog())
    
    286
    +
    
    267 287
             elif self._mode == FunctionMode.CHANGE_NICKNAME:
    
    268 288
                 assert isinstance(self._widget, InputWidget)
    
    269 289
                 nickname = self._widget.get_text()
    
    ... ... @@ -347,6 +367,24 @@ def _invite(self, invited_jid: JID) -> None:
    347 367
                 'message',
    
    348 368
                 _('%s has been invited to this group chat') % invited_contact.name)
    
    349 369
     
    
    370
    +    def _send_files(self, catalog: list[tuple[Path, str, JID]]) -> None:
    
    371
    +        assert self._contact is not None
    
    372
    +        client = app.get_client(self._contact.account)
    
    373
    +
    
    374
    +        # catalog: list[(file Path, transfer method, recipient JID)]
    
    375
    +        for path, method, jid in catalog:
    
    376
    +            if method == 'httpupload':
    
    377
    +                client.get_module('HTTPUpload').send_file(
    
    378
    +                    self._contact, path)
    
    379
    +                continue
    
    380
    +
    
    381
    +            # Send using Jingle
    
    382
    +            app.interface.instances['file_transfers'].send_file(
    
    383
    +                self._contact.account,
    
    384
    +                self._contact,
    
    385
    +                jid,
    
    386
    +                str(path))
    
    387
    +
    
    350 388
     
    
    351 389
     class InputWidget(Gtk.Box):
    
    352 390
     
    

  • gajim/gtk/chat_stack.py
    ... ... @@ -564,16 +564,19 @@ def _on_action(self,
    564 564
                 app.call_manager.start_call(account, jid, CallType.VIDEO)
    
    565 565
     
    
    566 566
             elif action_name.startswith('send-file'):
    
    567
    +            method = None
    
    567 568
                 name = action.get_name()
    
    568 569
                 if 'httpupload' in name:
    
    569
    -                app.interface.start_file_transfer(contact, method='httpupload')
    
    570
    -                return
    
    570
    +                method = 'httpupload'
    
    571 571
     
    
    572 572
                 if 'jingle' in name:
    
    573
    -                app.interface.start_file_transfer(contact, method='jingle')
    
    574
    -                return
    
    573
    +                method = 'jingle'
    
    575 574
     
    
    576
    -            app.interface.start_file_transfer(contact)
    
    575
    +            file_paths = None
    
    576
    +            if param is not None:
    
    577
    +                file_paths = param.get_strv() or None
    
    578
    +            self._show_chat_function_page(
    
    579
    +                FunctionMode.SEND_FILE, method, file_paths)
    
    577 580
     
    
    578 581
             elif action_name == 'add-to-roster':
    
    579 582
                 if (isinstance(contact, GroupchatParticipant) and
    
    ... ... @@ -676,12 +679,13 @@ def _on_drag_motion(self,
    676 679
     
    
    677 680
         def _show_chat_function_page(self,
    
    678 681
                                      function_mode: FunctionMode,
    
    679
    -                                 data: Optional[str] = None
    
    682
    +                                 data: Optional[str] = None,
    
    683
    +                                 files: Optional[list[str]] = None
    
    680 684
                                      ) -> None:
    
    681 685
     
    
    682 686
             assert self._current_contact is not None
    
    683 687
             self._chat_function_page.set_mode(
    
    684
    -            self._current_contact, function_mode, data)
    
    688
    +            self._current_contact, function_mode, data, files)
    
    685 689
             self.set_transition_type(Gtk.StackTransitionType.SLIDE_UP_DOWN)
    
    686 690
             self.set_visible_child_name('function')
    
    687 691
     
    

  • gajim/gtk/const.py
    ... ... @@ -198,9 +198,9 @@ def __str__(self):
    198 198
         ('retract-message', 'a{sv}', False),
    
    199 199
         ('quote', 's', False),
    
    200 200
         ('mention', 's', False),
    
    201
    -    ('send-file-httpupload', None, False),
    
    202
    -    ('send-file-jingle', None, False),
    
    203
    -    ('send-file', None, False),
    
    201
    +    ('send-file-httpupload', 'as', False),
    
    202
    +    ('send-file-jingle', 'as', False),
    
    203
    +    ('send-file', 'as', False),
    
    204 204
         ('add-to-roster', None, True),
    
    205 205
         ('start-voice-call', None, False),
    
    206 206
         ('start-video-call', None, False),
    

  • gajim/gtk/control.py
    ... ... @@ -233,6 +233,8 @@ def _register_events(self) -> None:
    233 233
                  ged.GUI2, self._on_jingle_request_received),
    
    234 234
                 ('file-request-received', ged.GUI2, self._on_file_request_event),
    
    235 235
                 ('file-request-sent', ged.GUI2, self._on_file_request_event),
    
    236
    +            ('http-upload-started', ged.GUI2, self._on_http_upload_started),
    
    237
    +            ('http-upload-error', ged.GUI2, self._on_http_upload_error),
    
    236 238
             ])
    
    237 239
     
    
    238 240
         def _is_event_processable(self, event: Any) -> bool:
    
    ... ... @@ -440,6 +442,12 @@ def _on_file_request_event(self,
    440 442
     
    
    441 443
             self.add_jingle_file_transfer(event=event)
    
    442 444
     
    
    445
    +    def _on_http_upload_started(self, event: events.HTTPUploadStarted) -> None:
    
    446
    +        self.add_file_transfer(event.transfer)
    
    447
    +
    
    448
    +    def _on_http_upload_error(self, event: events.HTTPUploadError) -> None:
    
    449
    +        self.add_info_message(event.error_msg)
    
    450
    +
    
    443 451
         @property
    
    444 452
         def is_chat(self) -> bool:
    
    445 453
             return isinstance(self.contact, BareContact)
    
    ... ... @@ -488,22 +496,18 @@ def _on_jump_to_end(self, _button: Gtk.Button) -> None:
    488 496
             self.reset_view()
    
    489 497
     
    
    490 498
         def drag_data_file_transfer(self, selection: Gtk.SelectionData) -> None:
    
    491
    -        # we may have more than one file dropped
    
    492
    -        uri_splitted = selection.get_uris()
    
    493
    -        for uri in uri_splitted:
    
    499
    +        uris = selection.get_uris()
    
    500
    +        paths: list[str] = []
    
    501
    +        for uri in uris:
    
    494 502
                 path = get_file_path_from_dnd_dropped_uri(uri)
    
    495
    -            if not path:
    
    496
    -                self.add_info_message(
    
    497
    -                    _('The following address was not understood and was '
    
    498
    -                      'not uploaded: %s') % uri)
    
    499
    -                continue
    
    500
    -            if not path.is_file():  # is it a regular file?
    
    503
    +            if path is None or not path.is_file():
    
    501 504
                     self.add_info_message(
    
    502
    -                    _('The following file could not be accessed and was '
    
    503
    -                      'not uploaded: %s') % path)
    
    505
    +                    _('You dropped something Gajim could not '
    
    506
    +                      'process: %s' % path))
    
    504 507
                     continue
    
    508
    +            paths.append(str(path))
    
    505 509
     
    
    506
    -            app.interface.start_file_transfer(self.contact, str(path))
    
    510
    +        app.window.activate_action('send-file', GLib.Variant('as', paths))
    
    507 511
     
    
    508 512
         def get_our_nick(self) -> str:
    
    509 513
             if isinstance(self.contact, GroupchatParticipant):
    

  • gajim/gtk/file_transfer_selector.py
    1
    +# This file is part of Gajim.
    
    2
    +#
    
    3
    +# Gajim is free software; you can redistribute it and/or modify
    
    4
    +# it under the terms of the GNU General Public License as published
    
    5
    +# by the Free Software Foundation; version 3 only.
    
    6
    +#
    
    7
    +# Gajim is distributed in the hope that it will be useful,
    
    8
    +# but WITHOUT ANY WARRANTY; without even the implied warranty of
    
    9
    +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    
    10
    +# GNU General Public License for more details.
    
    11
    +#
    
    12
    +# You should have received a copy of the GNU General Public License
    
    13
    +# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
    
    14
    +
    
    15
    +from __future__ import annotations
    
    16
    +
    
    17
    +from typing import Any
    
    18
    +from typing import cast
    
    19
    +from typing import Optional
    
    20
    +from typing import Union
    
    21
    +
    
    22
    +import logging
    
    23
    +from pathlib import Path
    
    24
    +
    
    25
    +from gi.repository import Gdk
    
    26
    +from gi.repository import GLib
    
    27
    +from gi.repository import GObject
    
    28
    +from gi.repository import Gtk
    
    29
    +
    
    30
    +from nbxmpp.namespaces import Namespace
    
    31
    +from nbxmpp.protocol import JID
    
    32
    +
    
    33
    +from gajim.common import app
    
    34
    +from gajim.common import types
    
    35
    +from gajim.common.helpers import get_file_path_from_dnd_dropped_uri
    
    36
    +from gajim.common.helpers import load_file_async
    
    37
    +from gajim.common.i18n import _
    
    38
    +from gajim.common.modules.contacts import BareContact
    
    39
    +from gajim.common.preview import PREVIEWABLE_MIME_TYPES
    
    40
    +from gajim.common.preview_helpers import create_thumbnail
    
    41
    +from gajim.common.preview_helpers import guess_mime_type
    
    42
    +from gajim.common.preview_helpers import get_icon_for_mime_type
    
    43
    +from gajim.common.preview_helpers import pixbuf_from_data
    
    44
    +
    
    45
    +from .builder import get_builder
    
    46
    +from .filechoosers import FileChooserDialog
    
    47
    +from .resource_selector import ResourceSelector
    
    48
    +
    
    49
    +PREVIEW_SIZE = 72
    
    50
    +
    
    51
    +log = logging.getLogger('gajim.gui.file_transfer_selector')
    
    52
    +
    
    53
    +
    
    54
    +class FileTransferSelector(Gtk.Box):
    
    55
    +
    
    56
    +    __gsignals__ = {
    
    57
    +        'changed': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
    
    58
    +    }
    
    59
    +
    
    60
    +    def __init__(self,
    
    61
    +                 contact: types.ChatContactT,
    
    62
    +                 method: Optional[str] = None
    
    63
    +                 ) -> None:
    
    64
    +
    
    65
    +        Gtk.Box.__init__(self)
    
    66
    +
    
    67
    +        self._contact = contact
    
    68
    +        self._method = method or app.window.get_preferred_ft_method(contact)
    
    69
    +
    
    70
    +        client = app.get_client(contact.account)
    
    71
    +        self._max_file_size = client.get_module('HTTPUpload').max_file_size or 0
    
    72
    +
    
    73
    +        self._ui = get_builder('file_transfer_selector.ui')
    
    74
    +        self.add(self._ui.stack)
    
    75
    +
    
    76
    +        self.connect('destroy', self._on_destroy)
    
    77
    +        self._ui.connect_signals(self)
    
    78
    +
    
    79
    +        self._resource_selector = None
    
    80
    +        if isinstance(contact, BareContact):
    
    81
    +            # No jingle file transfers in group chats
    
    82
    +            self._resource_selector = ResourceSelector(
    
    83
    +                contact,
    
    84
    +                constraints=[Namespace.JINGLE_FILE_TRANSFER_5])
    
    85
    +            self._resource_selector.connect(
    
    86
    +                'selection-changed', self._on_resource_selection)
    
    87
    +            self._ui.resource_box.pack_start(
    
    88
    +                self._resource_selector, True, False, 0)
    
    89
    +
    
    90
    +            self._ui.resource_instructions.set_text(
    
    91
    +                _('%s is online with multiple devices.\n'
    
    92
    +                  'Choose the device you would like to send the '
    
    93
    +                  'files to.') % self._contact.name)
    
    94
    +
    
    95
    +        uri_entry = Gtk.TargetEntry.new(
    
    96
    +            'text/uri-list', Gtk.TargetFlags.OTHER_APP, 80)
    
    97
    +        dst_targets = Gtk.TargetList.new([uri_entry])
    
    98
    +
    
    99
    +        self._ui.listbox.drag_dest_set(
    
    100
    +            Gtk.DestDefaults.ALL,
    
    101
    +            [uri_entry],
    
    102
    +            Gdk.DragAction.COPY | Gdk.DragAction.MOVE)
    
    103
    +        self._ui.listbox.drag_dest_set_target_list(dst_targets)
    
    104
    +        self._ui.listbox.connect(
    
    105
    +            'drag-data-received', self._on_drag_data_received)
    
    106
    +
    
    107
    +        self.show_all()
    
    108
    +
    
    109
    +    def transfer_resource_required(self) -> bool:
    
    110
    +        if not isinstance(self._contact, BareContact):
    
    111
    +            # No jingle file transfer for group chats
    
    112
    +            return False
    
    113
    +
    
    114
    +        if self._method is None:
    
    115
    +            log.error('No file transfer method available')
    
    116
    +            return False
    
    117
    +
    
    118
    +        current_page = self._ui.stack.get_visible_child_name()
    
    119
    +        if self._method == 'httpupload':
    
    120
    +            for path in self._get_file_paths():
    
    121
    +                if path.stat().st_size > self._max_file_size:
    
    122
    +                    if current_page == 'resource-selection':
    
    123
    +                        return False
    
    124
    +
    
    125
    +                    self._ui.stack.set_visible_child_name('resource-selection')
    
    126
    +                    self.emit('changed', False)
    
    127
    +                    return True
    
    128
    +            return False
    
    129
    +
    
    130
    +        if current_page == 'resource-selection':
    
    131
    +            return False
    
    132
    +
    
    133
    +        self._ui.stack.set_visible_child_name('resource-selection')
    
    134
    +        self.emit('changed', False)
    
    135
    +        return True
    
    136
    +
    
    137
    +    def get_catalog(self) -> list[tuple[Path, str, JID]]:
    
    138
    +        # catalog: list[(file Path, transfer method, recipient JID)]
    
    139
    +        catalog: list[tuple[Path, str, JID]] = []
    
    140
    +
    
    141
    +        file_paths = self._get_file_paths()
    
    142
    +        for path in file_paths:
    
    143
    +            if (self._method == 'jingle' or
    
    144
    +                    path.stat().st_size > self._max_file_size):
    
    145
    +                assert self._resource_selector is not None
    
    146
    +                item = (path, 'jingle', self._resource_selector.get_jid())
    
    147
    +            elif self._method == 'httpupload':
    
    148
    +                item = (path, 'httpupload', self._contact.jid)
    
    149
    +            else:
    
    150
    +                # No file transfer method available
    
    151
    +                continue
    
    152
    +
    
    153
    +            catalog.append(item)
    
    154
    +
    
    155
    +        return catalog
    
    156
    +
    
    157
    +    def add_files(self, file_paths: Union[list[Path], list[str]]) -> None:
    
    158
    +        for path in file_paths:
    
    159
    +            if isinstance(path, str):
    
    160
    +                path = Path(path)
    
    161
    +
    
    162
    +            if path.is_dir():
    
    163
    +                continue
    
    164
    +
    
    165
    +            size_warning = bool(self._method == 'httpupload' and
    
    166
    +                                path.stat().st_size > self._max_file_size)
    
    167
    +            jingle_warning = bool(self._method == 'jingle')
    
    168
    +
    
    169
    +            row = FileRow(path, size_warning, jingle_warning)
    
    170
    +            self._ui.listbox.add(row)
    
    171
    +            app.settings.set('last_send_dir', str(path))
    
    172
    +
    
    173
    +        self._ui.listbox.show_all()
    
    174
    +
    
    175
    +    def _on_destroy(self, _widget: FileTransferSelector) -> None:
    
    176
    +        app.check_finalize(self)
    
    177
    +
    
    178
    +    def _get_file_paths(self) -> list[Path]:
    
    179
    +        paths: list[Path] = []
    
    180
    +        for row in cast(list[FileRow], self._ui.listbox.get_children()):
    
    181
    +            paths.append(row.file_path)
    
    182
    +
    
    183
    +        return paths
    
    184
    +
    
    185
    +    def _on_drag_data_received(self,
    
    186
    +                               _widget: Gtk.Widget,
    
    187
    +                               _context: Gdk.DragContext,
    
    188
    +                               _x_coord: int,
    
    189
    +                               _y_coord: int,
    
    190
    +                               selection: Gtk.SelectionData,
    
    191
    +                               target_type: int,
    
    192
    +                               _timestamp: int
    
    193
    +                               ) -> None:
    
    194
    +        if not selection.get_data():
    
    195
    +            return
    
    196
    +
    
    197
    +        if target_type == 80:
    
    198
    +            uris = selection.get_uris()
    
    199
    +            paths: list[Path] = []
    
    200
    +            for uri in uris:
    
    201
    +                path = get_file_path_from_dnd_dropped_uri(uri)
    
    202
    +                if path is None or not path.is_file():
    
    203
    +                    log.warning('Could not add file %s', str(path))
    
    204
    +                    continue
    
    205
    +                paths.append(path)
    
    206
    +
    
    207
    +            self.add_files(paths)
    
    208
    +
    
    209
    +    def _on_files_changed(self, _listbox: Gtk.ListBox, _row: FileRow) -> None:
    
    210
    +        file_paths = self._get_file_paths()
    
    211
    +        if not file_paths:
    
    212
    +            self.emit('changed', False)
    
    213
    +            return
    
    214
    +
    
    215
    +        if self._method == 'httpupload' and self._contact.is_groupchat:
    
    216
    +            # Enforce HTTPUpload file size limit for group chats
    
    217
    +            for path in file_paths:
    
    218
    +                if path.stat().st_size > self._max_file_size:
    
    219
    +                    self.emit('changed', False)
    
    220
    +                    return
    
    221
    +
    
    222
    +        self.emit('changed', True)
    
    223
    +
    
    224
    +    def _on_resource_selection(self,
    
    225
    +                               _selector: ResourceSelector,
    
    226
    +                               state: bool
    
    227
    +                               ) -> None:
    
    228
    +
    
    229
    +        self.emit('changed', state)
    
    230
    +
    
    231
    +    def _on_choose_files_clicked(self, _button: Gtk.Button) -> None:
    
    232
    +        FileChooserDialog(self.add_files,
    
    233
    +                          select_multiple=True,
    
    234
    +                          transient_for=app.window,
    
    235
    +                          path=app.settings.get('last_send_dir'))
    
    236
    +
    
    237
    +
    
    238
    +class FileRow(Gtk.ListBoxRow):
    
    239
    +    def __init__(self,
    
    240
    +                 path: Path,
    
    241
    +                 size_warning: bool,
    
    242
    +                 jingle_warning: bool
    
    243
    +                 ) -> None:
    
    244
    +
    
    245
    +        Gtk.ListBoxRow.__init__(self)
    
    246
    +        self.file_path = path
    
    247
    +
    
    248
    +        self._ui = get_builder('file_transfer_selector.ui')
    
    249
    +        self.add(self._ui.file_box)
    
    250
    +
    
    251
    +        self._ui.remove_file_button.connect('clicked', self._on_remove_clicked)
    
    252
    +
    
    253
    +        self._ui.file_name_label.set_text(path.name)
    
    254
    +
    
    255
    +        if app.settings.get('use_kib_mib'):
    
    256
    +            units = GLib.FormatSizeFlags.IEC_UNITS
    
    257
    +        else:
    
    258
    +            units = GLib.FormatSizeFlags.DEFAULT
    
    259
    +
    
    260
    +        file_size_string = GLib.format_size_full(path.stat().st_size, units)
    
    261
    +        self._ui.file_size_label.set_text(file_size_string)
    
    262
    +
    
    263
    +        if size_warning:
    
    264
    +            self._ui.warning_label.set_text(
    
    265
    +                _('File too big, will use direct transfer (not encrypted)'))
    
    266
    +            self._ui.warning_label.show()
    
    267
    +
    
    268
    +        if jingle_warning:
    
    269
    +            self._ui.warning_label.set_text(
    
    270
    +                _('Direct transfer (not encrypted)'))
    
    271
    +            self._ui.warning_label.show()
    
    272
    +
    
    273
    +        self._ui.preview_image_box.set_size_request(PREVIEW_SIZE, -1)
    
    274
    +        self._set_preview(path)
    
    275
    +
    
    276
    +    def _set_preview(self, path: Path) -> None:
    
    277
    +        mime_type = guess_mime_type(path)
    
    278
    +        if mime_type not in PREVIEWABLE_MIME_TYPES:
    
    279
    +            self._set_icon_for_mime_type(mime_type)
    
    280
    +            return
    
    281
    +
    
    282
    +        load_file_async(self.file_path, self._on_load_finished, mime_type)
    
    283
    +
    
    284
    +    def _on_load_finished(self,
    
    285
    +                          data: Optional[bytes],
    
    286
    +                          _error: Optional[GLib.Error],
    
    287
    +                          mime_type: Any
    
    288
    +                          ) -> None:
    
    289
    +
    
    290
    +        if data is None:
    
    291
    +            self._set_icon_for_mime_type(mime_type)
    
    292
    +            return
    
    293
    +
    
    294
    +        preview_bytes = create_thumbnail(data, PREVIEW_SIZE, mime_type)
    
    295
    +        if preview_bytes is None:
    
    296
    +            self._set_icon_for_mime_type(mime_type)
    
    297
    +            return
    
    298
    +
    
    299
    +        pixbuf = pixbuf_from_data(preview_bytes)
    
    300
    +        if pixbuf is None:
    
    301
    +            self._set_icon_for_mime_type(mime_type)
    
    302
    +            return
    
    303
    +
    
    304
    +        self._ui.preview_image.set_from_pixbuf(pixbuf)
    
    305
    +
    
    306
    +    def _set_icon_for_mime_type(self, mime_type: str) -> None:
    
    307
    +        icon = get_icon_for_mime_type(mime_type)
    
    308
    +        self._ui.preview_image.set_from_gicon(icon, Gtk.IconSize.DIALOG)
    
    309
    +
    
    310
    +    def _on_remove_clicked(self, _button: Gtk.Button) -> None:
    
    311
    +        listbox = cast(Gtk.ListBox, self.get_parent())
    
    312
    +        listbox.remove(self)
    
    313
    +        self.destroy()

  • gajim/gtk/file_transfer_send.py deleted
    1
    -# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
    
    2
    -# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
    
    3
    -# Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
    
    4
    -# Copyright (C) 2006 Travis Shirk <travis AT pobox.com>
    
    5
    -#
    
    6
    -# This file is part of Gajim.
    
    7
    -#
    
    8
    -# Gajim is free software; you can redistribute it and/or modify
    
    9
    -# it under the terms of the GNU General Public License as published
    
    10
    -# by the Free Software Foundation; version 3 only.
    
    11
    -#
    
    12
    -# Gajim is distributed in the hope that it will be useful,
    
    13
    -# but WITHOUT ANY WARRANTY; without even the implied warranty of
    
    14
    -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    
    15
    -# GNU General Public License for more details.
    
    16
    -#
    
    17
    -# You should have received a copy of the GNU General Public License
    
    18
    -# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
    
    19
    -
    
    20
    -from __future__ import annotations
    
    21
    -
    
    22
    -from typing import Callable
    
    23
    -from typing import cast
    
    24
    -from typing import Optional
    
    25
    -
    
    26
    -from pathlib import Path
    
    27
    -
    
    28
    -from gi.repository import Gdk
    
    29
    -from gi.repository import Gtk
    
    30
    -from gi.repository import Pango
    
    31
    -
    
    32
    -from nbxmpp.namespaces import Namespace
    
    33
    -from nbxmpp.protocol import JID
    
    34
    -
    
    35
    -from gajim.common import app
    
    36
    -from gajim.common.i18n import _
    
    37
    -from gajim.common.modules.contacts import BareContact
    
    38
    -
    
    39
    -from .filechoosers import FileChooserDialog
    
    40
    -from .resource_selector import ResourceSelector
    
    41
    -from .builder import get_builder
    
    42
    -
    
    43
    -
    
    44
    -class SendFileDialog(Gtk.ApplicationWindow):
    
    45
    -    def __init__(self,
    
    46
    -                 contact: BareContact,
    
    47
    -                 send_callback: Callable[..., bool],
    
    48
    -                 transient_for: Gtk.Window,
    
    49
    -                 files: Optional[list[str]] = None
    
    50
    -                 ) -> None:
    
    51
    -        Gtk.ApplicationWindow.__init__(self)
    
    52
    -        self.set_application(app.app)
    
    53
    -        self.set_show_menubar(False)
    
    54
    -        self.set_resizable(True)
    
    55
    -        self.set_default_size(500, 350)
    
    56
    -        self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
    
    57
    -        self.set_transient_for(transient_for)
    
    58
    -        self.set_title(_('Choose a File to Send…'))
    
    59
    -        self.set_destroy_with_parent(True)
    
    60
    -
    
    61
    -        self._contact = contact
    
    62
    -        self._send_callback = send_callback
    
    63
    -
    
    64
    -        self._ui = get_builder('file_transfer_send.ui')
    
    65
    -        self.add(self._ui.send_stack)
    
    66
    -
    
    67
    -        self._ui.resource_instructions.set_text(
    
    68
    -            _('%s is online with multiple devices.\n'
    
    69
    -              'Choose the device you would like to send the '
    
    70
    -              'file to.') % self._contact.name)
    
    71
    -
    
    72
    -        self._resource_selector = ResourceSelector(
    
    73
    -            contact,
    
    74
    -            constraints=[Namespace.JINGLE_FILE_TRANSFER_5])
    
    75
    -        self._resource_selector.connect(
    
    76
    -            'selection-changed', self._on_resource_selection)
    
    77
    -        self._ui.resource_box.pack_start(
    
    78
    -            self._resource_selector, True, False, 0)
    
    79
    -
    
    80
    -        if files is not None:
    
    81
    -            self.set_files(files)
    
    82
    -
    
    83
    -        self.connect('key-press-event', self._on_key_press)
    
    84
    -        self._ui.connect_signals(self)
    
    85
    -        self.show_all()
    
    86
    -
    
    87
    -    def _on_files_changed(self, _listbox: Gtk.ListBox, _row: FileRow) -> None:
    
    88
    -        sensitive = bool(len(self._ui.listbox.get_children()) > 0)
    
    89
    -        self._ui.files_send.set_sensitive(sensitive)
    
    90
    -
    
    91
    -    def _on_resource_selection(self,
    
    92
    -                               _selector: ResourceSelector,
    
    93
    -                               state: bool
    
    94
    -                               ) -> None:
    
    95
    -        self._ui.resource_send.set_sensitive(state)
    
    96
    -
    
    97
    -    def _on_send_to_resource(self, _button: Gtk.Button) -> None:
    
    98
    -        resource_jid = self._resource_selector.get_jid()
    
    99
    -        self._send_files(resource_jid)
    
    100
    -
    
    101
    -    def _on_send_clicked(self, _button: Gtk.Button) -> None:
    
    102
    -        count = len(self._contact.get_resources())
    
    103
    -        if count == 0 or count > 1:
    
    104
    -            self._ui.send_stack.set_visible_child_name('resource_selection')
    
    105
    -            return
    
    106
    -
    
    107
    -        resource_jid = self._contact.get_resources()[0].jid
    
    108
    -        self._send_files(resource_jid)
    
    109
    -
    
    110
    -    def _send_files(self, resource_jid: JID) -> None:
    
    111
    -        files = cast(list[FileRow], self._ui.listbox.get_children())
    
    112
    -        for file in files:
    
    113
    -            self._send_callback(
    
    114
    -                resource_jid, str(file.file_path), self._get_description())
    
    115
    -        self.destroy()
    
    116
    -
    
    117
    -    def _select_files(self, _button: Gtk.Button) -> None:
    
    118
    -        FileChooserDialog(self.set_files,
    
    119
    -                          select_multiple=True,
    
    120
    -                          transient_for=self,
    
    121
    -                          path=app.settings.get('last_send_dir'))
    
    122
    -
    
    123
    -    def _remove_files(self, _button: Gtk.Button) -> None:
    
    124
    -        selected = self._ui.listbox.get_selected_rows()
    
    125
    -        for item in selected:
    
    126
    -            self._ui.listbox.remove(item)
    
    127
    -
    
    128
    -    def set_files(self, file_names: list[str]) -> None:
    
    129
    -        last_dir = ''
    
    130
    -        for file in file_names:
    
    131
    -            row = FileRow(file)
    
    132
    -            if row.file_path.is_dir():
    
    133
    -                continue
    
    134
    -            last_dir = row.file_path.parent
    
    135
    -            self._ui.listbox.add(row)
    
    136
    -        self._ui.listbox.show_all()
    
    137
    -        app.settings.set('last_send_dir', str(last_dir))
    
    138
    -        self._ui.files_send.set_sensitive(True)
    
    139
    -
    
    140
    -    def _get_description(self) -> str:
    
    141
    -        buffer_ = self._ui.description.get_buffer()
    
    142
    -        start, end = buffer_.get_bounds()
    
    143
    -        return buffer_.get_text(start, end, False)
    
    144
    -
    
    145
    -    def _on_key_press(self, _widget: Gtk.Widget, event: Gdk.EventKey) -> None:
    
    146
    -        if event.keyval == Gdk.KEY_Escape:
    
    147
    -            self.destroy()
    
    148
    -
    
    149
    -
    
    150
    -class FileRow(Gtk.ListBoxRow):
    
    151
    -    def __init__(self, path: str) -> None:
    
    152
    -        Gtk.ListBoxRow.__init__(self)
    
    153
    -        self.file_path: Path = Path(path)
    
    154
    -        label = Gtk.Label(label=self.file_path.name)
    
    155
    -        label.set_ellipsize(Pango.EllipsizeMode.END)
    
    156
    -        label.set_xalign(0)
    
    157
    -        self.add(label)

  • gajim/gtk/filetransfer.py
    ... ... @@ -60,7 +60,6 @@
    60 60
     from .dialogs import ConfirmationDialog
    
    61 61
     from .dialogs import ErrorDialog
    
    62 62
     from .filechoosers import FileSaveDialog
    
    63
    -from .file_transfer_send import SendFileDialog
    
    64 63
     from .tooltips import FileTransfersTooltip
    
    65 64
     from .builder import get_builder
    
    66 65
     from .util import format_eta
    
    ... ... @@ -405,16 +404,9 @@ def _on_yes():
    405 404
                                    text=_('_Download Again'),
    
    406 405
                                    callback=_on_yes)]).show()
    
    407 406
     
    
    408
    -    def show_file_send_request(self,
    
    409
    -                               account: str,
    
    410
    -                               contact: types.BareContact
    
    411
    -                               ) -> None:
    
    412
    -        send_callback = partial(self.send_file, account, contact)
    
    413
    -        SendFileDialog(contact, send_callback, self.window)
    
    414
    -
    
    415 407
         def send_file(self,
    
    416 408
                       account: str,
    
    417
    -                  contact: types.BareContact,
    
    409
    +                  contact: types.ResourceContact,
    
    418 410
                       resource_jid: JID,
    
    419 411
                       file_path: str,
    
    420 412
                       file_desc: str = ''
    

  • gajim/gtk/main.py
    ... ... @@ -39,6 +39,7 @@
    39 39
     from gajim.common.i18n import _
    
    40 40
     from gajim.common.helpers import play_sound
    
    41 41
     from gajim.common.modules.bytestream import is_transfer_active
    
    42
    +from gajim.common.modules.contacts import GroupchatContact
    
    42 43
     from gajim.plugins.pluginmanager import PluginManifest
    
    43 44
     from gajim.plugins.repository import PluginRepository
    
    44 45
     
    
    ... ... @@ -984,6 +985,33 @@ def _on_window_active(self,
    984 985
             if control.get_autoscroll():
    
    985 986
                 self.mark_as_read(control.contact.account, control.contact.jid)
    
    986 987
     
    
    988
    +    def get_preferred_ft_method(self,
    
    989
    +                                contact: types.ChatContactT
    
    990
    +                                ) -> Optional[str]:
    
    991
    +
    
    992
    +        ft_pref = app.settings.get_account_setting(
    
    993
    +            contact.account,
    
    994
    +            'filetransfer_preference')
    
    995
    +        httpupload_enabled = app.window.get_action_enabled(
    
    996
    +            'send-file-httpupload')
    
    997
    +        jingle_enabled = app.window.get_action_enabled('send-file-jingle')
    
    998
    +
    
    999
    +        if isinstance(contact, GroupchatContact):
    
    1000
    +            if httpupload_enabled:
    
    1001
    +                return 'httpupload'
    
    1002
    +            return None
    
    1003
    +
    
    1004
    +        if httpupload_enabled and jingle_enabled:
    
    1005
    +            return ft_pref
    
    1006
    +
    
    1007
    +        if httpupload_enabled:
    
    1008
    +            return 'httpupload'
    
    1009
    +
    
    1010
    +        if jingle_enabled:
    
    1011
    +            return 'jingle'
    
    1012
    +
    
    1013
    +        return None
    
    1014
    +
    
    987 1015
         def show_add_join_groupchat(self,
    
    988 1016
                                     account: str,
    
    989 1017
                                     jid: str,
    

  • gajim/gtk/menus.py
    ... ... @@ -103,7 +103,7 @@ def get_singlechat_menu(contact: types.BareContact) -> GajimMenu:
    103 103
     def get_private_chat_menu(contact: types.GroupchatParticipant) -> GajimMenu:
    
    104 104
         menu = GajimMenu()
    
    105 105
         menu.add_item(_('Details'), 'win.show-contact-info')
    
    106
    -    menu.add_item(_('Upload File…'), 'win.send-file-httpupload')
    
    106
    +    menu.add_item(_('Upload File…'), 'win.send-file-httpupload([""])')
    
    107 107
         menu.add_item(_('Search…'), 'win.search-history')
    
    108 108
     
    
    109 109
         if can_add_to_roster(contact):
    
    ... ... @@ -116,8 +116,8 @@ def get_private_chat_menu(contact: types.GroupchatParticipant) -> GajimMenu:
    116 116
     def get_send_file_submenu() -> GajimMenu:
    
    117 117
         menu = GajimMenu()
    
    118 118
     
    
    119
    -    menu.add_item(_('Upload File…'), 'win.send-file-httpupload')
    
    120
    -    menu.add_item(_('Send File Directly…'), 'win.send-file-jingle')
    
    119
    +    menu.add_item(_('Upload File…'), 'win.send-file-httpupload([""])')
    
    120
    +    menu.add_item(_('Send File Directly…'), 'win.send-file-jingle([""])')
    
    121 121
         return menu
    
    122 122
     
    
    123 123
     
    

  • gajim/gtk/message_actions_box.py
    ... ... @@ -25,7 +25,6 @@
    25 25
     
    
    26 26
     from gi.repository import GLib
    
    27 27
     from gi.repository import Gdk
    
    28
    -from gi.repository import GdkPixbuf
    
    29 28
     from gi.repository import Gio
    
    30 29
     from gi.repository import Gtk
    
    31 30
     from gi.repository import GObject
    
    ... ... @@ -45,9 +44,7 @@
    45 44
     from gajim.gui.security_label_selector import SecurityLabelSelector
    
    46 45
     
    
    47 46
     from .builder import get_builder
    
    48
    -from .dialogs import DialogButton
    
    49 47
     from .dialogs import ErrorDialog
    
    50
    -from .dialogs import PastePreviewDialog
    
    51 48
     from .emoji_chooser import emoji_chooser
    
    52 49
     from .menus import get_encryption_menu
    
    53 50
     from .menus import get_format_menu
    
    ... ... @@ -577,32 +574,16 @@ def _on_paste_clipboard(self,
    577 574
             clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
    
    578 575
             image = clipboard.wait_for_image()
    
    579 576
             if image is None:
    
    577
    +            log.info('No image pasted')
    
    580 578
                 return
    
    581 579
     
    
    582
    -        PastePreviewDialog(
    
    583
    -            _('Paste Image'),
    
    584
    -            _('You are trying to paste an image'),
    
    585
    -            _('Are you sure you want to paste your '
    
    586
    -              "clipboard's image into the chat window?"),
    
    587
    -            _('_Do not ask me again'),
    
    588
    -            image,
    
    589
    -            [DialogButton.make('Cancel'),
    
    590
    -             DialogButton.make('Accept',
    
    591
    -                               text=_('_Paste'),
    
    592
    -                               callback=self._paste_event_confirmed,
    
    593
    -                               args=[image])]).show()
    
    594
    -
    
    595
    -    def _paste_event_confirmed(self,
    
    596
    -                               image: GdkPixbuf.Pixbuf
    
    597
    -                               ) -> None:
    
    598
    -
    
    599
    -        dir_ = tempfile.gettempdir()
    
    600
    -        path = os.path.join(dir_, f'{uuid.uuid4()}.png')
    
    580
    +        temp_dir = tempfile.gettempdir()
    
    581
    +        image_path = os.path.join(temp_dir, f'{uuid.uuid4()}.png')
    
    582
    +
    
    583
    +        image.savev(image_path, 'png', [], [])
    
    601 584
             if image is None:
    
    602 585
                 log.error('Could not process pasted image')
    
    603 586
                 return
    
    604 587
     
    
    605
    -        image.savev(path, 'png', [], [])
    
    606
    -
    
    607
    -        assert self._contact is not None
    
    608
    -        app.interface.start_file_transfer(self._contact, path)
    588
    +        app.window.activate_action(
    
    589
    +            'send-file', GLib.Variant('as', [image_path]))

  • gajim/gui_interface.py
    ... ... @@ -39,7 +39,6 @@
    39 39
     import sys
    
    40 40
     import time
    
    41 41
     import logging
    
    42
    -from functools import partial
    
    43 42
     from threading import Thread
    
    44 43
     
    
    45 44
     from gi.repository import Gtk
    
    ... ... @@ -50,30 +49,16 @@
    50 49
     from nbxmpp import JID
    
    51 50
     
    
    52 51
     from gajim.common import app
    
    53
    -from gajim.common import exceptions
    
    54 52
     from gajim.common import proxy65_manager
    
    55 53
     from gajim.common import socks5
    
    56
    -
    
    57 54
     from gajim.common.events import FileCompleted
    
    58 55
     from gajim.common.events import FileHashError
    
    59 56
     from gajim.common.events import FileProgress
    
    60 57
     from gajim.common.events import FileError
    
    61
    -from gajim.common.modules.contacts import BareContact
    
    62
    -from gajim.common.modules.contacts import GroupchatContact
    
    63
    -from gajim.common.structs import OutgoingMessage
    
    64
    -from gajim.common.i18n import _
    
    65
    -from gajim.common.const import FTState
    
    66 58
     from gajim.common.file_props import FileProp
    
    67
    -from gajim.common.types import ChatContactT
    
    68
    -
    
    69
    -from gajim.common.modules.httpupload import HTTPFileTransfer
    
    70 59
     
    
    71 60
     from gajim.gui.dialogs import ErrorDialog
    
    72
    -from gajim.gui.filechoosers import FileChooserDialog
    
    73
    -from gajim.gui.file_transfer_send import SendFileDialog
    
    74 61
     from gajim.gui.filetransfer import FileTransfersWindow
    
    75
    -from gajim.gui.control import ChatControl
    
    76
    -
    
    77 62
     
    
    78 63
     log = logging.getLogger('gajim.interface')
    
    79 64
     
    
    ... ... @@ -202,130 +187,6 @@ def __compare_hashes(account: str, file_props: FileProp) -> None:
    202 187
             if session:
    
    203 188
                 session.end_session()
    
    204 189
     
    
    205
    -    def send_httpupload(self,
    
    206
    -                        chat_control: ChatControl,
    
    207
    -                        path: Optional[str] = None
    
    208
    -                        ) -> None:
    
    209
    -        if path is not None:
    
    210
    -            self._send_httpupload(chat_control, path)
    
    211
    -            return
    
    212
    -
    
    213
    -        accept_cb = partial(self._on_file_dialog_ok, chat_control)
    
    214
    -        FileChooserDialog(accept_cb,
    
    215
    -                          select_multiple=True,
    
    216
    -                          transient_for=app.window)
    
    217
    -
    
    218
    -    def _on_file_dialog_ok(self,
    
    219
    -                           chat_control: ChatControl,
    
    220
    -                           paths: list[str]
    
    221
    -                           ) -> None:
    
    222
    -        for path in paths:
    
    223
    -            self._send_httpupload(chat_control, path)
    
    224
    -
    
    225
    -    def _send_httpupload(self, chat_control: ChatControl, path: str) -> None:
    
    226
    -        client = app.get_client(chat_control.contact.account)
    
    227
    -        encryption = chat_control.contact.settings.get('encryption') or None
    
    228
    -        try:
    
    229
    -            transfer = client.get_module('HTTPUpload').make_transfer(
    
    230
    -                path,
    
    231
    -                encryption,
    
    232
    -                chat_control.contact,
    
    233
    -                chat_control.is_groupchat)
    
    234
    -        except exceptions.FileError as error:
    
    235
    -            ErrorDialog(_('Could not Open File'), str(error))
    
    236
    -            return
    
    237
    -
    
    238
    -        transfer.connect('cancel', self._on_cancel_upload)
    
    239
    -        transfer.connect('state-changed',
    
    240
    -                         self._on_http_upload_state_changed)
    
    241
    -        chat_control.add_file_transfer(transfer)
    
    242
    -        client.get_module('HTTPUpload').start_transfer(transfer)
    
    243
    -
    
    244
    -    def _on_http_upload_state_changed(self,
    
    245
    -                                      transfer: HTTPFileTransfer,
    
    246
    -                                      _signal_name: str,
    
    247
    -                                      state: FTState
    
    248
    -                                      ) -> None:
    
    249
    -        # Note: This has to be a bound method in order to connect the signal
    
    250
    -        if state.is_finished:
    
    251
    -            uri = transfer.get_transformed_uri()
    
    252
    -
    
    253
    -            type_ = 'chat'
    
    254
    -            if transfer.is_groupchat:
    
    255
    -                type_ = 'groupchat'
    
    256
    -
    
    257
    -            message = OutgoingMessage(account=transfer.account,
    
    258
    -                                      contact=transfer.contact,
    
    259
    -                                      message=uri,
    
    260
    -                                      type_=type_,
    
    261
    -                                      oob_url=uri)
    
    262
    -
    
    263
    -            client = app.get_client(transfer.account)
    
    264
    -            client.send_message(message)
    
    265
    -
    
    266
    -    def _on_cancel_upload(self,
    
    267
    -                          transfer: HTTPFileTransfer,
    
    268
    -                          _signal_name: str
    
    269
    -                          ) -> None:
    
    270
    -        # Note: This has to be a bound method in order to connect the signal
    
    271
    -        client = app.get_client(transfer.account)
    
    272
    -        client.get_module('HTTPUpload').cancel_transfer(transfer)
    
    273
    -
    
    274
    -    def _get_pref_ft_method(self, contact: ChatContactT) -> Optional[str]:
    
    275
    -        ft_pref = app.settings.get_account_setting(contact.account,
    
    276
    -                                                   'filetransfer_preference')
    
    277
    -        httpupload = app.window.get_action_enabled('send-file-httpupload')
    
    278
    -        jingle = app.window.get_action_enabled('send-file-jingle')
    
    279
    -
    
    280
    -        if isinstance(contact, GroupchatContact):
    
    281
    -            if httpupload:
    
    282
    -                return 'httpupload'
    
    283
    -            return None
    
    284
    -
    
    285
    -        if httpupload and jingle:
    
    286
    -            return ft_pref
    
    287
    -
    
    288
    -        if httpupload:
    
    289
    -            return 'httpupload'
    
    290
    -
    
    291
    -        if jingle:
    
    292
    -            return 'jingle'
    
    293
    -        return None
    
    294
    -
    
    295
    -    def start_file_transfer(self,
    
    296
    -                            contact: ChatContactT,
    
    297
    -                            path: Optional[str] = None,
    
    298
    -                            method: Optional[str] = None
    
    299
    -                            ) -> None:
    
    300
    -
    
    301
    -        if method is None:
    
    302
    -            method = self._get_pref_ft_method(contact)
    
    303
    -            if method is None:
    
    304
    -                return
    
    305
    -
    
    306
    -        control = app.window.get_control()
    
    307
    -        if not control.has_active_chat():
    
    308
    -            return
    
    309
    -
    
    310
    -        if path is None:
    
    311
    -            if method == 'httpupload':
    
    312
    -                self.send_httpupload(control)
    
    313
    -                return
    
    314
    -            if method == 'jingle':
    
    315
    -                self.instances['file_transfers'].show_file_send_request(
    
    316
    -                    contact.account, contact)
    
    317
    -            return
    
    318
    -
    
    319
    -        if method == 'httpupload':
    
    320
    -            self.send_httpupload(control, path)
    
    321
    -        else:
    
    322
    -            assert isinstance(contact, BareContact)
    
    323
    -            send_callback = partial(
    
    324
    -                self.instances['file_transfers'].send_file,
    
    325
    -                contact.account,
    
    326
    -                contact)
    
    327
    -            SendFileDialog(contact, send_callback, app.window, [path])
    
    328
    -
    
    329 190
         def process_connections(self) -> bool:
    
    330 191
             '''
    
    331 192
             Called each foo (200) milliseconds. Check for idlequeue timeouts