Philipp Hörist pushed to branch archive_cache at gajim / gajim

Commits:

19 changed files:

Changes:

  • gajim/common/app.py
    ... ... @@ -59,6 +59,7 @@
    59 59
         from gajim.gui.application import GajimApplication
    
    60 60
         from gajim.common.storage.cache import CacheStorage
    
    61 61
         from gajim.common.storage.archive import MessageArchiveStorage
    
    62
    +    from gajim.common.storage.events import EventStorage
    
    62 63
         from gajim.common.cert_store import CertificateStore
    
    63 64
         from gajim.common.call_manager import CallManager
    
    64 65
         from gajim.common.preview import PreviewManager
    
    ... ... @@ -88,6 +89,7 @@ class Storage:
    88 89
         def __init__(self):
    
    89 90
             self.cache: CacheStorage = None
    
    90 91
             self.archive: MessageArchiveStorage = None
    
    92
    +        self.events: EventStorage = None
    
    91 93
     
    
    92 94
     
    
    93 95
     storage = Storage()
    

  • gajim/common/application.py
    ... ... @@ -44,6 +44,7 @@
    44 44
     from gajim.common.events import GajimUpdateAvailable
    
    45 45
     from gajim.common.client import Client
    
    46 46
     from gajim.common.helpers import make_http_request
    
    47
    +from gajim.common.storage.events import EventStorage
    
    47 48
     from gajim.common.task_manager import TaskManager
    
    48 49
     from gajim.common.settings import Settings
    
    49 50
     from gajim.common.settings import LegacyConfig
    
    ... ... @@ -74,6 +75,9 @@ def _init_core(self) -> None:
    74 75
             app.storage.cache = CacheStorage()
    
    75 76
             app.storage.cache.init()
    
    76 77
     
    
    78
    +        app.storage.events = EventStorage()
    
    79
    +        app.storage.events.init()
    
    80
    +
    
    77 81
             app.storage.archive = MessageArchiveStorage()
    
    78 82
             app.storage.archive.init()
    
    79 83
     
    

  • gajim/common/events.py
    ... ... @@ -32,7 +32,10 @@
    32 32
     from nbxmpp.structs import LocationData
    
    33 33
     from nbxmpp.structs import RosterItem
    
    34 34
     from nbxmpp.structs import TuneData
    
    35
    +from nbxmpp.const import Affiliation
    
    35 36
     from nbxmpp.const import InviteType
    
    37
    +from nbxmpp.const import Role
    
    38
    +from nbxmpp.const import StatusCode
    
    36 39
     
    
    37 40
     from gajim.common.file_props import FileProp
    
    38 41
     from gajim.common.const import JingleState
    
    ... ... @@ -676,3 +679,101 @@ class AllowGajimUpdateCheck(ApplicationEvent):
    676 679
     class GajimUpdateAvailable(ApplicationEvent):
    
    677 680
         name: str = field(init=False, default='gajim-update-available')
    
    678 681
         version: str
    
    682
    +
    
    683
    +
    
    684
    +@dataclass
    
    685
    +class MUCNicknameChanged(ApplicationEvent):
    
    686
    +    name: str = field(init=False, default='muc-nickname-changed')
    
    687
    +    is_self: bool
    
    688
    +    new_name: str
    
    689
    +    old_name: str
    
    690
    +    timestamp: float
    
    691
    +
    
    692
    +
    
    693
    +@dataclass
    
    694
    +class MUCRoomConfigChanged(ApplicationEvent):
    
    695
    +    name: str = field(init=False, default='muc-room-config-changed')
    
    696
    +    timestamp: float
    
    697
    +    status_codes: set[StatusCode]
    
    698
    +
    
    699
    +
    
    700
    +@dataclass
    
    701
    +class MUCRoomConfigFinished(ApplicationEvent):
    
    702
    +    name: str = field(init=False, default='muc-room-config-finished')
    
    703
    +    timestamp: float
    
    704
    +
    
    705
    +
    
    706
    +@dataclass
    
    707
    +class MUCRoomPresenceError(ApplicationEvent):
    
    708
    +    name: str = field(init=False, default='muc-room-presence-error')
    
    709
    +    timestamp: float
    
    710
    +    error: Any
    
    711
    +
    
    712
    +
    
    713
    +@dataclass
    
    714
    +class MUCRoomKicked(ApplicationEvent):
    
    715
    +    name: str = field(init=False, default='muc-room-kicked')
    
    716
    +    timestamp: float
    
    717
    +    status_codes: Optional[set[StatusCode]]
    
    718
    +    reason: Optional[str]
    
    719
    +    actor: Optional[str]
    
    720
    +
    
    721
    +
    
    722
    +@dataclass
    
    723
    +class MUCRoomDestroyed(ApplicationEvent):
    
    724
    +    name: str = field(init=False, default='muc-room-destroyed')
    
    725
    +    timestamp: float
    
    726
    +    reason: Optional[str]
    
    727
    +    alternate: Optional[JID]
    
    728
    +
    
    729
    +
    
    730
    +@dataclass
    
    731
    +class MUCUserJoined(ApplicationEvent):
    
    732
    +    name: str = field(init=False, default='muc-user-joined')
    
    733
    +    timestamp: float
    
    734
    +    is_self: bool
    
    735
    +    nick: str
    
    736
    +    status_codes: Optional[set[StatusCode]]
    
    737
    +
    
    738
    +
    
    739
    +@dataclass
    
    740
    +class MUCUserLeft(ApplicationEvent):
    
    741
    +    name: str = field(init=False, default='muc-user-left')
    
    742
    +    timestamp: float
    
    743
    +    is_self: bool
    
    744
    +    nick: str
    
    745
    +    status_codes: Optional[set[StatusCode]]
    
    746
    +    reason: Optional[str]
    
    747
    +    actor: Optional[str]
    
    748
    +
    
    749
    +
    
    750
    +@dataclass
    
    751
    +class MUCUserRoleChanged(ApplicationEvent):
    
    752
    +    name: str = field(init=False, default='muc-user-role-changed')
    
    753
    +    timestamp: float
    
    754
    +    is_self: bool
    
    755
    +    nick: str
    
    756
    +    role: Role
    
    757
    +    reason: Optional[str]
    
    758
    +    actor: Optional[str]
    
    759
    +
    
    760
    +
    
    761
    +@dataclass
    
    762
    +class MUCUserAffiliationChanged(ApplicationEvent):
    
    763
    +    name: str = field(init=False, default='muc-user-affiliation-changed')
    
    764
    +    timestamp: float
    
    765
    +    is_self: bool
    
    766
    +    nick: str
    
    767
    +    affiliation: Affiliation
    
    768
    +    reason: Optional[str]
    
    769
    +    actor: Optional[str]
    
    770
    +
    
    771
    +
    
    772
    +@dataclass
    
    773
    +class MUCUserStatusShowChanged(ApplicationEvent):
    
    774
    +    name: str = field(init=False, default='muc-user-status-show-changed')
    
    775
    +    timestamp: float
    
    776
    +    is_self: bool
    
    777
    +    nick: str
    
    778
    +    status: str
    
    779
    +    show_value: str

  • gajim/common/modules/contacts.py
    ... ... @@ -15,11 +15,14 @@
    15 15
     from __future__ import annotations
    
    16 16
     
    
    17 17
     from typing import Any
    
    18
    +from typing import cast
    
    18 19
     from typing import Iterator
    
    19 20
     from typing import Optional
    
    20 21
     from typing import Union
    
    21 22
     from typing import overload
    
    22 23
     
    
    24
    +import time
    
    25
    +
    
    23 26
     import cairo
    
    24 27
     from nbxmpp.const import Affiliation
    
    25 28
     from nbxmpp.const import Chatstate
    
    ... ... @@ -29,9 +32,12 @@
    29 32
     from nbxmpp.structs import DiscoInfo
    
    30 33
     from nbxmpp.structs import LocationData
    
    31 34
     from nbxmpp.structs import TuneData
    
    35
    +from nbxmpp.structs import MessageProperties
    
    32 36
     from nbxmpp.structs import MucSubject
    
    37
    +from nbxmpp.structs import PresenceProperties
    
    33 38
     
    
    34 39
     from gajim.common import app
    
    40
    +from gajim.common import events
    
    35 41
     from gajim.common import types
    
    36 42
     from gajim.common.const import PresenceShowExt
    
    37 43
     from gajim.common.const import SimpleClientState
    
    ... ... @@ -942,12 +948,32 @@ def update_presence(self,
    942 948
     
    
    943 949
             if not self._presence.available and presence.available:
    
    944 950
                 self._presence = presence
    
    945
    -            self.notify('user-joined', *args)
    
    951
    +
    
    952
    +            properties = cast(MessageProperties, args[0])
    
    953
    +            event = events.MUCUserJoined(
    
    954
    +                timestamp=time.time(),
    
    955
    +                is_self=properties.is_muc_self_presence,
    
    956
    +                nick=self.name,
    
    957
    +                status_codes=properties.muc_status_codes)
    
    958
    +
    
    959
    +            app.storage.events.store(self.room, event)
    
    960
    +            self.notify('user-joined', event)
    
    946 961
                 return
    
    947 962
     
    
    948 963
             if not presence.available:
    
    949 964
                 self._presence = presence
    
    950
    -            self.notify('user-left', *args)
    
    965
    +
    
    966
    +            properties = cast(MessageProperties, args[0])
    
    967
    +            event = events.MUCUserLeft(
    
    968
    +                timestamp=time.time(),
    
    969
    +                is_self=properties.is_muc_self_presence,
    
    970
    +                nick=self.name,
    
    971
    +                status_codes=properties.muc_status_codes,
    
    972
    +                reason=properties.muc_user.reason,
    
    973
    +                actor=properties.muc_user.actor)
    
    974
    +
    
    975
    +            app.storage.events.store(self.room, event)
    
    976
    +            self.notify('user-left', event)
    
    951 977
                 return
    
    952 978
     
    
    953 979
             signals: list[str] = []
    
    ... ... @@ -962,8 +988,37 @@ def update_presence(self,
    962 988
                 signals.append('user-status-show-changed')
    
    963 989
     
    
    964 990
             self._presence = presence
    
    991
    +
    
    965 992
             for signal in signals:
    
    966
    -            self.notify(signal, *args)
    
    993
    +            properties = cast(PresenceProperties, args[0])
    
    994
    +            if signal == 'user-affiliation-changed':
    
    995
    +                event = events.MUCUserAffiliationChanged(
    
    996
    +                    timestamp=time.time(),
    
    997
    +                    is_self=properties.is_muc_self_presence,
    
    998
    +                    nick=self.name,
    
    999
    +                    affiliation=self.affiliation,
    
    1000
    +                    reason=properties.muc_user.reason,
    
    1001
    +                    actor=properties.muc_user.actor)
    
    1002
    +
    
    1003
    +            if signal == 'user-role-changed':
    
    1004
    +                event = events.MUCUserRoleChanged(
    
    1005
    +                    timestamp=time.time(),
    
    1006
    +                    is_self=properties.is_muc_self_presence,
    
    1007
    +                    nick=self.name,
    
    1008
    +                    role=self.role,
    
    1009
    +                    reason=properties.muc_user.reason,
    
    1010
    +                    actor=properties.muc_user.actor)
    
    1011
    +
    
    1012
    +            if signal == 'user-status-show-changed':
    
    1013
    +                event = events.MUCUserStatusShowChanged(
    
    1014
    +                    timestamp=time.time(),
    
    1015
    +                    is_self=properties.is_muc_self_presence,
    
    1016
    +                    nick=self.name,
    
    1017
    +                    status=properties.status,
    
    1018
    +                    show_value=properties.show.value)
    
    1019
    +
    
    1020
    +            app.storage.events.store(self.room, event)
    
    1021
    +            self.notify(signal, event)
    
    967 1022
     
    
    968 1023
         def update_avatar(self, *args: Any) -> None:
    
    969 1024
             app.app.avatar_storage.invalidate_cache(self._jid)
    

  • gajim/common/modules/muc.py
    ... ... @@ -44,6 +44,7 @@
    44 44
     from gi.repository import GLib
    
    45 45
     
    
    46 46
     from gajim.common import app
    
    47
    +from gajim.common import events
    
    47 48
     from gajim.common import helpers
    
    48 49
     from gajim.common import types
    
    49 50
     from gajim.common.const import ClientState, KindConstant
    
    ... ... @@ -456,7 +457,11 @@ def _on_disco_result_after_config(self, task: Task) -> None:
    456 457
             self._log.info('Configuration finished: %s', jid)
    
    457 458
     
    
    458 459
             room = self._get_contact(jid.bare)
    
    459
    -        room.notify('room-config-finished')
    
    460
    +        event = events.MUCRoomConfigFinished(timestamp=time.time())
    
    461
    +
    
    462
    +        assert isinstance(room, GroupchatContact)
    
    463
    +        app.storage.events.store(room, event)
    
    464
    +        room.notify('room-config-finished', event)
    
    460 465
     
    
    461 466
         def update_presence(self) -> None:
    
    462 467
             mucs = self._get_mucs_with_state([MUCJoinedState.JOINED,
    
    ... ... @@ -536,6 +541,11 @@ def _on_error_presence(self,
    536 541
                 self._set_muc_state(room_jid, MUCJoinedState.NOT_JOINED)
    
    537 542
     
    
    538 543
             else:
    
    544
    +            event = events.MUCRoomPresenceError(
    
    545
    +                timestamp=time.time(),
    
    546
    +                error=properties.error)
    
    547
    +            assert isinstance(room, GroupchatContact)
    
    548
    +            app.storage.events.store(room, event)
    
    539 549
                 room.notify('room-presence-error', properties)
    
    540 550
     
    
    541 551
         def _on_muc_user_presence(self,
    
    ... ... @@ -560,7 +570,15 @@ def _on_muc_user_presence(self,
    560 570
                 self._set_muc_state(room_jid, MUCJoinedState.NOT_JOINED)
    
    561 571
                 self._con.get_module('Bookmarks').remove(room_jid)
    
    562 572
                 room.set_not_joined()
    
    563
    -            room.notify('room-destroyed', properties)
    
    573
    +
    
    574
    +            event = events.MUCRoomDestroyed(
    
    575
    +                timestamp=time.time(),
    
    576
    +                reason=properties.muc_destroyed.reason,
    
    577
    +                alternate=properties.muc_destroyed.alternate)
    
    578
    +            assert isinstance(room, GroupchatContact)
    
    579
    +            app.storage.events.store(room, event)
    
    580
    +
    
    581
    +            room.notify('room-destroyed', event)
    
    564 582
                 return
    
    565 583
     
    
    566 584
             if properties.is_nickname_changed:
    
    ... ... @@ -586,10 +604,16 @@ def _on_muc_user_presence(self,
    586 604
                 presence = self._process_user_presence(properties)
    
    587 605
                 occupant.update_presence(presence, properties)
    
    588 606
     
    
    589
    -            room.notify('user-nickname-changed',
    
    590
    -                        occupant,
    
    591
    -                        new_occupant,
    
    592
    -                        properties)
    
    607
    +            event = events.MUCNicknameChanged(
    
    608
    +                timestamp=time.time(),
    
    609
    +                is_self=properties.is_muc_self_presence,
    
    610
    +                new_name=new_occupant.name,
    
    611
    +                old_name=occupant.name)
    
    612
    +
    
    613
    +            assert isinstance(room, GroupchatContact)
    
    614
    +            app.storage.events.store(room, event)
    
    615
    +
    
    616
    +            room.notify('user-nickname-changed', event, occupant, new_occupant)
    
    593 617
                 return
    
    594 618
     
    
    595 619
             is_joined = self._is_user_joined(properties.jid)
    
    ... ... @@ -616,7 +640,16 @@ def _on_muc_user_presence(self,
    616 640
             if properties.is_muc_self_presence and properties.is_kicked:
    
    617 641
                 self._set_muc_state(room_jid, MUCJoinedState.NOT_JOINED)
    
    618 642
                 room.set_not_joined()
    
    619
    -            room.notify('room-kicked', properties)
    
    643
    +
    
    644
    +            event = events.MUCRoomKicked(
    
    645
    +                timestamp=time.time(),
    
    646
    +                status_codes=properties.muc_status_codes,
    
    647
    +                reason=properties.muc_user.reason,
    
    648
    +                actor=properties.muc_user.actor)
    
    649
    +            assert isinstance(room, GroupchatContact)
    
    650
    +            app.storage.events.store(room, event)
    
    651
    +            room.notify('room-kicked', event)
    
    652
    +
    
    620 653
                 status_codes = properties.muc_status_codes or []
    
    621 654
                 if StatusCode.REMOVED_SERVICE_SHUTDOWN in status_codes:
    
    622 655
                     self._start_rejoin_timeout(room_jid)
    
    ... ... @@ -889,12 +922,21 @@ def _on_config_change(self,
    889 922
             if not properties.is_muc_config_change:
    
    890 923
                 return
    
    891 924
     
    
    892
    -        room_jid = str(properties.muc_jid)
    
    893 925
             self._log.info('Received config change: %s %s',
    
    894
    -                       room_jid, properties.muc_status_codes)
    
    926
    +                       properties.muc_jid, properties.muc_status_codes)
    
    895 927
     
    
    896
    -        room = self._get_contact(room_jid)
    
    897
    -        room.notify('room-config-changed', properties)
    
    928
    +        assert properties.muc_status_codes is not None
    
    929
    +        event = events.MUCRoomConfigChanged(
    
    930
    +            timestamp=time.time(),
    
    931
    +            status_codes=properties.muc_status_codes)
    
    932
    +
    
    933
    +        assert properties.muc_jid is not None
    
    934
    +        room = self._get_contact(properties.muc_jid)
    
    935
    +
    
    936
    +        assert isinstance(room, GroupchatContact)
    
    937
    +        app.storage.events.store(room, event)
    
    938
    +
    
    939
    +        room.notify('room-config-changed', event)
    
    898 940
     
    
    899 941
             raise nbxmpp.NodeProcessed
    
    900 942
     
    

  • gajim/common/storage/base.py
    ... ... @@ -30,11 +30,15 @@
    30 30
     
    
    31 31
     from gi.repository import GLib
    
    32 32
     
    
    33
    +import nbxmpp.const
    
    33 34
     from nbxmpp.protocol import Iq
    
    34 35
     from nbxmpp.protocol import JID
    
    35 36
     from nbxmpp.structs import RosterItem
    
    36 37
     from nbxmpp.structs import DiscoInfo
    
    37 38
     from nbxmpp.structs import CommonError
    
    39
    +from nbxmpp.const import Role
    
    40
    +from nbxmpp.const import Affiliation
    
    41
    +from nbxmpp.const import StatusCode
    
    38 42
     from nbxmpp.modules.discovery import parse_disco_info
    
    39 43
     
    
    40 44
     
    
    ... ... @@ -98,6 +102,13 @@ def _adapt_disco_info(disco_info: DiscoInfo) -> str:
    98 102
     sqlite3.register_adapter(DiscoInfo, _adapt_disco_info)
    
    99 103
     
    
    100 104
     
    
    105
    +def _convert_json(json_string: bytes) -> dict[str, Any]:
    
    106
    +    return json.loads(json_string, object_hook=json_decoder)
    
    107
    +
    
    108
    +
    
    109
    +sqlite3.register_converter('JSON', _convert_json)
    
    110
    +
    
    111
    +
    
    101 112
     class Encoder(json.JSONEncoder):
    
    102 113
         def default(self, o: Any) -> Any:
    
    103 114
             if isinstance(o, set):
    
    ... ... @@ -111,6 +122,10 @@ def default(self, o: Any) -> Any:
    111 122
                 dct['__type'] = 'RosterItem'
    
    112 123
                 return dct
    
    113 124
     
    
    125
    +        if isinstance(o, (Affiliation, Role, StatusCode)):
    
    126
    +            return {'value': o.value,
    
    127
    +                    '__type': o.__class__.__name__}
    
    128
    +
    
    114 129
             return json.JSONEncoder.default(self, o)
    
    115 130
     
    
    116 131
     
    
    ... ... @@ -118,8 +133,10 @@ def json_decoder(dct: dict[str, Any]) -> Any:
    118 133
         type_ = dct.get('__type')
    
    119 134
         if type_ is None:
    
    120 135
             return dct
    
    136
    +
    
    121 137
         if type_ == 'JID':
    
    122 138
             return JID.from_string(dct['value'])
    
    139
    +
    
    123 140
         if type_ == 'RosterItem':
    
    124 141
             return RosterItem(jid=dct['jid'],
    
    125 142
                               name=dct['name'],
    
    ... ... @@ -127,6 +144,10 @@ def json_decoder(dct: dict[str, Any]) -> Any:
    127 144
                               subscription=dct['subscription'],
    
    128 145
                               approved=dct['approved'],
    
    129 146
                               groups=set(dct['groups']))
    
    147
    +
    
    148
    +    if type_ in ('Affiliation', 'Role', 'StatusCode'):
    
    149
    +        return getattr(nbxmpp.const, type_)(dct['value'])
    
    150
    +
    
    130 151
         return dct
    
    131 152
     
    
    132 153
     
    

  • gajim/common/storage/events.py
    1
    +# This file is part of Gajim.
    
    2
    +#
    
    3
    +# Gajim is free software; you can redistribute it and/or modify
    
    4
    +# it under the terms of the GNU General Public License as published
    
    5
    +# by the Free Software Foundation; version 3 only.
    
    6
    +#
    
    7
    +# Gajim is distributed in the hope that it will be useful,
    
    8
    +# but WITHOUT ANY WARRANTY; without even the implied warranty of
    
    9
    +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    
    10
    +# GNU General Public License for more details.
    
    11
    +#
    
    12
    +# You should have received a copy of the GNU General Public License
    
    13
    +# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
    
    14
    +
    
    15
    +from __future__ import annotations
    
    16
    +import dataclasses
    
    17
    +
    
    18
    +from typing import Any
    
    19
    +from typing import NamedTuple
    
    20
    +
    
    21
    +import json
    
    22
    +import sqlite3
    
    23
    +import logging
    
    24
    +from collections import namedtuple
    
    25
    +
    
    26
    +from nbxmpp.protocol import JID
    
    27
    +
    
    28
    +from gajim.common import events
    
    29
    +from gajim.common.storage.base import SqliteStorage
    
    30
    +from gajim.common.storage.base import Encoder
    
    31
    +from gajim.common.types import ChatContactT
    
    32
    +
    
    33
    +
    
    34
    +EVENTS_SQL_STATEMENT = '''
    
    35
    +    CREATE TABLE events (
    
    36
    +            account TEXT,
    
    37
    +            jid TEXT,
    
    38
    +            event TEXT,
    
    39
    +            timestamp REAL,
    
    40
    +            data TEXT);
    
    41
    +
    
    42
    +    CREATE INDEX idx_account_jid ON events(account, jid);
    
    43
    +
    
    44
    +    PRAGMA user_version=1;
    
    45
    +    '''
    
    46
    +
    
    47
    +EVENT_CLASSES: dict[str, Any] = {
    
    48
    +    'muc-nickname-changed': events.MUCNicknameChanged,
    
    49
    +    'muc-room-config-changed': events.MUCRoomConfigChanged,
    
    50
    +    'muc-room-config-finished': events.MUCRoomConfigFinished,
    
    51
    +    'muc-room-presence-error': events.MUCRoomPresenceError,
    
    52
    +    'muc-room-kicked': events.MUCRoomKicked,
    
    53
    +    'muc-room-destroyed': events.MUCRoomDestroyed,
    
    54
    +    'muc-user-joined': events.MUCUserJoined,
    
    55
    +    'muc-user-left': events.MUCUserLeft,
    
    56
    +    'muc-user-role-changed': events.MUCUserRoleChanged,
    
    57
    +    'muc-user-affiliation-changed': events.MUCUserAffiliationChanged,
    
    58
    +    'muc-user-status-show-changed': events.MUCUserStatusShowChanged,
    
    59
    +}
    
    60
    +
    
    61
    +log = logging.getLogger('gajim.c.storage.events')
    
    62
    +
    
    63
    +
    
    64
    +class EventRow(NamedTuple):
    
    65
    +    account: str
    
    66
    +    jid: JID
    
    67
    +    event: str
    
    68
    +    timestamp: float
    
    69
    +    data: dict[str, Any]
    
    70
    +
    
    71
    +
    
    72
    +class EventStorage(SqliteStorage):
    
    73
    +    def __init__(self):
    
    74
    +        SqliteStorage.__init__(self,
    
    75
    +                               log,
    
    76
    +                               None,
    
    77
    +                               EVENTS_SQL_STATEMENT)
    
    78
    +
    
    79
    +    def init(self, **kwargs: Any) -> None:
    
    80
    +        SqliteStorage.init(self,
    
    81
    +                           detect_types=sqlite3.PARSE_COLNAMES)
    
    82
    +        self._con.row_factory = self._namedtuple_factory
    
    83
    +
    
    84
    +    @staticmethod
    
    85
    +    def _namedtuple_factory(cursor: sqlite3.Cursor,
    
    86
    +                            row: tuple[Any, ...]) -> NamedTuple:
    
    87
    +
    
    88
    +        assert cursor.description is not None
    
    89
    +        fields = [col[0] for col in cursor.description]
    
    90
    +        Row = namedtuple('Row', fields)  # type: ignore
    
    91
    +        return Row(*row)
    
    92
    +
    
    93
    +    def _migrate(self) -> None:
    
    94
    +        pass
    
    95
    +
    
    96
    +    def store(self,
    
    97
    +              contact: ChatContactT,
    
    98
    +              event: Any
    
    99
    +              ) -> None:
    
    100
    +
    
    101
    +        event_dict = dataclasses.asdict(event)
    
    102
    +        name = event_dict.pop('name')
    
    103
    +        timestamp = event_dict.pop('timestamp')
    
    104
    +
    
    105
    +        insert_sql = '''
    
    106
    +            INSERT INTO events(account, jid, event, timestamp, data)
    
    107
    +            VALUES(?, ?, ?, ?, ?)'''
    
    108
    +
    
    109
    +        self._con.execute(insert_sql, (contact.account,
    
    110
    +                                       contact.jid,
    
    111
    +                                       name,
    
    112
    +                                       timestamp,
    
    113
    +                                       json.dumps(event_dict, cls=Encoder)))
    
    114
    +
    
    115
    +    def load(self,
    
    116
    +             contact: ChatContactT,
    
    117
    +             before: bool,
    
    118
    +             timestamp: float
    
    119
    +             ) -> list[EventRow]:
    
    120
    +
    
    121
    +        time_operator = '<' if before else '>'
    
    122
    +
    
    123
    +        insert_sql = '''
    
    124
    +            SELECT account, jid, event, timestamp, data as "data [JSON]"
    
    125
    +            FROM events WHERE account=? AND jid=? AND timestamp %s ?
    
    126
    +            ''' % time_operator
    
    127
    +
    
    128
    +        for row in self._con.execute(insert_sql, (contact.account,
    
    129
    +                                                  contact.jid,
    
    130
    +                                                  timestamp)).fetchall():
    
    131
    +            event_class = EVENT_CLASSES[row.event]
    
    132
    +            yield event_class(**row.data, timestamp=row.timestamp)

  • gajim/gtk/chat_banner.py
    ... ... @@ -21,8 +21,6 @@
    21 21
     
    
    22 22
     import cairo
    
    23 23
     
    
    24
    -from nbxmpp.structs import PresenceProperties
    
    25
    -
    
    26 24
     from gajim.common import app
    
    27 25
     from gajim.common import ged
    
    28 26
     from gajim.common import types
    
    ... ... @@ -184,13 +182,7 @@ def _on_muc_state_changed(self,
    184 182
             if contact.is_joined:
    
    185 183
                 self._update_content()
    
    186 184
     
    
    187
    -    def _on_user_role_changed(self,
    
    188
    -                              _contact: GroupchatContact,
    
    189
    -                              _signal_name: str,
    
    190
    -                              user_contact: GroupchatParticipant,
    
    191
    -                              properties: PresenceProperties
    
    192
    -                              ) -> None:
    
    193
    -
    
    185
    +    def _on_user_role_changed(self, *args: Any) -> None:
    
    194 186
             self._update_content()
    
    195 187
     
    
    196 188
         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,
    243 243
             if not workspace_id:
    
    244 244
                 workspace_id = app.window.add_workspace(switch=False)
    
    245 245
     
    
    246
    -        current_chatlist = cast(ChatList, self.get_visible_child())
    
    247
    -        type_ = current_chatlist.get_chat_type(params.account, params.jid)
    
    246
    +        source_chatlist = self.get_chatlist(params.source_workspace_id)
    
    247
    +        type_ = source_chatlist.get_chat_type(params.account, params.jid)
    
    248 248
             if type_ is None:
    
    249 249
                 return
    
    250
    -        current_chatlist.remove_chat(params.account, params.jid)
    
    250
    +
    
    251
    +        source_chatlist.remove_chat(params.account, params.jid)
    
    251 252
     
    
    252 253
             new_chatlist = self.get_chatlist(workspace_id)
    
    253 254
             new_chatlist.add_chat(params.account, params.jid, type_, False, -1)
    
    254 255
     
    
    255
    -        self.store_open_chats(current_chatlist.workspace_id)
    
    256
    +        self.store_open_chats(source_chatlist.workspace_id)
    
    256 257
             self.store_open_chats(workspace_id)
    
    257 258
     
    
    258 259
         @structs.actionmethod
    

  • gajim/gtk/chat_stack.py
    ... ... @@ -28,7 +28,6 @@
    28 28
     from nbxmpp.errors import StanzaError
    
    29 29
     from nbxmpp.protocol import JID
    
    30 30
     from nbxmpp.structs import MessageProperties
    
    31
    -from nbxmpp.structs import PresenceProperties
    
    32 31
     
    
    33 32
     from gajim.common import app
    
    34 33
     from gajim.common import events
    
    ... ... @@ -297,23 +296,25 @@ def _on_user_joined(self,
    297 296
                             contact: GroupchatContact,
    
    298 297
                             _signal_name: str,
    
    299 298
                             _user_contact: GroupchatParticipant,
    
    300
    -                        _properties: PresenceProperties
    
    299
    +                        _event: events.MUCUserJoined
    
    301 300
                             ) -> None:
    
    301
    +
    
    302 302
             self._update_group_chat_actions(contact)
    
    303 303
     
    
    304 304
         def _on_user_role_changed(self,
    
    305 305
                                   contact: GroupchatContact,
    
    306 306
                                   _signal_name: str,
    
    307 307
                                   _user_contact: GroupchatParticipant,
    
    308
    -                              _properties: PresenceProperties
    
    308
    +                              _event: events.MUCUserRoleChanged
    
    309 309
                                   ) -> None:
    
    310
    +
    
    310 311
             self._update_group_chat_actions(contact)
    
    311 312
     
    
    312 313
         def _on_user_affiliation_changed(self,
    
    313 314
                                          contact: GroupchatContact,
    
    314 315
                                          _signal_name: str,
    
    315 316
                                          _user_contact: GroupchatParticipant,
    
    316
    -                                     _properties: PresenceProperties
    
    317
    +                                     _event: events.MUCUserAffiliationChanged
    
    317 318
                                          ) -> None:
    
    318 319
             self._update_group_chat_actions(contact)
    
    319 320
     
    

  • gajim/gtk/control.py
    ... ... @@ -19,9 +19,6 @@
    19 19
     from typing import Union
    
    20 20
     from typing import cast
    
    21 21
     
    
    22
    -from collections import defaultdict
    
    23
    -from collections import deque
    
    24
    -from functools import partial
    
    25 22
     import os
    
    26 23
     import logging
    
    27 24
     import time
    
    ... ... @@ -105,12 +102,6 @@ def __init__(self) -> None:
    105 102
             self._muc_subjects: dict[
    
    106 103
                 types.ChatContactT, tuple[float, MucSubject]] = {}
    
    107 104
     
    
    108
    -        # Store 100 info messages per contact on a FIFO basis
    
    109
    -        self._info_messages: dict[
    
    110
    -            types.ChatContactT,
    
    111
    -            deque[tuple[float, str]]] = defaultdict(
    
    112
    -                partial(deque, maxlen=100))  # pyright: ignore
    
    113
    -
    
    114 105
             self.widget = cast(Gtk.Box, self._ui.get_object('control_box'))
    
    115 106
             self.widget.show_all()
    
    116 107
     
    
    ... ... @@ -212,9 +203,6 @@ def switch_contact(self, contact: Union[BareContact,
    212 203
                 for transfer in transfers:
    
    213 204
                     self.add_file_transfer(transfer)
    
    214 205
     
    
    215
    -        for timestamp, message in self._info_messages[contact]:
    
    216
    -            self.add_info_message(message, timestamp)
    
    217
    -
    
    218 206
             if isinstance(contact, GroupchatContact):
    
    219 207
                 if (app.settings.get('show_subject_on_join') or
    
    220 208
                         not contact.is_joining):
    
    ... ... @@ -515,10 +503,6 @@ def add_info_message(self,
    515 503
                              timestamp: Optional[float] = None
    
    516 504
                              ) -> None:
    
    517 505
     
    
    518
    -        if timestamp is None:
    
    519
    -            assert self._contact is not None
    
    520
    -            self._info_messages[self._contact].append((time.time(), text))
    
    521
    -
    
    522 506
             self.conversation_view.add_info_message(text, timestamp)
    
    523 507
     
    
    524 508
         def add_file_transfer(self, transfer: HTTPFileTransfer) -> None:
    
    ... ... @@ -639,6 +623,42 @@ def _fetch_n_lines_history(self,
    639 623
             #    if self.conversation_view.reduce_message_count(before):
    
    640 624
             #        self._scrolled_view.set_history_complete(before, False)
    
    641 625
     
    
    626
    +        for event in app.storage.events.load(self._contact, before, timestamp):
    
    627
    +            if isinstance(event, events.MUCUserJoined):
    
    628
    +                self._process_muc_user_joined(event)
    
    629
    +
    
    630
    +            elif isinstance(event, events.MUCUserLeft):
    
    631
    +                self._process_muc_user_left(event)
    
    632
    +
    
    633
    +            elif isinstance(event, events.MUCNicknameChanged):
    
    634
    +                self._process_muc_nickname_changed(event)
    
    635
    +
    
    636
    +            elif isinstance(event, events.MUCRoomKicked):
    
    637
    +                self._process_muc_room_kicked(event)
    
    638
    +
    
    639
    +            elif isinstance(event, events.MUCUserAffiliationChanged):
    
    640
    +                self._process_muc_user_affiliation_changed(event)
    
    641
    +
    
    642
    +            elif isinstance(event, events.MUCUserRoleChanged):
    
    643
    +                self._process_muc_user_role_changed(event)
    
    644
    +
    
    645
    +            elif isinstance(event, events.MUCUserStatusShowChanged):
    
    646
    +                self._process_muc_user_status_show_changed(event)
    
    647
    +
    
    648
    +            elif isinstance(event, events.MUCRoomConfigChanged):
    
    649
    +                self._process_muc_room_config_changed(event)
    
    650
    +
    
    651
    +            elif isinstance(event, events.MUCRoomConfigFinished):
    
    652
    +                self._process_muc_room_config_finished(event)
    
    653
    +
    
    654
    +            elif isinstance(event, events.MUCRoomPresenceError):
    
    655
    +                self._process_muc_room_presence_error(event)
    
    656
    +
    
    657
    +            elif isinstance(event, events.MUCRoomDestroyed):
    
    658
    +                self._process_muc_room_destroyed(event)
    
    659
    +            else:
    
    660
    +                raise ValueError('Unknown event: %s' % event)
    
    661
    +
    
    642 662
             self._scrolled_view.block_signals(False)
    
    643 663
     
    
    644 664
         def add_messages(self, messages: list[ConversationRow]):
    
    ... ... @@ -723,143 +743,213 @@ def add_message(self,
    723 743
                               additional_data=additional_data)
    
    724 744
     
    
    725 745
         def _on_user_nickname_changed(self,
    
    726
    -                                  contact: types.GroupchatContact,
    
    746
    +                                  _contact: types.GroupchatContact,
    
    727 747
                                       _signal_name: str,
    
    728
    -                                  old_contact: types.GroupchatParticipant,
    
    729
    -                                  new_contact: types.GroupchatParticipant,
    
    730
    -                                  properties: PresenceProperties
    
    748
    +                                  event: events.MUCNicknameChanged,
    
    749
    +                                  _old_contact: types.GroupchatParticipant,
    
    750
    +                                  _new_contact: types.GroupchatParticipant
    
    731 751
                                       ) -> None:
    
    732 752
     
    
    733
    -        if properties.is_muc_self_presence:
    
    734
    -            message = _('You are now known as %s') % new_contact.name
    
    753
    +        self._process_muc_nickname_changed(event)
    
    754
    +
    
    755
    +    def _process_muc_nickname_changed(self,
    
    756
    +                                      event: events.MUCNicknameChanged
    
    757
    +                                      ) -> None:
    
    758
    +
    
    759
    +        if event.is_self:
    
    760
    +            message = _('You are now known as %s') % event.new_name
    
    735 761
             else:
    
    736 762
                 message = _('{nick} is now known '
    
    737
    -                        'as {new_nick}').format(nick=old_contact.name,
    
    738
    -                                                new_nick=new_contact.name)
    
    763
    +                        'as {new_nick}').format(nick=event.old_name,
    
    764
    +                                                new_nick=event.new_name)
    
    765
    +        self.add_info_message(message, event.timestamp)
    
    739 766
     
    
    740
    -        self.add_info_message(message)
    
    767
    +    def _on_room_kicked(self,
    
    768
    +                        _contact: GroupchatContact,
    
    769
    +                        _signal_name: str,
    
    770
    +                        event: events.MUCRoomKicked
    
    771
    +                        ) -> None:
    
    741 772
     
    
    742
    -    def _on_muc_user_status_show_changed(self,
    
    743
    -                                         contact: GroupchatContact,
    
    744
    -                                         _signal_name: str,
    
    745
    -                                         user_contact: GroupchatParticipant,
    
    746
    -                                         properties: PresenceProperties
    
    747
    -                                         ) -> None:
    
    773
    +        self._process_muc_room_kicked(event)
    
    748 774
     
    
    749
    -        if not contact.settings.get('print_status'):
    
    775
    +    def _process_muc_room_kicked(self, event: events.MUCRoomKicked) -> None:
    
    776
    +        status_codes = event.status_codes or []
    
    777
    +
    
    778
    +        reason = event.reason
    
    779
    +        reason = '' if reason is None else f': {reason}'
    
    780
    +
    
    781
    +        actor = event.actor
    
    782
    +        # Group Chat: You have been kicked by Alice
    
    783
    +        actor = '' if actor is None else _(' by {actor}').format(
    
    784
    +            actor=actor)
    
    785
    +
    
    786
    +        # Group Chat: We have been removed from the room by Alice: reason
    
    787
    +        message = _('You have been removed from the '
    
    788
    +                    'group chat{actor}{reason}')
    
    789
    +
    
    790
    +        if StatusCode.REMOVED_ERROR in status_codes:
    
    791
    +            # Handle 333 before 307, some MUCs add both
    
    792
    +            # Group Chat: Server kicked us because of an server error
    
    793
    +            message = _('You have left due '
    
    794
    +                        'to an error{reason}').format(reason=reason)
    
    795
    +
    
    796
    +        elif StatusCode.REMOVED_KICKED in status_codes:
    
    797
    +            # Group Chat: We have been kicked by Alice: reason
    
    798
    +            message = _('You have been '
    
    799
    +                        'kicked{actor}{reason}').format(actor=actor,
    
    800
    +                                                        reason=reason)
    
    801
    +
    
    802
    +        elif StatusCode.REMOVED_BANNED in status_codes:
    
    803
    +            # Group Chat: We have been banned by Alice: reason
    
    804
    +            message = _('You have been '
    
    805
    +                        'banned{actor}{reason}').format(actor=actor,
    
    806
    +                                                        reason=reason)
    
    807
    +
    
    808
    +        elif StatusCode.REMOVED_AFFILIATION_CHANGE in status_codes:
    
    809
    +            # Group Chat: We were removed because of an affiliation change
    
    810
    +            reason = _(': Affiliation changed')
    
    811
    +            message = message.format(actor=actor, reason=reason)
    
    812
    +
    
    813
    +        elif StatusCode.REMOVED_NONMEMBER_IN_MEMBERS_ONLY in status_codes:
    
    814
    +            # Group Chat: Room configuration changed
    
    815
    +            reason = _(': Group chat configuration changed to members-only')
    
    816
    +            message = message.format(actor=actor, reason=reason)
    
    817
    +
    
    818
    +        elif StatusCode.REMOVED_SERVICE_SHUTDOWN in status_codes:
    
    819
    +            # Group Chat: Kicked because of server shutdown
    
    820
    +            reason = ': System shutdown'
    
    821
    +            message = message.format(actor=actor, reason=reason)
    
    822
    +
    
    823
    +        else:
    
    824
    +            # No formatted message available
    
    750 825
                 return
    
    751 826
     
    
    752
    -        self.conversation_view.add_user_status(user_contact.name,
    
    753
    -                                               user_contact.show.value,
    
    754
    -                                               user_contact.status)
    
    827
    +        self.add_info_message(message, event.timestamp)
    
    755 828
     
    
    756
    -    def _on_user_status_show_changed(self,
    
    757
    -                                     _user_contact: GroupchatParticipant,
    
    829
    +    def _on_user_affiliation_changed(self,
    
    830
    +                                     _contact: GroupchatContact,
    
    758 831
                                          _signal_name: str,
    
    759
    -                                     properties: PresenceProperties
    
    832
    +                                     user_contact: GroupchatParticipant,
    
    833
    +                                     event: events.MUCUserAffiliationChanged
    
    760 834
                                          ) -> None:
    
    761 835
     
    
    762
    -        nick = properties.muc_nickname
    
    763
    -        status = properties.status
    
    764
    -        status = '' if not status else f' - {status}'
    
    765
    -        assert properties.show is not None
    
    766
    -        show = helpers.get_uf_show(properties.show.value)
    
    836
    +        self._process_muc_user_affiliation_changed(event)
    
    767 837
     
    
    768
    -        assert isinstance(self.contact, GroupchatParticipant)
    
    769
    -        if not self.contact.room.settings.get('print_status'):
    
    770
    -            return
    
    838
    +    def _process_muc_user_affiliation_changed(
    
    839
    +            self,
    
    840
    +            event: events.MUCUserAffiliationChanged) -> None:
    
    771 841
     
    
    772
    -        if properties.is_muc_self_presence:
    
    773
    -            message = _('You are now {show}{status}').format(show=show,
    
    774
    -                                                             status=status)
    
    842
    +        affiliation = helpers.get_uf_affiliation(event.affiliation)
    
    843
    +
    
    844
    +        reason = event.reason
    
    845
    +        reason = '' if reason is None else f': {reason}'
    
    846
    +
    
    847
    +        actor = event.actor
    
    848
    +        # Group Chat: You have been kicked by Alice
    
    849
    +        actor = '' if actor is None else _(' by {actor}').format(
    
    850
    +            actor=actor)
    
    775 851
     
    
    852
    +        if event.is_self:
    
    853
    +            message = _('** Your Affiliation has been set to '
    
    854
    +                        '{affiliation}{actor}{reason}').format(
    
    855
    +                            affiliation=affiliation,
    
    856
    +                            actor=actor,
    
    857
    +                            reason=reason)
    
    776 858
             else:
    
    777
    -            message = _('{nick} is now {show}{status}').format(nick=nick,
    
    778
    -                                                               show=show,
    
    779
    -                                                               status=status)
    
    780
    -        self.add_info_message(message)
    
    859
    +            message = _('** Affiliation of {nick} has been set to '
    
    860
    +                        '{affiliation}{actor}{reason}').format(
    
    861
    +                            nick=event.nick,
    
    862
    +                            affiliation=affiliation,
    
    863
    +                            actor=actor,
    
    864
    +                            reason=reason)
    
    781 865
     
    
    782
    -    def _on_room_voice_request(self,
    
    783
    -                               _contact: GroupchatContact,
    
    784
    -                               _signal_name: str,
    
    785
    -                               properties: MessageProperties
    
    786
    -                               ) -> None:
    
    787
    -        voice_request = properties.voice_request
    
    788
    -        assert voice_request is not None
    
    866
    +        self.add_info_message(message, event.timestamp)
    
    789 867
     
    
    790
    -        def on_approve() -> None:
    
    791
    -            self.client.get_module('MUC').approve_voice_request(
    
    792
    -                self.contact.jid, voice_request)
    
    868
    +    def _on_user_role_changed(self,
    
    869
    +                              _contact: GroupchatContact,
    
    870
    +                              _signal_name: str,
    
    871
    +                              user_contact: GroupchatParticipant,
    
    872
    +                              event: events.MUCUserRoleChanged
    
    873
    +                              ) -> None:
    
    793 874
     
    
    794
    -        ConfirmationDialog(
    
    795
    -            _('Voice Request'),
    
    796
    -            _('Voice Request'),
    
    797
    -            _('<b>%(nick)s</b> from <b>%(room_name)s</b> requests voice') % {
    
    798
    -                'nick': voice_request.nick, 'room_name': self.contact.name},
    
    799
    -            [DialogButton.make('Cancel'),
    
    800
    -             DialogButton.make('Accept',
    
    801
    -                               text=_('_Approve'),
    
    802
    -                               callback=on_approve)],
    
    803
    -            modal=False).show()
    
    875
    +        self._process_muc_user_role_changed(event)
    
    804 876
     
    
    805
    -    def add_muc_message(self,
    
    806
    -                        text: str,
    
    807
    -                        tim: float,
    
    808
    -                        contact: str = '',
    
    809
    -                        displaymarking: Optional[Displaymarking] = None,
    
    810
    -                        message_id: Optional[str] = None,
    
    811
    -                        stanza_id: Optional[str] = None,
    
    812
    -                        additional_data: Optional[AdditionalDataDict] = None,
    
    813
    -                        ) -> None:
    
    877
    +    def _process_muc_user_role_changed(self,
    
    878
    +                                       event: events.MUCUserRoleChanged
    
    879
    +                                       ) -> None:
    
    814 880
     
    
    815
    -        assert isinstance(self._contact, GroupchatContact)
    
    881
    +        role = helpers.get_uf_role(event.role)
    
    882
    +        nick = event.nick
    
    816 883
     
    
    817
    -        if contact == self._contact.nickname:
    
    818
    -            kind = 'outgoing'
    
    884
    +        reason = event.reason
    
    885
    +        reason = '' if reason is None else f': {reason}'
    
    886
    +
    
    887
    +        actor = event.actor
    
    888
    +        # Group Chat: You have been kicked by Alice
    
    889
    +        actor = '' if actor is None else _(' by {actor}').format(actor=actor)
    
    890
    +
    
    891
    +        if event.is_self:
    
    892
    +            message = _('** Your Role has been set to '
    
    893
    +                        '{role}{actor}{reason}').format(role=role,
    
    894
    +                                                        actor=actor,
    
    895
    +                                                        reason=reason)
    
    819 896
             else:
    
    820
    -            kind = 'incoming'
    
    821
    -            # muc-specific chatstate
    
    897
    +            message = _('** Role of {nick} has been set to '
    
    898
    +                        '{role}{actor}{reason}').format(nick=nick,
    
    899
    +                                                        role=role,
    
    900
    +                                                        actor=actor,
    
    901
    +                                                        reason=reason)
    
    902
    +        self.add_info_message(message, event.timestamp)
    
    822 903
     
    
    823
    -        self._add_message(text,
    
    824
    -                          kind,
    
    825
    -                          contact,
    
    826
    -                          tim,
    
    827
    -                          displaymarking=displaymarking,
    
    828
    -                          message_id=message_id,
    
    829
    -                          stanza_id=stanza_id,
    
    830
    -                          additional_data=additional_data)
    
    904
    +    def _on_user_status_show_changed(self,
    
    905
    +                                     _user_contact: GroupchatParticipant,
    
    906
    +                                     _signal_name: str,
    
    907
    +                                     event: events.MUCUserStatusShowChanged
    
    908
    +                                     ) -> None:
    
    831 909
     
    
    832
    -    def _on_room_subject(self,
    
    833
    -                         contact: GroupchatContact,
    
    834
    -                         _signal_name: str,
    
    835
    -                         subject: Optional[MucSubject]
    
    836
    -                         ) -> None:
    
    910
    +        self._process_muc_user_status_show_changed(event)
    
    837 911
     
    
    838
    -        if subject is None:
    
    839
    -            return
    
    912
    +    def _process_muc_user_status_show_changed(
    
    913
    +            self,
    
    914
    +            event: events.MUCUserStatusShowChanged) -> None:
    
    840 915
     
    
    841
    -        _timestamp, old_subject = self._muc_subjects.get(contact, (None, None))
    
    842
    -        if old_subject is not None and old_subject.text == subject.text:
    
    843
    -            # Probably a rejoin, we already showed that subject
    
    916
    +        nick = event.nick
    
    917
    +        status = event.status
    
    918
    +        status = '' if not status else f' - {status}'
    
    919
    +        show = helpers.get_uf_show(event.show_value)
    
    920
    +
    
    921
    +        assert isinstance(self.contact, GroupchatParticipant)
    
    922
    +        if not self.contact.room.settings.get('print_status'):
    
    844 923
                 return
    
    845 924
     
    
    846
    -        self._muc_subjects[contact] = (time.time(), subject)
    
    925
    +        if event.is_self:
    
    926
    +            message = _('You are now {show}{status}').format(show=show,
    
    927
    +                                                             status=status)
    
    847 928
     
    
    848
    -        if (app.settings.get('show_subject_on_join') or
    
    849
    -                not contact.is_joining):
    
    850
    -            self.conversation_view.add_muc_subject(subject)
    
    929
    +        else:
    
    930
    +            message = _('{nick} is now {show}{status}').format(
    
    931
    +                nick=nick,
    
    932
    +                show=show,
    
    933
    +                status=status)
    
    934
    +
    
    935
    +        self.add_info_message(message, event.timestamp)
    
    851 936
     
    
    852 937
         def _on_room_config_changed(self,
    
    853 938
                                     _contact: GroupchatContact,
    
    854 939
                                     _signal_name: str,
    
    855
    -                                properties: MessageProperties
    
    940
    +                                event: events.MUCRoomConfigChanged
    
    856 941
                                     ) -> None:
    
    857
    -        # http://www.xmpp.org/extensions/xep-0045.html#roomconfig-notify
    
    858 942
     
    
    859
    -        status_codes = properties.muc_status_codes
    
    860
    -        assert status_codes is not None
    
    943
    +        self._process_muc_room_config_changed(event)
    
    944
    +
    
    945
    +    def _process_muc_room_config_changed(self,
    
    946
    +                                         event: events.MUCRoomConfigChanged
    
    947
    +                                         ) -> None:
    
    861 948
     
    
    949
    +        # http://www.xmpp.org/extensions/xep-0045.html#roomconfig-notify
    
    950
    +        status_codes = event.status_codes
    
    862 951
             changes: list[str] = []
    
    952
    +
    
    863 953
             if StatusCode.SHOWING_UNAVAILABLE in status_codes:
    
    864 954
                 changes.append(_('Group chat now shows unavailable members'))
    
    865 955
     
    
    ... ... @@ -873,7 +963,8 @@ def _on_room_config_changed(self,
    873 963
                 self.client.get_module('Discovery').disco_muc(self.contact.jid)
    
    874 964
     
    
    875 965
             if StatusCode.CONFIG_ROOM_LOGGING in status_codes:
    
    876
    -            # Can be a presence (see chg_contact_status in groupchat_control.py)
    
    966
    +            # Can be a presence
    
    967
    +            # (see chg_contact_status in groupchat_control.py)
    
    877 968
                 changes.append(_('Conversations are stored on the server'))
    
    878 969
     
    
    879 970
             if StatusCode.CONFIG_NO_ROOM_LOGGING in status_codes:
    
    ... ... @@ -888,194 +979,130 @@ def _on_room_config_changed(self,
    888 979
             if StatusCode.CONFIG_FULL_ANONYMOUS in status_codes:
    
    889 980
                 changes.append(_('Group chat is now fully anonymous'))
    
    890 981
     
    
    891
    -        for change in changes:
    
    892
    -            self.add_info_message(change)
    
    893
    -
    
    894
    -    def rejoin(self) -> None:
    
    895
    -        self.client.get_module('MUC').join(self.contact.jid)
    
    896
    -
    
    897
    -    def _on_user_joined(self,
    
    898
    -                        contact: GroupchatContact,
    
    899
    -                        _signal_name: str,
    
    900
    -                        user_contact: GroupchatParticipant,
    
    901
    -                        properties: PresenceProperties
    
    902
    -                        ) -> None:
    
    903
    -
    
    904
    -        nick = user_contact.name
    
    905
    -        if not properties.is_muc_self_presence:
    
    906
    -            if contact.is_joined:
    
    907
    -                self.conversation_view.add_muc_user_joined(nick)
    
    908
    -            return
    
    909
    -
    
    910
    -        status_codes = properties.muc_status_codes or []
    
    911
    -
    
    912
    -        if not contact.is_joined:
    
    913
    -            # We just joined the room
    
    914
    -            self.add_info_message(_('You (%s) joined the group chat') % nick)
    
    915
    -
    
    916
    -        if StatusCode.NON_ANONYMOUS in status_codes:
    
    917
    -            self.add_info_message(
    
    918
    -                _('Any participant is allowed to see your full XMPP Address'))
    
    919
    -
    
    920
    -        if StatusCode.CONFIG_ROOM_LOGGING in status_codes:
    
    921
    -            self.add_info_message(_('Conversations are stored on the server'))
    
    922
    -
    
    923
    -        if StatusCode.NICKNAME_MODIFIED in status_codes:
    
    924
    -            self.add_info_message(
    
    925
    -                _('The server has assigned or modified your nickname in this '
    
    926
    -                  'group chat'))
    
    982
    +        for message in changes:
    
    983
    +            self.add_info_message(message, event.timestamp)
    
    927 984
     
    
    928 985
         def _on_room_config_finished(self,
    
    929 986
                                      _contact: GroupchatContact,
    
    930
    -                                 _signal_name: str
    
    987
    +                                 _signal_name: str,
    
    988
    +                                 event: events.MUCRoomConfigFinished
    
    931 989
                                      ) -> None:
    
    990
    +        self._process_muc_room_config_finished(event)
    
    991
    +
    
    992
    +    def _process_muc_room_config_finished(self,
    
    993
    +                                          event: events.MUCRoomConfigFinished
    
    994
    +                                          ) -> None:
    
    995
    +
    
    932 996
             self.add_info_message(_('A new group chat has been created'))
    
    933 997
     
    
    934
    -    def _on_user_affiliation_changed(self,
    
    935
    -                                     _contact: GroupchatContact,
    
    936
    -                                     _signal_name: str,
    
    937
    -                                     user_contact: GroupchatParticipant,
    
    938
    -                                     properties: PresenceProperties
    
    939
    -                                     ) -> None:
    
    940
    -        affiliation = helpers.get_uf_affiliation(user_contact.affiliation)
    
    941
    -        nick = user_contact.name
    
    998
    +    def _on_room_presence_error(self,
    
    999
    +                                _contact: GroupchatContact,
    
    1000
    +                                _signal_name: str,
    
    1001
    +                                event: events.MUCRoomPresenceError
    
    1002
    +                                ) -> None:
    
    942 1003
     
    
    943
    -        assert properties.muc_user is not None
    
    944
    -        reason = properties.muc_user.reason
    
    945
    -        reason = '' if reason is None else f': {reason}'
    
    1004
    +        self._process_muc_room_presence_error(event)
    
    946 1005
     
    
    947
    -        actor = properties.muc_user.actor
    
    948
    -        # Group Chat: You have been kicked by Alice
    
    949
    -        actor = '' if actor is None else _(' by {actor}').format(actor=actor)
    
    1006
    +    def _process_muc_room_presence_error(self,
    
    1007
    +                                         event: events.MUCRoomPresenceError
    
    1008
    +                                         ) -> None:
    
    950 1009
     
    
    951
    -        if properties.is_muc_self_presence:
    
    952
    -            message = _('** Your Affiliation has been set to '
    
    953
    -                        '{affiliation}{actor}{reason}').format(
    
    954
    -                            affiliation=affiliation,
    
    955
    -                            actor=actor,
    
    956
    -                            reason=reason)
    
    957
    -        else:
    
    958
    -            message = _('** Affiliation of {nick} has been set to '
    
    959
    -                        '{affiliation}{actor}{reason}').format(
    
    960
    -                            nick=nick,
    
    961
    -                            affiliation=affiliation,
    
    962
    -                            actor=actor,
    
    963
    -                            reason=reason)
    
    1010
    +        error_message = to_user_string(event.error)
    
    1011
    +        self.add_info_message(_('Error: %s') % error_message)
    
    964 1012
     
    
    965
    -        self.add_info_message(message)
    
    1013
    +    def _on_room_destroyed(self,
    
    1014
    +                           _contact: GroupchatContact,
    
    1015
    +                           _signal_name: str,
    
    1016
    +                           event: events.MUCRoomDestroyed
    
    1017
    +                           ) -> None:
    
    966 1018
     
    
    967
    -    def _on_user_role_changed(self,
    
    968
    -                              _contact: GroupchatContact,
    
    969
    -                              _signal_name: str,
    
    970
    -                              user_contact: GroupchatParticipant,
    
    971
    -                              properties: PresenceProperties
    
    972
    -                              ) -> None:
    
    973
    -        role = helpers.get_uf_role(user_contact.role)
    
    974
    -        nick = user_contact.name
    
    1019
    +        self._process_muc_room_destroyed(event)
    
    1020
    +
    
    1021
    +    def _process_muc_room_destroyed(self,
    
    1022
    +                                    event: events.MUCRoomDestroyed
    
    1023
    +                                    ) -> None:
    
    975 1024
     
    
    976
    -        assert properties.muc_user is not None
    
    977
    -        reason = properties.muc_user.reason
    
    1025
    +        reason = event.reason
    
    978 1026
             reason = '' if reason is None else f': {reason}'
    
    979 1027
     
    
    980
    -        actor = properties.muc_user.actor
    
    981
    -        # Group Chat: You have been kicked by Alice
    
    982
    -        actor = '' if actor is None else _(' by {actor}').format(actor=actor)
    
    1028
    +        message = _('Group chat has been destroyed')
    
    983 1029
     
    
    984
    -        if properties.is_muc_self_presence:
    
    985
    -            message = _('** Your Role has been set to '
    
    986
    -                        '{role}{actor}{reason}').format(role=role,
    
    987
    -                                                        actor=actor,
    
    988
    -                                                        reason=reason)
    
    989
    -        else:
    
    990
    -            message = _('** Role of {nick} has been set to '
    
    991
    -                        '{role}{actor}{reason}').format(nick=nick,
    
    992
    -                                                        role=role,
    
    993
    -                                                        actor=actor,
    
    994
    -                                                        reason=reason)
    
    1030
    +        if event.alternate is not None:
    
    1031
    +            message += '\n' + _('You can join this group chat instead: '
    
    1032
    +                                'xmpp:%s?join') % str(event.alternate)
    
    995 1033
     
    
    996
    -        self.add_info_message(message)
    
    1034
    +        self.add_info_message(message, event.timestamp)
    
    997 1035
     
    
    998
    -    def _on_room_kicked(self,
    
    1036
    +    def _on_user_joined(self,
    
    999 1037
                             _contact: GroupchatContact,
    
    1000 1038
                             _signal_name: str,
    
    1001
    -                        properties: MessageProperties
    
    1039
    +                        _user_contact: GroupchatParticipant,
    
    1040
    +                        event: events.MUCUserJoined
    
    1002 1041
                             ) -> None:
    
    1003
    -        status_codes = properties.muc_status_codes or []
    
    1004 1042
     
    
    1005
    -        assert properties.muc_user is not None
    
    1006
    -        reason = properties.muc_user.reason
    
    1007
    -        reason = '' if reason is None else f': {reason}'
    
    1043
    +        self._process_muc_user_joined(event)
    
    1008 1044
     
    
    1009
    -        actor = properties.muc_user.actor
    
    1010
    -        # Group Chat: You have been kicked by Alice
    
    1011
    -        actor = '' if actor is None else _(' by {actor}').format(actor=actor)
    
    1045
    +    def _process_muc_user_joined(self, event: events.MUCUserJoined) -> None:
    
    1046
    +        assert isinstance(self.contact, GroupchatContact)
    
    1012 1047
     
    
    1013
    -        # Group Chat: We have been removed from the room by Alice: reason
    
    1014
    -        message = _('You have been removed from the group chat{actor}{reason}')
    
    1048
    +        if not event.is_self:
    
    1049
    +            if self.contact.is_joined:
    
    1050
    +                self.conversation_view.add_muc_user_joined(event.nick)
    
    1051
    +            return
    
    1015 1052
     
    
    1016
    -        if StatusCode.REMOVED_ERROR in status_codes:
    
    1017
    -            # Handle 333 before 307, some MUCs add both
    
    1018
    -            # Group Chat: Server kicked us because of an server error
    
    1019
    -            message = _('You have left due '
    
    1020
    -                        'to an error{reason}').format(reason=reason)
    
    1021
    -            self.add_info_message(message)
    
    1053
    +        status_codes = event.status_codes or []
    
    1022 1054
     
    
    1023
    -        elif StatusCode.REMOVED_KICKED in status_codes:
    
    1024
    -            # Group Chat: We have been kicked by Alice: reason
    
    1025
    -            message = _('You have been '
    
    1026
    -                        'kicked{actor}{reason}').format(actor=actor,
    
    1027
    -                                                        reason=reason)
    
    1028
    -            self.add_info_message(message)
    
    1055
    +        message = None
    
    1056
    +        if not self.contact.is_joined:
    
    1057
    +            # We just joined the room
    
    1058
    +            message = _('You (%s) joined the group chat') % event.nick
    
    1029 1059
     
    
    1030
    -        elif StatusCode.REMOVED_BANNED in status_codes:
    
    1031
    -            # Group Chat: We have been banned by Alice: reason
    
    1032
    -            message = _('You have been '
    
    1033
    -                        'banned{actor}{reason}').format(actor=actor,
    
    1034
    -                                                        reason=reason)
    
    1035
    -            self.add_info_message(message)
    
    1060
    +        if StatusCode.NON_ANONYMOUS in status_codes:
    
    1061
    +            message = _('Any participant is allowed to see your full '
    
    1062
    +                        'XMPP Address')
    
    1036 1063
     
    
    1037
    -        elif StatusCode.REMOVED_AFFILIATION_CHANGE in status_codes:
    
    1038
    -            # Group Chat: We were removed because of an affiliation change
    
    1039
    -            reason = _(': Affiliation changed')
    
    1040
    -            message = message.format(actor=actor, reason=reason)
    
    1041
    -            self.add_info_message(message)
    
    1064
    +        if StatusCode.CONFIG_ROOM_LOGGING in status_codes:
    
    1065
    +            message = _('Conversations are stored on the server')
    
    1042 1066
     
    
    1043
    -        elif StatusCode.REMOVED_NONMEMBER_IN_MEMBERS_ONLY in status_codes:
    
    1044
    -            # Group Chat: Room configuration changed
    
    1045
    -            reason = _(': Group chat configuration changed to members-only')
    
    1046
    -            message = message.format(actor=actor, reason=reason)
    
    1047
    -            self.add_info_message(message)
    
    1067
    +        if StatusCode.NICKNAME_MODIFIED in status_codes:
    
    1068
    +            message = _('The server has assigned or modified your '
    
    1069
    +                        'nickname in this group chat')
    
    1048 1070
     
    
    1049
    -        elif StatusCode.REMOVED_SERVICE_SHUTDOWN in status_codes:
    
    1050
    -            # Group Chat: Kicked because of server shutdown
    
    1051
    -            reason = ': System shutdown'
    
    1052
    -            message = message.format(actor=actor, reason=reason)
    
    1053
    -            self.add_info_message(message)
    
    1071
    +        if message is not None:
    
    1072
    +            self.add_info_message(message, event.timestamp)
    
    1054 1073
     
    
    1055 1074
         def _on_user_left(self,
    
    1056 1075
                           _contact: GroupchatContact,
    
    1057 1076
                           _signal_name: str,
    
    1058
    -                      user_contact: GroupchatParticipant,
    
    1059
    -                      properties: MessageProperties
    
    1077
    +                      _user_contact: GroupchatParticipant,
    
    1078
    +                      event: events.MUCUserLeft
    
    1060 1079
                           ) -> None:
    
    1061
    -        status_codes = properties.muc_status_codes or []
    
    1062
    -        nick = user_contact.name
    
    1063 1080
     
    
    1064
    -        assert properties.muc_user is not None
    
    1065
    -        reason = properties.muc_user.reason
    
    1081
    +        self._process_muc_user_left(event)
    
    1082
    +
    
    1083
    +    def _process_muc_user_left(self, event: events.MUCUserLeft) -> None:
    
    1084
    +        if event.is_self:
    
    1085
    +            return
    
    1086
    +
    
    1087
    +        status_codes = event.status_codes or []
    
    1088
    +        nick = event.nick
    
    1089
    +
    
    1090
    +        reason = event.reason
    
    1066 1091
             reason = '' if reason is None else f': {reason}'
    
    1067 1092
     
    
    1068
    -        actor = properties.muc_user.actor
    
    1093
    +        actor = event.actor
    
    1069 1094
             # Group Chat: You have been kicked by Alice
    
    1070
    -        actor = '' if actor is None else _(' by {actor}').format(actor=actor)
    
    1095
    +        actor = '' if actor is None else _(' by {actor}').format(
    
    1096
    +            actor=actor)
    
    1071 1097
     
    
    1072 1098
             # Group Chat: We have been removed from the room
    
    1073
    -        message = _('{nick} has been removed from the group chat{by}{reason}')
    
    1099
    +        message = _('{nick} has been removed from the group '
    
    1100
    +                    'chat{by}{reason}')
    
    1074 1101
     
    
    1075 1102
             if StatusCode.REMOVED_ERROR in status_codes:
    
    1076 1103
                 # Handle 333 before 307, some MUCs add both
    
    1077 1104
                 self.conversation_view.add_muc_user_left(
    
    1078
    -                nick, properties.muc_user.reason, error=True)
    
    1105
    +                nick, event.reason, error=True)
    
    1079 1106
     
    
    1080 1107
             elif StatusCode.REMOVED_KICKED in status_codes:
    
    1081 1108
                 # Group Chat: User was kicked by Alice: reason
    
    ... ... @@ -1083,7 +1110,6 @@ def _on_user_left(self,
    1083 1110
                             'kicked{actor}{reason}').format(nick=nick,
    
    1084 1111
                                                             actor=actor,
    
    1085 1112
                                                             reason=reason)
    
    1086
    -            self.add_info_message(message)
    
    1087 1113
     
    
    1088 1114
             elif StatusCode.REMOVED_BANNED in status_codes:
    
    1089 1115
                 # Group Chat: User was banned by Alice: reason
    
    ... ... @@ -1091,49 +1117,104 @@ def _on_user_left(self,
    1091 1117
                             'banned{actor}{reason}').format(nick=nick,
    
    1092 1118
                                                             actor=actor,
    
    1093 1119
                                                             reason=reason)
    
    1094
    -            self.add_info_message(message)
    
    1095 1120
     
    
    1096 1121
             elif StatusCode.REMOVED_AFFILIATION_CHANGE in status_codes:
    
    1097 1122
                 reason = _(': Affiliation changed')
    
    1098 1123
                 message = message.format(nick=nick, by=actor, reason=reason)
    
    1099
    -            self.add_info_message(message)
    
    1100 1124
     
    
    1101 1125
             elif StatusCode.REMOVED_NONMEMBER_IN_MEMBERS_ONLY in status_codes:
    
    1102 1126
                 reason = _(': Group chat configuration changed to members-only')
    
    1103 1127
                 message = message.format(nick=nick, by=actor, reason=reason)
    
    1104
    -            self.add_info_message(message)
    
    1105 1128
     
    
    1106 1129
             else:
    
    1107
    -            self.conversation_view.add_muc_user_left(
    
    1108
    -                nick, properties.muc_user.reason)
    
    1130
    +            self.conversation_view.add_muc_user_left(nick, event.reason)
    
    1131
    +            return
    
    1109 1132
     
    
    1110
    -    def _on_room_presence_error(self,
    
    1111
    -                                _contact: GroupchatContact,
    
    1112
    -                                _signal_name: str,
    
    1113
    -                                properties: PresenceProperties
    
    1114
    -                                ) -> None:
    
    1133
    +        self.add_info_message(message, event.timestamp)
    
    1115 1134
     
    
    1116
    -        assert properties.error is not None
    
    1117
    -        error_message = to_user_string(properties.error)
    
    1118
    -        self.add_info_message(_('Error: %s') % error_message)
    
    1135
    +    def _on_muc_user_status_show_changed(self,
    
    1136
    +                                         contact: GroupchatContact,
    
    1137
    +                                         _signal_name: str,
    
    1138
    +                                         user_contact: GroupchatParticipant,
    
    1139
    +                                         properties: PresenceProperties
    
    1140
    +                                         ) -> None:
    
    1119 1141
     
    
    1120
    -    def _on_room_destroyed(self,
    
    1121
    -                           _contact: GroupchatContact,
    
    1122
    -                           _signal_name: str,
    
    1123
    -                           properties: PresenceProperties
    
    1124
    -                           ) -> None:
    
    1142
    +        if not contact.settings.get('print_status'):
    
    1143
    +            return
    
    1125 1144
     
    
    1126
    -        destroyed = properties.muc_destroyed
    
    1127
    -        assert destroyed is not None
    
    1145
    +        self.conversation_view.add_user_status(user_contact.name,
    
    1146
    +                                               user_contact.show.value,
    
    1147
    +                                               user_contact.status)
    
    1128 1148
     
    
    1129
    -        reason = destroyed.reason
    
    1130
    -        reason = '' if reason is None else f': {reason}'
    
    1149
    +    def _on_room_voice_request(self,
    
    1150
    +                               _contact: GroupchatContact,
    
    1151
    +                               _signal_name: str,
    
    1152
    +                               properties: MessageProperties
    
    1153
    +                               ) -> None:
    
    1154
    +        voice_request = properties.voice_request
    
    1155
    +        assert voice_request is not None
    
    1131 1156
     
    
    1132
    -        message = _('Group chat has been destroyed')
    
    1133
    -        self.add_info_message(message)
    
    1157
    +        def on_approve() -> None:
    
    1158
    +            self.client.get_module('MUC').approve_voice_request(
    
    1159
    +                self.contact.jid, voice_request)
    
    1160
    +
    
    1161
    +        ConfirmationDialog(
    
    1162
    +            _('Voice Request'),
    
    1163
    +            _('Voice Request'),
    
    1164
    +            _('<b>%(nick)s</b> from <b>%(room_name)s</b> requests voice') % {
    
    1165
    +                'nick': voice_request.nick, 'room_name': self.contact.name},
    
    1166
    +            [DialogButton.make('Cancel'),
    
    1167
    +             DialogButton.make('Accept',
    
    1168
    +                               text=_('_Approve'),
    
    1169
    +                               callback=on_approve)],
    
    1170
    +            modal=False).show()
    
    1134 1171
     
    
    1135
    -        alternate = destroyed.alternate
    
    1136
    -        if alternate is not None:
    
    1137
    -            join_message = _('You can join this group chat '
    
    1138
    -                             'instead: xmpp:%s?join') % str(alternate)
    
    1139
    -            self.add_info_message(join_message)
    1172
    +    def add_muc_message(self,
    
    1173
    +                        text: str,
    
    1174
    +                        tim: float,
    
    1175
    +                        contact: str = '',
    
    1176
    +                        displaymarking: Optional[Displaymarking] = None,
    
    1177
    +                        message_id: Optional[str] = None,
    
    1178
    +                        stanza_id: Optional[str] = None,
    
    1179
    +                        additional_data: Optional[AdditionalDataDict] = None,
    
    1180
    +                        ) -> None:
    
    1181
    +
    
    1182
    +        assert isinstance(self._contact, GroupchatContact)
    
    1183
    +
    
    1184
    +        if contact == self._contact.nickname:
    
    1185
    +            kind = 'outgoing'
    
    1186
    +        else:
    
    1187
    +            kind = 'incoming'
    
    1188
    +            # muc-specific chatstate
    
    1189
    +
    
    1190
    +        self._add_message(text,
    
    1191
    +                          kind,
    
    1192
    +                          contact,
    
    1193
    +                          tim,
    
    1194
    +                          displaymarking=displaymarking,
    
    1195
    +                          message_id=message_id,
    
    1196
    +                          stanza_id=stanza_id,
    
    1197
    +                          additional_data=additional_data)
    
    1198
    +
    
    1199
    +    def _on_room_subject(self,
    
    1200
    +                         contact: GroupchatContact,
    
    1201
    +                         _signal_name: str,
    
    1202
    +                         subject: Optional[MucSubject]
    
    1203
    +                         ) -> None:
    
    1204
    +
    
    1205
    +        if subject is None:
    
    1206
    +            return
    
    1207
    +
    
    1208
    +        _timestamp, old_subject = self._muc_subjects.get(contact, (None, None))
    
    1209
    +        if old_subject is not None and old_subject.text == subject.text:
    
    1210
    +            # Probably a rejoin, we already showed that subject
    
    1211
    +            return
    
    1212
    +
    
    1213
    +        self._muc_subjects[contact] = (time.time(), subject)
    
    1214
    +
    
    1215
    +        if (app.settings.get('show_subject_on_join') or
    
    1216
    +                not contact.is_joining):
    
    1217
    +            self.conversation_view.add_muc_subject(subject)
    
    1218
    +
    
    1219
    +    def rejoin(self) -> None:
    
    1220
    +        self.client.get_module('MUC').join(self.contact.jid)

  • gajim/gtk/conversation/view.py
    ... ... @@ -176,7 +176,7 @@ def add_muc_subject(self,
    176 176
     
    
    177 177
         def add_muc_user_left(self,
    
    178 178
                               nick: str,
    
    179
    -                          reason: str,
    
    179
    +                          reason: Optional[str],
    
    180 180
                               error: bool = False) -> None:
    
    181 181
             assert isinstance(self._contact, GroupchatContact)
    
    182 182
             if not self._contact.settings.get('print_join_left'):
    

  • gajim/gtk/groupchat_nick_completion.py
    ... ... @@ -18,7 +18,6 @@
    18 18
     
    
    19 19
     import logging
    
    20 20
     
    
    21
    -from nbxmpp.structs import PresenceProperties
    
    22 21
     
    
    23 22
     from gi.repository import Gtk
    
    24 23
     from gi.repository import Gdk
    
    ... ... @@ -27,6 +26,7 @@
    27 26
     from gajim.common import ged
    
    28 27
     from gajim.common import types
    
    29 28
     from gajim.common.events import GcMessageReceived
    
    29
    +from gajim.common.events import MUCNicknameChanged
    
    30 30
     from gajim.common.ged import EventHelper
    
    31 31
     from gajim.common.helpers import jid_is_blocked
    
    32 32
     from gajim.common.helpers import message_needs_highlight
    
    ... ... @@ -76,12 +76,12 @@ def switch_contact(self, contact: types.GroupchatContactT) -> None:
    76 76
         def _on_user_nickname_changed(self,
    
    77 77
                                       _contact: types.GroupchatContact,
    
    78 78
                                       _signal_name: str,
    
    79
    +                                  event: MUCNicknameChanged,
    
    79 80
                                       old_contact: types.GroupchatParticipant,
    
    80
    -                                  new_contact: types.GroupchatParticipant,
    
    81
    -                                  properties: PresenceProperties
    
    81
    +                                  new_contact: types.GroupchatParticipant
    
    82 82
                                       ) -> None:
    
    83 83
     
    
    84
    -        if properties.is_muc_self_presence:
    
    84
    +        if event.is_self:
    
    85 85
                 return
    
    86 86
     
    
    87 87
             old_name = old_contact.name
    

  • gajim/gtk/groupchat_roster.py
    ... ... @@ -25,7 +25,6 @@
    25 25
     from gi.repository import Gtk
    
    26 26
     from gi.repository import GLib
    
    27 27
     from nbxmpp.const import Affiliation
    
    28
    -from nbxmpp.structs import PresenceProperties
    
    29 28
     
    
    30 29
     from gajim.common import app
    
    31 30
     from gajim.common import ged
    
    ... ... @@ -36,6 +35,7 @@
    36 35
     from gajim.common.const import AvatarSize
    
    37 36
     from gajim.common.const import StyleAttr
    
    38 37
     from gajim.common.events import ApplicationEvent
    
    38
    +from gajim.common.events import MUCNicknameChanged
    
    39 39
     from gajim.common.modules.contacts import GroupchatContact
    
    40 40
     
    
    41 41
     from .menus import get_groupchat_roster_menu
    
    ... ... @@ -281,11 +281,11 @@ def _update_contact(self,
    281 281
             self._add_contact(user_contact)
    
    282 282
     
    
    283 283
         def _on_user_nickname_changed(self,
    
    284
    -                                  contact: types.GroupchatContact,
    
    284
    +                                  _contact: types.GroupchatContact,
    
    285 285
                                       _signal_name: str,
    
    286
    +                                  _event: MUCNicknameChanged,
    
    286 287
                                       old_contact: types.GroupchatParticipant,
    
    287
    -                                  new_contact: types.GroupchatParticipant,
    
    288
    -                                  properties: PresenceProperties
    
    288
    +                                  new_contact: types.GroupchatParticipant
    
    289 289
                                       ) -> None:
    
    290 290
     
    
    291 291
             self._remove_contact(old_contact)
    

  • gajim/gtk/main.py
    ... ... @@ -62,6 +62,7 @@
    62 62
     from .structs import AccountJidParam
    
    63 63
     from .structs import AddChatActionParams
    
    64 64
     from .structs import actionmethod
    
    65
    +from .structs import ChatListEntryParam
    
    65 66
     
    
    66 67
     if TYPE_CHECKING:
    
    67 68
         from .control import ChatControl
    
    ... ... @@ -626,18 +627,52 @@ def _remove_workspace(self,
    626 627
                 self.remove_workspace(workspace_id)
    
    627 628
     
    
    628 629
         def remove_workspace(self, workspace_id: str) -> None:
    
    630
    +        if len(app.settings.get_workspaces()) == 1:
    
    631
    +            log.warning('Tried to remove the only workspace')
    
    632
    +            return
    
    633
    +
    
    629 634
             was_active = self.get_active_workspace() == workspace_id
    
    635
    +        chat_list = self.get_chat_list(workspace_id)
    
    636
    +        open_chats = chat_list.get_open_chats()
    
    637
    +
    
    638
    +        def _continue_removing_workspace():
    
    639
    +            new_workspace_id = self._workspace_side_bar.get_other_workspace(
    
    640
    +                workspace_id)
    
    641
    +            if new_workspace_id is None:
    
    642
    +                log.warning('No other workspaces found')
    
    643
    +                return
    
    630 644
     
    
    631
    -        success = self._workspace_side_bar.remove_workspace(workspace_id)
    
    632
    -        if not success:
    
    645
    +            for open_chat in open_chats:
    
    646
    +                params = ChatListEntryParam(
    
    647
    +                    workspace_id=new_workspace_id,
    
    648
    +                    source_workspace_id=workspace_id,
    
    649
    +                    account=open_chat['account'],
    
    650
    +                    jid=open_chat['jid'])
    
    651
    +                self.activate_action('move-chat-to-workspace',
    
    652
    +                                     params.to_variant())
    
    653
    +
    
    654
    +            if was_active:
    
    655
    +                self.activate_workspace(new_workspace_id)
    
    656
    +
    
    657
    +            self._workspace_side_bar.remove_workspace(workspace_id)
    
    658
    +            self._chat_page.remove_chat_list(workspace_id)
    
    659
    +            app.settings.remove_workspace(workspace_id)
    
    660
    +
    
    661
    +        if open_chats:
    
    662
    +            ConfirmationDialog(
    
    663
    +                _('Remove Workspace'),
    
    664
    +                _('Remove Workspace'),
    
    665
    +                _('This workspace contains chats. All chats will be moved to '
    
    666
    +                  'the next workspace. Remove anyway?'),
    
    667
    +                [DialogButton.make('Cancel',
    
    668
    +                                   text=_('_No')),
    
    669
    +                 DialogButton.make('Remove',
    
    670
    +                                   callback=_continue_removing_workspace)]
    
    671
    +            ).show()
    
    633 672
                 return
    
    634 673
     
    
    635
    -        if was_active:
    
    636
    -            new_active_id = self._workspace_side_bar.get_first_workspace()
    
    637
    -            self.activate_workspace(new_active_id)
    
    638
    -
    
    639
    -        self._chat_page.remove_chat_list(workspace_id)
    
    640
    -        app.settings.remove_workspace(workspace_id)
    
    674
    +        # No chats in chat list, it is save to remove this workspace
    
    675
    +        _continue_removing_workspace()
    
    641 676
     
    
    642 677
         def _activate_workspace(self,
    
    643 678
                                 _action: Gio.SimpleAction,
    

  • gajim/gtk/menus.py
    ... ... @@ -410,6 +410,7 @@ def get_chat_list_row_menu(workspace_id: str,
    410 410
         menu = GajimMenu()
    
    411 411
     
    
    412 412
         params = ChatListEntryParam(workspace_id=workspace_id,
    
    413
    +                                source_workspace_id=workspace_id,
    
    413 414
                                     account=account,
    
    414 415
                                     jid=jid)
    
    415 416
     
    
    ... ... @@ -423,6 +424,7 @@ def get_chat_list_row_menu(workspace_id: str,
    423 424
                 submenu.add_item(name, 'win.move-chat-to-workspace', params)
    
    424 425
     
    
    425 426
         params = ChatListEntryParam(workspace_id='',
    
    427
    +                                source_workspace_id=workspace_id,
    
    426 428
                                     account=account,
    
    427 429
                                     jid=jid)
    
    428 430
     
    
    ... ... @@ -450,6 +452,7 @@ def get_workspace_params(current_workspace_id: str,
    450 452
                 continue
    
    451 453
             name = app.settings.get_workspace_setting(workspace_id, 'name')
    
    452 454
             params = ChatListEntryParam(workspace_id=workspace_id,
    
    455
    +                                    source_workspace_id=current_workspace_id,
    
    453 456
                                         account=account,
    
    454 457
                                         jid=jid)
    
    455 458
             yield name, params
    
    ... ... @@ -575,9 +578,13 @@ def get_format_menu() -> GajimMenu:
    575 578
     
    
    576 579
     
    
    577 580
     def get_workspace_menu(workspace_id: str) -> GajimMenu:
    
    581
    +    remove_action = 'win.dummy'
    
    582
    +    if len(app.settings.get_workspaces()) > 1:
    
    583
    +        remove_action = 'win.remove-workspace'
    
    584
    +
    
    578 585
         menuitems: MenuItemListT = [
    
    579 586
             (_('Edit…'), 'win.edit-workspace', f'"{workspace_id}"'),
    
    580
    -        (_('Remove'), 'win.remove-workspace', f'"{workspace_id}"'),
    
    587
    +        (_('Remove'), remove_action, f'"{workspace_id}"'),
    
    581 588
         ]
    
    582 589
     
    
    583 590
         return GajimMenu.from_list(menuitems)
    

  • gajim/gtk/structs.py
    ... ... @@ -55,6 +55,7 @@ class AddChatActionParams(VariantMixin):
    55 55
     @dataclass
    
    56 56
     class ChatListEntryParam(VariantMixin):
    
    57 57
         workspace_id: str
    
    58
    +    source_workspace_id: str
    
    58 59
         account: str
    
    59 60
         jid: JID
    
    60 61
     
    

  • gajim/gtk/workspace_dialog.py
    ... ... @@ -24,8 +24,6 @@
    24 24
     
    
    25 25
     from .avatar import make_workspace_avatar
    
    26 26
     from .avatar_selector import AvatarSelector
    
    27
    -from .dialogs import ConfirmationDialog
    
    28
    -from .dialogs import DialogButton
    
    29 27
     from .builder import get_builder
    
    30 28
     from .util import rgba_to_float
    
    31 29
     from .util import make_rgba
    
    ... ... @@ -93,25 +91,9 @@ def _on_key_press(self, _widget: Gtk.Widget, event: Gdk.EventKey) -> None:
    93 91
                 self.destroy()
    
    94 92
     
    
    95 93
         def _on_remove_workspace(self, _button: Gtk.Button) -> None:
    
    96
    -        def _on_remove():
    
    97
    -            assert self._workspace_id is not None
    
    98
    -            app.window.remove_workspace(self._workspace_id)
    
    99
    -            self.destroy()
    
    100 94
             assert self._workspace_id is not None
    
    101
    -        chat_list = app.window.get_chat_list(self._workspace_id)
    
    102
    -        open_chats = chat_list.get_open_chats()
    
    103
    -        if len(open_chats) > 0:
    
    104
    -            ConfirmationDialog(
    
    105
    -                _('Remove Workspace'),
    
    106
    -                _('Remove Workspace'),
    
    107
    -                _('This workspace contains chats. Remove anyway?'),
    
    108
    -                [DialogButton.make('Cancel',
    
    109
    -                                   text=_('_No')),
    
    110
    -                 DialogButton.make('Remove',
    
    111
    -                                   callback=_on_remove)]).show()
    
    112
    -            return
    
    113
    -        # No chats in chat list, it is save to remove workspace
    
    114
    -        _on_remove()
    
    95
    +        app.window.remove_workspace(self._workspace_id)
    
    96
    +        self.destroy()
    
    115 97
     
    
    116 98
         def _on_cancel(self, _button: Gtk.Button) -> None:
    
    117 99
             self.destroy()
    

  • gajim/gtk/workspace_side_bar.py
    ... ... @@ -158,6 +158,7 @@ def _process_chat_list_drop(self,
    158 158
                 workspace_id = ''
    
    159 159
     
    
    160 160
             params = ChatListEntryParam(workspace_id=workspace_id,
    
    161
    +                                    source_workspace_id='',
    
    161 162
                                         account=account,
    
    162 163
                                         jid=jid)
    
    163 164
             app.window.activate_action('move-chat-to-workspace',
    
    ... ... @@ -221,12 +222,18 @@ def store_workspace_order(self) -> None:
    221 222
             order.remove('add')
    
    222 223
             app.settings.set_app_setting('workspace_order', order)
    
    223 224
     
    
    224
    -    def remove_workspace(self, workspace_id: str) -> bool:
    
    225
    -        if len(self._workspaces) == 1:
    
    226
    -            return False
    
    225
    +    def remove_workspace(self, workspace_id: str) -> None:
    
    227 226
             row = self._workspaces.pop(workspace_id)
    
    228 227
             self.remove(row)
    
    229
    -        return True
    
    228
    +
    
    229
    +    def get_other_workspace(self,
    
    230
    +                            exclude_workspace_id: str
    
    231
    +                            ) -> Optional[str]:
    
    232
    +
    
    233
    +        for workspace in self._workspaces.values():
    
    234
    +            if workspace.workspace_id != exclude_workspace_id:
    
    235
    +                return workspace.workspace_id
    
    236
    +        return None
    
    230 237
     
    
    231 238
         def activate_workspace(self, workspace_id: str) -> None:
    
    232 239
             row = cast(CommonWorkspace, self.get_selected_row())