Welcome to mirror list, hosted at ThFree Co, Russian Federation.

dev.gajim.org/gajim/gajim.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhilipp Hörist <philipp@hoerist.com>2023-10-15 12:39:18 +0300
committerPhilipp Hörist <philipp@hoerist.com>2023-10-22 01:10:24 +0300
commit85dfba6322f8dcca6c45919daa39cfdb14f7f923 (patch)
treebaf3bae9a0000aa735e730cbd8d9270f7e01fc74
parent7ce832fe2c80d31b21f189a339e600c0afed5165 (diff)
feat: Display composing participants in MUC chat banner
-rw-r--r--gajim/common/modules/chatstates.py45
-rw-r--r--gajim/common/modules/contacts.py12
-rw-r--r--gajim/gtk/chat_banner.py34
-rw-r--r--gajim/gtk/chat_list_row.py12
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: