Remove flutter chat #26
9 changed files with 233 additions and 163 deletions
add reaction support
commit
2451555479
56
lib/controllers/reactions_controller.dart
Normal file
56
lib/controllers/reactions_controller.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/client_controller.dart";
|
||||||
|
import "package:nexus/controllers/rooms_controller.dart";
|
||||||
|
import "package:nexus/models/configs/reactions_config.dart";
|
||||||
|
import "package:nexus/models/content/reaction.dart";
|
||||||
|
import "package:nexus/models/requests/get_related_events_request.dart";
|
||||||
|
|
||||||
|
class ReactionsController extends AsyncNotifier<IMap<String, IList<String>>> {
|
||||||
|
final ReactionsConfig config;
|
||||||
|
ReactionsController(this.config);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<IMap<String, IList<String>>> build() async {
|
||||||
|
final eventInfo = ref.watch(
|
||||||
|
RoomsController.provider.select((value) {
|
||||||
|
final event = value[config.roomId]?.events[config.eventRowId];
|
||||||
|
return event == null ? null : (event.eventId, event.reactions);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
final reactionEvents = eventInfo?.$2.isNotEmpty == true
|
||||||
|
? await ref
|
||||||
|
.watch(ClientController.provider.notifier)
|
||||||
|
.getRelatedEvents(
|
||||||
|
GetRelatedEventsRequest(
|
||||||
|
roomId: config.roomId,
|
||||||
|
eventId: eventInfo!.$1,
|
||||||
|
relationType: "m.annotation",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return reactionEvents
|
||||||
|
?.where((event) => event.redactedBy == null)
|
||||||
|
.fold<IMap<String, IList<String>>>(IMap(), (acc, event) {
|
||||||
|
if (event.content case ReactionContent(:final key?)) {
|
||||||
|
return acc.update(
|
||||||
|
key,
|
||||||
|
(list) => list.add(event.sender),
|
||||||
|
ifAbsent: () => IList([event.sender]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}) ??
|
||||||
|
const IMap.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
AsyncNotifierProvider.family<
|
||||||
|
ReactionsController,
|
||||||
|
IMap<String, IList<String>>,
|
||||||
|
ReactionsConfig
|
||||||
|
>(ReactionsController.new);
|
||||||
|
}
|
||||||
|
|
@ -67,8 +67,8 @@ void main() async {
|
||||||
await windowManager.setMinimumSize(Size.square(500));
|
await windowManager.setMinimumSize(Size.square(500));
|
||||||
}
|
}
|
||||||
|
|
||||||
// FlutterError.onError = (FlutterErrorDetails details) =>
|
FlutterError.onError = (FlutterErrorDetails details) =>
|
||||||
// showError(details.exception.toString(), details.stack);
|
showError(details.exception.toString(), details.stack);
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
ProviderScope(
|
ProviderScope(
|
||||||
|
|
|
||||||
14
lib/models/configs/reactions_config.dart
Normal file
14
lib/models/configs/reactions_config.dart
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
part "reactions_config.freezed.dart";
|
||||||
|
part "reactions_config.g.dart";
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class ReactionsConfig with _$ReactionsConfig {
|
||||||
|
const factory ReactionsConfig({
|
||||||
|
required String roomId,
|
||||||
|
required int eventRowId,
|
||||||
|
}) = _ReactionsConfig;
|
||||||
|
|
||||||
|
factory ReactionsConfig.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$ReactionsConfigFromJson(json);
|
||||||
|
}
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import "package:flutter/material.dart";
|
|
||||||
import "package:nexus/models/event.dart";
|
|
||||||
import "package:nexus/widgets/reaction_row.dart";
|
|
||||||
|
|
||||||
class EventWrapper extends StatelessWidget {
|
|
||||||
final Event event;
|
|
||||||
final Widget child;
|
|
||||||
final bool isFlashing;
|
|
||||||
const EventWrapper(
|
|
||||||
this.event,
|
|
||||||
this.child, {
|
|
||||||
this.isFlashing = false,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return ClipRRect(
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
|
||||||
child: AnimatedContainer(
|
|
||||||
padding: isFlashing ? EdgeInsets.all(8) : EdgeInsets.all(0),
|
|
||||||
color: isFlashing
|
|
||||||
? Theme.of(context).colorScheme.onSurface.withAlpha(50)
|
|
||||||
: Colors.transparent,
|
|
||||||
duration: Duration(milliseconds: 250),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
child,
|
|
||||||
if (event.sendError != null && event.sendError != "not sent")
|
|
||||||
Text(
|
|
||||||
event.sendError!,
|
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
|
||||||
color: theme.colorScheme.error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ReactionRow(event),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
20
lib/widgets/flash_wrapper.dart
Normal file
20
lib/widgets/flash_wrapper.dart
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
|
class FlashWrapper extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
final bool isFlashing;
|
||||||
|
const FlashWrapper(this.child, {this.isFlashing = false, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => ClipRRect(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
padding: isFlashing ? EdgeInsets.all(8) : EdgeInsets.all(0),
|
||||||
|
color: isFlashing
|
||||||
|
? Theme.of(context).colorScheme.onSurface.withAlpha(50)
|
||||||
|
: Colors.transparent,
|
||||||
|
duration: Duration(milliseconds: 250),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -4,11 +4,15 @@ import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/client_state_controller.dart";
|
import "package:nexus/controllers/client_state_controller.dart";
|
||||||
import "package:nexus/controllers/cross_cache_controller.dart";
|
import "package:nexus/controllers/cross_cache_controller.dart";
|
||||||
|
import "package:nexus/controllers/reactions_controller.dart";
|
||||||
import "package:nexus/controllers/room_chat_controller.dart";
|
import "package:nexus/controllers/room_chat_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/get_headers.dart";
|
import "package:nexus/helpers/extensions/get_headers.dart";
|
||||||
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/models/configs/reactions_config.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
|
import "package:nexus/widgets/error_dialog.dart";
|
||||||
|
import "package:nexus/main.dart";
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
|
||||||
class ReactionRow extends ConsumerWidget {
|
class ReactionRow extends ConsumerWidget {
|
||||||
final Event event;
|
final Event event;
|
||||||
|
|
@ -18,100 +22,99 @@ class ReactionRow extends ConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final clientState = ref.watch(ClientStateController.provider);
|
final clientState = ref.watch(ClientStateController.provider);
|
||||||
|
|
||||||
return SizedBox.shrink();
|
return switch (ref.watch(
|
||||||
|
ReactionsController.provider(
|
||||||
|
ReactionsConfig(roomId: event.roomId, eventRowId: event.rowId),
|
||||||
|
),
|
||||||
|
)) {
|
||||||
|
AsyncData(value: final IMap<String, IList<String>>? reactors) ||
|
||||||
|
AsyncLoading(value: final reactors) => Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: event.reactions
|
||||||
|
.where((_, value) => value != 0)
|
||||||
|
.mapTo(
|
||||||
|
(reaction, count) => HookBuilder(
|
||||||
|
builder: (context) {
|
||||||
|
final enabled = useState(true);
|
||||||
|
|
||||||
// TODO: IMPL
|
final selected =
|
||||||
// return Wrap(
|
reactors?[reaction]?.contains(clientState!.userId) ??
|
||||||
// spacing: 4,
|
false;
|
||||||
// runSpacing: 4,
|
return Tooltip(
|
||||||
// children: clientState?.homeserverUrl == null
|
message: reactors?[reaction]?.join(", ") ?? "",
|
||||||
// ? []
|
child: ChoiceChip(
|
||||||
// : event.reactions
|
showCheckmark: false,
|
||||||
// .mapTo(
|
selected: selected,
|
||||||
// (reaction, reactors) => HookBuilder(
|
label: Row(
|
||||||
// builder: (context) {
|
mainAxisSize: MainAxisSize.min,
|
||||||
// final enabled = useState(true);
|
spacing: 8,
|
||||||
// final selected = reactors.contains(clientState!.userId);
|
children: [
|
||||||
// return Tooltip(
|
Flexible(
|
||||||
// message: reactors.join(", "),
|
child: reaction.startsWith("mxc://")
|
||||||
// child: ChoiceChip(
|
? Image(
|
||||||
// showCheckmark: false,
|
height: 20,
|
||||||
// selected: selected,
|
image: CachedNetworkImage(
|
||||||
// label: Row(
|
headers: ref.headers,
|
||||||
// mainAxisSize: MainAxisSize.min,
|
Uri.parse(reaction)
|
||||||
// spacing: 8,
|
.mxcToHttps(
|
||||||
// children: [
|
clientState!.homeserverUrl!,
|
||||||
// Flexible(
|
)
|
||||||
// child: reaction.startsWith("mxc://")
|
.toString(),
|
||||||
// ? Image(
|
ref.watch(CrossCacheController.provider),
|
||||||
// height: 20,
|
),
|
||||||
// image: CachedNetworkImage(
|
)
|
||||||
// headers: ref.headers,
|
: Text(
|
||||||
// Uri.parse(reaction)
|
reaction,
|
||||||
// .mxcToHttps(
|
overflow: TextOverflow.ellipsis,
|
||||||
// clientState.homeserverUrl!,
|
),
|
||||||
// )
|
),
|
||||||
// .toString(),
|
Text(
|
||||||
// ref.watch(
|
count.toString(),
|
||||||
// CrossCacheController.provider,
|
overflow: TextOverflow.ellipsis,
|
||||||
// ),
|
),
|
||||||
// ),
|
],
|
||||||
// )
|
),
|
||||||
// : Text(
|
onSelected: enabled.value
|
||||||
// reaction,
|
? (value) async {
|
||||||
// overflow: TextOverflow.ellipsis,
|
enabled.value = false;
|
||||||
// ),
|
try {
|
||||||
// ),
|
final controller = ref.watch(
|
||||||
// Text(
|
RoomChatController.provider(
|
||||||
// reactors.length.toString(),
|
event.roomId,
|
||||||
// overflow: TextOverflow.ellipsis,
|
).notifier,
|
||||||
// ),
|
);
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// onSelected: enabled.value
|
|
||||||
// ? (value) async {
|
|
||||||
// enabled.value = false;
|
|
||||||
// try {
|
|
||||||
// final roomId = ref.watch(
|
|
||||||
// SelectedRoomController.provider.select(
|
|
||||||
// (value) => value?.metadata?.id,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// if (roomId == null ||
|
|
||||||
// clientState.userId == null) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// final controller = ref.watch(
|
if (selected) {
|
||||||
// RoomChatController.provider(
|
await controller
|
||||||
// roomId,
|
.removeReaction(
|
||||||
// ).notifier,
|
reaction,
|
||||||
// );
|
event,
|
||||||
|
clientState!.userId!,
|
||||||
|
)
|
||||||
|
.onError(showError);
|
||||||
|
} else {
|
||||||
|
await controller
|
||||||
|
.sendReaction(reaction, event)
|
||||||
|
.onError(showError);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
enabled.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
|
||||||
// if (selected) {
|
AsyncError(:final error, :final stackTrace) => ErrorDialog(
|
||||||
// await controller
|
error,
|
||||||
// .removeReaction(
|
stackTrace,
|
||||||
// reaction,
|
),
|
||||||
// event,
|
};
|
||||||
// clientState.userId!,
|
|
||||||
// )
|
|
||||||
// .onError(showError);
|
|
||||||
// } else {
|
|
||||||
// await controller
|
|
||||||
// .sendReaction(reaction, event)
|
|
||||||
// .onError(showError);
|
|
||||||
// }
|
|
||||||
// } finally {
|
|
||||||
// enabled.value = true;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// : null,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// )
|
|
||||||
// .toList(),
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,11 @@ import "package:nexus/widgets/expandable_image.dart";
|
||||||
import "package:nexus/widgets/html/html.dart";
|
import "package:nexus/widgets/html/html.dart";
|
||||||
import "package:nexus/widgets/lazy_loading/message_avatar.dart";
|
import "package:nexus/widgets/lazy_loading/message_avatar.dart";
|
||||||
import "package:nexus/widgets/lazy_loading/message_displayname.dart";
|
import "package:nexus/widgets/lazy_loading/message_displayname.dart";
|
||||||
import "package:nexus/widgets/link_preview.dart";
|
import "package:nexus/widgets/url_preview.dart";
|
||||||
import "package:nexus/widgets/loading.dart";
|
import "package:nexus/widgets/loading.dart";
|
||||||
import "package:nexus/widgets/players/video.dart";
|
import "package:nexus/widgets/players/video.dart";
|
||||||
import "package:nexus/widgets/players/audio.dart";
|
import "package:nexus/widgets/players/audio.dart";
|
||||||
|
import "package:nexus/widgets/reaction_row.dart";
|
||||||
import "package:nexus/widgets/renderers/membership.dart";
|
import "package:nexus/widgets/renderers/membership.dart";
|
||||||
import "package:nexus/widgets/renderers/generic_event.dart";
|
import "package:nexus/widgets/renderers/generic_event.dart";
|
||||||
import "package:nexus/widgets/file_card.dart";
|
import "package:nexus/widgets/file_card.dart";
|
||||||
|
|
@ -356,16 +357,21 @@ class EventRenderer extends ConsumerWidget {
|
||||||
style: errorStyle,
|
style: errorStyle,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
if (event.lastEditRowId != 0)
|
if (event.lastEditRowId != 0)
|
||||||
Text(
|
Text(
|
||||||
"(edited)",
|
"(edited)",
|
||||||
style: theme.textTheme.labelSmall,
|
style: theme.textTheme.labelSmall,
|
||||||
),
|
),
|
||||||
|
|
||||||
if (linkify(body).firstWhereOrNull(
|
if (linkify(body).firstWhereOrNull(
|
||||||
(element) => element is UrlElement,
|
(element) => element is UrlElement,
|
||||||
)
|
)
|
||||||
case final UrlElement link?)
|
case final UrlElement link?)
|
||||||
LinkPreview(link.url),
|
UrlPreview(link.url),
|
||||||
|
|
||||||
|
SizedBox(height: 4),
|
||||||
|
ReactionRow(event),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -408,21 +414,38 @@ class EventRenderer extends ConsumerWidget {
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
return child == null
|
return Column(
|
||||||
? textOnly
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
? Text("Unknown event type", style: errorStyle)
|
children: [
|
||||||
: SizedBox.shrink()
|
if (child != null) ...[
|
||||||
: (textOnly
|
if (textOnly)
|
||||||
? child
|
child
|
||||||
: GestureDetector(
|
else
|
||||||
|
GestureDetector(
|
||||||
onSecondaryTapUp: contextMenuCallback,
|
onSecondaryTapUp: contextMenuCallback,
|
||||||
onLongPressStart: contextMenuCallback,
|
onLongPressStart: contextMenuCallback,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: isGrouped
|
padding: isGrouped ? EdgeInsets.zero : EdgeInsets.only(top: 8),
|
||||||
? EdgeInsets.zero
|
|
||||||
: EdgeInsets.only(top: 8),
|
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
));
|
),
|
||||||
|
|
||||||
|
if (event.content is! MessageContent)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsetsGeometry.only(left: 12),
|
||||||
|
child: ReactionRow(event),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (event.sendError != null && event.sendError != "not sent")
|
||||||
|
Text(
|
||||||
|
event.sendError!,
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
color: theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else if (textOnly)
|
||||||
|
Text("Unknown event type", style: errorStyle),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import "package:nexus/widgets/emoji_picker_button.dart";
|
||||||
import "package:nexus/widgets/renderers/event.dart";
|
import "package:nexus/widgets/renderers/event.dart";
|
||||||
import "package:nexus/widgets/member_list.dart";
|
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/event_wrapper.dart";
|
import "package:nexus/widgets/flash_wrapper.dart";
|
||||||
import "package:nexus/widgets/error_dialog.dart";
|
import "package:nexus/widgets/error_dialog.dart";
|
||||||
import "package:nexus/widgets/form_text_input.dart";
|
import "package:nexus/widgets/form_text_input.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
|
|
@ -352,8 +352,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
itemBuilder: (_, index) {
|
itemBuilder: (_, index) {
|
||||||
final event = value[index];
|
final event = value[index];
|
||||||
final previousEvent = value.getOrNull(index + 1);
|
final previousEvent = value.getOrNull(index + 1);
|
||||||
return EventWrapper(
|
return FlashWrapper(
|
||||||
event,
|
|
||||||
EventRenderer(
|
EventRenderer(
|
||||||
event,
|
event,
|
||||||
onTapReply: () async {
|
onTapReply: () async {
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
import "package:nexus/helpers/extensions/get_headers.dart";
|
import "package:nexus/helpers/extensions/get_headers.dart";
|
||||||
import "package:nexus/helpers/launch_helper.dart";
|
import "package:nexus/helpers/launch_helper.dart";
|
||||||
|
|
||||||
class LinkPreview extends ConsumerWidget {
|
class UrlPreview extends ConsumerWidget {
|
||||||
final String link;
|
final String link;
|
||||||
const LinkPreview(this.link, {super.key});
|
const UrlPreview(this.link, {super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) => ConstrainedBox(
|
Widget build(BuildContext context, WidgetRef ref) => ConstrainedBox(
|
||||||
Loading…
Add table
Add a link
Reference in a new issue