Rework models hierarchy, room categories models
This commit is contained in:
@@ -110,22 +110,3 @@ class Backend(QObject):
|
||||
from PyQt5.QtCore import pyqtRemoveInputHook
|
||||
pyqtRemoveInputHook()
|
||||
pdb.set_trace()
|
||||
|
||||
|
||||
@pyqtSlot("QVariant", str, result=bool)
|
||||
def EventIsOurProfileChanged(self, event: RoomEvent, account_id) -> bool:
|
||||
# pylint: disable=unused-self
|
||||
info = event.dict.get("content")
|
||||
previous = event.dict.get("prev_content")
|
||||
|
||||
return (
|
||||
event.type == "RoomMemberEvent" and
|
||||
event.dict["sender"] == account_id and
|
||||
bool(info) and
|
||||
bool(previous) and
|
||||
info["membership"] == previous["membership"] and
|
||||
(
|
||||
info.get("displayname") != previous.get("displayname") or
|
||||
info.get("avatar_url") != previous.get("avatar_url")
|
||||
)
|
||||
)
|
||||
|
@@ -89,7 +89,6 @@ class ClientManager(QObject):
|
||||
def deleteAll(self) -> None:
|
||||
for user_id in self.clients.copy():
|
||||
self.delete(user_id)
|
||||
print("deleted", user_id, self.clients)
|
||||
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
|
@@ -3,29 +3,7 @@ from typing import Any, Dict, List, Optional
|
||||
from PyQt5.QtCore import QDateTime
|
||||
|
||||
from .list_item import ListItem
|
||||
|
||||
|
||||
class User(ListItem):
|
||||
_required_init_values = {"userId"}
|
||||
_constant = {"userId"}
|
||||
|
||||
userId: str = ""
|
||||
displayName: Optional[str] = None
|
||||
avatarUrl: Optional[str] = None
|
||||
statusMessage: Optional[str] = None
|
||||
|
||||
|
||||
class Room(ListItem):
|
||||
_required_init_values = {"roomId", "displayName"}
|
||||
_constant = {"roomId"}
|
||||
|
||||
roomId: str = ""
|
||||
displayName: str = ""
|
||||
category: str = "Rooms"
|
||||
topic: Optional[str] = None
|
||||
typingUsers: List[str] = []
|
||||
inviter: Optional[Dict[str, str]] = None
|
||||
leftEvent: Optional[Dict[str, str]] = None
|
||||
from .list_model import ListModel
|
||||
|
||||
|
||||
class RoomEvent(ListItem):
|
||||
@@ -36,3 +14,38 @@ class RoomEvent(ListItem):
|
||||
dict: Dict[str, Any] = {}
|
||||
dateTime: QDateTime = QDateTime.currentDateTime()
|
||||
isLocalEcho: bool = False
|
||||
|
||||
|
||||
class Room(ListItem):
|
||||
_required_init_values = {"roomId", "displayName"}
|
||||
_constant = {"roomId"}
|
||||
|
||||
roomId: str = ""
|
||||
displayName: str = ""
|
||||
topic: Optional[str] = None
|
||||
typingUsers: List[str] = []
|
||||
|
||||
inviter: Optional[Dict[str, str]] = None
|
||||
leftEvent: Optional[Dict[str, str]] = None
|
||||
|
||||
|
||||
class RoomCategory(ListItem):
|
||||
_required_init_values = {"name", "rooms"}
|
||||
_constant = {"rooms"}
|
||||
|
||||
name: str = ""
|
||||
|
||||
# Must be provided at init, else it will be the same object
|
||||
# for every RoomCategory
|
||||
rooms: ListModel = ListModel()
|
||||
|
||||
|
||||
class Account(ListItem):
|
||||
_required_init_values = {"userId", "roomCategories"}
|
||||
_constant = {"userId", "roomCategories"}
|
||||
|
||||
userId: str = ""
|
||||
roomCategories: ListModel = ListModel() # same as RoomCategory.rooms
|
||||
displayName: Optional[str] = None
|
||||
avatarUrl: Optional[str] = None
|
||||
statusMessage: Optional[str] = None
|
||||
|
@@ -15,15 +15,23 @@ Index = Union[int, str]
|
||||
NewItem = Union[ListItem, Mapping[str, Any], Sequence]
|
||||
|
||||
|
||||
class _GetFail:
|
||||
pass
|
||||
|
||||
|
||||
class _PopFail:
|
||||
pass
|
||||
|
||||
|
||||
class ListModel(QAbstractListModel):
|
||||
rolesSet = pyqtSignal()
|
||||
changed = pyqtSignal()
|
||||
countChanged = pyqtSignal(int)
|
||||
|
||||
def __init__(self,
|
||||
parent: QObject,
|
||||
initial_data: Optional[List[NewItem]] = None,
|
||||
container: Callable[..., MutableSequence] = list) -> None:
|
||||
container: Callable[..., MutableSequence] = list,
|
||||
parent: QObject = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._data: MutableSequence[ListItem] = container()
|
||||
|
||||
@@ -135,11 +143,19 @@ class ListModel(QAbstractListModel):
|
||||
|
||||
@pyqtSlot(int, result="QVariant")
|
||||
@pyqtSlot(str, result="QVariant")
|
||||
def get(self, index: Index) -> ListItem:
|
||||
if isinstance(index, str):
|
||||
index = self.indexWhere(self.mainKey, index)
|
||||
@pyqtSlot(int, "QVariant", result="QVariant")
|
||||
@pyqtSlot(str, "QVariant", result="QVariant")
|
||||
def get(self, index: Index, default: Any = _GetFail()) -> ListItem:
|
||||
try:
|
||||
i_index: int = self.indexWhere(self.mainKey, index) \
|
||||
if isinstance(index, str) else index
|
||||
|
||||
return self._data[index] # type: ignore
|
||||
return self._data[i_index]
|
||||
|
||||
except (ValueError, IndexError):
|
||||
if isinstance(default, _GetFail):
|
||||
raise
|
||||
return default
|
||||
|
||||
|
||||
@pyqtSlot(int, "QVariantMap")
|
||||
@@ -170,20 +186,20 @@ class ListModel(QAbstractListModel):
|
||||
self.append(val)
|
||||
|
||||
|
||||
@pyqtSlot(int, "QVariantMap")
|
||||
@pyqtSlot(int, "QVariantMap", "QStringList")
|
||||
@pyqtSlot(str, "QVariantMap")
|
||||
@pyqtSlot(str, "QVariantMap", "QStringList")
|
||||
@pyqtSlot(int, "QVariantMap", result=int)
|
||||
@pyqtSlot(int, "QVariantMap", "QStringList", result=int)
|
||||
@pyqtSlot(str, "QVariantMap", result=int)
|
||||
@pyqtSlot(str, "QVariantMap", "QStringList", result=int)
|
||||
def update(self,
|
||||
index: Index,
|
||||
value: NewItem,
|
||||
ignore_roles: Sequence[str] = ()) -> None:
|
||||
ignore_roles: Sequence[str] = ()) -> int:
|
||||
value = self._convert_new_value(value)
|
||||
|
||||
if isinstance(index, str):
|
||||
index = self.indexWhere(self.mainKey or value.mainKey, index)
|
||||
i_index: int = self.indexWhere(self.mainKey, index) \
|
||||
if isinstance(index, str) else index
|
||||
|
||||
to_update = self._data[index] # type: ignore
|
||||
to_update = self[i_index]
|
||||
|
||||
for role in self.roles:
|
||||
if role not in ignore_roles:
|
||||
@@ -192,35 +208,40 @@ class ListModel(QAbstractListModel):
|
||||
except AttributeError: # constant/not settable
|
||||
pass
|
||||
|
||||
qidx = QAbstractListModel.index(self, index, 0)
|
||||
qidx = QAbstractListModel.index(self, i_index, 0)
|
||||
self.dataChanged.emit(qidx, qidx, self.roleNames())
|
||||
self.changed.emit()
|
||||
return i_index
|
||||
|
||||
|
||||
@pyqtSlot(str, "QVariantMap")
|
||||
@pyqtSlot(str, "QVariantMap", int)
|
||||
@pyqtSlot(str, "QVariantMap", int, "QStringList")
|
||||
@pyqtSlot(str, "QVariantMap", int, int)
|
||||
@pyqtSlot(str, "QVariantMap", int, int, "QStringList")
|
||||
def upsert(self,
|
||||
where_main_key_is_value: Any,
|
||||
update_with: NewItem,
|
||||
index_if_insert: Optional[int] = None,
|
||||
ignore_roles: Sequence[str] = ()) -> None:
|
||||
where_main_key_is: Any,
|
||||
update_with: NewItem,
|
||||
new_index_if_insert: Optional[int] = None,
|
||||
new_index_if_update: Optional[int] = None,
|
||||
ignore_roles: Sequence[str] = ()) -> None:
|
||||
try:
|
||||
self.update(where_main_key_is_value, update_with, ignore_roles)
|
||||
index = self.update(where_main_key_is, update_with, ignore_roles)
|
||||
except (IndexError, ValueError):
|
||||
self.insert(index_if_insert or len(self), update_with)
|
||||
|
||||
self.insert(new_index_if_insert or len(self), update_with)
|
||||
else:
|
||||
if new_index_if_update:
|
||||
self.move(index, new_index_if_update)
|
||||
|
||||
|
||||
@pyqtSlot(int, list)
|
||||
@pyqtSlot(str, list)
|
||||
def set(self, index: Index, value: NewItem) -> None:
|
||||
if isinstance(index, str):
|
||||
index = self.indexWhere(self.mainKey, index)
|
||||
i_index: int = self.indexWhere(self.mainKey, index) \
|
||||
if isinstance(index, str) else index
|
||||
|
||||
qidx = QAbstractListModel.index(self, index, 0)
|
||||
value = self._convert_new_value(value)
|
||||
self._data[index] = value # type: ignore
|
||||
qidx = QAbstractListModel.index(self, i_index, 0)
|
||||
value = self._convert_new_value(value)
|
||||
self._data[i_index] = value
|
||||
self.dataChanged.emit(qidx, qidx, self.roleNames())
|
||||
self.changed.emit()
|
||||
|
||||
@@ -228,11 +249,11 @@ class ListModel(QAbstractListModel):
|
||||
@pyqtSlot(int, str, "QVariant")
|
||||
@pyqtSlot(str, str, "QVariant")
|
||||
def setProperty(self, index: Index, prop: str, value: Any) -> None:
|
||||
if isinstance(index, str):
|
||||
index = self.indexWhere(self.mainKey, index)
|
||||
i_index: int = self.indexWhere(self.mainKey, index) \
|
||||
if isinstance(index, str) else index
|
||||
|
||||
setattr(self._data[index], prop, value) # type: ignore
|
||||
qidx = QAbstractListModel.index(self, index, 0)
|
||||
setattr(self[i_index], prop, value)
|
||||
qidx = QAbstractListModel.index(self, i_index, 0)
|
||||
self.dataChanged.emit(qidx, qidx, self.roleNames())
|
||||
self.changed.emit()
|
||||
|
||||
@@ -269,17 +290,39 @@ class ListModel(QAbstractListModel):
|
||||
@pyqtSlot(int)
|
||||
@pyqtSlot(str)
|
||||
def remove(self, index: Index) -> None:
|
||||
if isinstance(index, str):
|
||||
index = self.indexWhere(self.mainKey, index)
|
||||
i_index: int = self.indexWhere(self.mainKey, index) \
|
||||
if isinstance(index, str) else index
|
||||
|
||||
self.beginRemoveRows(QModelIndex(), index, index)
|
||||
del self._data[index] # type: ignore
|
||||
self.beginRemoveRows(QModelIndex(), i_index, i_index)
|
||||
del self._data[i_index]
|
||||
self.endRemoveRows()
|
||||
|
||||
self.countChanged.emit(len(self))
|
||||
self.changed.emit()
|
||||
|
||||
|
||||
@pyqtSlot(int, result="QVariant")
|
||||
@pyqtSlot(str, result="QVariant")
|
||||
def pop(self, index: Index, default: Any = _PopFail()) -> ListItem:
|
||||
try:
|
||||
i_index: int = self.indexWhere(self.mainKey, index) \
|
||||
if isinstance(index, str) else index
|
||||
item = self[i_index]
|
||||
|
||||
except (ValueError, IndexError):
|
||||
if isinstance(default, _PopFail):
|
||||
raise
|
||||
return default
|
||||
|
||||
self.beginRemoveRows(QModelIndex(), i_index, i_index)
|
||||
del self._data[i_index]
|
||||
self.endRemoveRows()
|
||||
|
||||
self.countChanged.emit(len(self))
|
||||
self.changed.emit()
|
||||
return item
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def clear(self) -> None:
|
||||
# Reimplemented for performance reasons (begin/endRemoveRows)
|
||||
|
@@ -7,15 +7,15 @@ from .list_model import ListModel
|
||||
|
||||
class ListModelMap(QObject):
|
||||
def __init__(self,
|
||||
parent: QObject,
|
||||
models_container: Callable[..., MutableSequence] = list
|
||||
) -> None:
|
||||
models_container: Callable[..., MutableSequence] = list,
|
||||
parent: QObject = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
# Set the parent to prevent item garbage-collection on the C++ side
|
||||
self.dict: DefaultDict[Any, ListModel] = \
|
||||
DefaultDict(lambda: ListModel(parent = self,
|
||||
container = models_container))
|
||||
DefaultDict(
|
||||
lambda: ListModel(container=models_container, parent=self)
|
||||
)
|
||||
|
||||
|
||||
@pyqtSlot(str, result="QVariant")
|
||||
|
@@ -12,10 +12,8 @@ from .list_model_map import ListModelMap
|
||||
class QMLModels(QObject):
|
||||
def __init__(self, parent: QObject) -> None:
|
||||
super().__init__(parent)
|
||||
self._accounts: ListModel = ListModel(parent)
|
||||
self._rooms: ListModelMap = ListModelMap(parent)
|
||||
self._room_events: ListModelMap = ListModelMap(parent,
|
||||
models_container=Deque)
|
||||
self._accounts: ListModel = ListModel(parent=parent)
|
||||
self._room_events: ListModelMap = ListModelMap(Deque, parent)
|
||||
|
||||
|
||||
@pyqtProperty(ListModel, constant=True)
|
||||
@@ -23,11 +21,6 @@ class QMLModels(QObject):
|
||||
return self._accounts
|
||||
|
||||
|
||||
@pyqtProperty("QVariant", constant=True)
|
||||
def rooms(self):
|
||||
return self._rooms
|
||||
|
||||
|
||||
@pyqtProperty("QVariant", constant=True)
|
||||
def roomEvents(self):
|
||||
return self._room_events
|
||||
|
@@ -2,7 +2,7 @@
|
||||
# This file is part of harmonyqml, licensed under GPLv3.
|
||||
|
||||
from threading import Lock
|
||||
from typing import Any, Deque, Dict, List, Optional, Sequence
|
||||
from typing import Any, Deque, Dict, List, Optional
|
||||
|
||||
from PyQt5.QtCore import QDateTime, QObject, pyqtBoundSignal
|
||||
|
||||
@@ -11,7 +11,8 @@ from nio.rooms import MatrixRoom
|
||||
|
||||
from .backend import Backend
|
||||
from .client import Client
|
||||
from .model.items import Room, RoomEvent, User
|
||||
from .model.items import Account, Room, RoomCategory, RoomEvent
|
||||
from .model.list_model import ListModel
|
||||
|
||||
Inviter = Optional[Dict[str, str]]
|
||||
LeftEvent = Optional[Dict[str, str]]
|
||||
@@ -34,8 +35,14 @@ class SignalManager(QObject):
|
||||
|
||||
def onClientAdded(self, client: Client) -> None:
|
||||
self.connectClient(client)
|
||||
self.backend.models.accounts.append(User(
|
||||
userId = client.userId,
|
||||
|
||||
self.backend.models.accounts.append(Account(
|
||||
userId = client.userId,
|
||||
roomCategories = ListModel([
|
||||
RoomCategory("Invites", ListModel()),
|
||||
RoomCategory("Rooms", ListModel()),
|
||||
RoomCategory("Left", ListModel()),
|
||||
]),
|
||||
displayName = self.backend.getUserDisplayName(client.userId),
|
||||
))
|
||||
|
||||
@@ -56,104 +63,65 @@ class SignalManager(QObject):
|
||||
attr.connect(onSignal)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _get_room_displayname(nio_room: MatrixRoom) -> Optional[str]:
|
||||
name = nio_room.name or nio_room.canonical_alias
|
||||
if name:
|
||||
return name
|
||||
|
||||
name = nio_room.group_name()
|
||||
return None if name == "Empty room?" else name
|
||||
|
||||
|
||||
def onRoomInvited(self,
|
||||
client: Client,
|
||||
room_id: str,
|
||||
inviter: Inviter = None) -> None:
|
||||
|
||||
self._add_room(client, room_id, client.nio.invited_rooms[room_id],
|
||||
"Invites", inviter=inviter)
|
||||
nio_room = client.nio.invited_rooms[room_id]
|
||||
categories = self.backend.models.accounts[client.userId].roomCategories
|
||||
|
||||
categories["Rooms"].rooms.pop(room_id, None)
|
||||
categories["Left"].rooms.pop(room_id, None)
|
||||
|
||||
categories["Invites"].rooms.upsert(room_id, Room(
|
||||
roomId = room_id,
|
||||
displayName = self._get_room_displayname(nio_room),
|
||||
topic = nio_room.topic,
|
||||
inviter = inviter,
|
||||
), 0, 0)
|
||||
|
||||
|
||||
def onRoomJoined(self, client: Client, room_id: str) -> None:
|
||||
self._add_room(client, room_id, client.nio.rooms[room_id], "Rooms")
|
||||
nio_room = client.nio.rooms[room_id]
|
||||
categories = self.backend.models.accounts[client.userId].roomCategories
|
||||
|
||||
categories["Invites"].rooms.pop(room_id, None)
|
||||
categories["Left"].rooms.pop(room_id, None)
|
||||
|
||||
categories["Rooms"].rooms.upsert(room_id, Room(
|
||||
roomId = room_id,
|
||||
displayName = self._get_room_displayname(nio_room),
|
||||
topic = nio_room.topic,
|
||||
), 0, 0)
|
||||
|
||||
|
||||
def onRoomLeft(self,
|
||||
client: Client,
|
||||
room_id: str,
|
||||
left_event: LeftEvent = None) -> None:
|
||||
categories = self.backend.models.accounts[client.userId].roomCategories
|
||||
|
||||
self._add_room(client, room_id, client.nio.rooms.get(room_id), "Left",
|
||||
left_event=left_event)
|
||||
previous = categories["Rooms"].rooms.pop(room_id, None)
|
||||
previous = previous or categories["Invites"].rooms.pop(room_id, None)
|
||||
previous = previous or categories["Left"].rooms.get(room_id, None)
|
||||
|
||||
|
||||
def _move_room(self, account_id: str, room_id: str) -> None:
|
||||
def get_newest_event_date_time(room_id: str) -> QDateTime:
|
||||
for ev in self.backend.models.roomEvents[room_id]:
|
||||
if not self.backend.EventIsOurProfileChanged(ev, account_id):
|
||||
return ev.dateTime
|
||||
|
||||
return QDateTime.fromMSecsSinceEpoch(0)
|
||||
|
||||
rooms_model = self.backend.models.rooms[account_id]
|
||||
room_index = rooms_model.indexWhere("roomId", room_id)
|
||||
category = rooms_model[room_index].category
|
||||
timestamp = get_newest_event_date_time(room_id)
|
||||
|
||||
def get_index(put_before_categories: Sequence[str],
|
||||
put_after_categories: Sequence[str]) -> int:
|
||||
for i, room in enumerate(rooms_model):
|
||||
if room.category not in put_after_categories and \
|
||||
(room.category in put_before_categories or
|
||||
timestamp >= get_newest_event_date_time(room.roomId)):
|
||||
return i
|
||||
|
||||
return len(rooms_model) - 1
|
||||
|
||||
to = 0
|
||||
|
||||
if category == "Invites":
|
||||
to = get_index(["Rooms", "Left"], [])
|
||||
|
||||
if category == "Rooms":
|
||||
to = get_index(["Left"], ["Invites"])
|
||||
|
||||
elif category == "Left":
|
||||
to = get_index([], ["Invites", "Rooms", "Left"])
|
||||
|
||||
rooms_model.move(room_index, to)
|
||||
|
||||
|
||||
def _add_room(self,
|
||||
client: Client,
|
||||
room_id: str,
|
||||
room: MatrixRoom,
|
||||
category: str = "Rooms",
|
||||
inviter: Inviter = None,
|
||||
left_event: LeftEvent = None) -> None:
|
||||
|
||||
if (inviter and left_event):
|
||||
raise ValueError()
|
||||
|
||||
model = self.backend.models.rooms[client.userId]
|
||||
no_update = []
|
||||
|
||||
def get_displayname() -> Optional[str]:
|
||||
if not room:
|
||||
no_update.append("displayName")
|
||||
return room_id
|
||||
|
||||
name = room.name or room.canonical_alias
|
||||
if name:
|
||||
return name
|
||||
|
||||
name = room.group_name()
|
||||
return None if name == "Empty room?" else name
|
||||
|
||||
item = Room(
|
||||
categories["Left"].rooms.upsert(0, Room(
|
||||
roomId = room_id,
|
||||
displayName = get_displayname(),
|
||||
category = category,
|
||||
topic = room.topic if room else None,
|
||||
inviter = inviter,
|
||||
displayName = previous.displayName if previous else None,
|
||||
topic = previous.topic if previous else None,
|
||||
leftEvent = left_event,
|
||||
)
|
||||
|
||||
model.upsert(room_id, item, ignore_roles=no_update)
|
||||
with self._lock:
|
||||
self._move_room(client.userId, room_id)
|
||||
|
||||
), 0, 0)
|
||||
|
||||
def onRoomSyncPrevBatchTokenReceived(self,
|
||||
_: Client,
|
||||
@@ -176,7 +144,7 @@ class SignalManager(QObject):
|
||||
|
||||
|
||||
def onRoomEventReceived(self,
|
||||
client: Client,
|
||||
_: Client,
|
||||
room_id: str,
|
||||
etype: str,
|
||||
edict: Dict[str, Any]) -> None:
|
||||
@@ -192,6 +160,16 @@ class SignalManager(QObject):
|
||||
.fromMSecsSinceEpoch(edict["server_timestamp"])
|
||||
new_event = RoomEvent(type=etype, dateTime=date_time, dict=edict)
|
||||
|
||||
event_is_our_profile_changed = (
|
||||
etype == "RoomMemberEvent" and
|
||||
edict.get("sender") in self.backend.clientManager.clients and
|
||||
((edict.get("content") or {}).get("membership") ==
|
||||
(edict.get("prev_content") or {}).get("membership"))
|
||||
)
|
||||
|
||||
if event_is_our_profile_changed:
|
||||
return
|
||||
|
||||
if etype == "RoomCreateEvent":
|
||||
self.backend.fully_loaded_rooms.add(room_id)
|
||||
|
||||
@@ -233,14 +211,20 @@ class SignalManager(QObject):
|
||||
|
||||
with self._lock:
|
||||
process()
|
||||
self._move_room(client.userId, room_id)
|
||||
# self._move_room(client.userId, room_id)
|
||||
|
||||
|
||||
def onRoomTypingUsersUpdated(self,
|
||||
client: Client,
|
||||
room_id: str,
|
||||
users: List[str]) -> None:
|
||||
self.backend.models.rooms[client.userId][room_id].typingUsers = users
|
||||
categories = self.backend.models.accounts[client.userId].roomCategories
|
||||
for categ in categories:
|
||||
try:
|
||||
categ.rooms[room_id].typingUsers = users
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def onMessageAboutToBeSent(self,
|
||||
@@ -264,10 +248,13 @@ class SignalManager(QObject):
|
||||
model.insert(0, event)
|
||||
self._events_in_transfer += 1
|
||||
|
||||
self._move_room(client.userId, room_id)
|
||||
# self._move_room(client.userId, room_id)
|
||||
|
||||
|
||||
def onRoomAboutToBeForgotten(self, client: Client, room_id: str) -> None:
|
||||
with self._lock:
|
||||
del self.backend.models.rooms[client.userId][room_id]
|
||||
self.backend.models.roomEvents[room_id].clear()
|
||||
categories = self.backend.models.accounts[client.userId].roomCategories
|
||||
|
||||
for categ in categories:
|
||||
categ.rooms.pop(room_id, None)
|
||||
|
||||
self.backend.models.roomEvents[room_id].clear()
|
||||
|
Reference in New Issue
Block a user