Daniel Brötzmann pushed to branch time-formats at gajim / gajim

Commits:

14 changed files:

Changes:

  • gajim/common/preview.py
    ... ... @@ -282,12 +282,11 @@ def is_previewable(self,
    282 282
             if not self._accept_uri(urlparts, uri, additional_data):
    
    283 283
                 return False
    
    284 284
     
    
    285
    -        if uri.startswith('geo:'):
    
    285
    +        if urlparts.scheme == 'geo':
    
    286 286
                 try:
    
    287 287
                     split_geo_uri(uri)
    
    288 288
                 except Exception as err:
    
    289
    -                log.error(uri)
    
    290
    -                log.error(err)
    
    289
    +                log.info('Bad geo URI %s: %s', uri, err)
    
    291 290
                     return False
    
    292 291
     
    
    293 292
             return True
    

  • gajim/common/preview_helpers.py
    ... ... @@ -47,6 +47,7 @@
    47 47
     
    
    48 48
     from gajim.common.helpers import sanitize_filename
    
    49 49
     from gajim.common.i18n import _
    
    50
    +from gajim.common.i18n import P_
    
    50 51
     
    
    51 52
     log = logging.getLogger('gajim.c.preview_helpers')
    
    52 53
     
    
    ... ... @@ -382,6 +383,26 @@ def split_geo_uri(uri: str) -> Coords:
    382 383
         return Coords(location=location, lat=lat, lon=lon)
    
    383 384
     
    
    384 385
     
    
    386
    +def format_geo_coords(lat: float, lon: float) -> str:
    
    387
    +    def fmt(f: float) -> str:
    
    388
    +        i = int(round(f * 3600.0))
    
    389
    +        seconds = i % 60
    
    390
    +        i //= 60
    
    391
    +        minutes = i % 60
    
    392
    +        i //= 60
    
    393
    +        degrees = i
    
    394
    +        return '%d°%02d′%02d′′' % (degrees, minutes, seconds)
    
    395
    +    if lat >= 0:
    
    396
    +        slat = P_('positive latitude', '%sN') % fmt(lat)
    
    397
    +    else:
    
    398
    +        slat = P_('negative latitude', '%sS') % fmt(-lat)
    
    399
    +    if lon >= 0:
    
    400
    +        slon = P_('positive longitude', '%sE') % fmt(lon)
    
    401
    +    else:
    
    402
    +        slon = P_('negative longitude', '%sW') % fmt(-lon)
    
    403
    +    return '%s %s' % (slat, slon)
    
    404
    +
    
    405
    +
    
    385 406
     def filename_from_uri(uri: str) -> str:
    
    386 407
         urlparts = urlparse(unquote(uri))
    
    387 408
         path = Path(urlparts.path)
    
    ... ... @@ -389,7 +410,7 @@ def filename_from_uri(uri: str) -> str:
    389 410
     
    
    390 411
     
    
    391 412
     def aes_decrypt(key: bytes, iv: bytes, payload: bytes) -> bytes:
    
    392
    -    # Use AES128 GCM with the given key and iv to decrypt the payload
    
    413
    +    # Use AES256 GCM with the given key and iv to decrypt the payload
    
    393 414
         data = payload[:-16]
    
    394 415
         tag = payload[-16:]
    
    395 416
         decryptor = Cipher(algorithms.AES(key),
    
    ... ... @@ -447,8 +468,6 @@ def guess_simple_file_type(file_path: str,
    447 468
         if mime_type == 'application/octet-stream':
    
    448 469
             mime_type = ''
    
    449 470
         icon = get_icon_for_mime_type(mime_type)
    
    450
    -    if file_path.startswith('geo:'):
    
    451
    -        return icon, _('Location')
    
    452 471
         if mime_type.startswith('audio/'):
    
    453 472
             return icon, _('Audio File')
    
    454 473
         if mime_type.startswith('image/'):
    

  • gajim/common/setting_values.py
    ... ... @@ -132,9 +132,9 @@ class _ACCOUNT_DEFAULT:
    132 132
         'audio_output_device',
    
    133 133
         'autoaway_message',
    
    134 134
         'autoxa_message',
    
    135
    -    'chat_timestamp_format',
    
    136 135
         'confirm_block',
    
    137
    -    'date_timestamp_format',
    
    136
    +    'date_format',
    
    137
    +    'date_time_format',
    
    138 138
         'dictionary_url',
    
    139 139
         'emoticons_theme',
    
    140 140
         'gc_refer_to_nick_char',
    
    ... ... @@ -154,6 +154,7 @@ class _ACCOUNT_DEFAULT:
    154 154
         'show_main_window_on_startup',
    
    155 155
         'speller_language',
    
    156 156
         'stun_server',
    
    157
    +    'time_format',
    
    157 158
         'video_framerate',
    
    158 159
         'video_input_device',
    
    159 160
         'video_size',
    
    ... ... @@ -180,13 +181,13 @@ class _ACCOUNT_DEFAULT:
    180 181
         'autoxatime': 15,
    
    181 182
         'chat_handle_position': 350,
    
    182 183
         'chat_merge_consecutive_nickname': True,
    
    183
    -    'chat_timestamp_format': '%H:%M',
    
    184 184
         'check_for_update': True,
    
    185 185
         'confirm_block': '',
    
    186 186
         'confirm_close_muc': True,
    
    187 187
         'confirm_on_window_delete': True,
    
    188 188
         'dark_theme': 2,
    
    189
    -    'date_timestamp_format': '%x',
    
    189
    +    'date_format': '%x',
    
    190
    +    'date_time_format': '%c',
    
    190 191
         'dev_force_bookmark_2': False,
    
    191 192
         'dev_use_message_label': True,
    
    192 193
         'developer_modus': False,
    
    ... ... @@ -268,6 +269,7 @@ class _ACCOUNT_DEFAULT:
    268 269
         'sounds_on': True,
    
    269 270
         'speller_language': '',
    
    270 271
         'stun_server': '',
    
    272
    +    'time_format': '%H:%M',
    
    271 273
         'trayicon_notification_on_events': True,
    
    272 274
         'use_keyring': True,
    
    273 275
         'use_kib_mib': False,
    
    ... ... @@ -635,14 +637,14 @@ class WorkspaceSettings(TypedDict):
    635 637
                 'Show message meta data (avatar, nickname, timestamp) only once, '
    
    636 638
                 'if there are multiple messages from the same sender within a '
    
    637 639
                 'specific timespan.'),
    
    638
    -        'chat_timestamp_format': 'https://docs.python.org/3/library/time.html#time.strftime',  # noqa: E501
    
    639 640
             'confirm_block': _(
    
    640 641
                 'Show a confirmation dialog to block a contact? Empty string '
    
    641 642
                 'means never show the dialog.'),
    
    642 643
             'confirm_close_muc': _('Ask before closing a group chat tab/window.'),
    
    643 644
             'confirm_on_window_delete': _(
    
    644 645
                 'Ask before quitting when Gajim’s window is closed'),
    
    645
    -        'date_timestamp_format': 'https://docs.python.org/3/library/time.html#time.strftime',  # noqa: E501
    
    646
    +        'date_format': 'https://docs.python.org/3/library/time.html#time.strftime',  # noqa: E501
    
    647
    +        'date_time_format': 'https://docs.python.org/3/library/time.html#time.strftime',  # noqa: E501
    
    646 648
             'dev_force_bookmark_2': _('Force Bookmark 2 usage'),
    
    647 649
             'dev_use_message_label': '',
    
    648 650
             'developer_modus': '',
    
    ... ... @@ -691,6 +693,7 @@ class WorkspaceSettings(TypedDict):
    691 693
             'show_chatstate_in_banner': _(
    
    692 694
                 'Show chat state (e.g. "is typing…") next to your contact’s name'),
    
    693 695
             'stun_server': _('STUN server to use when using Jingle'),
    
    696
    +        'time_format': 'https://docs.python.org/3/library/time.html#time.strftime',  # noqa: E501
    
    694 697
             'trayicon_notification_on_events': _(
    
    695 698
                 'Notify of events in the notification area.'),
    
    696 699
             'use_kib_mib': _(
    

  • gajim/common/settings.py
    ... ... @@ -348,6 +348,17 @@ def _migrate(self) -> None:
    348 348
     
    
    349 349
                 self._set_user_version(3)
    
    350 350
     
    
    351
    +        if version < 4:
    
    352
    +            value = self._settings['app'].get('chat_timestamp_format')
    
    353
    +            if value is not None:
    
    354
    +                self._settings['app']['time_format'] = value
    
    355
    +
    
    356
    +            value = self._settings['app'].get('date_timestamp_format')
    
    357
    +            if value is not None:
    
    358
    +                self._settings['app']['date_format'] = value
    
    359
    +
    
    360
    +            self._set_user_version(4)
    
    361
    +
    
    351 362
         def _migrate_old_config(self) -> None:
    
    352 363
             config_file = configpaths.get('CONFIG_FILE')
    
    353 364
             if not config_file.exists():
    

  • gajim/gtk/chat_list_row.py
    ... ... @@ -18,6 +18,7 @@
    18 18
     from typing import Any
    
    19 19
     
    
    20 20
     import pickle
    
    21
    +from urllib.parse import urlparse
    
    21 22
     
    
    22 23
     from gi.repository import Gdk
    
    23 24
     from gi.repository import Gio
    
    ... ... @@ -39,6 +40,8 @@
    39 40
     from gajim.common.helpers import get_uf_relative_time
    
    40 41
     from gajim.common.helpers import message_needs_highlight
    
    41 42
     from gajim.common.helpers import AdditionalDataDict
    
    43
    +from gajim.common.preview_helpers import split_geo_uri
    
    44
    +from gajim.common.preview_helpers import format_geo_coords
    
    42 45
     from gajim.common.preview_helpers import filename_from_uri
    
    43 46
     from gajim.common.preview_helpers import guess_simple_file_type
    
    44 47
     from gajim.common.types import ChatContactT
    
    ... ... @@ -252,9 +255,16 @@ def set_message_text(self,
    252 255
                 icon = Gio.Icon.new_for_string(icon_name)
    
    253 256
             if additional_data is not None:
    
    254 257
                 if app.preview_manager.is_previewable(text, additional_data):
    
    255
    -                file_name = filename_from_uri(text)
    
    256
    -                icon, file_type = guess_simple_file_type(text)
    
    257
    -                text = f'{file_type} ({file_name})'
    
    258
    +                scheme = urlparse(text).scheme
    
    259
    +                if scheme == 'geo':
    
    260
    +                    location = split_geo_uri(text)
    
    261
    +                    icon = Gio.Icon.new_for_string('mark-location')
    
    262
    +                    text = format_geo_coords(
    
    263
    +                        float(location.lat), float(location.lon))
    
    264
    +                else:
    
    265
    +                    file_name = filename_from_uri(text)
    
    266
    +                    icon, file_type = guess_simple_file_type(text)
    
    267
    +                    text = f'{file_type} ({file_name})'
    
    258 268
     
    
    259 269
             text = GLib.markup_escape_text(text)
    
    260 270
             if text.startswith('/me') and nickname is not None:
    

  • gajim/gtk/chat_stack.py
    ... ... @@ -20,6 +20,7 @@
    20 20
     import sys
    
    21 21
     import logging
    
    22 22
     import time
    
    23
    +from urllib.parse import urlparse
    
    23 24
     
    
    24 25
     from gi.repository import Gdk
    
    25 26
     from gi.repository import GLib
    
    ... ... @@ -40,6 +41,8 @@
    40 41
     from gajim.common.modules.contacts import BareContact
    
    41 42
     from gajim.common.modules.contacts import GroupchatContact
    
    42 43
     from gajim.common.modules.contacts import GroupchatParticipant
    
    44
    +from gajim.common.preview_helpers import split_geo_uri
    
    45
    +from gajim.common.preview_helpers import format_geo_coords
    
    43 46
     from gajim.common.structs import OutgoingMessage
    
    44 47
     from gajim.common.types import ChatContactT
    
    45 48
     from gajim.gui.dialogs import ErrorDialog
    
    ... ... @@ -389,8 +392,11 @@ def _issue_notification(self, event: events.MessageReceived) -> None:
    389 392
             is_previewable = app.preview_manager.is_previewable(
    
    390 393
                 text, additional_data)
    
    391 394
             if is_previewable:
    
    392
    -            if text.startswith('geo:'):
    
    393
    -                text = _('Location')
    
    395
    +            scheme = urlparse(text).scheme
    
    396
    +            if scheme == 'geo':
    
    397
    +                location = split_geo_uri(text)
    
    398
    +                text = format_geo_coords(
    
    399
    +                    float(location.lat), float(location.lon))
    
    394 400
                 else:
    
    395 401
                     file_name = preview_helpers.filename_from_uri(text)
    
    396 402
                     _icon, file_type = preview_helpers.guess_simple_file_type(text)
    
    ... ... @@ -531,6 +537,7 @@ def _update_group_chat_actions(self, contact: GroupchatContact) -> None:
    531 537
     
    
    532 538
             app.window.get_action('quote').set_enabled(joined)
    
    533 539
             app.window.get_action('mention').set_enabled(joined)
    
    540
    +        app.window.get_action('retract-message').set_enabled(joined)
    
    534 541
     
    
    535 542
         def _update_participant_actions(self,
    
    536 543
                                         contact: GroupchatParticipant) -> None:
    

  • gajim/gtk/const.py
    ... ... @@ -194,6 +194,8 @@ def __str__(self):
    194 194
         ('show-emoji-chooser', None, True),
    
    195 195
         ('activate-message-selection', 'u', True),
    
    196 196
         ('correct-message', None, False),
    
    197
    +    ('copy-message', 's', True),
    
    198
    +    ('retract-message', 'a{sv}', False),
    
    197 199
         ('quote', 's', False),
    
    198 200
         ('mention', 's', False),
    
    199 201
         ('send-file-httpupload', None, False),
    

  • gajim/gtk/conversation/rows/message.py
    ... ... @@ -21,7 +21,6 @@
    21 21
     from datetime import timedelta
    
    22 22
     import textwrap
    
    23 23
     
    
    24
    -from gi.repository import Gdk
    
    25 24
     from gi.repository import GdkPixbuf
    
    26 25
     from gi.repository import GLib
    
    27 26
     from gi.repository import Gtk
    
    ... ... @@ -38,7 +37,6 @@
    38 37
     from gajim.common.const import Trust
    
    39 38
     from gajim.common.const import TRUST_SYMBOL_DATA
    
    40 39
     from gajim.common.helpers import AdditionalDataDict
    
    41
    -from gajim.common.helpers import from_one_line
    
    42 40
     from gajim.common.helpers import get_group_chat_nick
    
    43 41
     from gajim.common.helpers import message_needs_highlight
    
    44 42
     from gajim.common.helpers import to_user_string
    
    ... ... @@ -55,8 +53,6 @@
    55 53
     from .widgets import MessageIcons
    
    56 54
     from .widgets import MoreMenuButton
    
    57 55
     from ..message_widget import MessageWidget
    
    58
    -from ...dialogs import InputDialog
    
    59
    -from ...dialogs import DialogButton
    
    60 56
     from ...preview import PreviewWidget
    
    61 57
     from ...util import format_fingerprint
    
    62 58
     
    
    ... ... @@ -314,37 +310,6 @@ def is_mergeable(self, message: MessageRow) -> bool:
    314 310
         def get_text(self) -> str:
    
    315 311
             return self._message_widget.get_text()
    
    316 312
     
    
    317
    -    def on_copy_message(self, _widget: Gtk.Widget) -> None:
    
    318
    -        time_format = from_one_line(app.settings.get('chat_timestamp_format'))
    
    319
    -        timestamp_formatted = self.timestamp.strftime(time_format)
    
    320
    -
    
    321
    -        clip = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
    
    322
    -        text = self.get_text()
    
    323
    -        clip.set_text(f'{timestamp_formatted} - {self.name}: {text}', -1)
    
    324
    -
    
    325
    -    def on_quote_message(self, _widget: Gtk.Widget) -> None:
    
    326
    -        app.window.activate_action(
    
    327
    -            'quote', GLib.Variant('s', self.get_text()))
    
    328
    -
    
    329
    -    def on_correct_message(self, _widget: Gtk.Widget) -> None:
    
    330
    -        app.window.activate_action('correct-message', None)
    
    331
    -
    
    332
    -    def on_retract_message(self, _widget: Gtk.Widget) -> None:
    
    333
    -        def _on_retract(reason: str) -> None:
    
    334
    -            self._client.get_module('MUC').retract_message(
    
    335
    -                self._contact.jid, self.stanza_id, reason or None)
    
    336
    -
    
    337
    -        InputDialog(
    
    338
    -            _('Retract Message'),
    
    339
    -            _('Retract message?'),
    
    340
    -            _('Why do you want to retract this message?'),
    
    341
    -            [DialogButton.make('Cancel'),
    
    342
    -             DialogButton.make('Remove',
    
    343
    -                               text=_('_Retract'),
    
    344
    -                               callback=_on_retract)],
    
    345
    -            input_str=_('Spam'),
    
    346
    -            transient_for=app.window).show()
    
    347
    -
    
    348 313
         def _get_encryption_image(self,
    
    349 314
                                   additional_data: AdditionalDataDict,
    
    350 315
                                   ) -> Optional[Gtk.Image]:
    

  • gajim/gtk/conversation/rows/widgets.py
    ... ... @@ -28,13 +28,13 @@
    28 28
     from gajim.common import app
    
    29 29
     from gajim.common.const import AvatarSize
    
    30 30
     from gajim.common.i18n import Q_
    
    31
    -from gajim.common.helpers import is_retraction_allowed
    
    32 31
     from gajim.common.modules.contacts import GroupchatContact
    
    33 32
     from gajim.common.types import ChatContactT
    
    34 33
     
    
    35 34
     if TYPE_CHECKING:
    
    36 35
         from .message import MessageRow
    
    37 36
     
    
    37
    +from ...menus import get_chat_row_menu
    
    38 38
     from ...menus import get_groupchat_participant_menu
    
    39 39
     from ...util import wrap_with_event_box
    
    40 40
     from ...util import get_cursor
    
    ... ... @@ -76,120 +76,26 @@ def __init__(self,
    76 76
             self.connect('clicked', self._on_click)
    
    77 77
     
    
    78 78
         def _on_click(self, _button: Gtk.Button) -> None:
    
    79
    -        show_retract = False
    
    80
    -        if isinstance(self._contact, GroupchatContact):
    
    81
    -            if not self._contact.is_joined:
    
    82
    -                self._create_popover()
    
    83
    -                return
    
    84
    -
    
    85
    -            contact = self._contact.get_resource(self._name)
    
    86
    -            self_contact = self._contact.get_self()
    
    87
    -            assert self_contact is not None
    
    88
    -            is_allowed = is_retraction_allowed(self_contact, contact)
    
    89
    -
    
    90
    -            disco_info = app.storage.cache.get_last_disco_info(
    
    91
    -                self._contact.jid)
    
    92
    -            assert disco_info is not None
    
    93
    -
    
    94
    -            if disco_info.has_message_moderation and is_allowed:
    
    95
    -                show_retract = True
    
    96
    -
    
    97
    -        show_correction = False
    
    98
    -        if self._row.message_id is not None:
    
    99
    -            show_correction = app.window.is_message_correctable(
    
    100
    -                self._contact, self._row.message_id)
    
    101
    -
    
    102
    -        self._create_popover(show_retract,
    
    103
    -                             show_correction)
    
    104
    -
    
    105
    -    def _create_popover(self,
    
    106
    -                        show_retract: bool = False,
    
    107
    -                        show_correction: bool = False
    
    108
    -                        ) -> None:
    
    109
    -
    
    110
    -        menu_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
    
    111
    -        menu_box.get_style_context().add_class('padding-6')
    
    112
    -
    
    113
    -        quote_enabled = True
    
    114
    -        if isinstance(self._contact, GroupchatContact):
    
    115
    -            if self._contact.is_joined:
    
    116
    -                self_contact = self._contact.get_self()
    
    117
    -                assert self_contact is not None
    
    118
    -                quote_enabled = not self_contact.role.is_visitor
    
    119
    -            else:
    
    120
    -                quote_enabled = False
    
    121
    -
    
    122
    -        quote_button = Gtk.ModelButton()
    
    123
    -        quote_button.set_halign(Gtk.Align.START)
    
    124
    -        quote_button.connect('clicked', self._row.on_quote_message)
    
    125
    -        quote_button.set_label(Q_('?Message row action:Quote…'))
    
    126
    -        quote_button.set_image(Gtk.Image.new_from_icon_name(
    
    127
    -            'mail-reply-sender-symbolic', Gtk.IconSize.MENU))
    
    128
    -        quote_button.set_sensitive(quote_enabled)
    
    129
    -        menu_box.add(quote_button)
    
    130
    -
    
    131
    -        copy_button = Gtk.ModelButton()
    
    132
    -        copy_button.set_halign(Gtk.Align.START)
    
    133
    -        copy_button.connect('clicked', self._row.on_copy_message)
    
    134
    -        copy_button.set_label(Q_('?Message row action:Copy'))
    
    135
    -        copy_button.set_image(Gtk.Image.new_from_icon_name(
    
    136
    -            'edit-copy-symbolic', Gtk.IconSize.MENU))
    
    137
    -        menu_box.add(copy_button)
    
    138
    -
    
    139
    -        select_button = Gtk.ModelButton()
    
    140
    -        select_button.set_halign(Gtk.Align.START)
    
    141
    -        select_button.connect('clicked', self._on_activate_message_selection)
    
    142
    -        select_button.set_label(Q_('?Message row action:Select Messages…'))
    
    143
    -        select_button.set_image(Gtk.Image.new_from_icon_name(
    
    144
    -            'edit-select-symbolic', Gtk.IconSize.MENU))
    
    145
    -        menu_box.add(select_button)
    
    146
    -
    
    147
    -        if show_correction:
    
    148
    -            correct_button = Gtk.ModelButton()
    
    149
    -            correct_button.set_halign(Gtk.Align.START)
    
    150
    -            correct_button.connect(
    
    151
    -                'clicked', self._row.on_correct_message)
    
    152
    -            correct_button.set_label(Q_('?Message row action:Correct…'))
    
    153
    -            correct_button.set_image(Gtk.Image.new_from_icon_name(
    
    154
    -                'document-edit-symbolic', Gtk.IconSize.MENU))
    
    155
    -            menu_box.add(correct_button)
    
    156
    -
    
    157
    -        if show_retract:
    
    158
    -            retract_button = Gtk.ModelButton()
    
    159
    -            retract_button.set_halign(Gtk.Align.START)
    
    160
    -            retract_button.connect(
    
    161
    -                'clicked', self._row.on_retract_message)
    
    162
    -            retract_button.set_label(Q_('?Message row action:Retract…'))
    
    163
    -            retract_button.set_image(Gtk.Image.new_from_icon_name(
    
    164
    -                'edit-undo-symbolic', Gtk.IconSize.MENU))
    
    165
    -            menu_box.add(retract_button)
    
    166
    -
    
    167
    -        menu_box.show_all()
    
    168
    -
    
    169
    -        popover = Gtk.PopoverMenu()
    
    170
    -        popover.add(menu_box)
    
    171
    -        popover.set_relative_to(self)
    
    172
    -        popover.set_position(Gtk.PositionType.BOTTOM)
    
    173
    -        popover.connect('closed', self._on_closed)
    
    79
    +        menu = get_chat_row_menu(
    
    80
    +            self._contact,
    
    81
    +            self._name,
    
    82
    +            self._row.get_text(),
    
    83
    +            self._row.timestamp,
    
    84
    +            self._row.message_id,
    
    85
    +            self._row.stanza_id,
    
    86
    +            self._row.log_line_id)
    
    87
    +
    
    88
    +        popover = GajimPopover(menu, relative_to=self)
    
    174 89
             popover.popup()
    
    175 90
     
    
    176
    -    def _on_activate_message_selection(self, _button: Gtk.ModelButton) -> None:
    
    177
    -        app.window.activate_action(
    
    178
    -            'activate-message-selection',
    
    179
    -            GLib.Variant('u', self._row.log_line_id or 0))
    
    180
    -
    
    181
    -    @staticmethod
    
    182
    -    def _on_closed(popover: Gtk.Popover) -> None:
    
    183
    -        GLib.idle_add(popover.destroy)
    
    184
    -
    
    185 91
     
    
    186 92
     class DateTimeLabel(Gtk.Label):
    
    187 93
         def __init__(self, timestamp: datetime) -> None:
    
    188 94
             Gtk.Label.__init__(self)
    
    189 95
     
    
    190
    -        time_format = app.settings.get('chat_timestamp_format')
    
    96
    +        time_format = app.settings.get('time_format')
    
    191 97
             if timestamp.date() < datetime.today().date():
    
    192
    -            date_format = app.settings.get('date_timestamp_format')
    
    98
    +            date_format = app.settings.get('date_format')
    
    193 99
                 time_format = f'{time_format} - {date_format}'
    
    194 100
             timestamp_formatted = timestamp.strftime(time_format)
    
    195 101
     
    

  • gajim/gtk/conversation/view.py
    ... ... @@ -144,7 +144,7 @@ def __init__(self) -> None:
    144 144
                 'activate', self._on_scroll_view)
    
    145 145
     
    
    146 146
         def copy_selected_messages(self) -> None:
    
    147
    -        time_format = from_one_line(app.settings.get('chat_timestamp_format'))
    
    147
    +        time_format = from_one_line(app.settings.get('time_format'))
    
    148 148
             selection_text = ''
    
    149 149
             for row in cast(list[BaseRow], self._list_box.get_selected_rows()):
    
    150 150
                 if isinstance(row, MessageRow):
    

  • gajim/gtk/main.py
    ... ... @@ -54,6 +54,7 @@
    54 54
     from .dialogs import DialogButton
    
    55 55
     from .dialogs import ConfirmationDialog
    
    56 56
     from .dialogs import ConfirmationCheckDialog
    
    57
    +from .dialogs import InputDialog
    
    57 58
     from .builder import get_builder
    
    58 59
     from .util import get_app_window
    
    59 60
     from .util import resize_window
    
    ... ... @@ -65,6 +66,7 @@
    65 66
     from .structs import AddChatActionParams
    
    66 67
     from .structs import actionmethod
    
    67 68
     from .structs import ChatListEntryParam
    
    69
    +from .structs import RetractMessageParam
    
    68 70
     
    
    69 71
     if TYPE_CHECKING:
    
    70 72
         from .control import ChatControl
    
    ... ... @@ -401,6 +403,8 @@ def _connect_actions(self) -> None:
    401 403
                 ('switch-workspace-8', self._on_action),
    
    402 404
                 ('switch-workspace-9', self._on_action),
    
    403 405
                 ('toggle-chat-list', self._on_action),
    
    406
    +            ('copy-message', self._on_copy_message),
    
    407
    +            ('retract-message', self._on_retract_message),
    
    404 408
                 ('add-workspace', self._add_workspace),
    
    405 409
                 ('edit-workspace', self._edit_workspace),
    
    406 410
                 ('remove-workspace', self._remove_workspace),
    
    ... ... @@ -510,6 +514,36 @@ def _toggle_chat_list(self) -> None:
    510 514
                         'go-previous-symbolic', Gtk.IconSize.BUTTON)
    
    511 515
             self._chat_page.toggle_chat_list()
    
    512 516
     
    
    517
    +    def _on_copy_message(self,
    
    518
    +                         _action: Gio.SimpleAction,
    
    519
    +                         param: GLib.Variant
    
    520
    +                         ) -> None:
    
    521
    +
    
    522
    +        clip = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
    
    523
    +        clip.set_text(param.get_string(), -1)
    
    524
    +
    
    525
    +    @actionmethod
    
    526
    +    def _on_retract_message(self,
    
    527
    +                            _action: Gio.SimpleAction,
    
    528
    +                            params: RetractMessageParam
    
    529
    +                            ) -> None:
    
    530
    +
    
    531
    +        def _on_retract(reason: str) -> None:
    
    532
    +            client = app.get_client(params.account)
    
    533
    +            client.get_module('MUC').retract_message(
    
    534
    +                params.jid, params.stanza_id, reason or None)
    
    535
    +
    
    536
    +        InputDialog(
    
    537
    +            _('Retract Message'),
    
    538
    +            _('Retract message?'),
    
    539
    +            _('Why do you want to retract this message?'),
    
    540
    +            [DialogButton.make('Cancel'),
    
    541
    +             DialogButton.make('Remove',
    
    542
    +                               text=_('_Retract'),
    
    543
    +                               callback=_on_retract)],
    
    544
    +            input_str=_('Spam'),
    
    545
    +            transient_for=app.window).show()
    
    546
    +
    
    513 547
         def _on_window_motion_notify(self,
    
    514 548
                                      _widget: Gtk.ApplicationWindow,
    
    515 549
                                      _event: Gdk.EventMotion
    

  • gajim/gtk/menus.py
    ... ... @@ -21,6 +21,7 @@
    21 21
     from typing import Optional
    
    22 22
     from typing import Union
    
    23 23
     
    
    24
    +from datetime import datetime
    
    24 25
     import textwrap
    
    25 26
     from urllib.parse import quote
    
    26 27
     
    
    ... ... @@ -32,10 +33,13 @@
    32 33
     
    
    33 34
     from gajim.common import app
    
    34 35
     from gajim.common import types
    
    36
    +from gajim.common.helpers import from_one_line
    
    35 37
     from gajim.common.helpers import is_affiliation_change_allowed
    
    38
    +from gajim.common.helpers import is_retraction_allowed
    
    36 39
     from gajim.common.helpers import is_role_change_allowed
    
    37 40
     from gajim.common.helpers import jid_is_blocked
    
    38 41
     from gajim.common.i18n import _
    
    42
    +from gajim.common.i18n import Q_
    
    39 43
     from gajim.common.i18n import get_short_lang_code
    
    40 44
     from gajim.common.const import URIType
    
    41 45
     from gajim.common.const import XmppUriQuery
    
    ... ... @@ -48,6 +52,7 @@
    48 52
     from gajim.gui.structs import AccountJidParam
    
    49 53
     from gajim.gui.structs import ChatListEntryParam
    
    50 54
     from gajim.gui.structs import RemoveHistoryActionParams
    
    55
    +from gajim.gui.structs import RetractMessageParam
    
    51 56
     from gajim.gui.util import GajimMenu
    
    52 57
     
    
    53 58
     
    
    ... ... @@ -594,6 +599,77 @@ def get_component_search_menu(jid: Optional[str], copy_text: str) -> Gio.Menu:
    594 599
         return menu
    
    595 600
     
    
    596 601
     
    
    602
    +def get_chat_row_menu(contact: types.ChatContactT,
    
    603
    +                      name: str,
    
    604
    +                      text: str,
    
    605
    +                      timestamp: datetime,
    
    606
    +                      message_id: Optional[str],
    
    607
    +                      stanza_id: Optional[str],
    
    608
    +                      log_line_id: Optional[int]
    
    609
    +                      ) -> GajimMenu:
    
    610
    +
    
    611
    +    menu_items: MenuItemListT = []
    
    612
    +
    
    613
    +    text = text.replace('"', '\\"')
    
    614
    +
    
    615
    +    time_format = from_one_line(app.settings.get('time_format'))
    
    616
    +    timestamp_formatted = timestamp.strftime(time_format)
    
    617
    +    copy_text = f'{timestamp_formatted} - {name}: {text}'
    
    618
    +    menu_items.append(
    
    619
    +        (Q_('?Message row action:Copy'), 'win.copy-message', f'"{copy_text}"'))
    
    620
    +
    
    621
    +    menu_items.append(
    
    622
    +        (Q_('?Message row action:Select Messages…'),
    
    623
    +         'win.activate-message-selection',
    
    624
    +         GLib.Variant('u', log_line_id or 0)))
    
    625
    +
    
    626
    +    show_quote = True
    
    627
    +    if isinstance(contact, GroupchatContact):
    
    628
    +        if contact.is_joined:
    
    629
    +            self_contact = contact.get_self()
    
    630
    +            assert self_contact is not None
    
    631
    +            show_quote = not self_contact.role.is_visitor
    
    632
    +        else:
    
    633
    +            show_quote = False
    
    634
    +    if show_quote:
    
    635
    +        menu_items.append((
    
    636
    +            Q_('?Message row action:Quote…'),
    
    637
    +            'win.quote',
    
    638
    +            f'"{text}"'))
    
    639
    +
    
    640
    +    show_correction = False
    
    641
    +    if message_id is not None:
    
    642
    +        show_correction = app.window.is_message_correctable(
    
    643
    +            contact, message_id)
    
    644
    +    if show_correction:
    
    645
    +        menu_items.append((
    
    646
    +            Q_('?Message row action:Correct…'), 'win.correct-message', None))
    
    647
    +
    
    648
    +    show_retract = False
    
    649
    +    if isinstance(contact, GroupchatContact) and contact.is_joined:
    
    650
    +        resource_contact = contact.get_resource(name)
    
    651
    +        self_contact = contact.get_self()
    
    652
    +        assert self_contact is not None
    
    653
    +        is_allowed = is_retraction_allowed(self_contact, resource_contact)
    
    654
    +
    
    655
    +        disco_info = app.storage.cache.get_last_disco_info(contact.jid)
    
    656
    +        assert disco_info is not None
    
    657
    +
    
    658
    +        if disco_info.has_message_moderation and is_allowed:
    
    659
    +            show_retract = True
    
    660
    +    if show_retract:
    
    661
    +        assert stanza_id is not None
    
    662
    +        param = RetractMessageParam(
    
    663
    +            account=contact.account,
    
    664
    +            jid=contact.jid,
    
    665
    +            stanza_id=stanza_id)
    
    666
    +        menu_items.append((
    
    667
    +            Q_('?Message row action:Retract…'), 'win.retract-message', param))
    
    668
    +
    
    669
    +    menu = GajimMenu.from_list(menu_items)
    
    670
    +    return menu
    
    671
    +
    
    672
    +
    
    597 673
     def get_format_menu() -> GajimMenu:
    
    598 674
     
    
    599 675
         menuitems: MenuItemListT = [
    

  • gajim/gtk/preview.py
    ... ... @@ -33,6 +33,7 @@
    33 33
     from gajim.common.i18n import _
    
    34 34
     from gajim.common.preview import Preview
    
    35 35
     from gajim.common.preview_helpers import split_geo_uri
    
    36
    +from gajim.common.preview_helpers import format_geo_coords
    
    36 37
     from gajim.common.preview_helpers import contains_audio_streams
    
    37 38
     from gajim.common.preview_helpers import get_icon_for_mime_type
    
    38 39
     from gajim.common.types import GdkPixbufType
    
    ... ... @@ -136,12 +137,10 @@ def update(self, preview: Preview, data: Optional[GdkPixbufType]) -> None:
    136 137
                 self._ui.download_button.hide()
    
    137 138
     
    
    138 139
                 location = split_geo_uri(preview.uri)
    
    139
    -            self._ui.file_size.set_text(_('Lat: %s Lon: %s') % (
    
    140
    -                round(float(location.lat), 5),
    
    141
    -                round(float(location.lon), 5)))
    
    140
    +            text = format_geo_coords(float(location.lat), float(location.lon))
    
    141
    +            self._ui.file_size.set_text(text)
    
    142 142
                 self._ui.image_button.set_tooltip_text(
    
    143
    -                _('Location at Lat: %s Lon: %s') % (
    
    144
    -                    location.lat, location.lon))
    
    143
    +                _('Location at %s') % text)
    
    145 144
                 self._ui.preview_box.set_size_request(160, -1)
    
    146 145
                 return
    
    147 146
     
    

  • gajim/gtk/structs.py
    ... ... @@ -21,7 +21,9 @@
    21 21
     import inspect
    
    22 22
     import functools
    
    23 23
     from dataclasses import dataclass
    
    24
    -from gi.repository import GLib, Gio
    
    24
    +
    
    25
    +from gi.repository import Gio
    
    26
    +from gi.repository import GLib
    
    25 27
     
    
    26 28
     from nbxmpp.protocol import JID
    
    27 29
     
    
    ... ... @@ -66,6 +68,13 @@ class AccountJidParam(VariantMixin):
    66 68
         jid: JID
    
    67 69
     
    
    68 70
     
    
    71
    +@dataclass
    
    72
    +class RetractMessageParam(VariantMixin):
    
    73
    +    account: str
    
    74
    +    jid: JID
    
    75
    +    stanza_id: str
    
    76
    +
    
    77
    +
    
    69 78
     def get_params_class(func: Callable[..., Any]) -> Any:
    
    70 79
         module = sys.modules[__name__]
    
    71 80
         params = inspect.signature(func).parameters