339 lines
17 KiB
Dart
339 lines
17 KiB
Dart
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
|
import "package:flutter/foundation.dart";
|
|
import "package:flutter/material.dart";
|
|
import "package:flutter_chat_core/flutter_chat_core.dart";
|
|
import "package:flutter_chat_ui/flutter_chat_ui.dart";
|
|
import "package:flutter_hooks/flutter_hooks.dart";
|
|
import "package:flutter_link_previewer/flutter_link_previewer.dart";
|
|
import "package:flyer_chat_file_message/flyer_chat_file_message.dart";
|
|
import "package:flyer_chat_image_message/flyer_chat_image_message.dart";
|
|
import "package:flyer_chat_system_message/flyer_chat_system_message.dart";
|
|
import "package:flyer_chat_text_message/flyer_chat_text_message.dart";
|
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
|
import "package:nexus/controllers/current_room_controller.dart";
|
|
import "package:nexus/controllers/room_chat_controller.dart";
|
|
import "package:nexus/helpers/extension_helper.dart";
|
|
import "package:nexus/helpers/launch_helper.dart";
|
|
import "package:nexus/widgets/chat_page/chat_box.dart";
|
|
import "package:nexus/widgets/chat_page/code_block.dart";
|
|
import "package:nexus/widgets/chat_page/member_list.dart";
|
|
import "package:nexus/widgets/chat_page/room_appbar.dart";
|
|
import "package:nexus/widgets/chat_page/spoiler_text.dart";
|
|
import "package:nexus/widgets/chat_page/top_widget.dart";
|
|
import "package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart";
|
|
import "package:nexus/widgets/loading.dart";
|
|
|
|
class RoomChat extends HookConsumerWidget {
|
|
final bool isDesktop;
|
|
final bool showMembersByDefault;
|
|
const RoomChat({
|
|
required this.isDesktop,
|
|
required this.showMembersByDefault,
|
|
super.key,
|
|
});
|
|
|
|
void showContextMenu({
|
|
required BuildContext context,
|
|
required Offset globalPosition,
|
|
required VoidCallback onTap,
|
|
}) => showMenu(
|
|
context: context,
|
|
position: RelativeRect.fromRect(
|
|
Rect.fromPoints(globalPosition, globalPosition),
|
|
Offset.zero & (context.findRenderObject() as RenderBox).size,
|
|
),
|
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
|
items: [
|
|
PopupMenuItem(
|
|
onTap: onTap,
|
|
child: ListTile(leading: Icon(Icons.reply), title: Text("Reply")),
|
|
),
|
|
],
|
|
);
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final replyToMessage = useState<Message?>(null);
|
|
final memberListOpened = useState<bool>(showMembersByDefault);
|
|
final theme = Theme.of(context);
|
|
return ref
|
|
.watch(CurrentRoomController.provider)
|
|
.betterWhen(
|
|
data: (room) {
|
|
final controllerProvider = RoomChatController.provider(
|
|
room.roomData,
|
|
);
|
|
final notifier = ref.watch(controllerProvider.notifier);
|
|
return Scaffold(
|
|
appBar: RoomAppbar(
|
|
room,
|
|
isDesktop: isDesktop,
|
|
onOpenDrawer: (_) => Scaffold.of(context).openDrawer(),
|
|
onOpenMemberList: (thisContext) {
|
|
memberListOpened.value = !memberListOpened.value;
|
|
Scaffold.of(thisContext).openEndDrawer();
|
|
},
|
|
),
|
|
body: Row(
|
|
children: [
|
|
Expanded(
|
|
child: ref
|
|
.watch(controllerProvider)
|
|
.betterWhen(
|
|
data: (controller) => Chat(
|
|
currentUserId: room.roomData.client.userID!,
|
|
theme: ChatTheme.fromThemeData(theme).copyWith(
|
|
colors: ChatColors.fromThemeData(theme).copyWith(
|
|
primary: theme.colorScheme.primaryContainer,
|
|
onPrimary: theme.colorScheme.onPrimaryContainer,
|
|
),
|
|
),
|
|
onMessageSecondaryTap:
|
|
(
|
|
context,
|
|
message, {
|
|
required details,
|
|
required index,
|
|
}) => showContextMenu(
|
|
context: context,
|
|
globalPosition: details.globalPosition,
|
|
onTap: () => replyToMessage.value = message,
|
|
),
|
|
onMessageLongPress:
|
|
(
|
|
context,
|
|
message, {
|
|
required details,
|
|
required index,
|
|
}) => showContextMenu(
|
|
context: context,
|
|
globalPosition: details.globalPosition,
|
|
onTap: () => replyToMessage.value = message,
|
|
),
|
|
builders: Builders(
|
|
loadMoreBuilder: (_) => Loading(),
|
|
chatAnimatedListBuilder: (_, itemBuilder) =>
|
|
ChatAnimatedList(
|
|
itemBuilder:
|
|
itemBuilder, // TODO: Load earlier
|
|
onEndReached: notifier.loadOlder,
|
|
onStartReached: () async {
|
|
notifier.markRead();
|
|
},
|
|
),
|
|
composerBuilder: (_) => ChatBox(
|
|
replyToMessage: replyToMessage.value,
|
|
onDismiss: () => replyToMessage.value = null,
|
|
headers: room.roomData.client.headers,
|
|
),
|
|
textMessageBuilder:
|
|
(
|
|
context,
|
|
message,
|
|
index, {
|
|
required bool isSentByMe,
|
|
MessageGroupStatus? groupStatus,
|
|
}) => FlyerChatTextMessage(
|
|
customWidget: HtmlWidget(
|
|
message.metadata?["formatted"].replaceAllMapped(
|
|
RegExp(
|
|
r'(?<!href="|">)(https?:\/\/[^\s<]+)',
|
|
caseSensitive: false,
|
|
),
|
|
(m) =>
|
|
"<a href=\"${m.group(0)!}\">${m.group(0)!}</a>",
|
|
) +
|
|
((message.editedAt != null)
|
|
? "<sub edited>(edited)</sub>"
|
|
: ""),
|
|
customWidgetBuilder: (element) {
|
|
if (element.localName == "mx-reply") {
|
|
return SizedBox.shrink();
|
|
}
|
|
if (element.localName == "code") {
|
|
if (element.parent?.localName ==
|
|
"pre") {
|
|
return CodeBlock(
|
|
element.text,
|
|
lang: element.className
|
|
.replaceAll("language-", ""),
|
|
);
|
|
}
|
|
}
|
|
if (element.localName == "img") {
|
|
final src = Uri.tryParse(
|
|
element.attributes["src"] ?? "",
|
|
);
|
|
if (src?.scheme != "mxc") {
|
|
return SizedBox.shrink();
|
|
}
|
|
|
|
// TODO: Should do something like:
|
|
// return Image.network(
|
|
// src!.getThumbnailUri(
|
|
// room.roomData.client,
|
|
// ),
|
|
// );
|
|
|
|
return SizedBox.shrink();
|
|
}
|
|
if (element.attributes.keys.contains(
|
|
"data-mx-spoiler",
|
|
)) {
|
|
return SpoilerText(
|
|
text: element.text,
|
|
);
|
|
}
|
|
return null;
|
|
},
|
|
customStylesBuilder: (element) => {
|
|
"width": "auto",
|
|
...Map.fromEntries(
|
|
element.attributes
|
|
.mapTo<MapEntry<String, String>?>(
|
|
(key, value) => switch (key) {
|
|
"data-mx-color" => MapEntry(
|
|
"color",
|
|
value,
|
|
),
|
|
|
|
"data-mx-bg-color" =>
|
|
MapEntry(
|
|
"background-color",
|
|
value,
|
|
),
|
|
|
|
"edited" => MapEntry(
|
|
"display",
|
|
"block",
|
|
),
|
|
_ => null,
|
|
},
|
|
)
|
|
.nonNulls,
|
|
),
|
|
},
|
|
onTapUrl: (url) => ref
|
|
.watch(LaunchHelper.provider)
|
|
.launchUrl(Uri.parse(url)),
|
|
),
|
|
topWidget: TopWidget(
|
|
message,
|
|
headers: room.roomData.client.headers,
|
|
groupStatus: groupStatus,
|
|
),
|
|
message: message,
|
|
showTime: true,
|
|
index: index,
|
|
),
|
|
linkPreviewBuilder: (_, message, isSentByMe) =>
|
|
LinkPreview(
|
|
text: message.text,
|
|
backgroundColor: isSentByMe
|
|
? theme.colorScheme.inversePrimary
|
|
: theme.colorScheme.surfaceContainerLow,
|
|
insidePadding: EdgeInsets.symmetric(
|
|
vertical: 8,
|
|
horizontal: 16,
|
|
),
|
|
linkPreviewData: message.linkPreviewData,
|
|
onLinkPreviewDataFetched:
|
|
(linkPreviewData) =>
|
|
notifier.updateMessage(
|
|
message,
|
|
message.copyWith(
|
|
linkPreviewData:
|
|
linkPreviewData,
|
|
),
|
|
),
|
|
),
|
|
imageMessageBuilder:
|
|
(
|
|
_,
|
|
message,
|
|
index, {
|
|
required bool isSentByMe,
|
|
MessageGroupStatus? groupStatus,
|
|
}) => FlyerChatImageMessage(
|
|
topWidget: TopWidget(
|
|
message,
|
|
headers: room.roomData.client.headers,
|
|
groupStatus: groupStatus,
|
|
alwaysShow: true,
|
|
),
|
|
message: message,
|
|
index: index,
|
|
headers: room.roomData.client.headers,
|
|
),
|
|
fileMessageBuilder:
|
|
(
|
|
_,
|
|
message,
|
|
index, {
|
|
required bool isSentByMe,
|
|
MessageGroupStatus? groupStatus,
|
|
}) => InkWell(
|
|
onTap: () => showAboutDialog(
|
|
context: context,
|
|
), // TODO: Download
|
|
child: FlyerChatFileMessage(
|
|
topWidget: TopWidget(
|
|
message,
|
|
headers: room.roomData.client.headers,
|
|
groupStatus: groupStatus,
|
|
),
|
|
message: message,
|
|
index: index,
|
|
),
|
|
),
|
|
systemMessageBuilder:
|
|
(
|
|
_,
|
|
message,
|
|
index, {
|
|
required bool isSentByMe,
|
|
MessageGroupStatus? groupStatus,
|
|
}) => FlyerChatSystemMessage(
|
|
message: message,
|
|
index: index,
|
|
),
|
|
unsupportedMessageBuilder:
|
|
(
|
|
_,
|
|
message,
|
|
index, {
|
|
required bool isSentByMe,
|
|
MessageGroupStatus? groupStatus,
|
|
}) => kDebugMode
|
|
? Text(
|
|
"${message.authorId} sent ${message.metadata?["eventType"]}",
|
|
style: theme.textTheme.labelSmall
|
|
?.copyWith(color: Colors.grey),
|
|
)
|
|
: SizedBox.shrink(),
|
|
),
|
|
onMessageSend: (message) {
|
|
notifier.send(
|
|
message,
|
|
replyTo: replyToMessage.value,
|
|
);
|
|
replyToMessage.value = null;
|
|
},
|
|
resolveUser: notifier.resolveUser,
|
|
chatController: controller,
|
|
),
|
|
),
|
|
),
|
|
|
|
if (memberListOpened.value == true && showMembersByDefault)
|
|
MemberList(room.roomData),
|
|
],
|
|
),
|
|
endDrawer: showMembersByDefault
|
|
? null
|
|
: MemberList(room.roomData),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|