Philipp Hörist pushed to branch archive_cache at gajim / gajim
Commits: 307b7baf by wurstsalat at 2022-08-29T11:59:48+02:00 imprv: Workspaces: Move chats when removing workspace
Closes #11098
- - - - - 5de6f7ea by lovetox at 2022-08-31T21:46:40+02:00 new: Add event storage
- - - - -
19 changed files:
- gajim/common/app.py - gajim/common/application.py - gajim/common/events.py - gajim/common/modules/contacts.py - gajim/common/modules/muc.py - gajim/common/storage/base.py - + gajim/common/storage/events.py - gajim/gtk/chat_banner.py - gajim/gtk/chat_list_stack.py - gajim/gtk/chat_stack.py - gajim/gtk/control.py - gajim/gtk/conversation/view.py - gajim/gtk/groupchat_nick_completion.py - gajim/gtk/groupchat_roster.py - gajim/gtk/main.py - gajim/gtk/menus.py - gajim/gtk/structs.py - gajim/gtk/workspace_dialog.py - gajim/gtk/workspace_side_bar.py
Changes:
===================================== gajim/common/app.py ===================================== @@ -59,6 +59,7 @@ 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()
===================================== gajim/common/application.py ===================================== @@ -44,6 +44,7 @@ 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 @@ def _init_core(self) -> None: app.storage.cache = CacheStorage() app.storage.cache.init()
+ app.storage.events = EventStorage() + app.storage.events.init() + app.storage.archive = MessageArchiveStorage() app.storage.archive.init()
===================================== gajim/common/events.py ===================================== @@ -32,7 +32,10 @@ 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
===================================== gajim/common/modules/contacts.py ===================================== @@ -15,11 +15,14 @@ from __future__ import annotations
from typing import Any +from typing import cast from typing import Iterator from typing import Optional from typing import Union from typing import overload
+import time + import cairo from nbxmpp.const import Affiliation from nbxmpp.const import Chatstate @@ -29,9 +32,12 @@ from nbxmpp.structs import DiscoInfo from nbxmpp.structs import LocationData from nbxmpp.structs import TuneData +from nbxmpp.structs import MessageProperties from nbxmpp.structs import MucSubject +from nbxmpp.structs import PresenceProperties
from gajim.common import app +from gajim.common import events from gajim.common import types from gajim.common.const import PresenceShowExt from gajim.common.const import SimpleClientState @@ -942,12 +948,32 @@ def update_presence(self,
if not self._presence.available and presence.available: self._presence = presence - self.notify('user-joined', *args) + + properties = cast(MessageProperties, args[0]) + event = events.MUCUserJoined( + timestamp=time.time(), + is_self=properties.is_muc_self_presence, + nick=self.name, + status_codes=properties.muc_status_codes) + + app.storage.events.store(self.room, event) + self.notify('user-joined', event) return
if not presence.available: self._presence = presence - self.notify('user-left', *args) + + properties = cast(MessageProperties, args[0]) + event = events.MUCUserLeft( + timestamp=time.time(), + is_self=properties.is_muc_self_presence, + nick=self.name, + status_codes=properties.muc_status_codes, + reason=properties.muc_user.reason, + actor=properties.muc_user.actor) + + app.storage.events.store(self.room, event) + self.notify('user-left', event) return
signals: list[str] = [] @@ -962,8 +988,37 @@ def update_presence(self, signals.append('user-status-show-changed')
self._presence = presence + for signal in signals: - self.notify(signal, *args) + properties = cast(PresenceProperties, args[0]) + if signal == 'user-affiliation-changed': + event = events.MUCUserAffiliationChanged( + timestamp=time.time(), + is_self=properties.is_muc_self_presence, + nick=self.name, + affiliation=self.affiliation, + reason=properties.muc_user.reason, + actor=properties.muc_user.actor) + + if signal == 'user-role-changed': + event = events.MUCUserRoleChanged( + timestamp=time.time(), + is_self=properties.is_muc_self_presence, + nick=self.name, + role=self.role, + reason=properties.muc_user.reason, + actor=properties.muc_user.actor) + + if signal == 'user-status-show-changed': + event = events.MUCUserStatusShowChanged( + timestamp=time.time(), + is_self=properties.is_muc_self_presence, + nick=self.name, + status=properties.status, + show_value=properties.show.value) + + app.storage.events.store(self.room, event) + self.notify(signal, event)
def update_avatar(self, *args: Any) -> None: app.app.avatar_storage.invalidate_cache(self._jid)
===================================== gajim/common/modules/muc.py ===================================== @@ -44,6 +44,7 @@ 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 @@ -456,7 +457,11 @@ def _on_disco_result_after_config(self, task: Task) -> None: 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, @@ -536,6 +541,11 @@ def _on_error_presence(self, 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, @@ -560,7 +570,15 @@ def _on_muc_user_presence(self, 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: @@ -586,10 +604,16 @@ def _on_muc_user_presence(self, presence = self._process_user_presence(properties) occupant.update_presence(presence, properties)
- 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) @@ -616,7 +640,16 @@ def _on_muc_user_presence(self, 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) @@ -889,12 +922,21 @@ def _on_config_change(self, 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
===================================== gajim/common/storage/base.py ===================================== @@ -30,11 +30,15 @@
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 @@ def _adapt_disco_info(disco_info: DiscoInfo) -> str: 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 @@ def default(self, o: Any) -> Any: 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
===================================== gajim/common/storage/events.py ===================================== @@ -0,0 +1,132 @@ +# 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 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 + ) -> list[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)
===================================== gajim/gtk/chat_banner.py ===================================== @@ -21,8 +21,6 @@
import cairo
-from nbxmpp.structs import PresenceProperties - from gajim.common import app from gajim.common import ged from gajim.common import types @@ -184,13 +182,7 @@ def _on_muc_state_changed(self, if contact.is_joined: self._update_content()
- 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:
===================================== gajim/gtk/chat_list_stack.py ===================================== @@ -243,16 +243,17 @@ def _move_chat_to_workspace(self, if not workspace_id: workspace_id = app.window.add_workspace(switch=False)
- current_chatlist = cast(ChatList, self.get_visible_child()) - type_ = current_chatlist.get_chat_type(params.account, params.jid) + source_chatlist = self.get_chatlist(params.source_workspace_id) + type_ = source_chatlist.get_chat_type(params.account, params.jid) if type_ is None: return - current_chatlist.remove_chat(params.account, params.jid) + + source_chatlist.remove_chat(params.account, params.jid)
new_chatlist = self.get_chatlist(workspace_id) new_chatlist.add_chat(params.account, params.jid, type_, False, -1)
- self.store_open_chats(current_chatlist.workspace_id) + self.store_open_chats(source_chatlist.workspace_id) self.store_open_chats(workspace_id)
@structs.actionmethod
===================================== gajim/gtk/chat_stack.py ===================================== @@ -28,7 +28,6 @@ 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 @@ def _on_user_joined(self, 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)
===================================== gajim/gtk/control.py ===================================== @@ -19,9 +19,6 @@ 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 @@ -105,12 +102,6 @@ def __init__(self) -> None: 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()
@@ -212,9 +203,6 @@ def switch_contact(self, contact: Union[BareContact, 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): @@ -515,10 +503,6 @@ def add_info_message(self, 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: @@ -639,6 +623,42 @@ def _fetch_n_lines_history(self, # if self.conversation_view.reduce_message_count(before): # self._scrolled_view.set_history_complete(before, False)
+ 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]): @@ -723,143 +743,213 @@ def add_message(self, 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 _on_room_voice_request(self, - _contact: GroupchatContact, - _signal_name: str, - properties: MessageProperties - ) -> None: - voice_request = properties.voice_request - assert voice_request is not None + self.add_info_message(message, event.timestamp)
- def on_approve() -> None: - self.client.get_module('MUC').approve_voice_request( - self.contact.jid, voice_request) + def _on_user_role_changed(self, + _contact: GroupchatContact, + _signal_name: str, + user_contact: GroupchatParticipant, + event: events.MUCUserRoleChanged + ) -> None:
- ConfirmationDialog( - _('Voice Request'), - _('Voice Request'), - _('<b>%(nick)s</b> from <b>%(room_name)s</b> requests voice') % { - 'nick': voice_request.nick, 'room_name': self.contact.name}, - [DialogButton.make('Cancel'), - DialogButton.make('Accept', - text=_('_Approve'), - callback=on_approve)], - modal=False).show() + self._process_muc_user_role_changed(event)
- 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 _process_muc_user_role_changed(self, + event: events.MUCUserRoleChanged + ) -> None:
- assert isinstance(self._contact, GroupchatContact) + role = helpers.get_uf_role(event.role) + nick = event.nick
- if contact == self._contact.nickname: - kind = 'outgoing' + 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, + _user_contact: GroupchatParticipant, + _signal_name: str, + 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 _process_muc_user_status_show_changed( + self, + 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 + nick = event.nick + status = event.status + status = '' if not status else f' - {status}' + show = helpers.get_uf_show(event.show_value) + + assert isinstance(self.contact, GroupchatParticipant) + if not self.contact.room.settings.get('print_status'): return
- self._muc_subjects[contact] = (time.time(), subject) + if event.is_self: + message = _('You are now {show}{status}').format(show=show, + status=status)
- if (app.settings.get('show_subject_on_join') or - not contact.is_joining): - self.conversation_view.add_muc_subject(subject) + 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'))
@@ -873,7 +963,8 @@ def _on_room_config_changed(self, 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: @@ -888,194 +979,130 @@ def _on_room_config_changed(self, 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.nick) + 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 + self._process_muc_user_left(event) + + def _process_muc_user_left(self, event: events.MUCUserLeft) -> None: + if event.is_self: + return + + status_codes = event.status_codes or [] + nick = event.nick + + reason = event.reason reason = '' if reason is None else f': {reason}'
- actor = properties.muc_user.actor + actor = event.actor # Group Chat: You have been kicked by Alice - actor = '' if actor is None else _(' by {actor}').format(actor=actor) + actor = '' if actor is None else _(' by {actor}').format( + actor=actor)
# Group Chat: We have been removed from the room - message = _('{nick} has been removed from the group chat{by}{reason}') + message = _('{nick} has been removed from the group ' + 'chat{by}{reason}')
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) + nick, event.reason, error=True)
elif StatusCode.REMOVED_KICKED in status_codes: # Group Chat: User was kicked by Alice: reason @@ -1083,7 +1110,6 @@ def _on_user_left(self, '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 @@ -1091,49 +1117,104 @@ def _on_user_left(self, '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(nick, event.reason) + 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 _on_muc_user_status_show_changed(self, + contact: GroupchatContact, + _signal_name: str, + user_contact: GroupchatParticipant, + properties: PresenceProperties + ) -> None:
- def _on_room_destroyed(self, - _contact: GroupchatContact, - _signal_name: str, - properties: PresenceProperties - ) -> None: + if not contact.settings.get('print_status'): + return
- destroyed = properties.muc_destroyed - assert destroyed is not None + self.conversation_view.add_user_status(user_contact.name, + user_contact.show.value, + user_contact.status)
- reason = destroyed.reason - reason = '' if reason is None else f': {reason}' + def _on_room_voice_request(self, + _contact: GroupchatContact, + _signal_name: str, + properties: MessageProperties + ) -> None: + voice_request = properties.voice_request + assert voice_request is not None
- message = _('Group chat has been destroyed') - self.add_info_message(message) + def on_approve() -> None: + self.client.get_module('MUC').approve_voice_request( + self.contact.jid, voice_request) + + ConfirmationDialog( + _('Voice Request'), + _('Voice Request'), + _('<b>%(nick)s</b> from <b>%(room_name)s</b> requests voice') % { + 'nick': voice_request.nick, 'room_name': self.contact.name}, + [DialogButton.make('Cancel'), + DialogButton.make('Accept', + text=_('_Approve'), + callback=on_approve)], + modal=False).show()
- 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) + 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: + + assert isinstance(self._contact, GroupchatContact) + + if contact == self._contact.nickname: + kind = 'outgoing' + else: + kind = 'incoming' + # muc-specific chatstate + + self._add_message(text, + kind, + contact, + tim, + displaymarking=displaymarking, + message_id=message_id, + stanza_id=stanza_id, + additional_data=additional_data) + + def _on_room_subject(self, + contact: GroupchatContact, + _signal_name: str, + subject: Optional[MucSubject] + ) -> None: + + 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)
===================================== gajim/gtk/conversation/view.py ===================================== @@ -176,7 +176,7 @@ def add_muc_subject(self,
def add_muc_user_left(self, nick: str, - reason: str, + reason: Optional[str], error: bool = False) -> None: assert isinstance(self._contact, GroupchatContact) if not self._contact.settings.get('print_join_left'):
===================================== gajim/gtk/groupchat_nick_completion.py ===================================== @@ -18,7 +18,6 @@
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 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 @@ def switch_contact(self, contact: types.GroupchatContactT) -> None: 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
===================================== gajim/gtk/groupchat_roster.py ===================================== @@ -25,7 +25,6 @@ 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.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 @@ def _update_contact(self, 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)
===================================== gajim/gtk/main.py ===================================== @@ -62,6 +62,7 @@ from .structs import AccountJidParam from .structs import AddChatActionParams from .structs import actionmethod +from .structs import ChatListEntryParam
if TYPE_CHECKING: from .control import ChatControl @@ -626,18 +627,52 @@ def _remove_workspace(self, self.remove_workspace(workspace_id)
def remove_workspace(self, workspace_id: str) -> None: + if len(app.settings.get_workspaces()) == 1: + log.warning('Tried to remove the only workspace') + return + was_active = self.get_active_workspace() == workspace_id + chat_list = self.get_chat_list(workspace_id) + open_chats = chat_list.get_open_chats() + + def _continue_removing_workspace(): + new_workspace_id = self._workspace_side_bar.get_other_workspace( + workspace_id) + if new_workspace_id is None: + log.warning('No other workspaces found') + return
- success = self._workspace_side_bar.remove_workspace(workspace_id) - if not success: + for open_chat in open_chats: + params = ChatListEntryParam( + workspace_id=new_workspace_id, + source_workspace_id=workspace_id, + account=open_chat['account'], + jid=open_chat['jid']) + self.activate_action('move-chat-to-workspace', + params.to_variant()) + + if was_active: + self.activate_workspace(new_workspace_id) + + self._workspace_side_bar.remove_workspace(workspace_id) + self._chat_page.remove_chat_list(workspace_id) + app.settings.remove_workspace(workspace_id) + + if open_chats: + ConfirmationDialog( + _('Remove Workspace'), + _('Remove Workspace'), + _('This workspace contains chats. All chats will be moved to ' + 'the next workspace. Remove anyway?'), + [DialogButton.make('Cancel', + text=_('_No')), + DialogButton.make('Remove', + callback=_continue_removing_workspace)] + ).show() return
- if was_active: - new_active_id = self._workspace_side_bar.get_first_workspace() - self.activate_workspace(new_active_id) - - self._chat_page.remove_chat_list(workspace_id) - app.settings.remove_workspace(workspace_id) + # No chats in chat list, it is save to remove this workspace + _continue_removing_workspace()
def _activate_workspace(self, _action: Gio.SimpleAction,
===================================== gajim/gtk/menus.py ===================================== @@ -410,6 +410,7 @@ def get_chat_list_row_menu(workspace_id: str, menu = GajimMenu()
params = ChatListEntryParam(workspace_id=workspace_id, + source_workspace_id=workspace_id, account=account, jid=jid)
@@ -423,6 +424,7 @@ def get_chat_list_row_menu(workspace_id: str, submenu.add_item(name, 'win.move-chat-to-workspace', params)
params = ChatListEntryParam(workspace_id='', + source_workspace_id=workspace_id, account=account, jid=jid)
@@ -450,6 +452,7 @@ def get_workspace_params(current_workspace_id: str, continue name = app.settings.get_workspace_setting(workspace_id, 'name') params = ChatListEntryParam(workspace_id=workspace_id, + source_workspace_id=current_workspace_id, account=account, jid=jid) yield name, params @@ -575,9 +578,13 @@ def get_format_menu() -> GajimMenu:
def get_workspace_menu(workspace_id: str) -> GajimMenu: + remove_action = 'win.dummy' + if len(app.settings.get_workspaces()) > 1: + remove_action = 'win.remove-workspace' + menuitems: MenuItemListT = [ (_('Edit…'), 'win.edit-workspace', f'"{workspace_id}"'), - (_('Remove'), 'win.remove-workspace', f'"{workspace_id}"'), + (_('Remove'), remove_action, f'"{workspace_id}"'), ]
return GajimMenu.from_list(menuitems)
===================================== gajim/gtk/structs.py ===================================== @@ -55,6 +55,7 @@ class AddChatActionParams(VariantMixin): @dataclass class ChatListEntryParam(VariantMixin): workspace_id: str + source_workspace_id: str account: str jid: JID
===================================== gajim/gtk/workspace_dialog.py ===================================== @@ -24,8 +24,6 @@
from .avatar import make_workspace_avatar from .avatar_selector import AvatarSelector -from .dialogs import ConfirmationDialog -from .dialogs import DialogButton from .builder import get_builder from .util import rgba_to_float from .util import make_rgba @@ -93,25 +91,9 @@ def _on_key_press(self, _widget: Gtk.Widget, event: Gdk.EventKey) -> None: self.destroy()
def _on_remove_workspace(self, _button: Gtk.Button) -> None: - def _on_remove(): - assert self._workspace_id is not None - app.window.remove_workspace(self._workspace_id) - self.destroy() assert self._workspace_id is not None - chat_list = app.window.get_chat_list(self._workspace_id) - open_chats = chat_list.get_open_chats() - if len(open_chats) > 0: - ConfirmationDialog( - _('Remove Workspace'), - _('Remove Workspace'), - _('This workspace contains chats. Remove anyway?'), - [DialogButton.make('Cancel', - text=_('_No')), - DialogButton.make('Remove', - callback=_on_remove)]).show() - return - # No chats in chat list, it is save to remove workspace - _on_remove() + app.window.remove_workspace(self._workspace_id) + self.destroy()
def _on_cancel(self, _button: Gtk.Button) -> None: self.destroy()
===================================== gajim/gtk/workspace_side_bar.py ===================================== @@ -158,6 +158,7 @@ def _process_chat_list_drop(self, workspace_id = ''
params = ChatListEntryParam(workspace_id=workspace_id, + source_workspace_id='', account=account, jid=jid) app.window.activate_action('move-chat-to-workspace', @@ -221,12 +222,18 @@ def store_workspace_order(self) -> None: order.remove('add') app.settings.set_app_setting('workspace_order', order)
- def remove_workspace(self, workspace_id: str) -> bool: - if len(self._workspaces) == 1: - return False + def remove_workspace(self, workspace_id: str) -> None: row = self._workspaces.pop(workspace_id) self.remove(row) - return True + + def get_other_workspace(self, + exclude_workspace_id: str + ) -> Optional[str]: + + for workspace in self._workspaces.values(): + if workspace.workspace_id != exclude_workspace_id: + return workspace.workspace_id + return None
def activate_workspace(self, workspace_id: str) -> None: row = cast(CommonWorkspace, self.get_selected_row())
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/compare/60dc9a54a28dffaf9de6db44ff5f8397...