diff options
author | Philipp Hörist <philipp@hoerist.com> | 2023-10-15 12:39:18 +0300 |
---|---|---|
committer | Philipp Hörist <philipp@hoerist.com> | 2023-10-22 01:10:24 +0300 |
commit | 85dfba6322f8dcca6c45919daa39cfdb14f7f923 (patch) | |
tree | baf3bae9a0000aa735e730cbd8d9270f7e01fc74 | |
parent | 7ce832fe2c80d31b21f189a339e600c0afed5165 (diff) |
feat: Display composing participants in MUC chat banner
-rw-r--r-- | gajim/common/modules/chatstates.py | 45 | ||||
-rw-r--r-- | gajim/common/modules/contacts.py | 12 | ||||
-rw-r--r-- | gajim/gtk/chat_banner.py | 34 | ||||
-rw-r--r-- | gajim/gtk/chat_list_row.py | 12 |
4 files changed, 85 insertions, 18 deletions
diff --git a/gajim/common/modules/chatstates.py b/gajim/common/modules/chatstates.py index 84de9db2b..a5863d5c3 100644 --- a/gajim/common/modules/chatstates.py +++ b/gajim/common/modules/chatstates.py @@ -19,6 +19,7 @@ from __future__ import annotations from typing import Any import time +from collections import defaultdict from functools import wraps from itertools import chain @@ -77,6 +78,11 @@ class Chatstate(BaseModule): # The current chatstate we received from a contact self._remote_chatstate: dict[JID, State] = {} + # Cache set of participants that are composing for group chats, + # to avoid having to iterate over all their chat states to determine + # who is typing a message. + self._muc_composers: dict[JID, set[GroupchatParticipant]] = \ + defaultdict(set) self._remote_chatstate_composing_timeouts: dict[JID, int] = {} @@ -154,16 +160,19 @@ class Chatstate(BaseModule): _stanza: Any, properties: MessageProperties ) -> None: - if properties.type.is_error: + if not (properties.type.is_chat or properties.type.is_groupchat): return - if not properties.has_chatstate: + if properties.is_self_message: + return + + if properties.is_mam_message: + return + + if properties.is_carbon_message and properties.carbon.is_sent: return - if (properties.is_self_message or - not properties.type.is_chat or - properties.is_mam_message or - properties.is_carbon_message and properties.carbon.is_sent): + if not properties.has_chatstate: return jid = properties.jid @@ -190,6 +199,20 @@ class Chatstate(BaseModule): contact.notify('chatstate-update') + if not isinstance(contact, GroupchatParticipant): + return + + if contact.is_self: + return + + muc = contact.room + + if state == State.COMPOSING: + self._muc_composers[muc.jid].add(contact) + else: + self._muc_composers[muc.jid].discard(contact) + muc.notify('chatstate-update') + def _on_remote_composing_timeout(self, contact: types.ContactT): self._remote_chatstate_composing_timeouts.pop(contact.jid, None) self._log.info( @@ -197,6 +220,16 @@ class Chatstate(BaseModule): self._remote_chatstate[contact.jid] = State.ACTIVE contact.notify('chatstate-update') + if isinstance(contact, GroupchatParticipant): + self._muc_composers[contact.room.jid].discard(contact) + contact.room.notify('chatstate-update') + + def get_composers(self, jid: JID) -> list[GroupchatParticipant]: + ''' + List of group chat participants that are composing (=typing) for a MUC. + ''' + return list(self._muc_composers[jid]) + def _remove_remote_composing_timeout(self, contact: types.ContactT): source_id = self._remote_chatstate_composing_timeouts.pop( contact.jid, None) diff --git a/gajim/common/modules/contacts.py b/gajim/common/modules/contacts.py index e8c861143..1abd0b420 100644 --- a/gajim/common/modules/contacts.py +++ b/gajim/common/modules/contacts.py @@ -917,6 +917,12 @@ class GroupchatContact(CommonContact): def type_string(self) -> str: return 'groupchat' + def has_composing_participants(self) -> bool: + return bool(self.get_module('Chatstate').get_composers(self._jid)) + + def get_composers(self) -> list['GroupchatParticipant']: + return self.get_module('Chatstate').get_composers(self._jid) + class GroupchatParticipant(CommonContact): def __init__(self, logger: LogAdapter, jid: JID, account: str) -> None: @@ -1048,6 +1054,12 @@ class GroupchatParticipant(CommonContact): def occupant_id(self) -> str | None: return self._presence.occupant_id + @property + def is_self(self) -> bool: + data = self.get_module('MUC').get_muc_data(self.room.jid) + assert data is not None + return data.nick == self.name + def can_add_to_roster( contact: BareContact | GroupchatContact | GroupchatParticipant diff --git a/gajim/gtk/chat_banner.py b/gajim/gtk/chat_banner.py index d584291d7..cd9b0a939 100644 --- a/gajim/gtk/chat_banner.py +++ b/gajim/gtk/chat_banner.py @@ -164,11 +164,13 @@ class ChatBanner(Gtk.Box, EventHelper): self._update_description_label() def _on_chatstate_update(self, - _contact: types.BareContact, + contact: types.BareContact, _signal_name: str ) -> None: - - self._update_name_label() + if contact.is_groupchat: + self._update_description_label() + else: + self._update_name_label() def _on_nickname_update(self, _contact: types.BareContact, @@ -327,16 +329,32 @@ class ChatBanner(Gtk.Box, EventHelper): self._ui.name_label.set_tooltip_text(tooltip_text) + def _get_muc_description_text(self) -> str: + contact = self._contact + assert isinstance(contact, GroupchatContact) + + typing = contact.get_composers() + if not typing: + disco_info = app.storage.cache.get_last_disco_info(contact.jid) + if disco_info is None: + return '' + return disco_info.muc_description or '' + + composers = tuple(c.name for c in typing) + n = len(composers) + if n == 1: + return _('%s is typing…') % composers[0] + elif n == 2: + return _('%s and %s are typing…') % composers + else: + return _('%s participants are typing…') % n + def _update_description_label(self) -> None: contact = self._contact assert contact is not None if contact.is_groupchat: - disco_info = app.storage.cache.get_last_disco_info(contact.jid) - if disco_info is None: - text = '' - else: - text = disco_info.muc_description or '' + text = self._get_muc_description_text() else: assert not isinstance(contact, GroupchatContact) text = contact.status or '' diff --git a/gajim/gtk/chat_list_row.py b/gajim/gtk/chat_list_row.py index c0864e722..ed8cf3fec 100644 --- a/gajim/gtk/chat_list_row.py +++ b/gajim/gtk/chat_list_row.py @@ -48,7 +48,6 @@ from gajim.common.preview_helpers import guess_simple_file_type from gajim.common.preview_helpers import split_geo_uri from gajim.common.storage.draft import DraftStorage from gajim.common.types import ChatContactT -from gajim.common.types import OneOnOneContactT from gajim.gtk.builder import get_builder from gajim.gtk.menus import get_chat_list_row_menu @@ -528,9 +527,9 @@ class ChatListRow(Gtk.ListBoxRow): selection_data.set(drop_type, 8, byte_data) def _connect_contact_signals(self) -> None: + self.contact.connect('chatstate-update', self._on_chatstate_update) if isinstance(self.contact, BareContact): self.contact.connect('presence-update', self._on_presence_update) - self.contact.connect('chatstate-update', self._on_chatstate_update) self.contact.connect('nickname-update', self._on_nickname_update) self.contact.connect('caps-update', self._on_avatar_update) self.contact.connect('avatar-update', self._on_avatar_update) @@ -548,7 +547,6 @@ class ChatListRow(Gtk.ListBoxRow): self._on_client_state_changed) elif isinstance(self.contact, GroupchatParticipant): - self.contact.connect('chatstate-update', self._on_chatstate_update) self.contact.connect('user-joined', self._on_muc_user_update) self.contact.connect('user-left', self._on_muc_user_update) self.contact.connect('user-avatar-update', self._on_muc_user_update) @@ -661,9 +659,15 @@ class ChatListRow(Gtk.ListBoxRow): self.update_avatar() def _on_chatstate_update(self, - contact: OneOnOneContactT, + contact: ChatContactT, _signal_name: str ) -> None: + if contact.is_groupchat: + assert isinstance(contact, GroupchatContact) + self._ui.chatstate_image.set_visible( + contact.has_composing_participants()) + return + if contact.chatstate is None: self._ui.chatstate_image.hide() else: |