Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion commet/lib/client/matrix/matrix_room.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import 'package:commet/client/matrix/timeline_events/matrix_timeline_event_membe
import 'package:commet/client/matrix/timeline_events/matrix_timeline_event_message.dart';
import 'package:commet/client/matrix/timeline_events/matrix_timeline_event_pinned_messages.dart';
import 'package:commet/client/matrix/timeline_events/matrix_timeline_event_redaction.dart';
import 'package:commet/client/matrix/timeline_events/matrix_timeline_event_room_tombstone.dart';
import 'package:commet/client/matrix/timeline_events/matrix_timeline_event_sticker.dart';
import 'package:commet/client/matrix/timeline_events/matrix_timeline_event_unknown.dart';
import 'package:commet/client/member.dart';
Expand Down Expand Up @@ -94,6 +95,20 @@ class MatrixRoom extends Room {
@override
bool get isE2EE => _matrixRoom.encrypted;

@override
bool get isTombstoned =>
matrixRoom.getState(matrix.EventTypes.RoomTombstone) != null;

@override
String? get tombstoneReplacementRoomId => matrixRoom
.getState(matrix.EventTypes.RoomTombstone)
?.content["replacement_room"] as String?;

@override
String? get tombstoneBody =>
matrixRoom.getState(matrix.EventTypes.RoomTombstone)?.content["body"]
as String?;

@override
int get highlightedNotificationCount => _matrixRoom.highlightCount;

Expand Down Expand Up @@ -531,6 +546,8 @@ class MatrixRoom extends Room {
MatrixTimelineEventCall(event, client: c),
matrix.EventTypes.RoomPinnedEvents =>
MatrixTimelineEventPinnedMessages(event, client: c),
matrix.EventTypes.RoomTombstone =>
MatrixTimelineEventRoomTombstone(event, client: c),
"chat.commet.calendar_events" =>
MatrixTimelineEventEditCalendar(event, client: c),
_ => null
Expand Down Expand Up @@ -718,7 +735,8 @@ class MatrixRoom extends Room {
_displayName = _matrixRoom.getLocalizedDisplayname();
if (event.state.type == "m.room.name" ||
event.state.type == "m.room.avatar" ||
event.state.type == "m.room.topic") {
event.state.type == "m.room.topic" ||
event.state.type == matrix.EventTypes.RoomTombstone) {
_onUpdate.add(null);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'package:commet/client/matrix/timeline_events/matrix_timeline_event.dart';
import 'package:commet/client/timeline.dart';
import 'package:commet/client/timeline_events/timeline_event_generic.dart';
import 'package:commet/client/timeline_events/timeline_event_room_tombstone.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

class MatrixTimelineEventRoomTombstone extends MatrixTimelineEvent
implements TimelineEventRoomTombstone, TimelineEventGeneric {
MatrixTimelineEventRoomTombstone(super.event, {required super.client});

String messageRoomUpgraded(String sender) => Intl.message(
"$sender upgraded this room",
name: "messageRoomUpgraded",
args: [sender],
desc: "Shown when a room was replaced by another room",
);

String get fallbackMessage => Intl.message(
"This room has been upgraded or replaced",
name: "messageRoomReplaced",
desc: "Fallback tombstone text when no sender/body available",
);

@override
String? get replacementRoomId => event.content["replacement_room"] as String?;

@override
String getBody({Timeline? timeline}) {
final body = event.content["body"] as String?;
if (body != null && body.trim().isNotEmpty) return body;

final sender = timeline != null
? timeline.room.getMemberOrFallback(event.senderId).displayName
: event.senderId.split(":").first.replaceFirst("@", "");

if (sender != null && sender.isNotEmpty) {
return messageRoomUpgraded(sender);
}

return fallbackMessage;
}

@override
IconData? get icon => Icons.upgrade_rounded;

@override
bool get showSenderAvatar => false;
}
14 changes: 14 additions & 0 deletions commet/lib/client/matrix_background/matrix_background_room.dart
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,20 @@ class MatrixBackgroundRoom implements Room {
bool get isE2EE =>
_stateEvents.any((e) => e.type == matrix.EventTypes.Encryption);

@override
bool get isTombstoned => _stateEvents
.any((event) => event.type == matrix.EventTypes.RoomTombstone);

matrix.BasicEvent? get _tombstoneState => _stateEvents.firstWhereOrNull(
(event) => event.type == matrix.EventTypes.RoomTombstone);

@override
String? get tombstoneReplacementRoomId =>
_tombstoneState?.content["replacement_room"] as String?;

@override
String? get tombstoneBody => _tombstoneState?.content["body"] as String?;

@override
bool get isMembersListComplete => throw UnimplementedError();

Expand Down
9 changes: 9 additions & 0 deletions commet/lib/client/room.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ abstract class Room {
/// Returns true if the room is secured by end to end encryption
bool get isE2EE;

/// Returns true if the room has a tombstone state
bool get isTombstoned;

/// Returns the replacement room ID when tombstoned
String? get tombstoneReplacementRoomId;

/// Returns the tombstone body message when available
String? get tombstoneBody;

bool get isSpecialRoomType;

IconData get icon {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import 'package:commet/client/timeline_events/timeline_event.dart';

abstract class TimelineEventRoomTombstone extends TimelineEvent {
String? get replacementRoomId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:commet/client/timeline_events/timeline_event_emote.dart';
import 'package:commet/client/timeline_events/timeline_event_encrypted.dart';
import 'package:commet/client/timeline_events/timeline_event_generic.dart';
import 'package:commet/client/timeline_events/timeline_event_message.dart';
import 'package:commet/client/timeline_events/timeline_event_room_tombstone.dart';
import 'package:commet/client/timeline_events/timeline_event_sticker.dart';
import 'package:commet/config/layout_config.dart';
import 'package:commet/debug/log.dart';
Expand Down Expand Up @@ -134,6 +135,8 @@ class TimelineViewEntryState extends State<TimelineViewEntry>
event is TimelineEventSticker ||
event is TimelineEventEncrypted) {
return TimelineEventWidgetDisplayType.message;
} else if (event is TimelineEventRoomTombstone) {
return TimelineEventWidgetDisplayType.generic;
} else if (event is TimelineEventGeneric) {
return TimelineEventWidgetDisplayType.generic;
} else if (event.status == TimelineEventStatus.error) {
Expand Down
67 changes: 65 additions & 2 deletions commet/lib/ui/organisms/chat/chat_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,33 @@ class ChatView extends StatelessWidget {
name: "cantSentMessagePrompt",
desc: "Text that explains the user cannot send a message in this room");

String get tombstoneRoomReplacedMessage =>
Intl.message("This room has been replaced",
name: "tombstoneRoomReplacedMessage",
desc: "Text that explains a room was replaced by another room");

String get tombstoneEnterNewRoom => Intl.message("Enter new room",
name: "tombstoneEnterNewRoom",
desc: "Button label for navigating to the replacement room");

Future<void> openReplacementRoomAndLeave(String replacementRoomId) async {
final client = state.room.client;
Room? targetRoom = client.getRoom(replacementRoomId);

targetRoom ??= client.getRoomByAlias(replacementRoomId);

if (targetRoom == null) {
try {
targetRoom = await client.joinRoom(replacementRoomId);
} catch (_) {
return;
}
}

EventBus.openRoom.add((targetRoom.identifier, client.identifier));
await client.leaveRoom(state.room);
}

String? get relatedEventSenderName => state.interactingEvent == null
? null
: state.room
Expand All @@ -50,7 +77,7 @@ class ChatView extends StatelessWidget {
fit: StackFit.expand,
children: [timeline(), const ParticlePlayer()],
)),
input(),
input(context),
]);
}

Expand Down Expand Up @@ -89,7 +116,7 @@ class ChatView extends StatelessWidget {
NotificationManager.clearNotifications(room);
}

Widget input() {
Widget input(BuildContext context) {
String? interactingEventBody = state.interactingEvent?.plainTextBody;

if (state.interactingEvent case TimelineEventMessage m) {
Expand All @@ -98,6 +125,42 @@ class ChatView extends StatelessWidget {
}
}

if (state.room.isTombstoned) {
final replacementRoomId = state.room.tombstoneReplacementRoomId;
final body = state.room.tombstoneBody?.trim();
final displayMessage =
body != null && body.isNotEmpty ? body : tombstoneRoomReplacedMessage;

return ClipRRect(
child: Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(16, 10, 16, 10),
color: Theme.of(context).colorScheme.surface,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
displayMessage,
style: Theme.of(context).textTheme.bodySmall,
),
if (replacementRoomId != null && replacementRoomId.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8),
child: TextButton.icon(
onPressed: () async {
await openReplacementRoomAndLeave(replacementRoomId);
},
icon: const Icon(Icons.arrow_forward),
label: Text(tombstoneEnterNewRoom),
),
),
],
),
),
);
}

return ClipRRect(
child: MessageInput(
client: state.room.client,
Expand Down