forked from Henry-Hiles/nexus
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] HTML/Markdown
|
||||||
- [x] Replies
|
- [x] Replies
|
||||||
- [ ] Attachments
|
- [ ] Attachments
|
||||||
- [ ] Mentions
|
- [x] Mentions
|
||||||
- [ ] Users
|
- [x] Users
|
||||||
- [ ] Rooms
|
- [x] Rooms
|
||||||
- [ ] Custom emojis/stickers
|
- [ ] Custom emojis/stickers
|
||||||
- [ ] GIFs, maybe through Tenor or something
|
- [ ] GIFs, maybe through Tenor or something
|
||||||
- [ ] Encrypted messages
|
- [ ] Encrypted messages
|
||||||
|
|
|
||||||
|
|
@ -102,11 +102,12 @@ class RoomChatController extends AsyncNotifier<ChatController> {
|
||||||
var taggedMessage = message;
|
var taggedMessage = message;
|
||||||
|
|
||||||
for (final tag in tags) {
|
for (final tag in tags) {
|
||||||
final escaped = RegExp.escape(tag.id.substring(1));
|
final escaped = RegExp.escape(tag.id);
|
||||||
final pattern = RegExp(r"@@(" + escaped + r")#[^#]*#");
|
final pattern = RegExp(r"@+(" + escaped + r")(#[^#]*#)?");
|
||||||
|
|
||||||
taggedMessage = taggedMessage.replaceAllMapped(
|
taggedMessage = taggedMessage.replaceAllMapped(
|
||||||
pattern,
|
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?["formatted_body"] ??
|
||||||
newContent?["body"] ??
|
newContent?["body"] ??
|
||||||
event.content["formatted_body"] ??
|
event.content["formatted_body"] ??
|
||||||
event.body,
|
event.content["body"],
|
||||||
"reply": await replyEvent?.toMessage(mustBeText: true),
|
"reply": await replyEvent?.toMessage(mustBeText: true),
|
||||||
"eventType": event.type,
|
"eventType": event.type,
|
||||||
"avatarUrl": sender.avatarUrl.toString(),
|
"avatarUrl": sender.avatarUrl.toString(),
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,9 @@ import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
import "package:fluttertagger/fluttertagger.dart";
|
import "package:fluttertagger/fluttertagger.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:matrix/matrix.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/controllers/room_chat_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/better_when.dart";
|
import "package:nexus/widgets/chat_page/mention_overlay.dart";
|
||||||
import "package:nexus/helpers/extensions/get_headers.dart";
|
|
||||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
|
||||||
import "package:nexus/widgets/chat_page/reply_preview.dart";
|
import "package:nexus/widgets/chat_page/reply_preview.dart";
|
||||||
import "package:nexus/widgets/loading.dart";
|
|
||||||
|
|
||||||
class ChatBox extends HookConsumerWidget {
|
class ChatBox extends HookConsumerWidget {
|
||||||
final Message? replyToMessage;
|
final Message? replyToMessage;
|
||||||
|
|
@ -95,82 +90,14 @@ class ChatBox extends HookConsumerWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FlutterTagger(
|
child: FlutterTagger(
|
||||||
triggerStrategy: TriggerStrategy.eager,
|
triggerStrategy: TriggerStrategy.eager,
|
||||||
overlay: Padding(
|
overlay: MentionOverlay(
|
||||||
padding: EdgeInsets.all(8),
|
room,
|
||||||
child: ClipRRect(
|
query: query.value,
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
triggerCharacter: triggerCharacter.value,
|
||||||
child: Container(
|
addTag: ({required id, required name}) {
|
||||||
color: theme.colorScheme.surfaceContainerHigh,
|
controller.value.addTag(id: id, name: name);
|
||||||
padding: EdgeInsets.all(8),
|
node.requestFocus();
|
||||||
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(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
controller: controller.value,
|
controller: controller.value,
|
||||||
onSearch: (newQuery, newTriggerCharacter) {
|
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/extensions/get_headers.dart";
|
||||||
import "package:nexus/helpers/launch_helper.dart";
|
import "package:nexus/helpers/launch_helper.dart";
|
||||||
import "package:nexus/models/image_data.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/spoiler_text.dart";
|
||||||
import "package:nexus/widgets/chat_page/html/code_block.dart";
|
import "package:nexus/widgets/chat_page/html/code_block.dart";
|
||||||
import "package:nexus/widgets/chat_page/quoted.dart";
|
import "package:nexus/widgets/chat_page/quoted.dart";
|
||||||
|
|
@ -41,24 +42,7 @@ class Html extends ConsumerWidget {
|
||||||
|
|
||||||
"a" =>
|
"a" =>
|
||||||
Uri.tryParse(element.attributes["href"] ?? "")?.host == "matrix.to"
|
Uri.tryParse(element.attributes["href"] ?? "")?.host == "matrix.to"
|
||||||
? InlineCustomWidget(
|
? MentionChip(element.text)
|
||||||
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);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null,
|
: null,
|
||||||
|
|
||||||
"img" =>
|
"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