Skip to content
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 messageRoomReplaced => 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 messageRoomReplaced;
}

@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
80 changes: 78 additions & 2 deletions commet/lib/ui/organisms/chat/chat_view.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:commet/client/components/account_switch_prefix/account_switch_prefix.dart';
import 'package:commet/client/components/push_notification/notification_manager.dart';
import 'package:commet/client/matrix/matrix_client.dart';
import 'package:commet/client/room.dart';
import 'package:commet/client/timeline_events/timeline_event.dart';
import 'package:commet/client/timeline_events/timeline_event_message.dart';
Expand All @@ -13,6 +14,7 @@ import 'package:commet/utils/autofill_utils.dart';
import 'package:commet/utils/event_bus.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:commet/utils/error_utils.dart';

class ChatView extends StatelessWidget {
const ChatView(this.state, {super.key});
Expand All @@ -32,6 +34,40 @@ 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(
BuildContext context, String replacementRoomId) async {
final client = state.room.client;
final joinAddress = replacementRoomId;
var lookupAddress = replacementRoomId;

if (client is MatrixClient) {
final info = client.parseAddressToIdAndVia(replacementRoomId);
if (info != null) {
lookupAddress = info.$1;
}
}

Room? targetRoom = client.getRoom(lookupAddress);

targetRoom ??= client.getRoomByAlias(lookupAddress);
ErrorUtils.tryRun(context, () async {
if (targetRoom == null) {
targetRoom = await client.joinRoom(joinAddress);
}
EventBus.openRoom.add((targetRoom!.identifier, client.identifier));
await client.leaveRoom(state.room);
});
}

String? get relatedEventSenderName => state.interactingEvent == null
? null
: state.room
Expand All @@ -44,13 +80,16 @@ class ChatView extends StatelessWidget {

@override
Widget build(BuildContext context) {
final inputWidget =
state.room.isTombstoned ? buildTombstoneInput(context) : input(context);

return Column(children: [
Expanded(
child: Stack(
fit: StackFit.expand,
children: [timeline(), const ParticlePlayer()],
)),
input(),
inputWidget,
]);
}

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

Widget input() {
Widget buildTombstoneInput(BuildContext context) {
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(
context, replacementRoomId);
},
icon: const Icon(Icons.arrow_forward),
label: Text(tombstoneEnterNewRoom),
),
),
],
),
),
);
}

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

if (state.interactingEvent case TimelineEventMessage m) {
Expand Down