Daniel Brötzmann pushed to branch master at gajim / gajim
Commits:
-
a6037e25
by wurstsalat at 2022-11-24T22:23:04+00:00
-
53d01bd6
by wurstsalat at 2022-11-24T23:33:00+01:00
17 changed files:
- gajim/common/events.py
- gajim/common/modules/httpupload.py
- gajim/data/gui/file_transfer_send.ui → gajim/data/gui/file_transfer_selector.ui
- gajim/data/gui/message_actions_box.ui
- gajim/data/style/gajim.css
- gajim/gtk/builder.pyi
- gajim/gtk/chat_function_page.py
- gajim/gtk/chat_stack.py
- gajim/gtk/const.py
- gajim/gtk/control.py
- + gajim/gtk/file_transfer_selector.py
- − gajim/gtk/file_transfer_send.py
- gajim/gtk/filetransfer.py
- gajim/gtk/main.py
- gajim/gtk/menus.py
- gajim/gtk/message_actions_box.py
- gajim/gui_interface.py
Changes:
| ... | ... | @@ -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')
|
| ... | ... | @@ -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 |
| 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>
|
| ... | ... | @@ -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">
|
| ... | ... | @@ -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;
|
| ... | ... | @@ -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
|
| ... | ... | @@ -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 |
| ... | ... | @@ -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 |
| ... | ... | @@ -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),
|
| ... | ... | @@ -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):
|
| 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() |
| 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) |
| ... | ... | @@ -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 = ''
|
| ... | ... | @@ -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,
|
| ... | ... | @@ -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 |
| ... | ... | @@ -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])) |
| ... | ... | @@ -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
|