Philipp Hörist pushed to branch refactor_view at gajim / gajim
Commits: 005de852 by wurstsalat at 2022-09-18T22:18:48+02:00 imprv: SearchView: Add hint for search filters
Fixes #11136
- - - - - 2d261ff1 by wurstsalat at 2022-09-18T22:18:48+02:00 cfix: ChatBanner: Escape contact name for title and tooltip
Fixes #11154
- - - - - 11c55903 by wurstsalat at 2022-09-18T22:18:48+02:00 imprv: Preview: Improve mime type detection
Fixes #11150
- - - - - 23b4572a by Philipp Hörist at 2022-09-18T22:18:48+02:00 refactor: Contacts: Add chatstate_string property
- - - - - c3a3cb28 by Philipp Hörist at 2022-09-18T22:18:48+02:00 refactor: ChatBanner: Split update_content() into dedicated methods
- - - - - 532a463e by wurstsalat at 2022-09-18T22:18:48+02:00 fix: Corrections: Don’t correct message if text has not changed
Fixes #11146
- - - - - a829b10f by Philipp Hörist at 2022-09-18T22:18:48+02:00 refactor: ChatBanner: Move more code into dedicated methods
- - - - - 207f8155 by Philipp Hörist at 2022-09-18T22:18:48+02:00 refactor: ConversationView: Use action for Scroll Up/Down
- - - - -
10 changed files:
- gajim/common/helpers.py - gajim/common/modules/contacts.py - gajim/common/preview.py - gajim/data/gui/search_view.ui - gajim/data/other/shortcuts.json - gajim/gtk/chat_banner.py - gajim/gtk/chat_stack.py - gajim/gtk/const.py - gajim/gtk/conversation/view.py - gajim/gtk/message_actions_box.py
Changes:
===================================== gajim/common/helpers.py ===================================== @@ -64,6 +64,7 @@
from nbxmpp.namespaces import Namespace from nbxmpp.const import Role +from nbxmpp.const import Chatstate from nbxmpp.const import ConnectionProtocol from nbxmpp.const import ConnectionType from nbxmpp.const import Affiliation @@ -304,17 +305,23 @@ def from_one_line(msg: str) -> str: return msg
-def get_uf_chatstate(chatstate: str) -> str: - ''' - Remove chatstate jargon and returns user friendly messages - ''' - if chatstate in ('inactive', 'gone'): - return _('is doing something else') - if chatstate == 'composing': +def chatstate_to_string(chatstate: Optional[Chatstate]) -> str: + if chatstate is None: + return '' + + if chatstate == Chatstate.ACTIVE: + return '' + + if chatstate == Chatstate.COMPOSING: return _('is composing a message…') - if chatstate == 'paused': + + if chatstate in (Chatstate.INACTIVE, Chatstate.GONE): + return _('is doing something else') + + if chatstate == Chatstate.PAUSED: return _('paused composing a message') - return '' + + raise ValueError('unknown value: %s' % chatstate)
def exec_command(command: str,
===================================== gajim/common/modules/contacts.py ===================================== @@ -45,6 +45,7 @@ from gajim.common.structs import UNKNOWN_MUC_PRESENCE from gajim.common.structs import MUCPresenceData from gajim.common.helpers import Observable +from gajim.common.helpers import chatstate_to_string from gajim.common.modules.base import BaseModule from gajim.common.modules.util import LogAdapter from gajim.common.helpers import get_groupchat_name @@ -322,6 +323,14 @@ def is_jingle_available(self) -> bool: def get_address(self, _prefer_real: bool = True) -> JID: return self._jid
+ @property + def chatstate(self) -> Optional[Chatstate]: + return None + + @property + def chatstate_string(self) -> str: + return chatstate_to_string(self.chatstate) + def __repr__(self) -> str: return f'{self.jid} ({self._account})'
===================================== gajim/common/preview.py ===================================== @@ -237,8 +237,13 @@ def _accept_uri(urlparts: ParseResult, if urlparts.scheme in ('https', 'http'): if app.settings.get('preview_allow_all_images'): mime_type = guess_mime_type(uri) - if (mime_type not in MIME_TYPES or - mime_type == 'application/octet-stream'): + if mime_type not in MIME_TYPES: + return False + + if mime_type == 'application/octet-stream' and uri != oob_url: + # guess_mime_type yields 'application/octet-stream' for + # paths without suffix. Check oob_url to make sure we + # display a preview for files sent via http upload. return False return True
@@ -345,7 +350,7 @@ def _on_orig_load_finished(self, log.error('%s: %s', preview.orig_path.name, error) return
- preview.mime_type = guess_mime_type(preview.orig_path) + preview.mime_type = guess_mime_type(preview.orig_path, data) preview.file_size = os.path.getsize(preview.orig_path) if preview.is_previewable: if preview.create_thumbnail(data): @@ -368,7 +373,7 @@ def _on_thumb_load_finished(data: Optional[bytes], return
preview.thumbnail = data - preview.mime_type = guess_mime_type(preview.orig_path) + preview.mime_type = guess_mime_type(preview.orig_path, data) preview.file_size = os.path.getsize(preview.orig_path)
try:
===================================== gajim/data/gui/search_view.ui ===================================== @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.38.2 --> +<!-- Generated with glade 3.40.0 --> <interface> <requires lib="gtk+" version="3.24"/> <object class="GtkPopover" id="calendar_popover"> @@ -215,6 +215,10 @@ <property name="primary-icon-name">edit-find-symbolic</property> <property name="primary-icon-activatable">False</property> <property name="primary-icon-sensitive">False</property> + <property name="primary-icon-tooltip-text" translatable="yes">Use filters to narrow down your search: +from:username +before:yyyy-mm-dd +after:yyyy-mm-dd</property> <signal name="activate" handler="_on_search" swapped="no"/> </object> <packing>
===================================== gajim/data/other/shortcuts.json ===================================== @@ -37,5 +37,7 @@ "win.switch-workspace-7": ["<Primary>7", "<Primary>KP_7"], "win.switch-workspace-8": ["<Primary>8", "<Primary>KP_8"], "win.switch-workspace-9": ["<Primary>9", "<Primary>KP_9"], - "win.toggle-chat-list": ["<Primary>R"] + "win.toggle-chat-list": ["<Primary>R"], + "win.scroll-view-up": ["<Shift>Page_Up"], + "win.scroll-view-down": ["<Shift>Page_Down"] }
===================================== gajim/gtk/chat_banner.py ===================================== @@ -17,6 +17,7 @@ from typing import Any from typing import Optional
+from gi.repository import GLib from gi.repository import Gtk
import cairo @@ -31,7 +32,6 @@ from gajim.common.events import MessageReceived from gajim.common.events import MucDiscoUpdate from gajim.common.ged import EventHelper -from gajim.common.helpers import get_uf_chatstate from gajim.common.i18n import _ from gajim.common.modules.contacts import BareContact from gajim.common.modules.contacts import GroupchatContact @@ -63,37 +63,54 @@ def __init__(self) -> None:
hide_roster = app.settings.get('hide_groupchat_occupants_list') self._set_toggle_roster_button_icon(hide_roster) + app.settings.connect_signal( 'hide_groupchat_occupants_list', self._set_toggle_roster_button_icon)
- self.show_all() + app.settings.connect_signal( + 'hide_chat_banner', + self._update_hide_banner)
- def clear(self) -> None: - if self._contact is not None: - self._contact.disconnect_all_from_obj(self) - self._contact = None + app.settings.connect_signal( + 'hide_groupchat_banner', + self._update_hide_banner)
- if self._client is not None: - self._client.disconnect_all_from_obj(self) - self._client = None + self.show_all()
+ def clear(self) -> None: + self._disconnect_signals() self.unregister_events()
def switch_contact(self, contact: types.ChatContactT) -> None: - self._update_account_badge(contact.account) + self._disconnect_signals() + self._client = app.get_client(contact.account) + self._contact = contact + self._connect_signals()
- if self._client is not None: - self._client.disconnect_all_from_obj(self) + if not self.has_events_registered(): + self.register_events([ + ('message-received', ged.GUI2, self._on_message_received), + ('muc-disco-update', ged.GUI2, self._on_muc_disco_update), + ('bookmarks-received', ged.GUI2, self._on_bookmarks_received), + ('account-enabled', ged.GUI2, self._on_account_changed), + ('account-disabled', ged.GUI2, self._on_account_changed) + ])
- self._client = app.get_client(contact.account) - self._client.connect_signal('state-changed', - self._on_client_state_changed) + self._voice_requests_button.switch_contact(self._contact)
- if self._contact is not None: - self._contact.disconnect_all_from_obj(self) + self._update_phone_image() + self._update_roster_button() + self._update_avatar() + self._update_visitor_button() + self._update_name_label() + self._update_account_badge() + self._update_hide_banner() + + def _connect_signals(self) -> None: + assert self._contact is not None + assert self._client is not None
- self._contact = contact self._contact.multi_connect({ 'chatstate-update': self._on_chatstate_update, 'nickname-update': self._on_nickname_update, @@ -108,93 +125,81 @@ def switch_contact(self, contact: types.ChatContactT) -> None: 'state-changed': self._on_muc_state_changed, 'room-voice-request': self._on_room_voice_request }) - self._ui.toggle_roster_button.show() - hide_banner = app.settings.get('hide_groupchat_banner') - else: - self._ui.toggle_roster_button.hide() - hide_banner = app.settings.get('hide_chat_banner')
- if isinstance(self._contact, GroupchatParticipant): + elif isinstance(self._contact, GroupchatParticipant): self._contact.multi_connect({ 'user-joined': self._on_user_state_changed, 'user-left': self._on_user_state_changed, 'user-avatar-update': self._on_user_avatar_update, })
- if not self.has_events_registered(): - self.register_events([ - ('message-received', ged.GUI2, self._on_message_received), - ('muc-disco-update', ged.GUI2, self._on_muc_disco_update), - ('bookmarks-received', ged.GUI2, self._on_bookmarks_received), - ('account-enabled', ged.GUI2, self._on_account_changed), - ('account-disabled', ged.GUI2, self._on_account_changed) - ]) - - self._ui.phone_image.set_visible( - self._contact in self._last_message_from_phone) - - self._voice_requests_button.switch_contact(self._contact) + self._client.connect_signal('state-changed', + self._on_client_state_changed)
- if hide_banner: - self.set_no_show_all(True) - self.hide() - else: - self.set_no_show_all(False) - self.show_all() + def _disconnect_signals(self) -> None: + if self._contact is not None: + self._contact.disconnect_all_from_obj(self) + self._contact = None
- self._update_avatar() - self._update_content() + if self._client is not None: + self._client.disconnect_all_from_obj(self) + self._client = None
def _on_client_state_changed(self, _client: types.Client, _signal_name: str, state: SimpleClientState): + self._update_avatar() - self._update_content()
def _on_presence_update(self, _contact: types.BareContact, _signal_name: str ) -> None: + self._update_avatar()
def _on_chatstate_update(self, _contact: types.BareContact, _signal_name: str ) -> None: - self._update_content() + + self._update_name_label()
def _on_nickname_update(self, _contact: types.BareContact, _signal_name: str ) -> None: - self._update_content() + + self._update_name_label()
def _on_avatar_update(self, _contact: types.BareContact, _signal_name: str ) -> None: + self._update_avatar()
def _on_caps_update(self, _contact: types.BareContact, _signal_name: str ) -> None: + self._update_avatar()
def _on_muc_state_changed(self, contact: GroupchatContact, _signal_name: str ) -> None: - if contact.is_joined: - self._update_content() + + self._update_visitor_button()
def _on_room_voice_request(self, *args: Any) -> None: self._voice_requests_button.set_no_show_all(False) self._voice_requests_button.show_all()
def _on_user_role_changed(self, *args: Any) -> None: - self._update_content() + self._update_visitor_button()
def _on_user_state_changed(self, *args: Any) -> None: self._update_avatar() @@ -206,18 +211,17 @@ def _on_bookmarks_received(self, _event: BookmarksReceived) -> None: if not isinstance(self._contact, GroupchatContact): return
- self._update_content() + self._update_name_label()
def _on_muc_disco_update(self, event: MucDiscoUpdate) -> None: assert self._contact is not None if event.jid != self._contact.jid: return
- self._update_content() + self._update_name_label()
def _on_account_changed(self, event: AccountEnabled) -> None: - assert self._contact is not None - self._update_account_badge(self._contact.account) + self._update_account_badge()
def _on_message_received(self, event: MessageReceived) -> None: if (not isinstance(self._contact, BareContact) or @@ -233,7 +237,31 @@ def _on_message_received(self, event: MessageReceived) -> None: else: self._last_message_from_phone.discard(self._contact)
- self._ui.phone_image.set_visible(resource_contact.is_phone) + self._update_phone_image() + + def _update_phone_image(self) -> None: + self._ui.phone_image.set_visible( + self._contact in self._last_message_from_phone) + + def _update_roster_button(self) -> None: + self._ui.toggle_roster_button.set_visible( + isinstance(self._contact, GroupchatContact)) + + def _update_hide_banner(self, *args: Any) -> None: + if self._contact is None: + return + + if isinstance(self._contact, GroupchatContact): + hide_banner = app.settings.get('hide_groupchat_banner') + else: + hide_banner = app.settings.get('hide_chat_banner') + + if hide_banner: + self.set_no_show_all(True) + self.hide() + else: + self.set_no_show_all(False) + self.show_all()
def _update_avatar(self) -> None: scale = app.window.get_scale_factor() @@ -242,54 +270,50 @@ def _update_avatar(self) -> None: assert isinstance(surface, cairo.ImageSurface) self._ui.avatar_image.set_from_surface(surface)
- def _update_content(self) -> None: - assert self._contact is not None - assert self._client is not None + def _update_visitor_button(self) -> None: + if not isinstance(self._contact, GroupchatContact): + self._ui.visitor_box.set_visible(False) + return
- name = self._contact.name + if self._contact.is_not_joined: + self._ui.visitor_box.set_visible(False) + return
- if self._contact.jid.bare_match(self._client.get_own_jid()): - name = _('Note to myself') + self_contact = self._contact.get_self() + assert self_contact is not None + self._ui.visitor_box.set_visible(self_contact.role.is_visitor)
- if self._contact.is_pm_contact: - gc_contact = self._client.get_module('Contacts').get_contact( - self._contact.jid.bare) - name = f'{name} ({gc_contact.name})' + def _update_name_label(self) -> None: + assert self._contact is not None + + name = self._get_name_from_contact(self._contact)
- label_text = f'<span>{name}</span>' - label_tooltip = name - show_chatstate = app.settings.get('show_chatstate_in_banner') - if (show_chatstate and isinstance( - self._contact, (BareContact, GroupchatParticipant))): - chatstate = self._contact.chatstate - if chatstate is not None: - chatstate = get_uf_chatstate(chatstate.value) - else: - chatstate = '' + chatstate = '' + if app.settings.get('show_chatstate_in_banner'): + chatstate = self._contact.chatstate_string
- label_text = f'<span>{name}</span>' \ - f'<span size="60%" weight="light">' \ - f' {chatstate}</span>' - label_tooltip = f'{name} {chatstate}' + label_text = f'<span>{GLib.markup_escape_text(name)}</span>' + if chatstate: + label_markup = '<span size="60%" weight="light"> %s</span>' + label_text += label_markup % GLib.markup_escape_text(chatstate)
self._ui.name_label.set_markup(label_text) - self._ui.name_label.set_tooltip_text(label_tooltip)
- if isinstance(self._contact, GroupchatContact): - self_contact = self._contact.get_self() - if self_contact: - self._ui.visitor_box.set_visible(self_contact.role.is_visitor) - return + tooltip_text = name + if chatstate: + tooltip_text = f'{name} {chatstate}'
- self._ui.visitor_box.set_visible(False) + self._ui.name_label.set_tooltip_text(tooltip_text) + + def _update_account_badge(self) -> None: + assert self._contact is not None
- def _update_account_badge(self, account: str) -> None: if self._account_badge is not None: self._account_badge.destroy()
enabled_accounts = app.get_enabled_accounts_with_labels() if len(enabled_accounts) > 1: - self._account_badge = AccountBadge(account) + self._account_badge = AccountBadge(self._contact.account) self._ui.additional_items_box.pack_end( self._account_badge, False, True, 0)
@@ -308,3 +332,17 @@ def _set_toggle_roster_button_icon(self, icon = 'go-next-symbolic' if not hide_roster else 'go-previous-symbolic' self._ui.toggle_roster_image.set_from_icon_name( icon, Gtk.IconSize.BUTTON) + + @staticmethod + def _get_name_from_contact(contact: types.ChatContactT) -> str: + name = contact.name + + if isinstance(contact, BareContact): + if contact.is_self: + return _('Note to myself') + return name + + if isinstance(contact, GroupchatParticipant): + return f'{name} ({contact.room.name})' + + return name
===================================== gajim/gtk/chat_stack.py ===================================== @@ -704,9 +704,23 @@ def _on_send_message(self) -> None:
correct_id = None if self._message_action_box.is_correcting: + self._message_action_box.is_correcting = False + context = self._message_action_box.msg_textview.get_style_context() + context.remove_class('gajim-msg-correcting') + correct_id = self._message_action_box.last_message_id.get( (contact.account, contact.jid)) - self._message_action_box.is_correcting = False + message_row = app.storage.archive.get_last_correctable_message( + contact.account, contact.jid, correct_id) + if message_row is None: + self._message_action_box.msg_textview.clear() + log.info('Trying to correct message older than threshold') + return + + if message_row.message == message: + self._message_action_box.msg_textview.clear() + log.info('Trying to correct message with original text') + return
chatstate = client.get_module('Chatstate').get_active_chatstate( contact)
===================================== gajim/gtk/const.py ===================================== @@ -214,6 +214,8 @@ def __str__(self): ('muc-change-role', 'as', False), ('muc-change-affiliation', 'as', False), ('muc-request-voice', None, False), + ('scroll-view-up', None, True), + ('scroll-view-down', None, True), ]
===================================== gajim/gtk/conversation/view.py ===================================== @@ -15,6 +15,7 @@ from __future__ import annotations
from typing import Any +from typing import Literal from typing import Optional from typing import Union from typing import Generator @@ -29,6 +30,7 @@ from gi.repository import GLib from gi.repository import Gtk from gi.repository import GObject +from gi.repository import Gio
from nbxmpp.errors import StanzaError from nbxmpp.modules.security_labels import Displaymarking @@ -135,6 +137,22 @@ def __init__(self) -> None: self.add(self._list_box) self.set_focus_vadjustment(Gtk.Adjustment())
+ app.window.get_action('scroll-view-up').connect( + 'activate', self._on_scroll_view) + app.window.get_action('scroll-view-down').connect( + 'activate', self._on_scroll_view) + + def _on_scroll_view(self, + action: Gio.SimpleAction, + _param: Literal[None]) -> None: + + action_name = action.get_name() + if action_name == 'scroll-view-down': + self.emit('scroll-child', Gtk.ScrollType.PAGE_DOWN, False) + + elif action_name == 'scroll-view-up': + self.emit('scroll-child', Gtk.ScrollType.PAGE_UP, False) + def _enable_signal_handlers(self, enable: bool) -> None: if self._signal_handlers_enabled == enable: return
===================================== gajim/gtk/message_actions_box.py ===================================== @@ -515,12 +515,6 @@ def _on_msg_textview_key_press_event(self, Direction.PREV, unread_first=True) return True
- if event.keyval in (Gdk.KEY_Page_Down, Gdk.KEY_Page_Up): - control = app.window.get_control() - if control.has_active_chat(): - control.conversation_view.event(event) - return True - if event_state & Gdk.ModifierType.CONTROL_MASK: if event.keyval == Gdk.KEY_Tab: app.window.select_next_chat(Direction.NEXT, unread_first=True) @@ -659,6 +653,3 @@ def _on_message_sent(self, event: events.MessageSent) -> None: self.last_message_id[(account, jid)] = None else: self.last_message_id[(account, jid)] = event.message_id - - self.msg_textview.get_style_context().remove_class( - 'gajim-msg-correcting')
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/compare/8ceb84d5a5fbcbd36b7664db5e67d633...