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>2022-09-04 19:31:21 +0300
committerPhilipp Hörist <philipp@hoerist.com>2022-09-04 19:31:21 +0300
commit91cb768eb2173ce27492571fdf721255fb027080 (patch)
treefe9b242dff4a274378d0f08e45329eec757f9054
parent5dbcf0b2962d9b812a4ef8296e44e9414e108915 (diff)
new: Add chat event storage
This allows us to store events like group chat joins/lefts or other info messages. This way we can load them on chat switch and they don’t get lost.
-rw-r--r--gajim/common/app.py2
-rw-r--r--gajim/common/application.py4
-rw-r--r--gajim/common/events.py101
-rw-r--r--gajim/common/modules/muc.py134
-rw-r--r--gajim/common/storage/base.py21
-rw-r--r--gajim/common/storage/events.py133
-rw-r--r--gajim/gtk/chat_banner.py10
-rw-r--r--gajim/gtk/chat_stack.py9
-rw-r--r--gajim/gtk/control.py638
-rw-r--r--gajim/gtk/conversation/rows/muc_join_left.py12
-rw-r--r--gajim/gtk/conversation/view.py29
-rw-r--r--gajim/gtk/groupchat_nick_completion.py8
-rw-r--r--gajim/gtk/groupchat_roster.py8
-rw-r--r--pyrightconfig.json2
14 files changed, 773 insertions, 338 deletions
diff --git a/gajim/common/app.py b/gajim/common/app.py
index b27ed70bb..fa2155e97 100644
--- a/gajim/common/app.py
+++ b/gajim/common/app.py
@@ -59,6 +59,7 @@ if typing.TYPE_CHECKING:
from gajim.gui.application import GajimApplication
from gajim.common.storage.cache import CacheStorage
from gajim.common.storage.archive import MessageArchiveStorage
+ from gajim.common.storage.events import EventStorage
from gajim.common.cert_store import CertificateStore
from gajim.common.call_manager import CallManager
from gajim.common.preview import PreviewManager
@@ -88,6 +89,7 @@ class Storage:
def __init__(self):
self.cache: CacheStorage = None
self.archive: MessageArchiveStorage = None
+ self.events: EventStorage = None
storage = Storage()
diff --git a/gajim/common/application.py b/gajim/common/application.py
index 5817d9eb1..41971bfd4 100644
--- a/gajim/common/application.py
+++ b/gajim/common/application.py
@@ -44,6 +44,7 @@ from gajim.common.events import AllowGajimUpdateCheck
from gajim.common.events import GajimUpdateAvailable
from gajim.common.client import Client
from gajim.common.helpers import make_http_request
+from gajim.common.storage.events import EventStorage
from gajim.common.task_manager import TaskManager
from gajim.common.settings import Settings
from gajim.common.settings import LegacyConfig
@@ -74,6 +75,9 @@ class CoreApplication:
app.storage.cache = CacheStorage()
app.storage.cache.init()
+ app.storage.events = EventStorage()
+ app.storage.events.init()
+
app.storage.archive = MessageArchiveStorage()
app.storage.archive.init()
diff --git a/gajim/common/events.py b/gajim/common/events.py
index a4fac93d3..8640b2ebe 100644
--- a/gajim/common/events.py
+++ b/gajim/common/events.py
@@ -32,7 +32,10 @@ from nbxmpp.structs import ModerationData
from nbxmpp.structs import LocationData
from nbxmpp.structs import RosterItem
from nbxmpp.structs import TuneData
+from nbxmpp.const import Affiliation
from nbxmpp.const import InviteType
+from nbxmpp.const import Role
+from nbxmpp.const import StatusCode
from gajim.common.file_props import FileProp
from gajim.common.const import JingleState
@@ -676,3 +679,101 @@ class AllowGajimUpdateCheck(ApplicationEvent):
class GajimUpdateAvailable(ApplicationEvent):
name: str = field(init=False, default='gajim-update-available')
version: str
+
+
+@dataclass
+class MUCNicknameChanged(ApplicationEvent):
+ name: str = field(init=False, default='muc-nickname-changed')
+ is_self: bool
+ new_name: str
+ old_name: str
+ timestamp: float
+
+
+@dataclass
+class MUCRoomConfigChanged(ApplicationEvent):
+ name: str = field(init=False, default='muc-room-config-changed')
+ timestamp: float
+ status_codes: set[StatusCode]
+
+
+@dataclass
+class MUCRoomConfigFinished(ApplicationEvent):
+ name: str = field(init=False, default='muc-room-config-finished')
+ timestamp: float
+
+
+@dataclass
+class MUCRoomPresenceError(ApplicationEvent):
+ name: str = field(init=False, default='muc-room-presence-error')
+ timestamp: float
+ error: Any
+
+
+@dataclass
+class MUCRoomKicked(ApplicationEvent):
+ name: str = field(init=False, default='muc-room-kicked')
+ timestamp: float
+ status_codes: Optional[set[StatusCode]]
+ reason: Optional[str]
+ actor: Optional[str]
+
+
+@dataclass
+class MUCRoomDestroyed(ApplicationEvent):
+ name: str = field(init=False, default='muc-room-destroyed')
+ timestamp: float
+ reason: Optional[str]
+ alternate: Optional[JID]
+
+
+@dataclass
+class MUCUserJoined(ApplicationEvent):
+ name: str = field(init=False, default='muc-user-joined')
+ timestamp: float
+ is_self: bool
+ nick: str
+ status_codes: Optional[set[StatusCode]]
+
+
+@dataclass
+class MUCUserLeft(ApplicationEvent):
+ name: str = field(init=False, default='muc-user-left')
+ timestamp: float
+ is_self: bool
+ nick: str
+ status_codes: Optional[set[StatusCode]]
+ reason: Optional[str]
+ actor: Optional[str]
+
+
+@dataclass
+class MUCUserRoleChanged(ApplicationEvent):
+ name: str = field(init=False, default='muc-user-role-changed')
+ timestamp: float
+ is_self: bool
+ nick: str
+ role: Role
+ reason: Optional[str]
+ actor: Optional[str]
+
+
+@dataclass
+class MUCUserAffiliationChanged(ApplicationEvent):
+ name: str = field(init=False, default='muc-user-affiliation-changed')
+ timestamp: float
+ is_self: bool
+ nick: str
+ affiliation: Affiliation
+ reason: Optional[str]
+ actor: Optional[str]
+
+
+@dataclass
+class MUCUserStatusShowChanged(ApplicationEvent):
+ name: str = field(init=False, default='muc-user-status-show-changed')
+ timestamp: float
+ is_self: bool
+ nick: str
+ status: str
+ show_value: str
diff --git a/gajim/common/modules/muc.py b/gajim/common/modules/muc.py
index cff7e5e05..d222fc499 100644
--- a/gajim/common/modules/muc.py
+++ b/gajim/common/modules/muc.py
@@ -45,6 +45,7 @@ from nbxmpp.task import Task
from gi.repository import GLib
from gajim.common import app
+from gajim.common import events
from gajim.common import helpers
from gajim.common import types
from gajim.common.const import ClientState, KindConstant
@@ -460,7 +461,11 @@ class MUC(BaseModule):
self._log.info('Configuration finished: %s', jid)
room = self._get_contact(jid.bare)
- room.notify('room-config-finished')
+ event = events.MUCRoomConfigFinished(timestamp=time.time())
+
+ assert isinstance(room, GroupchatContact)
+ app.storage.events.store(room, event)
+ room.notify('room-config-finished', event)
def update_presence(self) -> None:
mucs = self._get_mucs_with_state([MUCJoinedState.JOINED,
@@ -540,6 +545,11 @@ class MUC(BaseModule):
self._set_muc_state(room_jid, MUCJoinedState.NOT_JOINED)
else:
+ event = events.MUCRoomPresenceError(
+ timestamp=time.time(),
+ error=properties.error)
+ assert isinstance(room, GroupchatContact)
+ app.storage.events.store(room, event)
room.notify('room-presence-error', properties)
def _on_muc_user_presence(self,
@@ -564,7 +574,15 @@ class MUC(BaseModule):
self._set_muc_state(room_jid, MUCJoinedState.NOT_JOINED)
self._con.get_module('Bookmarks').remove(room_jid)
room.set_not_joined()
- room.notify('room-destroyed', properties)
+
+ event = events.MUCRoomDestroyed(
+ timestamp=time.time(),
+ reason=properties.muc_destroyed.reason,
+ alternate=properties.muc_destroyed.alternate)
+ assert isinstance(room, GroupchatContact)
+ app.storage.events.store(room, event)
+
+ room.notify('room-destroyed', event)
return
if properties.is_nickname_changed:
@@ -592,10 +610,16 @@ class MUC(BaseModule):
presence,
occupant)
- room.notify('user-nickname-changed',
- occupant,
- new_occupant,
- properties)
+ event = events.MUCNicknameChanged(
+ timestamp=time.time(),
+ is_self=properties.is_muc_self_presence,
+ new_name=new_occupant.name,
+ old_name=occupant.name)
+
+ assert isinstance(room, GroupchatContact)
+ app.storage.events.store(room, event)
+
+ room.notify('user-nickname-changed', event, occupant, new_occupant)
return
is_joined = self._is_user_joined(properties.jid)
@@ -624,7 +648,16 @@ class MUC(BaseModule):
if properties.is_muc_self_presence and properties.is_kicked:
self._set_muc_state(room_jid, MUCJoinedState.NOT_JOINED)
room.set_not_joined()
- room.notify('room-kicked', properties)
+
+ event = events.MUCRoomKicked(
+ timestamp=time.time(),
+ status_codes=properties.muc_status_codes,
+ reason=properties.muc_user.reason,
+ actor=properties.muc_user.actor)
+ assert isinstance(room, GroupchatContact)
+ app.storage.events.store(room, event)
+ room.notify('room-kicked', event)
+
status_codes = properties.muc_status_codes or []
if StatusCode.REMOVED_SERVICE_SHUTDOWN in status_codes:
self._start_rejoin_timeout(room_jid)
@@ -652,30 +685,84 @@ class MUC(BaseModule):
presence: MUCPresenceData,
occupant: GroupchatParticipant) -> None:
+ timestamp = time.time()
+
if not occupant.is_available and presence.available:
+
+ event = events.MUCUserJoined(
+ timestamp=timestamp,
+ is_self=properties.is_muc_self_presence,
+ nick=occupant.name,
+ status_codes=properties.muc_status_codes)
+
+ if occupant.room.is_joined or properties.is_muc_self_presence:
+ # Don’t store initial presences on join
+ app.storage.events.store(occupant.room, event)
+
occupant.update_presence(presence)
- occupant.notify('user-joined', properties)
+ occupant.notify('user-joined', event)
return
if not presence.available:
+
+ event = events.MUCUserLeft(
+ timestamp=timestamp,
+ is_self=properties.is_muc_self_presence,
+ nick=occupant.name,
+ status_codes=properties.muc_status_codes,
+ reason=properties.muc_user.reason,
+ actor=properties.muc_user.actor)
+
+ app.storage.events.store(occupant.room, event)
occupant.update_presence(presence)
- occupant.notify('user-left', properties)
+ occupant.notify('user-left', event)
return
- signals: list[str] = []
+ signals_and_events: list[tuple[str, Any]] = []
+
if occupant.affiliation != presence.affiliation:
- signals.append('user-affiliation-changed')
+
+ event = events.MUCUserAffiliationChanged(
+ timestamp=timestamp,
+ is_self=properties.is_muc_self_presence,
+ nick=occupant.name,
+ affiliation=presence.affiliation,
+ reason=properties.muc_user.reason,
+ actor=properties.muc_user.actor)
+
+ app.storage.events.store(occupant.room, event)
+ signals_and_events.append(('user-affiliation-changed', event))
if occupant.role != presence.role:
- signals.append('user-role-changed')
+
+ event = events.MUCUserRoleChanged(
+ timestamp=timestamp,
+ is_self=properties.is_muc_self_presence,
+ nick=occupant.name,
+ role=presence.role,
+ reason=properties.muc_user.reason,
+ actor=properties.muc_user.actor)
+
+ app.storage.events.store(occupant.room, event)
+ signals_and_events.append(('user-role-changed', event))
if (occupant.status != presence.status or
occupant.show != presence.show):
- signals.append('user-status-show-changed')
+
+ event = events.MUCUserStatusShowChanged(
+ timestamp=timestamp,
+ is_self=properties.is_muc_self_presence,
+ nick=occupant.name,
+ status=properties.status,
+ show_value=properties.show.value)
+
+ app.storage.events.store(occupant, event)
+ app.storage.events.store(occupant.room, event)
+ signals_and_events.append(('user-status-show-changed', event))
occupant.update_presence(presence)
- for signal in signals:
- occupant.notify(signal, properties)
+ for signal, event in signals_and_events:
+ occupant.notify(signal, event)
def _process_user_presence(self,
properties: PresenceProperties
@@ -953,12 +1040,21 @@ class MUC(BaseModule):
if not properties.is_muc_config_change:
return
- room_jid = str(properties.muc_jid)
self._log.info('Received config change: %s %s',
- room_jid, properties.muc_status_codes)
+ properties.muc_jid, properties.muc_status_codes)
- room = self._get_contact(room_jid)
- room.notify('room-config-changed', properties)
+ assert properties.muc_status_codes is not None
+ event = events.MUCRoomConfigChanged(
+ timestamp=time.time(),
+ status_codes=properties.muc_status_codes)
+
+ assert properties.muc_jid is not None
+ room = self._get_contact(properties.muc_jid)
+
+ assert isinstance(room, GroupchatContact)
+ app.storage.events.store(room, event)
+
+ room.notify('room-config-changed', event)
raise nbxmpp.NodeProcessed
diff --git a/gajim/common/storage/base.py b/gajim/common/storage/base.py
index 8d59fcd8e..929c15232 100644
--- a/gajim/common/storage/base.py
+++ b/gajim/common/storage/base.py
@@ -30,11 +30,15 @@ from pathlib import Path
from gi.repository import GLib
+import nbxmpp.const
from nbxmpp.protocol import Iq
from nbxmpp.protocol import JID
from nbxmpp.structs import RosterItem
from nbxmpp.structs import DiscoInfo
from nbxmpp.structs import CommonError
+from nbxmpp.const import Role
+from nbxmpp.const import Affiliation
+from nbxmpp.const import StatusCode
from nbxmpp.modules.discovery import parse_disco_info
@@ -98,6 +102,13 @@ sqlite3.register_converter('disco_info', _convert_disco_info)
sqlite3.register_adapter(DiscoInfo, _adapt_disco_info)
+def _convert_json(json_string: bytes) -> dict[str, Any]:
+ return json.loads(json_string, object_hook=json_decoder)
+
+
+sqlite3.register_converter('JSON', _convert_json)
+
+
class Encoder(json.JSONEncoder):
def default(self, o: Any) -> Any:
if isinstance(o, set):
@@ -111,6 +122,10 @@ class Encoder(json.JSONEncoder):
dct['__type'] = 'RosterItem'
return dct
+ if isinstance(o, (Affiliation, Role, StatusCode)):
+ return {'value': o.value,
+ '__type': o.__class__.__name__}
+
return json.JSONEncoder.default(self, o)
@@ -118,8 +133,10 @@ def json_decoder(dct: dict[str, Any]) -> Any:
type_ = dct.get('__type')
if type_ is None:
return dct
+
if type_ == 'JID':
return JID.from_string(dct['value'])
+
if type_ == 'RosterItem':
return RosterItem(jid=dct['jid'],
name=dct['name'],
@@ -127,6 +144,10 @@ def json_decoder(dct: dict[str, Any]) -> Any:
subscription=dct['subscription'],
approved=dct['approved'],
groups=set(dct['groups']))
+
+ if type_ in ('Affiliation', 'Role', 'StatusCode'):
+ return getattr(nbxmpp.const, type_)(dct['value'])
+
return dct
diff --git a/gajim/common/storage/events.py b/gajim/common/storage/events.py
new file mode 100644
index 000000000..4a4d27d9b
--- /dev/null
+++ b/gajim/common/storage/events.py
@@ -0,0 +1,133 @@
+# This file is part of Gajim.
+#
+# Gajim is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published
+# by the Free Software Foundation; version 3 only.
+#
+# Gajim is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import annotations
+import dataclasses
+
+from typing import Any
+from typing import Iterator
+from typing import NamedTuple
+
+import json
+import sqlite3
+import logging
+from collections import namedtuple
+
+from nbxmpp.protocol import JID
+
+from gajim.common import events
+from gajim.common.storage.base import SqliteStorage
+from gajim.common.storage.base import Encoder
+from gajim.common.types import ChatContactT
+
+
+EVENTS_SQL_STATEMENT = '''
+ CREATE TABLE events (
+ account TEXT,
+ jid TEXT,
+ event TEXT,
+ timestamp REAL,
+ data TEXT);
+
+ CREATE INDEX idx_account_jid ON events(account, jid);
+
+ PRAGMA user_version=1;
+ '''
+
+EVENT_CLASSES: dict[str, Any] = {
+ 'muc-nickname-changed': events.MUCNicknameChanged,
+ 'muc-room-config-changed': events.MUCRoomConfigChanged,
+ 'muc-room-config-finished': events.MUCRoomConfigFinished,
+ 'muc-room-presence-error': events.MUCRoomPresenceError,
+ 'muc-room-kicked': events.MUCRoomKicked,
+ 'muc-room-destroyed': events.MUCRoomDestroyed,
+ 'muc-user-joined': events.MUCUserJoined,
+ 'muc-user-left': events.MUCUserLeft,
+ 'muc-user-role-changed': events.MUCUserRoleChanged,
+ 'muc-user-affiliation-changed': events.MUCUserAffiliationChanged,
+ 'muc-user-status-show-changed': events.MUCUserStatusShowChanged,
+}
+
+log = logging.getLogger('gajim.c.storage.events')
+
+
+class EventRow(NamedTuple):
+ account: str
+ jid: JID
+ event: str
+ timestamp: float
+ data: dict[str, Any]
+
+
+class EventStorage(SqliteStorage):
+ def __init__(self):
+ SqliteStorage.__init__(self,
+ log,
+ None,
+ EVENTS_SQL_STATEMENT)
+
+ def init(self, **kwargs: Any) -> None:
+ SqliteStorage.init(self,
+ detect_types=sqlite3.PARSE_COLNAMES)
+ self._con.row_factory = self._namedtuple_factory
+
+ @staticmethod
+ def _namedtuple_factory(cursor: sqlite3.Cursor,
+ row: tuple[Any, ...]) -> NamedTuple:
+
+ assert cursor.description is not None
+ fields = [col[0] for col in cursor.description]
+ Row = namedtuple('Row', fields) # type: ignore
+ return Row(*row)
+
+ def _migrate(self) -> None:
+ pass
+
+ def store(self,
+ contact: ChatContactT,
+ event: Any
+ ) -> None:
+
+ event_dict = dataclasses.asdict(event)
+ name = event_dict.pop('name')
+ timestamp = event_dict.pop('timestamp')
+
+ insert_sql = '''
+ INSERT INTO events(account, jid, event, timestamp, data)
+ VALUES(?, ?, ?, ?, ?)'''
+
+ self._con.execute(insert_sql, (contact.account,
+ contact.jid,
+ name,
+ timestamp,
+ json.dumps(event_dict, cls=Encoder)))
+
+ def load(self,
+ contact: ChatContactT,
+ before: bool,
+ timestamp: float
+ ) -> Iterator[EventRow]:
+
+ time_operator = '<' if before else '>'
+
+ insert_sql = '''
+ SELECT account, jid, event, timestamp, data as "data [JSON]"
+ FROM events WHERE account=? AND jid=? AND timestamp %s ?
+ ''' % time_operator
+
+ for row in self._con.execute(insert_sql, (contact.account,
+ contact.jid,
+ timestamp)).fetchall():
+ event_class = EVENT_CLASSES[row.event]
+ yield event_class(**row.data, timestamp=row.timestamp)
diff --git a/gajim/gtk/chat_banner.py b/gajim/gtk/chat_banner.py
index 6fc24d3ec..85a0de277 100644
--- a/gajim/gtk/chat_banner.py
+++ b/gajim/gtk/chat_banner.py
@@ -21,8 +21,6 @@ from gi.repository import Gtk
import cairo
-from nbxmpp.structs import PresenceProperties
-
from gajim.common import app
from gajim.common import ged
from gajim.common import types
@@ -195,13 +193,7 @@ class ChatBanner(Gtk.Box, EventHelper):
self._voice_requests_button.set_no_show_all(False)
self._voice_requests_button.show_all()
- def _on_user_role_changed(self,
- _contact: GroupchatContact,
- _signal_name: str,
- user_contact: GroupchatParticipant,
- properties: PresenceProperties
- ) -> None:
-
+ def _on_user_role_changed(self, *args: Any) -> None:
self._update_content()
def _on_user_state_changed(self, *args: Any) -> None:
diff --git a/gajim/gtk/chat_stack.py b/gajim/gtk/chat_stack.py
index 21faf766e..c10327b11 100644
--- a/gajim/gtk/chat_stack.py
+++ b/gajim/gtk/chat_stack.py
@@ -28,7 +28,6 @@ from gi.repository import Gtk
from nbxmpp.errors import StanzaError
from nbxmpp.protocol import JID
from nbxmpp.structs import MessageProperties
-from nbxmpp.structs import PresenceProperties
from gajim.common import app
from gajim.common import events
@@ -297,23 +296,25 @@ class ChatStack(Gtk.Stack, EventHelper):
contact: GroupchatContact,
_signal_name: str,
_user_contact: GroupchatParticipant,
- _properties: PresenceProperties
+ _event: events.MUCUserJoined
) -> None:
+
self._update_group_chat_actions(contact)
def _on_user_role_changed(self,
contact: GroupchatContact,
_signal_name: str,
_user_contact: GroupchatParticipant,
- _properties: PresenceProperties
+ _event: events.MUCUserRoleChanged
) -> None:
+
self._update_group_chat_actions(contact)
def _on_user_affiliation_changed(self,
contact: GroupchatContact,
_signal_name: str,
_user_contact: GroupchatParticipant,
- _properties: PresenceProperties
+ _event: events.MUCUserAffiliationChanged
) -> None:
self._update_group_chat_actions(contact)
diff --git a/gajim/gtk/control.py b/gajim/gtk/control.py
index c34cb831e..9b3821e16 100644
--- a/gajim/gtk/control.py
+++ b/gajim/gtk/control.py
@@ -19,9 +19,6 @@ from typing import Optional
from typing import Union
from typing import cast
-from collections import defaultdict
-from collections import deque
-from functools import partial
import os
import logging
import time
@@ -31,9 +28,7 @@ from gi.repository import GLib
from nbxmpp import JID
from nbxmpp.const import StatusCode
-from nbxmpp.structs import MessageProperties
from nbxmpp.structs import MucSubject
-from nbxmpp.structs import PresenceProperties
from nbxmpp.modules.security_labels import Displaymarking
from gajim.common import app
@@ -100,12 +95,6 @@ class ChatControl(EventHelper):
self._muc_subjects: dict[
types.ChatContactT, tuple[float, MucSubject]] = {}
- # Store 100 info messages per contact on a FIFO basis
- self._info_messages: dict[
- types.ChatContactT,
- deque[tuple[float, str]]] = defaultdict(
- partial(deque, maxlen=100)) # pyright: ignore
-
self.widget = cast(Gtk.Box, self._ui.get_object('control_box'))
self.widget.show_all()
@@ -177,7 +166,8 @@ class ChatControl(EventHelper):
if isinstance(contact, GroupchatParticipant):
contact.multi_connect({
- 'user-status-show-changed': self._on_user_status_show_changed,
+ 'user-status-show-changed':
+ self._on_participant_status_show_changed,
})
elif isinstance(contact, GroupchatContact):
@@ -186,8 +176,7 @@ class ChatControl(EventHelper):
'user-left': self._on_user_left,
'user-affiliation-changed': self._on_user_affiliation_changed,
'user-role-changed': self._on_user_role_changed,
- 'user-status-show-changed':
- self._on_muc_user_status_show_changed,
+ 'user-status-show-changed': self._on_user_status_show_changed,
'user-nickname-changed': self._on_user_nickname_changed,
'room-kicked': self._on_room_kicked,
'room-destroyed': self._on_room_destroyed,
@@ -205,9 +194,6 @@ class ChatControl(EventHelper):
for transfer in transfers:
self.add_file_transfer(transfer)
- for timestamp, message in self._info_messages[contact]:
- self.add_info_message(message, timestamp)
-
if isinstance(contact, GroupchatContact):
if (app.settings.get('show_subject_on_join') or
not contact.is_joining):
@@ -501,10 +487,6 @@ class ChatControl(EventHelper):
timestamp: Optional[float] = None
) -> None:
- if timestamp is None:
- assert self._contact is not None
- self._info_messages[self._contact].append((time.time(), text))
-
self.conversation_view.add_info_message(text, timestamp)
def add_file_transfer(self, transfer: HTTPFileTransfer) -> None:
@@ -619,6 +601,43 @@ class ChatControl(EventHelper):
# if self.conversation_view.reduce_message_count(before):
# self._scrolled_view.set_history_complete(before, False)
+ assert self._contact is not None
+ for event in app.storage.events.load(self._contact, before, timestamp):
+ if isinstance(event, events.MUCUserJoined):
+ self._process_muc_user_joined(event)
+
+ elif isinstance(event, events.MUCUserLeft):
+ self._process_muc_user_left(event)
+
+ elif isinstance(event, events.MUCNicknameChanged):
+ self._process_muc_nickname_changed(event)
+
+ elif isinstance(event, events.MUCRoomKicked):
+ self._process_muc_room_kicked(event)
+
+ elif isinstance(event, events.MUCUserAffiliationChanged):
+ self._process_muc_user_affiliation_changed(event)
+
+ elif isinstance(event, events.MUCUserRoleChanged):
+ self._process_muc_user_role_changed(event)
+
+ elif isinstance(event, events.MUCUserStatusShowChanged):
+ self._process_muc_user_status_show_changed(event)
+
+ elif isinstance(event, events.MUCRoomConfigChanged):
+ self._process_muc_room_config_changed(event)
+
+ elif isinstance(event, events.MUCRoomConfigFinished):
+ self._process_muc_room_config_finished(event)
+
+ elif isinstance(event, events.MUCRoomPresenceError):
+ self._process_muc_room_presence_error(event)
+
+ elif isinstance(event, events.MUCRoomDestroyed):
+ self._process_muc_room_destroyed(event)
+ else:
+ raise ValueError('Unknown event: %s' % event)
+
self._scrolled_view.block_signals(False)
def add_messages(self, messages: list[ConversationRow]):
@@ -703,120 +722,228 @@ class ChatControl(EventHelper):
additional_data=additional_data)
def _on_user_nickname_changed(self,
- contact: types.GroupchatContact,
+ _contact: types.GroupchatContact,
_signal_name: str,
- old_contact: types.GroupchatParticipant,
- new_contact: types.GroupchatParticipant,
- properties: PresenceProperties
+ event: events.MUCNicknameChanged,
+ _old_contact: types.GroupchatParticipant,
+ _new_contact: types.GroupchatParticipant
) -> None:
- if properties.is_muc_self_presence:
- message = _('You are now known as %s') % new_contact.name
+ self._process_muc_nickname_changed(event)
+
+ def _process_muc_nickname_changed(self,
+ event: events.MUCNicknameChanged
+ ) -> None:
+
+ if event.is_self:
+ message = _('You are now known as %s') % event.new_name
else:
message = _('{nick} is now known '
- 'as {new_nick}').format(nick=old_contact.name,
- new_nick=new_contact.name)
+ 'as {new_nick}').format(nick=event.old_name,
+ new_nick=event.new_name)
+ self.add_info_message(message, event.timestamp)
- self.add_info_message(message)
+ def _on_room_kicked(self,
+ _contact: GroupchatContact,
+ _signal_name: str,
+ event: events.MUCRoomKicked
+ ) -> None:
- def _on_muc_user_status_show_changed(self,
- contact: GroupchatContact,
- _signal_name: str,
- user_contact: GroupchatParticipant,
- properties: PresenceProperties
- ) -> None:
+ self._process_muc_room_kicked(event)
- if not contact.settings.get('print_status'):
+ def _process_muc_room_kicked(self, event: events.MUCRoomKicked) -> None:
+ status_codes = event.status_codes or []
+
+ reason = event.reason
+ reason = '' if reason is None else f': {reason}'
+
+ actor = event.actor
+ # Group Chat: You have been kicked by Alice
+ actor = '' if actor is None else _(' by {actor}').format(
+ actor=actor)
+
+ # Group Chat: We have been removed from the room by Alice: reason
+ message = _('You have been removed from the '
+ 'group chat{actor}{reason}')
+
+ if StatusCode.REMOVED_ERROR in status_codes:
+ # Handle 333 before 307, some MUCs add both
+ # Group Chat: Server kicked us because of an server error
+ message = _('You have left due '
+ 'to an error{reason}').format(reason=reason)
+
+ elif StatusCode.REMOVED_KICKED in status_codes:
+ # Group Chat: We have been kicked by Alice: reason
+ message = _('You have been '
+ 'kicked{actor}{reason}').format(actor=actor,
+ reason=reason)
+
+ elif StatusCode.REMOVED_BANNED in status_codes:
+ # Group Chat: We have been banned by Alice: reason
+ message = _('You have been '
+ 'banned{actor}{reason}').format(actor=actor,
+ reason=reason)
+
+ elif StatusCode.REMOVED_AFFILIATION_CHANGE in status_codes:
+ # Group Chat: We were removed because of an affiliation change
+ reason = _(': Affiliation changed')
+ message = message.format(actor=actor, reason=reason)
+
+ elif StatusCode.REMOVED_NONMEMBER_IN_MEMBERS_ONLY in status_codes:
+ # Group Chat: Room configuration changed
+ reason = _(': Group chat configuration changed to members-only')
+ message = message.format(actor=actor, reason=reason)
+
+ elif StatusCode.REMOVED_SERVICE_SHUTDOWN in status_codes:
+ # Group Chat: Kicked because of server shutdown
+ reason = ': System shutdown'
+ message = message.format(actor=actor, reason=reason)
+
+ else:
+ # No formatted message available
return
- self.conversation_view.add_user_status(user_contact.name,
- user_contact.show.value,
- user_contact.status)
+ self.add_info_message(message, event.timestamp)
- def _on_user_status_show_changed(self,
- _user_contact: GroupchatParticipant,
+ def _on_user_affiliation_changed(self,
+ _contact: GroupchatContact,
_signal_name: str,
- properties: PresenceProperties
+ user_contact: GroupchatParticipant,
+ event: events.MUCUserAffiliationChanged
) -> None:
- nick = properties.muc_nickname
- status = properties.status
- status = '' if not status else f' - {status}'
- assert properties.show is not None
- show = helpers.get_uf_show(properties.show.value)
+ self._process_muc_user_affiliation_changed(event)
- assert isinstance(self.contact, GroupchatParticipant)
- if not self.contact.room.settings.get('print_status'):
- return
+ def _process_muc_user_affiliation_changed(
+ self,
+ event: events.MUCUserAffiliationChanged) -> None:
- if properties.is_muc_self_presence:
- message = _('You are now {show}{status}').format(show=show,
- status=status)
+ affiliation = helpers.get_uf_affiliation(event.affiliation)
+
+ reason = event.reason
+ reason = '' if reason is None else f': {reason}'
+
+ actor = event.actor
+ # Group Chat: You have been kicked by Alice
+ actor = '' if actor is None else _(' by {actor}').format(
+ actor=actor)
+ if event.is_self:
+ message = _('** Your Affiliation has been set to '
+ '{affiliation}{actor}{reason}').format(
+ affiliation=affiliation,
+ actor=actor,
+ reason=reason)
else:
- message = _('{nick} is now {show}{status}').format(nick=nick,
- show=show,
- status=status)
- self.add_info_message(message)
+ message = _('** Affiliation of {nick} has been set to '
+ '{affiliation}{actor}{reason}').format(
+ nick=event.nick,
+ affiliation=affiliation,
+ actor=actor,
+ reason=reason)
- def add_muc_message(self,
- text: str,
- tim: float,
- contact: str = '',
- displaymarking: Optional[Displaymarking] = None,
- message_id: Optional[str] = None,
- stanza_id: Optional[str] = None,
- additional_data: Optional[AdditionalDataDict] = None,
- ) -> None:
+ self.add_info_message(message, event.timestamp)
- assert isinstance(self._contact, GroupchatContact)
+ def _on_user_role_changed(self,
+ _contact: GroupchatContact,
+ _signal_name: str,
+ user_contact: GroupchatParticipant,
+ event: events.MUCUserRoleChanged
+ ) -> None:
- if contact == self._contact.nickname:
- kind = 'outgoing'
+ self._process_muc_user_role_changed(event)
+
+ def _process_muc_user_role_changed(self,
+ event: events.MUCUserRoleChanged
+ ) -> None:
+
+ role = helpers.get_uf_role(event.role)
+ nick = event.nick
+
+ reason = event.reason
+ reason = '' if reason is None else f': {reason}'
+
+ actor = event.actor
+ # Group Chat: You have been kicked by Alice
+ actor = '' if actor is None else _(' by {actor}').format(actor=actor)
+
+ if event.is_self:
+ message = _('** Your Role has been set to '
+ '{role}{actor}{reason}').format(role=role,
+ actor=actor,
+ reason=reason)
else:
- kind = 'incoming'
- # muc-specific chatstate
+ message = _('** Role of {nick} has been set to '
+ '{role}{actor}{reason}').format(nick=nick,
+ role=role,
+ actor=actor,
+ reason=reason)
+ self.add_info_message(message, event.timestamp)
- self._add_message(text,
- kind,
- contact,
- tim,
- displaymarking=displaymarking,
- message_id=message_id,
- stanza_id=stanza_id,
- additional_data=additional_data)
+ def _on_user_status_show_changed(self,
+ contact: GroupchatContact,
+ _signal_name: str,
+ _user_contact: GroupchatParticipant,
+ event: events.MUCUserStatusShowChanged
+ ) -> None:
- def _on_room_subject(self,
- contact: GroupchatContact,
- _signal_name: str,
- subject: Optional[MucSubject]
- ) -> None:
+ self._process_muc_user_status_show_changed(event)
- if subject is None:
- return
+ def _on_participant_status_show_changed(
+ self,
+ contact: GroupchatParticipant,
+ _signal_name: str,
+ event: events.MUCUserStatusShowChanged) -> None:
- _timestamp, old_subject = self._muc_subjects.get(contact, (None, None))
- if old_subject is not None and old_subject.text == subject.text:
- # Probably a rejoin, we already showed that subject
+ self._process_muc_user_status_show_changed(event)
+
+ def _process_muc_user_status_show_changed(
+ self,
+ event: events.MUCUserStatusShowChanged) -> None:
+
+ if isinstance(self._contact, GroupchatContact):
+ contact = self._contact
+ elif isinstance(self._contact, GroupchatParticipant):
+ contact = self._contact.room
+ else:
+ raise AssertionError
+
+ if not contact.settings.get('print_status'):
return
- self._muc_subjects[contact] = (time.time(), subject)
+ nick = event.nick
+ status = event.status
+ status = '' if not status else f' - {status}'
+ show = helpers.get_uf_show(event.show_value)
- if (app.settings.get('show_subject_on_join') or
- not contact.is_joining):
- self.conversation_view.add_muc_subject(subject)
+ if event.is_self:
+ message = _('You are now {show}{status}').format(show=show,
+ status=status)
+
+ else:
+ message = _('{nick} is now {show}{status}').format(
+ nick=nick,
+ show=show,
+ status=status)
+
+ self.add_info_message(message, event.timestamp)
def _on_room_config_changed(self,
_contact: GroupchatContact,
_signal_name: str,
- properties: MessageProperties
+ event: events.MUCRoomConfigChanged
) -> None:
- # http://www.xmpp.org/extensions/xep-0045.html#roomconfig-notify
- status_codes = properties.muc_status_codes
- assert status_codes is not None
+ self._process_muc_room_config_changed(event)
+
+ def _process_muc_room_config_changed(self,
+ event: events.MUCRoomConfigChanged
+ ) -> None:
+ # http://www.xmpp.org/extensions/xep-0045.html#roomconfig-notify
+ status_codes = event.status_codes
changes: list[str] = []
+
if StatusCode.SHOWING_UNAVAILABLE in status_codes:
changes.append(_('Group chat now shows unavailable members'))
@@ -830,7 +957,8 @@ class ChatControl(EventHelper):
self.client.get_module('Discovery').disco_muc(self.contact.jid)
if StatusCode.CONFIG_ROOM_LOGGING in status_codes:
- # Can be a presence (see chg_contact_status in groupchat_control.py)
+ # Can be a presence
+ # (see chg_contact_status in groupchat_control.py)
changes.append(_('Conversations are stored on the server'))
if StatusCode.CONFIG_NO_ROOM_LOGGING in status_codes:
@@ -845,252 +973,202 @@ class ChatControl(EventHelper):
if StatusCode.CONFIG_FULL_ANONYMOUS in status_codes:
changes.append(_('Group chat is now fully anonymous'))
- for change in changes:
- self.add_info_message(change)
-
- def rejoin(self) -> None:
- self.client.get_module('MUC').join(self.contact.jid)
-
- def _on_user_joined(self,
- contact: GroupchatContact,
- _signal_name: str,
- user_contact: GroupchatParticipant,
- properties: PresenceProperties
- ) -> None:
-
- nick = user_contact.name
- if not properties.is_muc_self_presence:
- if contact.is_joined:
- self.conversation_view.add_muc_user_joined(nick)
- return
-
- status_codes = properties.muc_status_codes or []
-
- if not contact.is_joined:
- # We just joined the room
- self.add_info_message(_('You (%s) joined the group chat') % nick)
-
- if StatusCode.NON_ANONYMOUS in status_codes:
- self.add_info_message(
- _('Any participant is allowed to see your full XMPP Address'))
-
- if StatusCode.CONFIG_ROOM_LOGGING in status_codes:
- self.add_info_message(_('Conversations are stored on the server'))
-
- if StatusCode.NICKNAME_MODIFIED in status_codes:
- self.add_info_message(
- _('The server has assigned or modified your nickname in this '
- 'group chat'))
+ for message in changes:
+ self.add_info_message(message, event.timestamp)
def _on_room_config_finished(self,
_contact: GroupchatContact,
- _signal_name: str
+ _signal_name: str,
+ event: events.MUCRoomConfigFinished
) -> None:
+ self._process_muc_room_config_finished(event)
+
+ def _process_muc_room_config_finished(self,
+ event: events.MUCRoomConfigFinished
+ ) -> None:
+
self.add_info_message(_('A new group chat has been created'))
- def _on_user_affiliation_changed(self,
- _contact: GroupchatContact,
- _signal_name: str,
- user_contact: GroupchatParticipant,
- properties: PresenceProperties
- ) -> None:
- affiliation = helpers.get_uf_affiliation(user_contact.affiliation)
- nick = user_contact.name
+ def _on_room_presence_error(self,
+ _contact: GroupchatContact,
+ _signal_name: str,
+ event: events.MUCRoomPresenceError
+ ) -> None:
- assert properties.muc_user is not None
- reason = properties.muc_user.reason
- reason = '' if reason is None else f': {reason}'
+ self._process_muc_room_presence_error(event)
- actor = properties.muc_user.actor
- # Group Chat: You have been kicked by Alice
- actor = '' if actor is None else _(' by {actor}').format(actor=actor)
+ def _process_muc_room_presence_error(self,
+ event: events.MUCRoomPresenceError
+ ) -> None:
- if properties.is_muc_self_presence:
- message = _('** Your Affiliation has been set to '
- '{affiliation}{actor}{reason}').format(
- affiliation=affiliation,
- actor=actor,
- reason=reason)
- else:
- message = _('** Affiliation of {nick} has been set to '
- '{affiliation}{actor}{reason}').format(
- nick=nick,
- affiliation=affiliation,
- actor=actor,
- reason=reason)
+ error_message = to_user_string(event.error)
+ self.add_info_message(_('Error: %s') % error_message)
- self.add_info_message(message)
+ def _on_room_destroyed(self,
+ _contact: GroupchatContact,
+ _signal_name: str,
+ event: events.MUCRoomDestroyed
+ ) -> None:
- def _on_user_role_changed(self,
- _contact: GroupchatContact,
- _signal_name: str,
- user_contact: GroupchatParticipant,
- properties: PresenceProperties
- ) -> None:
- role = helpers.get_uf_role(user_contact.role)
- nick = user_contact.name
+ self._process_muc_room_destroyed(event)
+
+ def _process_muc_room_destroyed(self,
+ event: events.MUCRoomDestroyed
+ ) -> None:
- assert properties.muc_user is not None
- reason = properties.muc_user.reason
+ reason = event.reason
reason = '' if reason is None else f': {reason}'
- actor = properties.muc_user.actor
- # Group Chat: You have been kicked by Alice
- actor = '' if actor is None else _(' by {actor}').format(actor=actor)
+ message = _('Group chat has been destroyed')
- if properties.is_muc_self_presence:
- message = _('** Your Role has been set to '
- '{role}{actor}{reason}').format(role=role,
- actor=actor,
- reason=reason)
- else:
- message = _('** Role of {nick} has been set to '
- '{role}{actor}{reason}').format(nick=nick,
- role=role,
- actor=actor,
- reason=reason)
+ if event.alternate is not None:
+ message += '\n' + _('You can join this group chat instead: '
+ 'xmpp:%s?join') % str(event.alternate)
- self.add_info_message(message)
+ self.add_info_message(message, event.timestamp)
- def _on_room_kicked(self,
+ def _on_user_joined(self,
_contact: GroupchatContact,
_signal_name: str,
- properties: MessageProperties
+ _user_contact: GroupchatParticipant,
+ event: events.MUCUserJoined
) -> None:
- status_codes = properties.muc_status_codes or []
- assert properties.muc_user is not None
- reason = properties.muc_user.reason
- reason = '' if reason is None else f': {reason}'
+ self._process_muc_user_joined(event)
- actor = properties.muc_user.actor
- # Group Chat: You have been kicked by Alice
- actor = '' if actor is None else _(' by {actor}').format(actor=actor)
+ def _process_muc_user_joined(self, event: events.MUCUserJoined) -> None:
+ assert isinstance(self.contact, GroupchatContact)
- # Group Chat: We have been removed from the room by Alice: reason
- message = _('You have been removed from the group chat{actor}{reason}')
+ if not event.is_self:
+ if self.contact.is_joined:
+ self.conversation_view.add_muc_user_joined(event)
+ return
- if StatusCode.REMOVED_ERROR in status_codes:
- # Handle 333 before 307, some MUCs add both
- # Group Chat: Server kicked us because of an server error
- message = _('You have left due '
- 'to an error{reason}').format(reason=reason)
- self.add_info_message(message)
+ status_codes = event.status_codes or []
- elif StatusCode.REMOVED_KICKED in status_codes:
- # Group Chat: We have been kicked by Alice: reason
- message = _('You have been '
- 'kicked{actor}{reason}').format(actor=actor,
- reason=reason)
- self.add_info_message(message)
+ message = None
+ if not self.contact.is_joined:
+ # We just joined the room
+ message = _('You (%s) joined the group chat') % event.nick
- elif StatusCode.REMOVED_BANNED in status_codes:
- # Group Chat: We have been banned by Alice: reason
- message = _('You have been '
- 'banned{actor}{reason}').format(actor=actor,
- reason=reason)
- self.add_info_message(message)
+ if StatusCode.NON_ANONYMOUS in status_codes:
+ message = _('Any participant is allowed to see your full '
+ 'XMPP Address')
- elif StatusCode.REMOVED_AFFILIATION_CHANGE in status_codes:
- # Group Chat: We were removed because of an affiliation change
- reason = _(': Affiliation changed')
- message = message.format(actor=actor, reason=reason)
- self.add_info_message(message)
+ if StatusCode.CONFIG_ROOM_LOGGING in status_codes:
+ message = _('Conversations are stored on the server')
- elif StatusCode.REMOVED_NONMEMBER_IN_MEMBERS_ONLY in status_codes:
- # Group Chat: Room configuration changed
- reason = _(': Group chat configuration changed to members-only')
- message = message.format(actor=actor, reason=reason)
- self.add_info_message(message)
+ if StatusCode.NICKNAME_MODIFIED in status_codes:
+ message = _('The server has assigned or modified your '
+ 'nickname in this group chat')
- elif StatusCode.REMOVED_SERVICE_SHUTDOWN in status_codes:
- # Group Chat: Kicked because of server shutdown
- reason = ': System shutdown'
- message = message.format(actor=actor, reason=reason)
- self.add_info_message(message)
+ if message is not None:
+ self.add_info_message(message, event.timestamp)
def _on_user_left(self,
_contact: GroupchatContact,
_signal_name: str,
- user_contact: GroupchatParticipant,
- properties: MessageProperties
+ _user_contact: GroupchatParticipant,
+ event: events.MUCUserLeft
) -> None:
- status_codes = properties.muc_status_codes or []
- nick = user_contact.name
- assert properties.muc_user is not None
- reason = properties.muc_user.reason
- reason = '' if reason is None else f': {reason}'
+ self._process_muc_user_left(event)
- actor = properties.muc_user.actor
- # Group Chat: You have been kicked by Alice
- actor = '' if actor is None else _(' by {actor}').format(actor=actor)
+ def _process_muc_user_left(self, event: events.MUCUserLeft) -> None:
+ if event.is_self:
+ return
- # Group Chat: We have been removed from the room
- message = _('{nick} has been removed from the group chat{by}{reason}')
+ status_codes = event.status_codes or []
+ nick = event.nick
if StatusCode.REMOVED_ERROR in status_codes:
# Handle 333 before 307, some MUCs add both
- self.conversation_view.add_muc_user_left(
- nick, properties.muc_user.reason, error=True)
+ self.conversation_view.add_muc_user_left(event, error=True)
+ return
- elif StatusCode.REMOVED_KICKED in status_codes:
- # Group Chat: User was kicked by Alice: reason
+ reason = event.reason
+ reason = '' if reason is None else f': {reason}'
+
+ actor = event.actor
+ # Group Chat: You have been kicked by Alice
+ actor = '' if actor is None else _(' by {actor}').format(
+ actor=actor)
+
+ message = _('{nick} has been removed from the group '
+ 'chat{by}{reason}')
+
+ if StatusCode.REMOVED_KICKED in status_codes:
message = _('{nick} has been '
'kicked{actor}{reason}').format(nick=nick,
actor=actor,
reason=reason)
- self.add_info_message(message)
elif StatusCode.REMOVED_BANNED in status_codes:
- # Group Chat: User was banned by Alice: reason
message = _('{nick} has been '
'banned{actor}{reason}').format(nick=nick,
actor=actor,
reason=reason)
- self.add_info_message(message)
elif StatusCode.REMOVED_AFFILIATION_CHANGE in status_codes:
reason = _(': Affiliation changed')
message = message.format(nick=nick, by=actor, reason=reason)
- self.add_info_message(message)
elif StatusCode.REMOVED_NONMEMBER_IN_MEMBERS_ONLY in status_codes:
reason = _(': Group chat configuration changed to members-only')
message = message.format(nick=nick, by=actor, reason=reason)
- self.add_info_message(message)
else:
- self.conversation_view.add_muc_user_left(
- nick, properties.muc_user.reason)
+ self.conversation_view.add_muc_user_left(event)
+ return
- def _on_room_presence_error(self,
- _contact: GroupchatContact,
- _signal_name: str,
- properties: PresenceProperties
- ) -> None:
+ self.add_info_message(message, event.timestamp)
- assert properties.error is not None
- error_message = to_user_string(properties.error)
- self.add_info_message(_('Error: %s') % error_message)
+ def add_muc_message(self,
+ text: str,
+ tim: float,
+ contact: str = '',
+ displaymarking: Optional[Displaymarking] = None,
+ message_id: Optional[str] = None,
+ stanza_id: Optional[str] = None,
+ additional_data: Optional[AdditionalDataDict] = None,
+ ) -> None:
- def _on_room_destroyed(self,
- _contact: GroupchatContact,
- _signal_name: str,
- properties: PresenceProperties
- ) -> None:
+ assert isinstance(self._contact, GroupchatContact)
- destroyed = properties.muc_destroyed
- assert destroyed is not None
+ if contact == self._contact.nickname:
+ kind = 'outgoing'
+ else:
+ kind = 'incoming'
+ # muc-specific chatstate
- reason = destroyed.reason
- reason = '' if reason is None else f': {reason}'
+ self._add_message(text,
+ kind,
+ contact,
+ tim,
+ displaymarking=displaymarking,
+ message_id=message_id,
+ stanza_id=stanza_id,
+ additional_data=additional_data)
- message = _('Group chat has been destroyed')
- self.add_info_message(message)
+ def _on_room_subject(self,
+ contact: GroupchatContact,
+ _signal_name: str,
+ subject: Optional[MucSubject]
+ ) -> None:
- alternate = destroyed.alternate
- if alternate is not None:
- join_message = _('You can join this group chat '
- 'instead: xmpp:%s?join') % str(alternate)
- self.add_info_message(join_message)
+ if subject is None:
+ return
+
+ _timestamp, old_subject = self._muc_subjects.get(contact, (None, None))
+ if old_subject is not None and old_subject.text == subject.text:
+ # Probably a rejoin, we already showed that subject
+ return
+
+ self._muc_subjects[contact] = (time.time(), subject)
+
+ if (app.settings.get('show_subject_on_join') or
+ not contact.is_joining):
+ self.conversation_view.add_muc_subject(subject)
+
+ def rejoin(self) -> None:
+ self.client.get_module('MUC').join(self.contact.jid)
diff --git a/gajim/gtk/conversation/rows/muc_join_left.py b/gajim/gtk/conversation/rows/muc_join_left.py
index 88115f21d..1de61264a 100644
--- a/gajim/gtk/conversation/rows/muc_join_left.py
+++ b/gajim/gtk/conversation/rows/muc_join_left.py
@@ -27,13 +27,19 @@ from .base import BaseRow
class MUCJoinLeft(BaseRow):
- def __init__(self, type_: str, account: str, nick: str,
+ def __init__(self,
+ type_: str,
+ account: str,
+ nick: str,
reason: Optional[str] = None,
- error: bool = False):
+ error: bool = False,
+ timestamp: Optional[float] = None
+ ) -> None:
+
BaseRow.__init__(self, account)
self.type = type_
- timestamp = time.time()
+ timestamp = timestamp or time.time()
self.timestamp = datetime.fromtimestamp(timestamp)
self.db_timestamp = timestamp
diff --git a/gajim/gtk/conversation/view.py b/gajim/gtk/conversation/view.py
index 8c2f170f0..6b23939fa 100644
--- a/gajim/gtk/conversation/view.py
+++ b/gajim/gtk/conversation/view.py
@@ -37,9 +37,7 @@ from nbxmpp.protocol import JID
from gajim.common import app
from gajim.common import types
-from gajim.common.events import JingleRequestReceived
-from gajim.common.events import FileRequestReceivedEvent
-from gajim.common.events import FileRequestSent
+from gajim.common import events
from gajim.common.helpers import AdditionalDataDict
from gajim.common.helpers import to_user_string
from gajim.common.helpers import get_start_of_day
@@ -175,26 +173,29 @@ class ConversationView(Gtk.ListBox):
self._insert_message(muc_subject)
def add_muc_user_left(self,
- nick: str,
- reason: str,
- error: bool = False) -> None:
+ event: events.MUCUserLeft,
+ error: bool = False
+ ) -> None:
+
assert isinstance(self._contact, GroupchatContact)
if not self._contact.settings.get('print_join_left'):
return
join_left = MUCJoinLeft('muc-user-left',
self._contact.account,
- nick,
- reason=reason,
- error=error)
+ event.nick,
+ reason=event.reason,
+ error=error,
+ timestamp=event.timestamp)
self._insert_message(join_left)
- def add_muc_user_joined(self, nick: str) -> None:
+ def add_muc_user_joined(self, event: events.MUCUserJoined) -> None:
assert isinstance(self._contact, GroupchatContact)
if not self._contact.settings.get('print_join_left'):
return
join_left = MUCJoinLeft('muc-user-joined',
self._contact.account,
- nick)
+ event.nick,
+ timestamp=event.timestamp)
self._insert_message(join_left)
def add_user_status(self, name: str, show: str, status: str) -> None:
@@ -215,8 +216,8 @@ class ConversationView(Gtk.ListBox):
def add_jingle_file_transfer(self,
event: Union[
- FileRequestReceivedEvent,
- FileRequestSent,
+ events.FileRequestReceivedEvent,
+ events.FileRequestSent,
None] = None,
db_message: Optional[ConversationRow] = None
) -> None:
@@ -230,7 +231,7 @@ class ConversationView(Gtk.ListBox):
self._insert_message(jingle_transfer_row)
def add_call_message(self,
- event: Optional[JingleRequestReceived] = None,
+ event: Optional[events.JingleRequestReceived] = None,
db_message: Optional[ConversationRow] = None
) -> None:
assert isinstance(self._contact, BareContact)
diff --git a/gajim/gtk/groupchat_nick_completion.py b/gajim/gtk/groupchat_nick_completion.py
index fceb49bc4..89513aca1 100644
--- a/gajim/gtk/groupchat_nick_completion.py
+++ b/gajim/gtk/groupchat_nick_completion.py
@@ -18,7 +18,6 @@ from typing import Optional
import logging
-from nbxmpp.structs import PresenceProperties
from gi.repository import Gtk
from gi.repository import Gdk
@@ -27,6 +26,7 @@ from gajim.common import app
from gajim.common import ged
from gajim.common import types
from gajim.common.events import GcMessageReceived
+from gajim.common.events import MUCNicknameChanged
from gajim.common.ged import EventHelper
from gajim.common.helpers import jid_is_blocked
from gajim.common.helpers import message_needs_highlight
@@ -76,12 +76,12 @@ class GroupChatNickCompletion(EventHelper):
def _on_user_nickname_changed(self,
_contact: types.GroupchatContact,
_signal_name: str,
+ event: MUCNicknameChanged,
old_contact: types.GroupchatParticipant,
- new_contact: types.GroupchatParticipant,
- properties: PresenceProperties
+ new_contact: types.GroupchatParticipant
) -> None:
- if properties.is_muc_self_presence:
+ if event.is_self:
return
old_name = old_contact.name
diff --git a/gajim/gtk/groupchat_roster.py b/gajim/gtk/groupchat_roster.py
index 37365aa44..6a067d62c 100644
--- a/gajim/gtk/groupchat_roster.py
+++ b/gajim/gtk/groupchat_roster.py
@@ -25,7 +25,6 @@ from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import GLib
from nbxmpp.const import Affiliation
-from nbxmpp.structs import PresenceProperties
from gajim.common import app
from gajim.common import ged
@@ -36,6 +35,7 @@ from gajim.common.helpers import jid_is_blocked
from gajim.common.const import AvatarSize
from gajim.common.const import StyleAttr
from gajim.common.events import ApplicationEvent
+from gajim.common.events import MUCNicknameChanged
from gajim.common.modules.contacts import GroupchatContact
from .menus import get_groupchat_roster_menu
@@ -281,11 +281,11 @@ class GroupchatRoster(Gtk.Revealer, EventHelper):
self._add_contact(user_contact)
def _on_user_nickname_changed(self,
- contact: types.GroupchatContact,
+ _contact: types.GroupchatContact,
_signal_name: str,
+ _event: MUCNicknameChanged,
old_contact: types.GroupchatParticipant,
- new_contact: types.GroupchatParticipant,
- properties: PresenceProperties
+ new_contact: types.GroupchatParticipant
) -> None:
self._remove_contact(old_contact)
diff --git a/pyrightconfig.json b/pyrightconfig.json
index 05e6e8ed9..b833c0cee 100644
--- a/pyrightconfig.json
+++ b/pyrightconfig.json
@@ -38,7 +38,7 @@
"gajim/common/regex.py",
"gajim/common/setting_values.py",
"gajim/common/sound.py",
- "gajim/common/storage/",
+ "gajim/common/storage/*",
"gajim/common/task_manager.py",
"gajim/gajim.py",
"gajim/gajim_remote.py",