parent
522df86976
commit
029efe7c0a
6 changed files with 225 additions and 70 deletions
22
lib/controllers/tombstone.dart
Normal file
22
lib/controllers/tombstone.dart
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/rooms.dart";
|
||||||
|
import "package:nexus/models/content/content.dart";
|
||||||
|
import "package:nexus/models/event.dart";
|
||||||
|
|
||||||
|
class TombstoneController(final String roomId) extends Notifier<Event?> {
|
||||||
|
@override
|
||||||
|
Event? build() => ref.watch(
|
||||||
|
RoomsController.provider.select((rooms) {
|
||||||
|
final room = rooms[roomId];
|
||||||
|
return switch (room?.state[EventType.tombstone.type]?[""]) {
|
||||||
|
final rowId? => room?.events[rowId],
|
||||||
|
null => null,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
static final provider = NotifierProvider.family
|
||||||
|
.autoDispose<TombstoneController, Event?, String>(
|
||||||
|
TombstoneController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ import "package:nexus/models/content/redaction.dart";
|
||||||
import "package:nexus/models/content/server_acl.dart";
|
import "package:nexus/models/content/server_acl.dart";
|
||||||
import "package:nexus/models/content/space_child.dart";
|
import "package:nexus/models/content/space_child.dart";
|
||||||
import "package:nexus/models/content/topic.dart";
|
import "package:nexus/models/content/topic.dart";
|
||||||
|
import "package:nexus/models/content/tombstone.dart";
|
||||||
import "package:nexus/models/content/sticker.dart";
|
import "package:nexus/models/content/sticker.dart";
|
||||||
import "package:nexus/models/content/history_visibility.dart";
|
import "package:nexus/models/content/history_visibility.dart";
|
||||||
import "package:nexus/models/content/image_pack.dart";
|
import "package:nexus/models/content/image_pack.dart";
|
||||||
|
|
@ -64,7 +65,7 @@ enum EventType(
|
||||||
avatar("m.room.avatar", AvatarContent.fromJson),
|
avatar("m.room.avatar", AvatarContent.fromJson),
|
||||||
topic("m.room.topic", TopicContent.fromJson),
|
topic("m.room.topic", TopicContent.fromJson),
|
||||||
name("m.room.name", NameContent.fromJson),
|
name("m.room.name", NameContent.fromJson),
|
||||||
tombstone("m.room.tombstone", Content.fromJson),
|
tombstone("m.room.tombstone", TombstoneContent.fromJson),
|
||||||
reaction("m.reaction", ReactionContent.fromJson),
|
reaction("m.reaction", ReactionContent.fromJson),
|
||||||
pinnedEvents("m.room.pinned_events", PinnedEventsContent.fromJson),
|
pinnedEvents("m.room.pinned_events", PinnedEventsContent.fromJson),
|
||||||
spaceChild("m.space.child", SpaceChildContent.fromJson),
|
spaceChild("m.space.child", SpaceChildContent.fromJson),
|
||||||
|
|
|
||||||
17
lib/models/content/tombstone.dart
Normal file
17
lib/models/content/tombstone.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
import "package:nexus/models/content/content.dart";
|
||||||
|
part "tombstone.freezed.dart";
|
||||||
|
part "tombstone.g.dart";
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
@JsonSerializable()
|
||||||
|
class const TombstoneContent({
|
||||||
|
final String body = "",
|
||||||
|
required final String replacementRoom,
|
||||||
|
}) extends Content with _$TombstoneContent {
|
||||||
|
@override
|
||||||
|
Map<String, Object?> toJson() => _$TombstoneContentToJson(this);
|
||||||
|
|
||||||
|
factory TombstoneContent.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$TombstoneContentFromJson(json);
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,8 @@ import "package:nexus/helpers/extensions/send_failed.dart";
|
||||||
import "package:nexus/helpers/extensions/success_scheme.dart";
|
import "package:nexus/helpers/extensions/success_scheme.dart";
|
||||||
import "package:nexus/helpers/hooks/chat_scroll.dart";
|
import "package:nexus/helpers/hooks/chat_scroll.dart";
|
||||||
import "package:nexus/helpers/hooks/tap_recognizer.dart";
|
import "package:nexus/helpers/hooks/tap_recognizer.dart";
|
||||||
|
import "package:nexus/helpers/extensions/open_link_info.dart";
|
||||||
|
import "package:nexus/main.dart";
|
||||||
import "package:nexus/models/content/avatar.dart";
|
import "package:nexus/models/content/avatar.dart";
|
||||||
import "package:nexus/models/content/canonical_alias.dart";
|
import "package:nexus/models/content/canonical_alias.dart";
|
||||||
import "package:nexus/models/content/content.dart";
|
import "package:nexus/models/content/content.dart";
|
||||||
|
|
@ -27,6 +29,7 @@ import "package:nexus/models/content/pinned_events.dart";
|
||||||
import "package:nexus/models/content/poll_end.dart";
|
import "package:nexus/models/content/poll_end.dart";
|
||||||
import "package:nexus/models/content/power_levels.dart";
|
import "package:nexus/models/content/power_levels.dart";
|
||||||
import "package:nexus/models/content/server_acl.dart";
|
import "package:nexus/models/content/server_acl.dart";
|
||||||
|
import "package:nexus/models/content/tombstone.dart";
|
||||||
import "package:nexus/models/content/topic.dart";
|
import "package:nexus/models/content/topic.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/widgets/error_dialog.dart";
|
import "package:nexus/widgets/error_dialog.dart";
|
||||||
|
|
@ -140,6 +143,27 @@ class const EventRenderer(
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// For rooms an upgrade links, i.e. the old and new rooms
|
||||||
|
Widget roomLink(String label, String linkedRoomId) => InkWell(
|
||||||
|
onTap: () => ref
|
||||||
|
.openLinkInfo(context, (
|
||||||
|
identifier: linkedRoomId,
|
||||||
|
eventId: null,
|
||||||
|
// The sender is in both rooms, so their server can be joined
|
||||||
|
// through
|
||||||
|
via: [event.sender.substring(event.sender.indexOf(":") + 1)].lock,
|
||||||
|
))
|
||||||
|
.onError(showError),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
|
color: colorScheme.primary,
|
||||||
|
decoration: .underline,
|
||||||
|
decorationColor: colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final child = event.isShownAsMessage
|
final child = event.isShownAsMessage
|
||||||
? MessageRenderer(
|
? MessageRenderer(
|
||||||
event,
|
event,
|
||||||
|
|
@ -185,7 +209,25 @@ class const EventRenderer(
|
||||||
Icons.interests,
|
Icons.interests,
|
||||||
"changed the room avatar",
|
"changed the room avatar",
|
||||||
),
|
),
|
||||||
CreateContent() => generic(Icons.add, "created the room"),
|
CreateContent(:final predecessor) => generic(
|
||||||
|
Icons.add,
|
||||||
|
predecessor == null
|
||||||
|
? "created the room"
|
||||||
|
: "created the room, replacing an older one",
|
||||||
|
details: [
|
||||||
|
if (predecessor case PreviousRoom(:final roomId))
|
||||||
|
roomLink("Go to old room", roomId),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TombstoneContent(:final body, :final replacementRoom) => generic(
|
||||||
|
Icons.upgrade,
|
||||||
|
"replaced the room",
|
||||||
|
details: [
|
||||||
|
if (body.isNotEmpty)
|
||||||
|
Text(body, style: theme.textTheme.labelMedium),
|
||||||
|
roomLink("Go to new room", replacementRoom),
|
||||||
|
],
|
||||||
|
),
|
||||||
PollEndContent() => switch (event.relatesTo) {
|
PollEndContent() => switch (event.relatesTo) {
|
||||||
final pollId? => switch (ref
|
final pollId? => switch (ref
|
||||||
.watch(
|
.watch(
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import "package:nexus/controllers/client_state.dart";
|
||||||
import "package:nexus/controllers/member_list_opened.dart";
|
import "package:nexus/controllers/member_list_opened.dart";
|
||||||
import "package:nexus/controllers/power_level.dart";
|
import "package:nexus/controllers/power_level.dart";
|
||||||
import "package:nexus/controllers/settings.dart";
|
import "package:nexus/controllers/settings.dart";
|
||||||
|
import "package:nexus/controllers/tombstone.dart";
|
||||||
import "package:nexus/controllers/rooms.dart";
|
import "package:nexus/controllers/rooms.dart";
|
||||||
import "package:nexus/controllers/room_chat.dart";
|
import "package:nexus/controllers/room_chat.dart";
|
||||||
import "package:nexus/helpers/hooks/chat_scroll.dart";
|
import "package:nexus/helpers/hooks/chat_scroll.dart";
|
||||||
|
|
@ -23,6 +24,7 @@ import "package:nexus/widgets/member_list.dart";
|
||||||
import "package:nexus/widgets/room_appbar.dart";
|
import "package:nexus/widgets/room_appbar.dart";
|
||||||
import "package:nexus/widgets/search_drawer.dart";
|
import "package:nexus/widgets/search_drawer.dart";
|
||||||
import "package:nexus/widgets/sync_status_banner.dart";
|
import "package:nexus/widgets/sync_status_banner.dart";
|
||||||
|
import "package:nexus/widgets/tombstone_banner.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
import "package:nexus/widgets/room_chat/chat_timeline.dart";
|
import "package:nexus/widgets/room_chat/chat_timeline.dart";
|
||||||
import "package:nexus/helpers/extensions/build_event_options.dart";
|
import "package:nexus/helpers/extensions/build_event_options.dart";
|
||||||
|
|
@ -200,9 +202,14 @@ final class const RoomChat({
|
||||||
(settings) => settings.value?.swipeToReply ?? false,
|
(settings) => settings.value?.swipeToReply ?? false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final canReply = ref.watch(
|
final tombstone = ref.watch(TombstoneController.provider(roomId));
|
||||||
PowerLevelController.provider(.new(eventType: .message, roomId: roomId)),
|
final canReply =
|
||||||
);
|
tombstone == null &&
|
||||||
|
ref.watch(
|
||||||
|
PowerLevelController.provider(
|
||||||
|
.new(eventType: .message, roomId: roomId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
endDrawerEnableOpenDragGesture: false,
|
endDrawerEnableOpenDragGesture: false,
|
||||||
|
|
@ -308,71 +315,81 @@ final class const RoomChat({
|
||||||
child: MeasureSize(
|
child: MeasureSize(
|
||||||
onChange: (size) =>
|
onChange: (size) =>
|
||||||
composerSize.value = size.height,
|
composerSize.value = size.height,
|
||||||
child: Composer(
|
child: switch (tombstone) {
|
||||||
roomId,
|
final tombstone? => TombstoneBanner(
|
||||||
node: composerNode,
|
tombstone,
|
||||||
onSend:
|
),
|
||||||
(
|
null => Composer(
|
||||||
text, {
|
roomId,
|
||||||
required shouldMention,
|
node: composerNode,
|
||||||
required tags,
|
onSend:
|
||||||
}) => notifier
|
(
|
||||||
.send(
|
text, {
|
||||||
text,
|
required shouldMention,
|
||||||
tags: tags,
|
required tags,
|
||||||
relationType: relationType.value,
|
}) => notifier
|
||||||
shouldMention: shouldMention,
|
.send(
|
||||||
relatedEventRowId:
|
text,
|
||||||
relatedEvent.value,
|
tags: tags,
|
||||||
)
|
relationType:
|
||||||
.onError(showError),
|
relationType.value,
|
||||||
onSendSticker:
|
shouldMention: shouldMention,
|
||||||
(
|
relatedEventRowId:
|
||||||
shortcode,
|
relatedEvent.value,
|
||||||
image, {
|
)
|
||||||
required shouldMention,
|
.onError(showError),
|
||||||
}) => notifier
|
onSendSticker:
|
||||||
.sendSticker(
|
(
|
||||||
shortcode,
|
shortcode,
|
||||||
image,
|
image, {
|
||||||
shouldMention: shouldMention,
|
required shouldMention,
|
||||||
repliedEventRowId:
|
}) => notifier
|
||||||
repliedEventRowId,
|
.sendSticker(
|
||||||
)
|
shortcode,
|
||||||
.onError(showError),
|
image,
|
||||||
onSendPoll:
|
shouldMention: shouldMention,
|
||||||
(poll, {required shouldMention}) =>
|
repliedEventRowId:
|
||||||
notifier
|
repliedEventRowId,
|
||||||
.sendPoll(
|
)
|
||||||
poll,
|
.onError(showError),
|
||||||
shouldMention: shouldMention,
|
onSendPoll:
|
||||||
repliedEventRowId:
|
(poll, {required shouldMention}) =>
|
||||||
repliedEventRowId,
|
notifier
|
||||||
)
|
.sendPoll(
|
||||||
.onError(showError),
|
poll,
|
||||||
onSendLocation:
|
shouldMention:
|
||||||
(location, {required shouldMention}) =>
|
shouldMention,
|
||||||
notifier
|
repliedEventRowId:
|
||||||
.sendLocation(
|
repliedEventRowId,
|
||||||
location,
|
)
|
||||||
shouldMention: shouldMention,
|
.onError(showError),
|
||||||
repliedEventRowId:
|
onSendLocation:
|
||||||
repliedEventRowId,
|
(
|
||||||
)
|
location, {
|
||||||
.onError(showError),
|
required shouldMention,
|
||||||
onSendBotCommand: (draft) => notifier
|
}) => notifier
|
||||||
.sendBotCommand(
|
.sendLocation(
|
||||||
draft,
|
location,
|
||||||
repliedEventRowId: repliedEventRowId,
|
shouldMention: shouldMention,
|
||||||
)
|
repliedEventRowId:
|
||||||
.onError(showError),
|
repliedEventRowId,
|
||||||
relationType: relationType.value,
|
)
|
||||||
relatedEvent: relatedEvent.value,
|
.onError(showError),
|
||||||
onDismiss: () {
|
onSendBotCommand: (draft) => notifier
|
||||||
relatedEvent.value = null;
|
.sendBotCommand(
|
||||||
composerNode.requestFocus();
|
draft,
|
||||||
},
|
repliedEventRowId:
|
||||||
),
|
repliedEventRowId,
|
||||||
|
)
|
||||||
|
.onError(showError),
|
||||||
|
relationType: relationType.value,
|
||||||
|
relatedEvent: relatedEvent.value,
|
||||||
|
onDismiss: () {
|
||||||
|
relatedEvent.value = null;
|
||||||
|
composerNode.requestFocus();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
56
lib/widgets/tombstone_banner.dart
Normal file
56
lib/widgets/tombstone_banner.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:nexus/helpers/extensions/open_link_info.dart";
|
||||||
|
import "package:nexus/main.dart";
|
||||||
|
import "package:nexus/models/content/tombstone.dart";
|
||||||
|
import "package:nexus/models/event.dart";
|
||||||
|
|
||||||
|
class const TombstoneBanner(final Event tombstone, {super.key})
|
||||||
|
extends ConsumerWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final content = tombstone.content;
|
||||||
|
final sender = tombstone.sender;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
color: theme.colorScheme.surfaceContainerHighest,
|
||||||
|
padding: .symmetric(horizontal: 16, vertical: 8),
|
||||||
|
margin: .only(top: 4),
|
||||||
|
child: Row(
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.upgrade),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: .min,
|
||||||
|
crossAxisAlignment: .start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"This room has been replaced and is no longer active.",
|
||||||
|
style: .new(fontWeight: .bold),
|
||||||
|
),
|
||||||
|
if (content case TombstoneContent(:final body)
|
||||||
|
when body.isNotEmpty)
|
||||||
|
Text(body, style: theme.textTheme.bodySmall),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (content case TombstoneContent(:final replacementRoom))
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () => ref
|
||||||
|
.openLinkInfo(context, (
|
||||||
|
identifier: replacementRoom,
|
||||||
|
eventId: null,
|
||||||
|
via: [sender.substring(sender.indexOf(":") + 1)].lock,
|
||||||
|
))
|
||||||
|
.onError(showError),
|
||||||
|
label: Text("New room"),
|
||||||
|
icon: Icon(Icons.open_in_new),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue