reorganize and rename widgets
This commit is contained in:
parent
05d25869ef
commit
d5f29dc344
3 changed files with 568 additions and 547 deletions
25
lib/widgets/reactions/reaction_key.dart
Normal file
25
lib/widgets/reactions/reaction_key.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:nexus/helpers/mxc_image.dart";
|
||||
|
||||
/// A reaction's key, either an emoji or a custom (mxc) image.
|
||||
class const ReactionKey(
|
||||
final String reaction, {
|
||||
required final double height,
|
||||
final TextStyle? style,
|
||||
super.key,
|
||||
}) extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) =>
|
||||
reaction.startsWith("mxc://")
|
||||
? Image(
|
||||
height: height,
|
||||
image: MxcImage(ref, .new(mxc: Uri.parse(reaction))),
|
||||
errorBuilder: (context, _, _) => Icon(
|
||||
Icons.broken_image,
|
||||
size: height,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
)
|
||||
: Text(reaction, style: style, overflow: .ellipsis);
|
||||
}
|
||||
115
lib/widgets/reactions/reaction_row.dart
Normal file
115
lib/widgets/reactions/reaction_row.dart
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import "package:flutter/gestures.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/controllers/client_state.dart";
|
||||
import "package:nexus/controllers/reactions.dart";
|
||||
import "package:nexus/controllers/room_chat.dart";
|
||||
import "package:material_adaptive_sheets/material_adaptive_sheets.dart";
|
||||
import "package:nexus/models/event.dart";
|
||||
import "package:nexus/widgets/common/error_dialog.dart";
|
||||
import "package:nexus/widgets/reactions/reaction_key.dart";
|
||||
import "package:nexus/widgets/reactions/reactions_popover.dart";
|
||||
import "package:nexus/main.dart";
|
||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
|
||||
class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final clientState = ref.watch(ClientStateController.provider);
|
||||
|
||||
return Padding(
|
||||
padding: .only(top: 4),
|
||||
child: switch (ref.watch(
|
||||
ReactionsController.provider(
|
||||
.new(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);
|
||||
|
||||
final selected =
|
||||
reactors?[reaction]?.contains(clientState!.userId) ??
|
||||
false;
|
||||
void showReactions(PositionedGestureDetails details) =>
|
||||
context.showAdaptivePopover(
|
||||
(_) => ReactionsPopover(
|
||||
event,
|
||||
initialReaction: reaction,
|
||||
),
|
||||
anchor: context.globalRectAt(details.globalPosition),
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
onSecondaryTapUp: showReactions,
|
||||
onLongPressStart: showReactions,
|
||||
child: Tooltip(
|
||||
message: reactors?[reaction]?.join(", ") ?? "",
|
||||
child: ChoiceChip(
|
||||
showCheckmark: false,
|
||||
selected: selected,
|
||||
label: Row(
|
||||
mainAxisSize: .min,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Flexible(
|
||||
child: ReactionKey(reaction, height: 20),
|
||||
),
|
||||
Text(count.toString(), overflow: .ellipsis),
|
||||
],
|
||||
),
|
||||
onSelected: enabled.value
|
||||
? (value) async {
|
||||
enabled.value = false;
|
||||
try {
|
||||
final controller = ref.watch(
|
||||
RoomChatController.provider((
|
||||
roomId: event.roomId,
|
||||
contextualEvent: null,
|
||||
threadRoot: null,
|
||||
)).notifier,
|
||||
);
|
||||
|
||||
if (selected) {
|
||||
await controller
|
||||
.removeReaction(
|
||||
reaction,
|
||||
event,
|
||||
clientState!.userId!,
|
||||
)
|
||||
.onError(showError);
|
||||
} else {
|
||||
await controller
|
||||
.sendReaction(reaction, event)
|
||||
.onError(showError);
|
||||
}
|
||||
} finally {
|
||||
enabled.value = true;
|
||||
}
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
|
||||
AsyncError(:final error, :final stackTrace) => ErrorDialog(
|
||||
error,
|
||||
stackTrace,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
144
lib/widgets/reactions/reactions_popover.dart
Normal file
144
lib/widgets/reactions/reactions_popover.dart
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import "dart:math";
|
||||
|
||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:nexus/controllers/reactions.dart";
|
||||
import "package:nexus/controllers/user.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.dart";
|
||||
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||
import "package:nexus/helpers/extensions/show_user_popover.dart";
|
||||
import "package:nexus/models/event.dart";
|
||||
import "package:nexus/widgets/avatars/avatar_or_hash.dart";
|
||||
import "package:nexus/helpers/extensions/show_image_viewer.dart";
|
||||
import "package:nexus/widgets/reactions/reaction_key.dart";
|
||||
|
||||
/// Every reaction to an event in tabs, each listing who reacted with it.
|
||||
class const ReactionsPopover(
|
||||
final Event event, {
|
||||
final String? initialReaction,
|
||||
super.key,
|
||||
}) extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final reactions = ref.watch(
|
||||
ReactionsController.provider(
|
||||
.new(roomId: event.roomId, eventRowId: event.rowId),
|
||||
),
|
||||
);
|
||||
// Ordered like the reaction row on the timeline
|
||||
final order = event.reactions.keys.toIList();
|
||||
final keys =
|
||||
reactions.value?.keys.toIList().sortOrdered(
|
||||
(a, b) => order.indexOf(a).compareTo(order.indexOf(b)),
|
||||
) ??
|
||||
const .empty();
|
||||
|
||||
final vsync = useSingleTickerProvider(keys: [keys.length]);
|
||||
final tabController = useMemoized(
|
||||
() => TabController(
|
||||
length: keys.length,
|
||||
initialIndex: keys
|
||||
.indexOf(initialReaction ?? "")
|
||||
.clamp(0, max(0, keys.length - 1)),
|
||||
vsync: vsync,
|
||||
),
|
||||
[keys.length],
|
||||
);
|
||||
useEffect(() => tabController.dispose, [tabController]);
|
||||
useListenable(tabController);
|
||||
|
||||
return reactions.betterWhen(
|
||||
skipLoadingOnReload: true,
|
||||
data: (reactions) {
|
||||
if (keys.isEmpty) return SizedBox.shrink();
|
||||
|
||||
final reaction = keys[tabController.index];
|
||||
|
||||
return Column(
|
||||
mainAxisSize: .min,
|
||||
crossAxisAlignment: .stretch,
|
||||
children: [
|
||||
TabBar(
|
||||
controller: tabController,
|
||||
isScrollable: true,
|
||||
tabAlignment: .start,
|
||||
tabs: keys.map((reaction) {
|
||||
void expand() => context.showImageViewer(
|
||||
ref,
|
||||
.new(mxc: Uri.parse(reaction)),
|
||||
downloadFilename: reaction.split("/").last,
|
||||
);
|
||||
final isImage = reaction.startsWith("mxc://");
|
||||
|
||||
return GestureDetector(
|
||||
onSecondaryTap: isImage ? expand : null,
|
||||
onLongPress: isImage ? expand : null,
|
||||
child: Tab(
|
||||
child: Row(
|
||||
mainAxisSize: .min,
|
||||
spacing: 8,
|
||||
children: [
|
||||
ReactionKey(reaction, height: 20),
|
||||
Text(reactions[reaction]!.length.toString()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
Flexible(
|
||||
child: AnimatedSize(
|
||||
duration: Durations.medium2,
|
||||
curve: Curves.easeInOutCubicEmphasized,
|
||||
alignment: .topCenter,
|
||||
child: SingleChildScrollView(
|
||||
key: ValueKey(reaction),
|
||||
padding: .symmetric(vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: .stretch,
|
||||
children: reactions[reaction]!
|
||||
.map(
|
||||
(userId) => Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final member = ref
|
||||
.watch(
|
||||
UserController.provider(
|
||||
.new(
|
||||
roomId: event.roomId,
|
||||
userId: userId,
|
||||
),
|
||||
),
|
||||
)
|
||||
.value;
|
||||
final name =
|
||||
member?.displayName ?? userId.localpart;
|
||||
|
||||
return ListTile(
|
||||
leading: AvatarOrHash(
|
||||
member?.avatarUrl,
|
||||
name,
|
||||
dimension: 40,
|
||||
),
|
||||
title: Text(name, overflow: .ellipsis),
|
||||
subtitle: Text(userId, overflow: .ellipsis),
|
||||
onTap: () => context.showUserPopover(
|
||||
userId,
|
||||
roomId: event.roomId,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue