forked from Nexus/nexus
reorganize
This commit is contained in:
parent
b9a2e09e74
commit
220c13a245
12 changed files with 39 additions and 29 deletions
|
|
@ -1,7 +1,8 @@
|
||||||
import "dart:convert";
|
import "dart:convert";
|
||||||
import "dart:io";
|
import "dart:io";
|
||||||
import "package:flutter/foundation.dart";
|
import "package:flutter/foundation.dart";
|
||||||
import "package:flutter_vodozemac/flutter_vodozemac.dart";
|
import "package:vodozemac/vodozemac.dart" as voz;
|
||||||
|
import "package:flutter_vodozemac/flutter_vodozemac.dart" as voz_fl;
|
||||||
import "package:matrix/matrix.dart";
|
import "package:matrix/matrix.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/secure_storage_controller.dart";
|
import "package:nexus/controllers/secure_storage_controller.dart";
|
||||||
|
|
@ -14,7 +15,7 @@ class ClientController extends AsyncNotifier<Client> {
|
||||||
static const sessionBackupKey = "sessionBackup";
|
static const sessionBackupKey = "sessionBackup";
|
||||||
@override
|
@override
|
||||||
Future<Client> build() async {
|
Future<Client> build() async {
|
||||||
await init();
|
if (!voz.isInitialized()) await voz_fl.init();
|
||||||
final client = Client(
|
final client = Client(
|
||||||
"nexus",
|
"nexus",
|
||||||
logLevel: kReleaseMode ? Level.warning : Level.verbose,
|
logLevel: kReleaseMode ? Level.warning : Level.verbose,
|
||||||
|
|
@ -45,6 +46,10 @@ class ClientController extends AsyncNotifier<Client> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ref.onDispose(
|
||||||
|
client.onRoomState.stream.listen((_) => ref.notifyListeners()).cancel,
|
||||||
|
);
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:nexus/widgets/chat/room_chat.dart";
|
import "package:nexus/widgets/chat_page/room_chat.dart";
|
||||||
import "package:nexus/widgets/chat/sidebar.dart";
|
import "package:nexus/widgets/chat_page/sidebar.dart";
|
||||||
|
|
||||||
class ChatPage extends StatelessWidget {
|
class ChatPage extends StatelessWidget {
|
||||||
const ChatPage({super.key});
|
const ChatPage({super.key});
|
||||||
|
|
|
||||||
64
lib/widgets/chat_page/chat_box.dart
Normal file
64
lib/widgets/chat_page/chat_box.dart
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||||
|
import "package:flutter_chat_ui/flutter_chat_ui.dart";
|
||||||
|
|
||||||
|
class ChatBox extends StatelessWidget {
|
||||||
|
final Message? replyToMessage;
|
||||||
|
final VoidCallback onDismiss;
|
||||||
|
final Map<String, String> headers;
|
||||||
|
const ChatBox({
|
||||||
|
required this.replyToMessage,
|
||||||
|
required this.onDismiss,
|
||||||
|
required this.headers,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Composer(
|
||||||
|
sigmaX: 0,
|
||||||
|
sigmaY: 0,
|
||||||
|
sendIconColor: Theme.of(context).colorScheme.primary,
|
||||||
|
sendOnEnter: true,
|
||||||
|
topWidget: replyToMessage == null
|
||||||
|
? null
|
||||||
|
: ColoredBox(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Avatar(
|
||||||
|
userId: replyToMessage!.authorId,
|
||||||
|
headers: headers,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
replyToMessage!.metadata?["displayName"] ??
|
||||||
|
replyToMessage!.authorId,
|
||||||
|
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: (replyToMessage is TextMessage)
|
||||||
|
? Text(
|
||||||
|
(replyToMessage as TextMessage).text,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
|
maxLines: 1,
|
||||||
|
)
|
||||||
|
: SizedBox(),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: onDismiss,
|
||||||
|
icon: Icon(Icons.close),
|
||||||
|
iconSize: 20,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
autofocus: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
53
lib/widgets/chat_page/code_block.dart
Normal file
53
lib/widgets/chat_page/code_block.dart
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import "dart:math";
|
||||||
|
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
|
class CodeBlock extends StatelessWidget {
|
||||||
|
final String code;
|
||||||
|
final String lang;
|
||||||
|
const CodeBlock(this.code, {required this.lang, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
|
child: ColoredBox(
|
||||||
|
color: theme.colorScheme.surfaceContainerHighest,
|
||||||
|
child: IntrinsicWidth(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Text(
|
||||||
|
lang.substring(0, min(lang.length, 15)),
|
||||||
|
style: TextStyle(fontFamily: "monospace"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {},
|
||||||
|
child: Row(
|
||||||
|
spacing: 4,
|
||||||
|
children: [Icon(Icons.copy), Text("Copy")],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ColoredBox(
|
||||||
|
color: theme.colorScheme.surfaceContainerHigh,
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(minWidth: 250),
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
child: SelectableText(code),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
lib/widgets/chat_page/member_list.dart
Normal file
63
lib/widgets/chat_page/member_list.dart
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
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/helpers/extension_helper.dart";
|
||||||
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
|
||||||
|
class MemberList extends ConsumerWidget {
|
||||||
|
final Room room;
|
||||||
|
const MemberList(this.room, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) => Drawer(
|
||||||
|
shape: Border(),
|
||||||
|
child: ref
|
||||||
|
.watch(MembersController.provider(room))
|
||||||
|
.betterWhen(
|
||||||
|
data: (members) => ListView(
|
||||||
|
children: [
|
||||||
|
AppBar(
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
leading: Icon(Icons.people),
|
||||||
|
title: Text("Members"),
|
||||||
|
actionsPadding: EdgeInsets.only(right: 4),
|
||||||
|
actions: [
|
||||||
|
if (Scaffold.of(context).hasEndDrawer)
|
||||||
|
IconButton(
|
||||||
|
onPressed: Scaffold.of(context).closeEndDrawer,
|
||||||
|
icon: Icon(Icons.close),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
...members
|
||||||
|
.where(
|
||||||
|
(membership) =>
|
||||||
|
membership.content["membership"] ==
|
||||||
|
Membership.join.name,
|
||||||
|
)
|
||||||
|
.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"].toString(),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
57
lib/widgets/chat_page/room_appbar.dart
Normal file
57
lib/widgets/chat_page/room_appbar.dart
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:nexus/helpers/extension_helper.dart";
|
||||||
|
import "package:nexus/models/full_room.dart";
|
||||||
|
import "package:nexus/widgets/appbar.dart";
|
||||||
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
|
||||||
|
class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
|
final bool isDesktop;
|
||||||
|
final FullRoom room;
|
||||||
|
final void Function(BuildContext context) onOpenMemberList;
|
||||||
|
final void Function(BuildContext context) onOpenDrawer;
|
||||||
|
const RoomAppbar(
|
||||||
|
this.room, {
|
||||||
|
required this.isDesktop,
|
||||||
|
required this.onOpenMemberList,
|
||||||
|
required this.onOpenDrawer,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Size get preferredSize => AppBar().preferredSize;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Appbar(
|
||||||
|
leading: isDesktop
|
||||||
|
? AvatarOrHash(
|
||||||
|
room.avatar,
|
||||||
|
room.title,
|
||||||
|
height: 32,
|
||||||
|
fallback: Icon(Icons.numbers),
|
||||||
|
headers: room.roomData.client.headers,
|
||||||
|
)
|
||||||
|
: DrawerButton(onPressed: () => onOpenDrawer(context)),
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(room.title, overflow: TextOverflow.ellipsis),
|
||||||
|
if (room.roomData.topic.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
room.roomData.topic,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => onOpenMemberList(context),
|
||||||
|
icon: Icon(Icons.people),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
317
lib/widgets/chat_page/room_chat.dart
Normal file
317
lib/widgets/chat_page/room_chat.dart
Normal file
|
|
@ -0,0 +1,317 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
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(
|
||||||
|
chatAnimatedListBuilder: (_, itemBuilder) =>
|
||||||
|
ChatAnimatedList(
|
||||||
|
itemBuilder: itemBuilder,
|
||||||
|
onEndReached: notifier.loadOlder,
|
||||||
|
onStartReached: () => 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"],
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
_ => 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,
|
||||||
|
),
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
139
lib/widgets/chat_page/sidebar.dart
Normal file
139
lib/widgets/chat_page/sidebar.dart
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
import "package:collection/collection.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/current_room_controller.dart";
|
||||||
|
import "package:nexus/controllers/spaces_controller.dart";
|
||||||
|
import "package:nexus/helpers/extension_helper.dart";
|
||||||
|
import "package:nexus/pages/settings_page.dart";
|
||||||
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
|
||||||
|
class Sidebar extends HookConsumerWidget {
|
||||||
|
const Sidebar({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final selectedSpace = useState(0);
|
||||||
|
final selectedRoom = useState(0);
|
||||||
|
|
||||||
|
return Drawer(
|
||||||
|
shape: Border(),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
ref
|
||||||
|
.watch(SpacesController.provider)
|
||||||
|
.when(
|
||||||
|
loading: SizedBox.shrink,
|
||||||
|
error: (error, stack) {
|
||||||
|
debugPrintStack(label: error.toString(), stackTrace: stack);
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
data: (spaces) => NavigationRail(
|
||||||
|
scrollable: true,
|
||||||
|
onDestinationSelected: (value) {
|
||||||
|
selectedSpace.value = value;
|
||||||
|
selectedRoom.value = 0;
|
||||||
|
ref
|
||||||
|
.watch(CurrentRoomController.provider.notifier)
|
||||||
|
.set(spaces[selectedSpace.value].children[0]);
|
||||||
|
},
|
||||||
|
destinations: spaces
|
||||||
|
.map(
|
||||||
|
(space) => NavigationRailDestination(
|
||||||
|
icon: Badge(
|
||||||
|
smallSize: 8,
|
||||||
|
backgroundColor: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary,
|
||||||
|
isLabelVisible:
|
||||||
|
space.children.firstWhereOrNull(
|
||||||
|
(room) => room.roomData.hasNewMessages,
|
||||||
|
) !=
|
||||||
|
null,
|
||||||
|
child: AvatarOrHash(
|
||||||
|
space.avatar,
|
||||||
|
fallback: space.icon,
|
||||||
|
space.title,
|
||||||
|
headers: space.client.headers,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
label: Text(space.title),
|
||||||
|
padding: EdgeInsets.only(top: 4),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
selectedIndex: selectedSpace.value,
|
||||||
|
trailingAtBottom: true,
|
||||||
|
trailing: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () => Navigator.of(
|
||||||
|
context,
|
||||||
|
).push(MaterialPageRoute(builder: (_) => SettingsPage())),
|
||||||
|
icon: Icon(Icons.settings),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ref
|
||||||
|
.watch(SpacesController.provider)
|
||||||
|
.betterWhen(
|
||||||
|
data: (spaces) {
|
||||||
|
final space = spaces[selectedSpace.value];
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AvatarOrHash(
|
||||||
|
space.avatar,
|
||||||
|
fallback: space.icon,
|
||||||
|
space.title,
|
||||||
|
headers: space.client.headers,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
space.title,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
body: NavigationRail(
|
||||||
|
scrollable: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
extended: true,
|
||||||
|
selectedIndex: space.children.isEmpty
|
||||||
|
? null
|
||||||
|
: selectedRoom.value,
|
||||||
|
destinations: space.children
|
||||||
|
.map(
|
||||||
|
(room) => NavigationRailDestination(
|
||||||
|
label: Text(room.title),
|
||||||
|
icon: Badge(
|
||||||
|
isLabelVisible: room.roomData.hasNewMessages,
|
||||||
|
child: AvatarOrHash(
|
||||||
|
room.avatar,
|
||||||
|
room.title,
|
||||||
|
fallback: selectedSpace.value == 1
|
||||||
|
? null
|
||||||
|
: Icon(Icons.numbers),
|
||||||
|
headers: space.client.headers,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onDestinationSelected: (value) {
|
||||||
|
selectedRoom.value = value;
|
||||||
|
ref
|
||||||
|
.watch(CurrentRoomController.provider.notifier)
|
||||||
|
.set(space.children[value]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
lib/widgets/chat_page/spoiler_text.dart
Normal file
29
lib/widgets/chat_page/spoiler_text.dart
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
|
|
||||||
|
class SpoilerText extends HookWidget {
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
const SpoilerText({super.key, required this.text});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final revealed = useState(false);
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => revealed.value = !revealed.value,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: revealed.value ? Colors.transparent : Colors.blueGrey,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(color: revealed.value ? null : Colors.transparent),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
123
lib/widgets/chat_page/top_widget.dart
Normal file
123
lib/widgets/chat_page/top_widget.dart
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
import "dart:math";
|
||||||
|
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_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/message_controller.dart";
|
||||||
|
import "package:nexus/helpers/extension_helper.dart";
|
||||||
|
|
||||||
|
class TopWidget extends ConsumerWidget {
|
||||||
|
final Message message;
|
||||||
|
final Map<String, String> headers;
|
||||||
|
final MessageGroupStatus? groupStatus;
|
||||||
|
const TopWidget(
|
||||||
|
this.message, {
|
||||||
|
required this.headers,
|
||||||
|
required this.groupStatus,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) => Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (message.replyToMessageId != null) ...[
|
||||||
|
ref
|
||||||
|
.watch(MessageController.provider(message.replyToMessageId!))
|
||||||
|
.betterWhen(
|
||||||
|
loading: SizedBox.shrink,
|
||||||
|
data: (replyMessage) {
|
||||||
|
if (replyMessage == null) return SizedBox.shrink();
|
||||||
|
|
||||||
|
// Black magic to limit reply preview length
|
||||||
|
final replyText = message is TextMessage
|
||||||
|
? replyMessage.text.substring(
|
||||||
|
0,
|
||||||
|
min(
|
||||||
|
max(
|
||||||
|
min(
|
||||||
|
max(
|
||||||
|
(message as TextMessage).text.length - 20,
|
||||||
|
message.metadata?["displayName"].length,
|
||||||
|
),
|
||||||
|
replyMessage.text.length,
|
||||||
|
),
|
||||||
|
5,
|
||||||
|
),
|
||||||
|
replyMessage.text.length,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: replyMessage.text;
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => showAboutDialog(context: context),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
left: BorderSide(
|
||||||
|
width: 4,
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(left: 8),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Avatar(
|
||||||
|
userId: replyMessage.authorId,
|
||||||
|
headers: headers,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
replyMessage.metadata?["displayName"] ??
|
||||||
|
replyMessage.authorId,
|
||||||
|
style: Theme.of(context).textTheme.labelMedium
|
||||||
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
replyText,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
if (groupStatus?.isFirst != false)
|
||||||
|
InkWell(
|
||||||
|
onTap: () =>
|
||||||
|
showAboutDialog(context: context), // TODO: Show user profile
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Avatar(userId: message.authorId, headers: headers),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
message.metadata?["displayName"] ?? message.authorId,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 4),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1589,7 +1589,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.2"
|
version: "15.0.2"
|
||||||
vodozemac:
|
vodozemac:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: vodozemac
|
name: vodozemac
|
||||||
sha256: "39144e20740807731871c9248d811ed5a037b21d0aa9ffcfa630954de74139d9"
|
sha256: "39144e20740807731871c9248d811ed5a037b21d0aa9ffcfa630954de74139d9"
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ dependencies:
|
||||||
flutter_svg: ^2.2.2
|
flutter_svg: ^2.2.2
|
||||||
simple_secure_storage: ^0.3.6
|
simple_secure_storage: ^0.3.6
|
||||||
json_annotation: ^4.9.0
|
json_annotation: ^4.9.0
|
||||||
|
vodozemac: ^0.4.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.4.11
|
build_runner: ^2.4.11
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue