add mention support
This commit is contained in:
parent
6215537f8e
commit
bbe36ff86f
8 changed files with 198 additions and 107 deletions
|
|
@ -43,9 +43,9 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S
|
|||
- [x] HTML/Markdown
|
||||
- [x] Replies
|
||||
- [ ] Attachments
|
||||
- [ ] Mentions
|
||||
- [ ] Users
|
||||
- [ ] Rooms
|
||||
- [x] Mentions
|
||||
- [x] Users
|
||||
- [x] Rooms
|
||||
- [ ] Custom emojis/stickers
|
||||
- [ ] GIFs, maybe through Tenor or something
|
||||
- [ ] Encrypted messages
|
||||
|
|
|
|||
|
|
@ -102,11 +102,12 @@ class RoomChatController extends AsyncNotifier<ChatController> {
|
|||
var taggedMessage = message;
|
||||
|
||||
for (final tag in tags) {
|
||||
final escaped = RegExp.escape(tag.id.substring(1));
|
||||
final pattern = RegExp(r"@@(" + escaped + r")#[^#]*#");
|
||||
final escaped = RegExp.escape(tag.id);
|
||||
final pattern = RegExp(r"@+(" + escaped + r")(#[^#]*#)?");
|
||||
|
||||
taggedMessage = taggedMessage.replaceAllMapped(
|
||||
pattern,
|
||||
(m) => "@${m.group(1)}",
|
||||
(match) => match.group(1)!,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
23
lib/controllers/rooms_controller.dart
Normal file
23
lib/controllers/rooms_controller.dart
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
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/helpers/extensions/get_full_room.dart";
|
||||
import "package:nexus/models/full_room.dart";
|
||||
|
||||
class RoomsController extends AsyncNotifier<IList<FullRoom>> {
|
||||
@override
|
||||
Future<IList<FullRoom>> build() async {
|
||||
final client = await ref.watch(ClientController.provider.future);
|
||||
|
||||
ref.onDispose(
|
||||
client.onSync.stream.listen((_) => ref.invalidateSelf()).cancel,
|
||||
);
|
||||
|
||||
return IList(await Future.wait(client.rooms.map((room) => room.fullRoom)));
|
||||
}
|
||||
|
||||
static final provider =
|
||||
AsyncNotifierProvider<RoomsController, IList<FullRoom>>(
|
||||
RoomsController.new,
|
||||
);
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ extension EventToMessage on Event {
|
|||
newContent?["formatted_body"] ??
|
||||
newContent?["body"] ??
|
||||
event.content["formatted_body"] ??
|
||||
event.body,
|
||||
event.content["body"],
|
||||
"reply": await replyEvent?.toMessage(mustBeText: true),
|
||||
"eventType": event.type,
|
||||
"avatarUrl": sender.avatarUrl.toString(),
|
||||
|
|
|
|||
|
|
@ -6,14 +6,9 @@ import "package:flutter_hooks/flutter_hooks.dart";
|
|||
import "package:fluttertagger/fluttertagger.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:matrix/matrix.dart";
|
||||
import "package:nexus/controllers/avatar_controller.dart";
|
||||
import "package:nexus/controllers/members_controller.dart";
|
||||
import "package:nexus/controllers/room_chat_controller.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.dart";
|
||||
import "package:nexus/helpers/extensions/get_headers.dart";
|
||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||
import "package:nexus/widgets/chat_page/mention_overlay.dart";
|
||||
import "package:nexus/widgets/chat_page/reply_preview.dart";
|
||||
import "package:nexus/widgets/loading.dart";
|
||||
|
||||
class ChatBox extends HookConsumerWidget {
|
||||
final Message? replyToMessage;
|
||||
|
|
@ -95,82 +90,14 @@ class ChatBox extends HookConsumerWidget {
|
|||
Expanded(
|
||||
child: FlutterTagger(
|
||||
triggerStrategy: TriggerStrategy.eager,
|
||||
overlay: Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
child: Container(
|
||||
color: theme.colorScheme.surfaceContainerHigh,
|
||||
padding: EdgeInsets.all(8),
|
||||
child: switch (triggerCharacter.value) {
|
||||
"@" =>
|
||||
ref
|
||||
.watch(MembersController.provider(room))
|
||||
.betterWhen(
|
||||
data: (members) => ListView(
|
||||
children:
|
||||
(query.value.isEmpty
|
||||
? members
|
||||
: members.where(
|
||||
(member) =>
|
||||
member.senderId
|
||||
.contains(
|
||||
query.value,
|
||||
) ||
|
||||
(member.content["displayname"]
|
||||
as String?)
|
||||
?.contains(
|
||||
query
|
||||
.value,
|
||||
) ==
|
||||
true,
|
||||
))
|
||||
.map(
|
||||
(member) => ListTile(
|
||||
leading: AvatarOrHash(
|
||||
ref
|
||||
.watch(
|
||||
AvatarController.provider(
|
||||
member
|
||||
.content["avatar_url"]
|
||||
.toString(),
|
||||
),
|
||||
)
|
||||
.whenOrNull(
|
||||
data: (data) =>
|
||||
data,
|
||||
),
|
||||
member
|
||||
.content["displayname"]
|
||||
.toString(),
|
||||
headers:
|
||||
room.client.headers,
|
||||
),
|
||||
title: Text(
|
||||
member.content["displayname"]
|
||||
as String? ??
|
||||
member.senderId,
|
||||
),
|
||||
onTap: () => controller
|
||||
.value
|
||||
.addTag(
|
||||
id: member.senderId,
|
||||
name: member
|
||||
.senderId
|
||||
.substring(1)
|
||||
.split(":")
|
||||
.first,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
"#" => Text("Todo"),
|
||||
_ => Loading(),
|
||||
},
|
||||
),
|
||||
),
|
||||
overlay: MentionOverlay(
|
||||
room,
|
||||
query: query.value,
|
||||
triggerCharacter: triggerCharacter.value,
|
||||
addTag: ({required id, required name}) {
|
||||
controller.value.addTag(id: id, name: name);
|
||||
node.requestFocus();
|
||||
},
|
||||
),
|
||||
controller: controller.value,
|
||||
onSearch: (newQuery, newTriggerCharacter) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import "package:nexus/controllers/thumbnail_controller.dart";
|
|||
import "package:nexus/helpers/extensions/get_headers.dart";
|
||||
import "package:nexus/helpers/launch_helper.dart";
|
||||
import "package:nexus/models/image_data.dart";
|
||||
import "package:nexus/widgets/chat_page/html/mention_chip.dart";
|
||||
import "package:nexus/widgets/chat_page/html/spoiler_text.dart";
|
||||
import "package:nexus/widgets/chat_page/html/code_block.dart";
|
||||
import "package:nexus/widgets/chat_page/quoted.dart";
|
||||
|
|
@ -41,24 +42,7 @@ class Html extends ConsumerWidget {
|
|||
|
||||
"a" =>
|
||||
Uri.tryParse(element.attributes["href"] ?? "")?.host == "matrix.to"
|
||||
? InlineCustomWidget(
|
||||
child: ActionChip(
|
||||
label: Text(
|
||||
element.text
|
||||
.replaceFirst("https://matrix.to/#/", "")
|
||||
.replaceFirst("http://matrix.to/#/", ""),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
onPressed: () {
|
||||
// TODO: Open room or join room dialog, or user popover
|
||||
showAboutDialog(context: context);
|
||||
},
|
||||
),
|
||||
)
|
||||
? MentionChip(element.text)
|
||||
: null,
|
||||
|
||||
"img" =>
|
||||
|
|
|
|||
26
lib/widgets/chat_page/html/mention_chip.dart
Normal file
26
lib/widgets/chat_page/html/mention_chip.dart
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart";
|
||||
import "package:matrix/matrix.dart";
|
||||
|
||||
class MentionChip extends StatelessWidget {
|
||||
final String label;
|
||||
const MentionChip(this.label, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => InlineCustomWidget(
|
||||
child: ActionChip(
|
||||
label: Text(
|
||||
label.parseIdentifierIntoParts()?.primaryIdentifier ?? label,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
onPressed: () {
|
||||
// TODO: Open room or join room dialog, or user popover
|
||||
showAboutDialog(context: context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
130
lib/widgets/chat_page/mention_overlay.dart
Normal file
130
lib/widgets/chat_page/mention_overlay.dart
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:matrix/matrix.dart";
|
||||
import "package:nexus/controllers/avatar_controller.dart";
|
||||
import "package:nexus/controllers/members_controller.dart";
|
||||
import "package:nexus/controllers/rooms_controller.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.dart";
|
||||
import "package:nexus/helpers/extensions/get_headers.dart";
|
||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||
import "package:nexus/widgets/loading.dart";
|
||||
|
||||
class MentionOverlay extends ConsumerWidget {
|
||||
final String? triggerCharacter;
|
||||
final String query;
|
||||
final Room room;
|
||||
final void Function({required String id, required String name}) addTag;
|
||||
const MentionOverlay(
|
||||
this.room, {
|
||||
required this.query,
|
||||
required this.addTag,
|
||||
required this.triggerCharacter,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) => Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
padding: EdgeInsets.all(8),
|
||||
child: switch (triggerCharacter) {
|
||||
"@" =>
|
||||
ref
|
||||
.watch(MembersController.provider(room))
|
||||
.betterWhen(
|
||||
data: (members) => ListView(
|
||||
children:
|
||||
(query.isEmpty
|
||||
? members
|
||||
: members.where(
|
||||
(member) =>
|
||||
member.senderId.toLowerCase().contains(
|
||||
query.toLowerCase(),
|
||||
) ||
|
||||
(member.content["displayname"]
|
||||
as String?)
|
||||
?.toLowerCase()
|
||||
.contains(
|
||||
query.toLowerCase(),
|
||||
) ==
|
||||
true,
|
||||
))
|
||||
.map(
|
||||
(member) => ListTile(
|
||||
leading: AvatarOrHash(
|
||||
ref
|
||||
.watch(
|
||||
AvatarController.provider(
|
||||
member.content["avatar_url"]
|
||||
.toString(),
|
||||
),
|
||||
)
|
||||
.whenOrNull(data: (data) => data),
|
||||
member.content["displayname"].toString(),
|
||||
headers: room.client.headers,
|
||||
),
|
||||
title: Text(
|
||||
member.content["displayname"] as String? ??
|
||||
member.senderId,
|
||||
),
|
||||
onTap: () => addTag(
|
||||
id: member.senderId,
|
||||
name: member.senderId
|
||||
.substring(1)
|
||||
.split(":")
|
||||
.first,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
"#" =>
|
||||
ref
|
||||
.watch(RoomsController.provider)
|
||||
.betterWhen(
|
||||
data: (rooms) => ListView(
|
||||
children:
|
||||
(query.isEmpty
|
||||
? rooms
|
||||
: rooms.where(
|
||||
(room) => room.title.toLowerCase().contains(
|
||||
query.toLowerCase(),
|
||||
),
|
||||
))
|
||||
.map(
|
||||
(room) => ListTile(
|
||||
leading: AvatarOrHash(
|
||||
room.avatar,
|
||||
room.title,
|
||||
fallback: Icon(Icons.numbers),
|
||||
headers: room.roomData.client.headers,
|
||||
),
|
||||
title: Text(room.title),
|
||||
subtitle: room.roomData.topic.isEmpty
|
||||
? null
|
||||
: Text(room.roomData.topic),
|
||||
onTap: () => addTag(
|
||||
id: "[#${room.roomData.getLocalizedDisplayname()}](https://matrix.to/#/${room.roomData.id})",
|
||||
name:
|
||||
(room.roomData.canonicalAlias.isEmpty
|
||||
? room.roomData.id
|
||||
: room.roomData.canonicalAlias)
|
||||
.substring(1)
|
||||
.split(":")
|
||||
.first,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
_ => Loading(),
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue