diff --git a/src/backend/matrix_client.py b/src/backend/matrix_client.py
index b1a3a551..a290828e 100644
--- a/src/backend/matrix_client.py
+++ b/src/backend/matrix_client.py
@@ -875,9 +875,10 @@ class MatrixClient(nio.AsyncClient):
return (successes, errors)
+
async def room_mass_redact(
self, room_id: str, reason: str, *event_ids: str,
- ):
+ ) -> List[nio.responses.RoomRedactResponse]:
"""Redact events from a room in parallel.
Returns a list of sucessful redacts.
@@ -888,6 +889,7 @@ class MatrixClient(nio.AsyncClient):
for evt_id in event_ids
])
+
async def generate_thumbnail(
self, data: UploadData, is_svg: bool = False,
) -> Tuple[bytes, MatrixImageInfo]:
@@ -1117,6 +1119,7 @@ class MatrixClient(nio.AsyncClient):
guests_allowed = room.guest_access == "can_join",
can_invite = levels.can_user_invite(self.user),
+ can_redact = levels.can_user_redact(self.user),
can_send_messages = can_send_msg(),
can_set_name = can_send_state("m.room.name"),
can_set_topic = can_send_state("m.room.topic"),
@@ -1186,7 +1189,10 @@ class MatrixClient(nio.AsyncClient):
async def register_nio_event(
- self, room: nio.MatrixRoom, ev: nio.Event, event_id: str = None,
+ self,
+ room: nio.MatrixRoom,
+ ev: nio.Event,
+ event_id: str = "",
**fields,
) -> None:
"""Register a `nio.Event` as a `Event` object in our model."""
diff --git a/src/backend/models/items.py b/src/backend/models/items.py
index 4157b60a..511bb04b 100644
--- a/src/backend/models/items.py
+++ b/src/backend/models/items.py
@@ -70,6 +70,7 @@ class Room(ModelItem):
guests_allowed: bool = True
can_invite: bool = False
+ can_redact: bool = False
can_send_messages: bool = False
can_set_name: bool = False
can_set_topic: bool = False
diff --git a/src/backend/nio_callbacks.py b/src/backend/nio_callbacks.py
index ebb2c4ab..24e9d34f 100644
--- a/src/backend/nio_callbacks.py
+++ b/src/backend/nio_callbacks.py
@@ -165,32 +165,46 @@ class NioCallbacks:
model = self.client.models[self.client.user_id, room.room_id, "events"]
event = None
- for event in model._sorted_data:
- if event.event_id == ev.redacts:
+ for evt in model._sorted_data:
+ if evt.event_id == ev.redacts:
+ event = evt
break
- if (
- event and
- issubclass(event.event_type, nio.events.room_events.RoomMessage)
- ):
- event.source.source["content"] = {}
- event.source.source["unsigned"] = {
- "redacted_by": ev.event_id,
- "redacted_because": ev.source,
- }
+ if not (event and event.event_type is not nio.RedactedEvent):
+ return
- await self.client.register_nio_event(
- room,
- nio.events.room_events.RedactedEvent.from_dict(
- event.source.source,
- ),
- event_id = event.id,
- )
+ event.source.source["content"] = {}
+ event.source.source["unsigned"] = {
+ "redacted_by": ev.event_id,
+ "redacted_because": ev.source,
+ }
+
+ await self.onRedactedEvent(
+ room,
+ nio.RedactedEvent.from_dict(event.source.source),
+ event_id = event.id,
+ )
- async def onRedactedEvent(self, room, ev) -> None:
+ async def onRedactedEvent(self, room, ev, event_id: str = "") -> None:
+ # There is no way to know which kind of event was redacted in an
+ # encrypted room.
+ kind = "Message" if ev.type == "m.room.encrypted" \
+ else ev.type.split(".")[-1].capitalize() \
+ .replace("_", " ")
+
+ co = "%s event removed%s.%s" % (
+ kind,
+ f" by {ev.redacter}" if ev.redacter != ev.sender else "",
+ f" Reason: {ev.reason}." if ev.reason else "",
+ )
+
await self.client.register_nio_event(
- room, ev, reason=ev.reason,
+ room,
+ ev,
+ event_id = event_id,
+ reason = ev.reason or "",
+ content = co,
)
diff --git a/src/gui/Pages/Chat/Timeline/EventDelegate.qml b/src/gui/Pages/Chat/Timeline/EventDelegate.qml
index 244fc04d..baf73247 100644
--- a/src/gui/Pages/Chat/Timeline/EventDelegate.qml
+++ b/src/gui/Pages/Chat/Timeline/EventDelegate.qml
@@ -220,16 +220,28 @@ HColumnLayout {
HMenuItemPopupSpawner {
icon.name: "remove-message"
text: qsTr("Remove")
- enabled: ! isRedacted
+ enabled: properties.eventIds.length
popup: "Popups/RedactEvents.qml"
popupParent: chat
properties: ({
userId: chat.userId,
roomId: chat.roomId,
- eventIds: eventList.selectedCount ?
- eventList.getSortedChecked().map(ev => ev.event_id) :
- [model.event_id]
+ eventIds:
+ (
+ eventList.selectedCount ?
+ eventList.getSortedChecked() :
+ [model]
+ ).filter(ev =>
+ (
+ ev.sender_id === chat.userId ||
+ chat.roomInfo.can_redact
+ ) && ! isRedacted
+ ).map(ev => ev.event_id),
+ "details.text":
+ (! chat.roomInfo.can_redact && eventList.selectedCount) ?
+ qsTr("Only your messages will be removed") :
+ ""
})
}
diff --git a/src/gui/Popups/RedactEvents.qml b/src/gui/Popups/RedactEvents.qml
index bcb54c47..4bd64b40 100644
--- a/src/gui/Popups/RedactEvents.qml
+++ b/src/gui/Popups/RedactEvents.qml
@@ -10,6 +10,8 @@ BoxPopup {
qsTr("Remove selected messages?") :
qsTr("Remove selected message?")
+ details.color: theme.colors.warningText
+
HLabeledTextField {
id: reasonField
label.text: qsTr("Reason (optional):")
diff --git a/src/gui/Utils.qml b/src/gui/Utils.qml
index e3b821fe..5e15efa8 100644
--- a/src/gui/Utils.qml
+++ b/src/gui/Utils.qml
@@ -165,11 +165,11 @@ QtObject {
function escapeHtml(text) {
// Replace special HTML characters by encoded alternatives
- return text.replace("&", "&")
- .replace("<", "<")
- .replace(">", ">")
- .replace('"', """)
- .replace("'", "'")
+ return text.replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'")
}
@@ -202,9 +202,9 @@ QtObject {
if (type === "RedactedEvent") {
return qsTr(
- "Removed message" +
- `${ev.reason ? ". Reason: " + ev.reason : ""}` +
- ""
+ `` +
+ escapeHtml(ev.content) +
+ ""
)
}