diff --git a/lib/controllers/invites.dart b/lib/controllers/invites.dart index 55899b1..3effbf9 100644 --- a/lib/controllers/invites.dart +++ b/lib/controllers/invites.dart @@ -59,8 +59,13 @@ class InvitesController extends Notifier> { summary.copyWith( worldReadable: historyVisibility == .worldReadable, ), - MembershipContent(:final isDirect) when event.stateKey == userId => - summary.copyWith(invitedBy: event.sender, isDirect: isDirect), + MembershipContent(:final isDirect, :final reason) + when event.stateKey == userId => + summary.copyWith( + invitedBy: event.sender, + inviteReason: reason, + isDirect: isDirect, + ), _ => summary, }, ); diff --git a/lib/models/room_summary.dart b/lib/models/room_summary.dart index 35c86bd..f2c8852 100644 --- a/lib/models/room_summary.dart +++ b/lib/models/room_summary.dart @@ -31,6 +31,9 @@ class const RoomSummary({ /// Who invited the current user, for invites. @JsonKey(includeFromJson: false) final String? invitedBy, + /// The reason given for the invite, if any. + @JsonKey(includeFromJson: false) final String? inviteReason, + /// The inviter's membership, for their display name and avatar. @JsonKey(includeFromJson: false) final MembershipContent? inviterProfile, diff --git a/lib/widgets/invites_list.dart b/lib/widgets/invites_list.dart index ad07ce0..6dbb0a8 100644 --- a/lib/widgets/invites_list.dart +++ b/lib/widgets/invites_list.dart @@ -124,7 +124,6 @@ class const InviteCard(final RoomSummary invite, {super.key}) [ if (inviterName != null) "${invite.isDirect ? "Direct message from" : "Invited by"} $inviterName", - ?invite.topic, ].join("\n"), maxLines: 2, overflow: .ellipsis, diff --git a/lib/widgets/linkified_text.dart b/lib/widgets/linkified_text.dart index 4e621d2..1c12d22 100644 --- a/lib/widgets/linkified_text.dart +++ b/lib/widgets/linkified_text.dart @@ -10,14 +10,17 @@ final class const LinkifiedText( super.key, }) extends ConsumerWidget { @override - Widget build(BuildContext context, WidgetRef ref) => Linkify( - text: text, - maxLines: maxLines, - style: style, - options: .new(humanize: false), - onOpen: (link) => - ref.read(LaunchHelper.provider).launchUrl(.parse(link.url)), - linkStyle: .new(color: Theme.of(context).colorScheme.primary), - overflow: maxLines == null ? null : .ellipsis, - ); + Widget build(BuildContext context, WidgetRef ref) => + MaterialUiCompatibilityBridge( + child: Linkify( + text: text, + maxLines: maxLines, + style: style, + options: .new(humanize: false), + onOpen: (link) => + ref.read(LaunchHelper.provider).launchUrl(.parse(link.url)), + linkStyle: .new(color: Theme.of(context).colorScheme.primary), + overflow: maxLines == null ? null : .ellipsis, + ), + ); } diff --git a/lib/widgets/room_menu_button.dart b/lib/widgets/room_menu_button.dart index 2c5b347..d6a61b8 100644 --- a/lib/widgets/room_menu_button.dart +++ b/lib/widgets/room_menu_button.dart @@ -55,18 +55,21 @@ final class const RoomMenuButton( builder: (context) => HookBuilder( builder: (context) { final userIdController = useTextEditingController(); + final reasonController = useTextEditingController(); final text = useValueListenable(userIdController).text.trim(); final valid = RegExp(r"^@[^:\s]+:[^\s]+$").hasMatch(text); void submit() { if (!valid) return; Navigator.of(context).pop(); + final reason = reasonController.text.trim(); client .setMembership( .new( userId: text, roomId: room!.metadata!.id, action: .invite, + reason: reason.isEmpty ? null : reason, ), ) .onError(showError); @@ -74,18 +77,32 @@ final class const RoomMenuButton( return AlertDialog( title: Text("Invite User"), - content: TextField( - controller: userIdController, - autofocus: true, - keyboardType: .emailAddress, - onSubmitted: (_) => submit(), - decoration: .new( - labelText: "User ID", - hintText: "@user:example.com", - errorText: text.isEmpty || valid - ? null - : "Enter a valid user ID", - ), + content: Column( + mainAxisSize: .min, + spacing: 12, + children: [ + TextField( + controller: userIdController, + autofocus: true, + keyboardType: .emailAddress, + onSubmitted: (_) => submit(), + decoration: .new( + labelText: "User ID", + hintText: "@user:example.com", + errorText: text.isEmpty || valid + ? null + : "Enter a valid user ID", + ), + ), + TextField( + controller: reasonController, + textCapitalization: .sentences, + onSubmitted: (_) => submit(), + decoration: const .new( + labelText: "Invite reason (optional)", + ), + ), + ], ), actions: [ TextButton( diff --git a/lib/widgets/room_summary_dialog.dart b/lib/widgets/room_summary_dialog.dart index 9ccde20..b892f5d 100644 --- a/lib/widgets/room_summary_dialog.dart +++ b/lib/widgets/room_summary_dialog.dart @@ -61,6 +61,13 @@ class const RoomSummaryDialog( ), subtitle: SelectableText(invitedBy), ), + if (summary.inviteReason case final reason?) + ListTile( + titleAlignment: ListTileTitleAlignment.titleHeight, + leading: const Icon(Icons.format_quote), + title: const Text("Invite reason"), + subtitle: SelectableText(reason), + ), // Invite state never includes the member count if (summary.joinedMembers != null || !isInvite) ListTile( @@ -70,23 +77,23 @@ class const RoomSummaryDialog( leading: Icon(Icons.people), ), ListTile( - title: SelectableText(summary.canonicalAlias ?? summary.roomId), + title: Text( + summary.canonicalAlias == null ? "Room ID" : "Canonical Alias", + ), + subtitle: SelectableText(summary.canonicalAlias ?? summary.roomId), leading: Icon(Icons.numbers), ), if (summary.topic != null) ListTile( - titleAlignment: ListTileTitleAlignment.titleHeight, - title: LinkifiedText( - summary.topic!, - style: Theme.of(context).textTheme.bodyLarge, - ), + title: Text("Topic"), + subtitle: LinkifiedText(summary.topic!), leading: Icon(Icons.info), ), if (summary.encryption != null) ListTile(title: Text("Encrypted"), leading: Icon(Icons.lock)), if (summary.worldReadable == true) ListTile( - title: Text("Anyone can read the history"), + title: Text("Anyone can read room history"), leading: Icon(Icons.public), ), ],