headers;
const AvatarOrHash(
this.avatar,
this.title, {
this.fallback,
+ this.badgeNumber = 0,
this.hasBadge = false,
this.height = 24,
- required this.headers,
super.key,
});
@override
- Widget build(BuildContext context) {
+ Widget build(BuildContext context, WidgetRef ref) {
final box = ColoredBox(
color: ColorHash(title).color,
- child: Center(child: Text(title[0])),
+ child: Center(child: Text(title.isEmpty ? "" : title[0])),
);
return SizedBox(
width: height,
@@ -30,18 +36,31 @@ class AvatarOrHash extends StatelessWidget {
child: Center(
child: Badge(
isLabelVisible: hasBadge,
+ label: badgeNumber != 0 ? Text(badgeNumber.toString()) : null,
smallSize: 12,
backgroundColor: Theme.of(context).colorScheme.primary,
child: ClipRRect(
- borderRadius: BorderRadius.all(Radius.circular(4)),
+ borderRadius: BorderRadius.all(Radius.circular((height - 8) / 2.5)),
child: SizedBox(
width: height,
height: height,
child: avatar == null
? fallback ?? box
- : Image.network(
- avatar.toString(),
- headers: headers,
+ : Image(
+ image: CachedNetworkImage(
+ avatar!
+ .mxcToHttps(
+ ref.watch(
+ ClientStateController.provider.select(
+ (value) => value?.homeserverUrl,
+ ),
+ ) ??
+ "",
+ )
+ .toString(),
+ ref.watch(CrossCacheController.provider),
+ headers: ref.headers,
+ ),
fit: BoxFit.contain,
errorBuilder: (_, _, _) => box,
),
diff --git a/lib/widgets/chat_page/chat_box.dart b/lib/widgets/chat_page/chat_box.dart
index 8fd5acb..b9e7dbb 100644
--- a/lib/widgets/chat_page/chat_box.dart
+++ b/lib/widgets/chat_page/chat_box.dart
@@ -5,9 +5,9 @@ import "package:flutter_chat_core/flutter_chat_core.dart";
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/room_chat_controller.dart";
import "package:nexus/models/relation_type.dart";
+import "package:nexus/models/room.dart";
import "package:nexus/widgets/chat_page/mention_overlay.dart";
import "package:nexus/widgets/chat_page/relation_preview.dart";
@@ -29,27 +29,22 @@ class ChatBox extends HookConsumerWidget {
final theme = Theme.of(context);
final controller = useRef(FlutterTaggerController());
final triggerCharacter = useState("");
+ final shouldMention = useState(true);
final query = useState("");
if (relationType == RelationType.edit &&
relatedMessage is TextMessage &&
controller.value.text.isEmpty) {
- final text = (relatedMessage as TextMessage).text;
- final splitText = relatedMessage?.replyToMessageId == null
- ? text
- : text.split("\n\n").sublist(1).join("\n\n");
- final notEmpty = splitText.isEmpty ? text : splitText;
- controller.value.text = notEmpty.startsWith("* ")
- ? notEmpty.substring(2)
- : notEmpty;
+ controller.value.text = relatedMessage?.metadata?["editSource"] ?? "";
}
void send() {
- if (controller.value.text.trim().isEmpty) return;
+ if (controller.value.text.trim().isEmpty || room.metadata == null) return;
ref
- .watch(RoomChatController.provider(room).notifier)
+ .watch(RoomChatController.provider(room.metadata!.id).notifier)
.send(
controller.value.formattedText,
+ shouldMention: shouldMention.value,
relation: relatedMessage,
relationType: relationType,
tags: controller.value.tags,
@@ -91,10 +86,12 @@ class ChatBox extends HookConsumerWidget {
child: Column(
children: [
RelationPreview(
+ shouldMention: shouldMention.value,
+ toggleShouldMention: () =>
+ shouldMention.value = !shouldMention.value,
relatedMessage: relatedMessage,
relationType: relationType,
onDismiss: onDismiss,
- room: room,
),
Container(
color: theme.colorScheme.surfaceContainerHighest,
@@ -103,9 +100,29 @@ class ChatBox extends HookConsumerWidget {
spacing: 8,
children: [
PopupMenuButton(
- itemBuilder: (context) => [],
+ tooltip: "Add media",
+ itemBuilder: (context) => [
+ PopupMenuItem(
+ child: ListTile(
+ title: Text("Camera"),
+ leading: Icon(Icons.add_a_photo),
+ ),
+ ),
+ PopupMenuItem(
+ child: ListTile(
+ title: Text("Gallery"),
+ leading: Icon(Icons.add_photo_alternate),
+ ),
+ ),
+ PopupMenuItem(
+ child: ListTile(
+ title: Text("Files"),
+ leading: Icon(Icons.attachment),
+ ),
+ ),
+ ],
icon: Icon(Icons.add),
- enabled: room.canSendDefaultMessages,
+ // enabled: room.canSendDefaultMessages, TODO: Permissions check
),
Expanded(
child: FlutterTagger(
@@ -126,11 +143,12 @@ class ChatBox extends HookConsumerWidget {
},
triggerCharacterAndStyles: {"@": style, "#": style},
builder: (context, key) => TextFormField(
- enabled: room.canSendDefaultMessages,
+ // enabled: room.canSendDefaultMessages,
maxLines: 12,
minLines: 1,
decoration: InputDecoration(
- hintText: room.canSendDefaultMessages
+ hintText:
+ true // TODO: room.canSendDefaultMessages
? "Your message here..."
: "You don't have permission to send messages in this room...",
border: InputBorder.none,
@@ -143,8 +161,10 @@ class ChatBox extends HookConsumerWidget {
),
),
IconButton(
- onPressed: room.canSendDefaultMessages ? send : null,
+ onPressed: send,
+ // onPressed: room.canSendDefaultMessages ? send : null,
icon: Icon(Icons.send),
+ tooltip: "Send message",
),
],
),
diff --git a/lib/widgets/chat_page/html/code_block.dart b/lib/widgets/chat_page/html/code_block.dart
index fe5b492..80950ce 100644
--- a/lib/widgets/chat_page/html/code_block.dart
+++ b/lib/widgets/chat_page/html/code_block.dart
@@ -41,6 +41,8 @@ class CodeBlock extends StatelessWidget {
padding: EdgeInsets.all(8),
child: SelectableText(
code,
+ minLines: 1,
+ maxLines: 99,
style: TextStyle(fontFamily: "monospace"),
),
),
diff --git a/lib/widgets/chat_page/html/html.dart b/lib/widgets/chat_page/html/html.dart
index 04a5a1b..1e1ab82 100644
--- a/lib/widgets/chat_page/html/html.dart
+++ b/lib/widgets/chat_page/html/html.dart
@@ -2,25 +2,25 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart";
-import "package:matrix/matrix.dart";
-import "package:nexus/controllers/thumbnail_controller.dart";
+import "package:nexus/controllers/client_state_controller.dart";
import "package:nexus/helpers/extensions/get_headers.dart";
+import "package:nexus/helpers/extensions/link_to_mention.dart";
+import "package:nexus/helpers/extensions/mxc_to_https.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/html/quoted.dart";
-import "package:nexus/widgets/error_dialog.dart";
class Html extends ConsumerWidget {
final String html;
- final Client client;
- const Html(this.html, {required this.client, super.key});
+ final TextStyle? textStyle;
+ const Html(this.html, {this.textStyle, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) => HtmlWidget(
html,
+ textStyle: textStyle,
customWidgetBuilder: (element) {
if (element.attributes.keys.contains("data-mx-spoiler")) {
return InlineCustomWidget(child: SpoilerText(text: element.text));
@@ -32,67 +32,54 @@ class Html extends ConsumerWidget {
return switch (element.localName) {
"code" =>
element.parent?.localName == "pre"
- ? CodeBlock(
- element.text,
- lang: element.className.replaceAll("language-", ""),
- )
+ ? element.outerHtml.contains("
")
+ ? Html(
+ """${element.outerHtml.replaceAll("
", "\n")}""",
+ )
+ : CodeBlock(
+ element.text,
+ lang: element.className.replaceAll("language-", ""),
+ )
: null,
- "blockquote" => Quoted(Html(element.innerHtml, client: client)),
+ "blockquote" => Quoted(Html(element.innerHtml)),
"a" =>
- element.attributes["href"]?.parseIdentifierIntoParts() == null
+ element.attributes["href"]?.mention == null
? null
: InlineCustomWidget(child: MentionChip(element.text)),
"img" =>
element.attributes["src"] == null
- ? null
- : Consumer(
- builder: (_, ref, _) => ref
- .watch(
- ThumbnailController.provider(
- ImageData(
- uri: element.attributes["src"]!,
- height: height,
- width: width,
- ),
- ),
- )
- .when(
- data: (uri) {
- if (uri == null) return SizedBox.shrink();
-
- return InlineCustomWidget(
- child: Image.network(
- uri,
- headers: client.headers,
- errorBuilder: (_, error, _) => Text(
- "Image Failed to Load",
- style: TextStyle(
- color: Theme.of(context).colorScheme.error,
+ ? SizedBox.shrink()
+ : InlineCustomWidget(
+ alignment: PlaceholderAlignment.middle,
+ child: Image.network(
+ Uri.parse(element.attributes["src"]!)
+ .mxcToHttps(
+ ref.watch(
+ ClientStateController.provider.select(
+ (value) => value?.homeserverUrl,
),
- ),
- height: height.toDouble(),
- width: width?.toDouble(),
- loadingBuilder: (_, child, loadingProgress) =>
- loadingProgress == null
- ? child
- : CircularProgressIndicator(),
- ),
- );
- },
- error: ErrorDialog.new,
- loading: () => InlineCustomWidget(
- child: SizedBox(
- width: width?.toDouble(),
- height: height.toDouble(),
- child: CircularProgressIndicator(),
- ),
- ),
+ ) ??
+ "",
+ )
+ .toString(),
+ headers: ref.headers,
+ errorBuilder: (_, error, _) => Text(
+ "Image Failed to Load",
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.error,
),
+ ),
+ height: height.toDouble(),
+ width: width?.toDouble(),
+ loadingBuilder: (_, child, loadingProgress) =>
+ loadingProgress == null
+ ? child
+ : CircularProgressIndicator(),
+ ),
),
-
("del" ||
"h1" ||
"h2" ||
@@ -139,9 +126,7 @@ class Html extends ConsumerWidget {
.mapTo?>(
(key, value) => switch (key) {
"data-mx-color" => MapEntry("color", value),
-
"data-mx-bg-color" => MapEntry("background-color", value),
-
_ => null,
},
)
diff --git a/lib/widgets/chat_page/html/mention_chip.dart b/lib/widgets/chat_page/html/mention_chip.dart
index e2c5003..c2b832d 100644
--- a/lib/widgets/chat_page/html/mention_chip.dart
+++ b/lib/widgets/chat_page/html/mention_chip.dart
@@ -1,5 +1,5 @@
import "package:flutter/material.dart";
-import "package:matrix/matrix.dart";
+import "package:nexus/helpers/extensions/link_to_mention.dart";
class MentionChip extends StatelessWidget {
final String label;
@@ -8,7 +8,7 @@ class MentionChip extends StatelessWidget {
@override
Widget build(BuildContext context) => ActionChip(
label: Text(
- label.parseIdentifierIntoParts()?.primaryIdentifier ?? label,
+ label.mention ?? label,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onPrimary,
@@ -19,7 +19,7 @@ class MentionChip extends StatelessWidget {
context: context,
builder: (_) => Dialog(
child: Text("TODO: Open room or join room dialog, or user popover"),
- ), // TODO
+ ),
),
);
}
diff --git a/lib/widgets/chat_page/image_message.dart b/lib/widgets/chat_page/image_message.dart
new file mode 100644
index 0000000..103fdd2
--- /dev/null
+++ b/lib/widgets/chat_page/image_message.dart
@@ -0,0 +1,58 @@
+import "dart:math";
+import "package:cross_cache/cross_cache.dart";
+import "package:flutter/material.dart";
+import "package:flutter_chat_core/flutter_chat_core.dart";
+import "package:flutter_riverpod/flutter_riverpod.dart";
+import "package:flyer_chat_image_message/flyer_chat_image_message.dart";
+import "package:nexus/controllers/cross_cache_controller.dart";
+import "package:nexus/helpers/extensions/get_headers.dart";
+
+class ExpandableImageMessage extends ConsumerWidget {
+ final ImageMessage message;
+ final int index;
+
+ const ExpandableImageMessage(this.message, {required this.index, super.key});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) => InkWell(
+ onTap: () => showDialog(
+ context: context,
+ builder: (_) => LayoutBuilder(
+ builder: (context, constraints) => Dialog(
+ backgroundColor: Colors.transparent,
+ insetPadding: EdgeInsets.all(constraints.maxWidth / 100),
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minWidth: min(constraints.maxWidth, 1000),
+ ),
+ child: InteractiveViewer(
+ child: Image(
+ fit: BoxFit.contain,
+ image: CachedNetworkImage(
+ message.source,
+ ref.watch(CrossCacheController.provider),
+ headers: ref.headers,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ child: FlyerChatImageMessage(
+ customImageProvider: CachedNetworkImage(
+ message.source,
+ ref.watch(CrossCacheController.provider),
+ headers: ref.headers,
+ ),
+ errorBuilder: (context, error, stackTrace) => Center(
+ child: Text(
+ "Image Failed to Load",
+ style: TextStyle(color: Theme.of(context).colorScheme.error),
+ ),
+ ),
+ message: message,
+ index: index,
+ ),
+ );
+}
diff --git a/lib/widgets/chat_page/member_list.dart b/lib/widgets/chat_page/member_list.dart
index 8130de8..24d22e4 100644
--- a/lib/widgets/chat_page/member_list.dart
+++ b/lib/widgets/chat_page/member_list.dart
@@ -1,10 +1,7 @@
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/extensions/better_when.dart";
-import "package:nexus/helpers/extensions/get_headers.dart";
+import "package:nexus/models/room.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
class MemberList extends ConsumerWidget {
@@ -12,54 +9,49 @@ class MemberList extends ConsumerWidget {
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(
- onTap: () {},
- 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,
- ),
- ),
- ),
+ Widget build(BuildContext context, WidgetRef ref) {
+ final members = ref.watch(MembersController.provider(room));
+ return Drawer(
+ shape: Border(),
+ child: ListView(
+ children: [
+ AppBar(
+ scrolledUnderElevation: 0,
+ leading: Icon(Icons.people),
+ title: Text("Members (${members.length})"),
+ actionsPadding: EdgeInsets.only(right: 4),
+ actions: [
+ if (Scaffold.of(context).hasEndDrawer)
+ IconButton(
+ onPressed: Scaffold.of(context).closeEndDrawer,
+ icon: Icon(Icons.close),
+ tooltip: "Close member list",
+ ),
],
),
- ),
- );
+ ...members.map(
+ (member) => ListTile(
+ onTap: () => showDialog(
+ context: context,
+ builder: (context) =>
+ Dialog(child: Text("TODO: Open member popover")),
+ ),
+ leading: AvatarOrHash(
+ Uri.tryParse(member.content["avatar_url"] ?? ""),
+ member.content["displayname"].toString(),
+ ),
+ title: Text(
+ member.content["displayname"].toString(),
+ overflow: TextOverflow.ellipsis,
+ ),
+ subtitle: Text(
+ member.stateKey ?? "Unknown User",
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
}
diff --git a/lib/widgets/chat_page/mention_overlay.dart b/lib/widgets/chat_page/mention_overlay.dart
index f6c7fe3..9858574 100644
--- a/lib/widgets/chat_page/mention_overlay.dart
+++ b/lib/widgets/chat_page/mention_overlay.dart
@@ -1,11 +1,8 @@
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/models/room.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/widgets/loading.dart";
@@ -23,108 +20,105 @@ class MentionOverlay extends ConsumerWidget {
});
@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,
+ Widget build(BuildContext context, WidgetRef ref) {
+ final rooms = ref.watch(RoomsController.provider);
+
+ return 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) {
+ "@" => Consumer(
+ builder: (_, ref, _) {
+ final members = ref.watch(MembersController.provider(room));
+ return ListView(
+ children:
+ (query.isEmpty
+ ? members
+ : members.where(
+ (member) =>
+ member.stateKey?.toLowerCase().contains(
+ query.toLowerCase(),
+ ) ==
+ true ||
+ (member.content["displayname"] as String?)
+ ?.toLowerCase()
+ .contains(query.toLowerCase()) ==
+ true,
+ ))
+ .map(
+ (member) => ListTile(
+ leading: AvatarOrHash(
+ Uri.tryParse(
+ member.content["avatar_url"] ?? "",
),
+ member.content["displayname"] ?? "",
),
- )
- .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, maxLines: 1),
- 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,
- ),
+ title: Text(
+ member.content["displayname"] as String? ??
+ member.stateKey ??
+ "Unknown User",
),
- )
- .toList(),
- ),
- ),
- _ => Loading(),
- },
+ subtitle: member.stateKey != null
+ ? Text(member.stateKey!)
+ : null,
+ onTap: () => addTag(
+ id: "[@${member.content["displayname"]}](https://matrix.to/#/${member.stateKey})",
+ name:
+ member.stateKey
+ ?.substring(1)
+ .split(":")
+ .first ??
+ "Unknown User",
+ ),
+ ),
+ )
+ .toList(),
+ );
+ },
+ ),
+ "#" => ListView(
+ children:
+ (query.isEmpty
+ ? rooms.values
+ : rooms.values.where(
+ (room) => (room.metadata?.name ?? "Unnamed Room")
+ .toLowerCase()
+ .contains(query.toLowerCase()),
+ ))
+ .map(
+ (room) => ListTile(
+ leading: AvatarOrHash(
+ room.metadata?.avatar,
+ room.metadata?.name ?? "Unnamed Room",
+ fallback: Icon(Icons.numbers),
+ ),
+ title: Text(room.metadata?.name ?? "Unnamed Room"),
+ subtitle: room.metadata?.topic == null
+ ? null
+ : Text(room.metadata!.topic!, maxLines: 1),
+ onTap: () => addTag(
+ id: "[#${room.metadata?.name ?? "Unnamed Room"}](https://matrix.to/#/${room.metadata?.id})",
+ name:
+ (room.metadata?.canonicalAlias ??
+ room.metadata?.id)
+ ?.substring(1)
+ .split(":")
+ .first ??
+ "",
+ ),
+ ),
+ )
+ .toList(),
+ ),
+
+ _ => Loading(),
+ },
+ ),
),
- ),
- );
+ );
+ }
}
diff --git a/lib/widgets/chat_page/message_wrapper.dart b/lib/widgets/chat_page/message_wrapper.dart
new file mode 100644
index 0000000..da53be0
--- /dev/null
+++ b/lib/widgets/chat_page/message_wrapper.dart
@@ -0,0 +1,54 @@
+import "package:flutter/material.dart";
+import "package:flutter_chat_core/flutter_chat_core.dart";
+import "package:nexus/widgets/avatar_or_hash.dart";
+
+class MessageWrapper extends StatelessWidget {
+ final Message message;
+ final Widget child;
+ final MessageGroupStatus? groupStatus;
+ const MessageWrapper(this.message, this.child, this.groupStatus, {super.key});
+
+ @override
+ Widget build(BuildContext context) => ClipRRect(
+ borderRadius: BorderRadius.all(Radius.circular(12)),
+ child: AnimatedContainer(
+ padding: message.metadata?["flashing"] == true
+ ? EdgeInsets.all(8)
+ : EdgeInsets.all(0),
+ color: message.metadata?["flashing"] == true
+ ? Theme.of(context).colorScheme.onSurface.withAlpha(50)
+ : Colors.transparent,
+ duration: Duration(milliseconds: 250),
+ child: Row(
+ spacing: 8,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ groupStatus?.isFirst != false
+ ? AvatarOrHash(
+ Uri.parse(message.metadata?["avatarUrl"] ?? ""),
+ height: 40,
+ message.metadata?["displayName"] ?? "",
+ )
+ : SizedBox(width: 40),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ spacing: 4,
+ children: [
+ if (groupStatus?.isFirst != false)
+ Text(
+ message.metadata?["displayName"] ?? message.authorId,
+ overflow: TextOverflow.ellipsis,
+ style: Theme.of(context).textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ child,
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+}
diff --git a/lib/widgets/chat_page/relation_preview.dart b/lib/widgets/chat_page/relation_preview.dart
index 07bac4e..7aa3ae8 100644
--- a/lib/widgets/chat_page/relation_preview.dart
+++ b/lib/widgets/chat_page/relation_preview.dart
@@ -1,9 +1,6 @@
import "package:flutter/material.dart";
import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
-import "package:matrix/matrix.dart";
-import "package:nexus/controllers/avatar_controller.dart";
-import "package:nexus/helpers/extensions/get_headers.dart";
import "package:nexus/models/relation_type.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
@@ -11,12 +8,14 @@ class RelationPreview extends ConsumerWidget {
final Message? relatedMessage;
final RelationType relationType;
final VoidCallback onDismiss;
- final Room room;
+ final bool shouldMention;
+ final VoidCallback toggleShouldMention;
const RelationPreview({
required this.relatedMessage,
required this.relationType,
required this.onDismiss,
- required this.room,
+ required this.shouldMention,
+ required this.toggleShouldMention,
super.key,
});
@@ -38,15 +37,8 @@ class RelationPreview extends ConsumerWidget {
style: TextStyle(fontWeight: FontWeight.bold),
),
AvatarOrHash(
- ref
- .watch(
- AvatarController.provider(
- relatedMessage!.metadata!["avatarUrl"],
- ),
- )
- .whenOrNull(data: (data) => data),
- relatedMessage!.metadata!["displayName"].toString(),
- headers: room.client.headers,
+ Uri.tryParse(relatedMessage?.metadata?["avatarUrl"] ?? ""),
+ relatedMessage?.metadata?["displayName"]?.toString() ?? "",
height: 16,
),
Text(
@@ -58,16 +50,28 @@ class RelationPreview extends ConsumerWidget {
),
Expanded(
child: Text(
- (relatedMessage is TextMessage)
- ? (relatedMessage as TextMessage).text
- : relatedMessage?.metadata?["body"] ??
- relatedMessage?.metadata?["eventType"],
+ relatedMessage?.metadata?["body"] ??
+ relatedMessage?.metadata?["eventType"],
overflow: TextOverflow.ellipsis,
style: theme.textTheme.labelMedium,
maxLines: 1,
),
),
+
+ if (relationType == RelationType.reply)
+ TextButton(
+ onPressed: toggleShouldMention,
+ child: Text(
+ shouldMention ? "@On" : "@Off",
+ style: TextStyle(
+ fontWeight: FontWeight.w900,
+ color: shouldMention ? null : Theme.of(context).disabledColor,
+ ),
+ ),
+ ),
IconButton(
+ tooltip:
+ "Cancel ${relationType == RelationType.edit ? "edit" : "reply"}",
onPressed: onDismiss,
icon: Icon(Icons.close),
iconSize: 20,
diff --git a/lib/widgets/chat_page/reply_widget.dart b/lib/widgets/chat_page/reply_widget.dart
new file mode 100644
index 0000000..cd30acc
--- /dev/null
+++ b/lib/widgets/chat_page/reply_widget.dart
@@ -0,0 +1,146 @@
+import "dart:math";
+import "package:flutter/material.dart";
+import "package:flutter_chat_core/flutter_chat_core.dart";
+import "package:flutter_riverpod/flutter_riverpod.dart";
+import "package:nexus/controllers/event_controller.dart";
+import "package:nexus/controllers/message_controller.dart";
+import "package:nexus/helpers/extensions/better_when.dart";
+import "package:nexus/models/message_config.dart";
+import "package:nexus/models/requests/get_event_request.dart";
+import "package:nexus/models/room.dart";
+import "package:nexus/widgets/avatar_or_hash.dart";
+import "package:nexus/widgets/chat_page/html/quoted.dart";
+
+typedef OnTapReply = void Function(Message message)?;
+
+class ReplyWidget extends ConsumerWidget {
+ final Message message;
+ final bool alwaysShow;
+ final Room room;
+ final MessageGroupStatus? groupStatus;
+ final OnTapReply onTapReply;
+ const ReplyWidget(
+ this.message, {
+ required this.room,
+ required this.groupStatus,
+ this.onTapReply,
+ this.alwaysShow = false,
+ super.key,
+ });
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) =>
+ message.replyToMessageId == null
+ ? SizedBox.shrink()
+ : Padding(
+ padding: EdgeInsets.only(bottom: 12),
+ child: Quoted(
+ ref
+ .watch(
+ EventController.provider(
+ GetEventRequest(
+ room: room,
+ eventId: message.replyToMessageId!,
+ ),
+ ),
+ )
+ .betterWhen(
+ loading: () => Text("Fetching event..."),
+ data: (event) => event == null
+ ? SizedBox.shrink()
+ : ref
+ .watch(
+ MessageController.provider(
+ MessageConfig(room: room, event: event),
+ ),
+ )
+ .betterWhen(
+ loading: () => Text("Parsing message..."),
+ data: (replyMessage) {
+ if (replyMessage == null) {
+ return SizedBox.shrink();
+ }
+
+ final smallerText =
+ message is TextMessage &&
+ replyMessage.metadata?["body"] != null
+ ? replyMessage.metadata!["body"].substring(
+ 0,
+ min(
+ max(
+ max(
+ (message as TextMessage)
+ .text
+ .length -
+ (replyMessage
+ .metadata?["displayName"]
+ as String)
+ .length -
+ 5,
+ message
+ .metadata?["displayName"]
+ .length,
+ ),
+ 5,
+ ),
+ replyMessage.metadata!["body"].length,
+ ),
+ )
+ : null;
+ final replyText =
+ (smallerText == null ||
+ smallerText.length ==
+ replyMessage
+ .metadata!["body"]
+ .length)
+ ? replyMessage.metadata!["body"]
+ : "$smallerText...";
+
+ return InkWell(
+ onTap: () => onTapReply?.call(replyMessage),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ spacing: 8,
+ children: [
+ AvatarOrHash(
+ Uri.tryParse(
+ replyMessage.metadata?["avatarUrl"] ??
+ "",
+ ),
+ replyMessage.metadata?["displayName"] ??
+ "",
+ height: 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,
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ ),
+ ),
+ ),
+ );
+}
diff --git a/lib/widgets/chat_page/room_appbar.dart b/lib/widgets/chat_page/room_appbar.dart
index b36a3ad..436bcb9 100644
--- a/lib/widgets/chat_page/room_appbar.dart
+++ b/lib/widgets/chat_page/room_appbar.dart
@@ -1,13 +1,13 @@
+import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/material.dart";
-import "package:nexus/helpers/extensions/get_headers.dart";
-import "package:nexus/models/full_room.dart";
+import "package:nexus/models/room.dart";
import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/widgets/chat_page/room_menu.dart";
class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
final bool isDesktop;
- final FullRoom room;
+ final Room room;
final void Function(BuildContext context) onOpenMemberList;
final void Function(BuildContext context) onOpenDrawer;
const RoomAppbar(
@@ -25,21 +25,24 @@ class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
Widget build(BuildContext context) => Appbar(
leading: isDesktop
? AvatarOrHash(
- room.avatar,
- room.title,
+ room.metadata?.avatar,
+ room.metadata?.name ?? "Unnamed Rooms",
height: 24,
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, maxLines: 1),
- if (room.roomData.topic.isNotEmpty)
+ Text(
+ room.metadata?.name ?? "Unnamed Room",
+ overflow: TextOverflow.ellipsis,
+ maxLines: 1,
+ ),
+ if (room.metadata?.topic?.isNotEmpty == true)
Text(
- room.roomData.topic,
+ room.metadata!.topic!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.labelMedium?.copyWith(
@@ -49,12 +52,17 @@ class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
],
),
actions: [
- IconButton(onPressed: () {}, icon: Icon(Icons.push_pin)),
+ IconButton(
+ onPressed: null,
+ icon: Icon(Icons.push_pin),
+ tooltip: "Open pinned messages",
+ ),
IconButton(
onPressed: () => onOpenMemberList(context),
+ tooltip: "Open member list",
icon: Icon(Icons.people),
),
- RoomMenu(room.roomData),
- ],
+ RoomMenu(room),
+ ].toIList(),
);
}
diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart
index 0d57663..839109f 100644
--- a/lib/widgets/chat_page/room_chat.dart
+++ b/lib/widgets/chat_page/room_chat.dart
@@ -1,30 +1,28 @@
-import "package:cross_cache/cross_cache.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/cross_cache_controller.dart";
+import "package:nexus/controllers/client_controller.dart";
+import "package:nexus/controllers/client_state_controller.dart";
import "package:nexus/controllers/selected_room_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/helpers/extensions/show_context_menu.dart";
import "package:nexus/models/relation_type.dart";
+import "package:nexus/models/requests/report_request.dart";
import "package:nexus/widgets/chat_page/chat_box.dart";
-import "package:nexus/widgets/chat_page/html/html.dart";
+import "package:nexus/widgets/chat_page/image_message.dart";
import "package:nexus/widgets/chat_page/member_list.dart";
+import "package:nexus/widgets/chat_page/message_wrapper.dart";
import "package:nexus/widgets/chat_page/room_appbar.dart";
-import "package:nexus/widgets/chat_page/top_widget.dart";
+import "package:nexus/widgets/chat_page/text_message_wrapper.dart";
+import "package:nexus/widgets/chat_page/reply_widget.dart";
import "package:nexus/widgets/form_text_input.dart";
import "package:nexus/widgets/loading.dart";
// import "package:dynamic_polls/dynamic_polls.dart";
-// import "package:matrix/matrix.dart";
class RoomChat extends HookConsumerWidget {
final bool isDesktop;
@@ -37,112 +35,70 @@ class RoomChat extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
+ final client = ref.watch(ClientController.provider.notifier);
final replyToMessage = useState(null);
final memberListOpened = useState(showMembersByDefault);
final relationType = useState(RelationType.reply);
+ final room = ref.watch(SelectedRoomController.provider);
+ final userId = ref.watch(ClientStateController.provider)?.userId;
+
final theme = Theme.of(context);
final danger = theme.colorScheme.error;
- return ref
- .watch(SelectedRoomController.provider)
- .betterWhen(
- data: (room) {
- if (room == null) {
- return Center(
- child: Text(
- "Nothing to see here...",
- style: theme.textTheme.headlineMedium,
- ),
- );
- }
- final controllerProvider = RoomChatController.provider(
- room.roomData,
- );
- final notifier = ref.watch(controllerProvider.notifier);
+ if (room == null || userId == null || room.metadata?.id == null) {
+ return Center(
+ child: Text(
+ "Nothing to see here...",
+ style: theme.textTheme.headlineMedium,
+ ),
+ );
+ }
- List getMessageOptions(Message message) => [
- PopupMenuItem(
- onTap: () {
- replyToMessage.value = message;
- relationType.value = RelationType.reply;
- },
- child: ListTile(
- leading: Icon(Icons.reply),
- title: Text("Reply"),
- ),
- ),
- // Should check if is state event (has state_key), if so, don't show edit option
- if (message is TextMessage &&
- message.authorId == room.roomData.client.userID)
- PopupMenuItem(
- onTap: () {
- replyToMessage.value = message;
- relationType.value = RelationType.edit;
- },
- child: ListTile(
- leading: Icon(Icons.edit),
- title: Text("Edit"),
- ),
- ),
- if (message.authorId == room.roomData.client.userID ||
- room.roomData.canRedact)
- PopupMenuItem(
- onTap: () => showDialog(
- context: context,
- builder: (context) => HookBuilder(
- builder: (_) {
- final deleteReasonController =
- useTextEditingController();
- return AlertDialog(
- title: Text("Delete Message"),
- content: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- "Are you sure you want to delete this message? This can not be reversed.",
- ),
- SizedBox(height: 12),
- FormTextInput(
- required: false,
- capitalize: true,
- controller: deleteReasonController,
- title: "Reason for deletion (optional)",
- ),
- ],
- ),
- actions: [
- TextButton(
- onPressed: Navigator.of(context).pop,
- child: Text("Cancel"),
- ),
- TextButton(
- onPressed: () async {
- notifier.deleteMessage(
- message,
- reason: deleteReasonController.text,
- );
- Navigator.of(context).pop();
- },
- child: Text("Delete"),
- ),
- ],
- );
- },
- ),
- ),
- child: ListTile(
- leading: Icon(Icons.delete),
- title: Text("Delete"),
- ),
- ),
- PopupMenuItem(
- onTap: () => showDialog(
- context: context,
- builder: (context) => AlertDialog(
- title: Text("Report"),
- content: Text(
- "Report this message to your server administrators, who can take action like banning that user or blocking that server from federating.",
+ final controllerProvider = RoomChatController.provider(room.metadata!.id);
+ final notifier = ref.watch(controllerProvider.notifier);
+
+ List getMessageOptions(Message message) {
+ final isSentByMe = message.authorId == userId;
+ return [
+ PopupMenuItem(
+ onTap: () {
+ replyToMessage.value = message;
+ relationType.value = RelationType.reply;
+ },
+ child: ListTile(leading: Icon(Icons.reply), title: Text("Reply")),
+ ),
+ if (message is TextMessage && isSentByMe)
+ PopupMenuItem(
+ onTap: () {
+ replyToMessage.value = message;
+ relationType.value = RelationType.edit;
+ },
+ child: ListTile(leading: Icon(Icons.edit), title: Text("Edit")),
+ ),
+ if (isSentByMe) // TODO: Or if user has permission to redact others' messages
+ PopupMenuItem(
+ onTap: () => showDialog(
+ context: context,
+ builder: (context) => HookBuilder(
+ builder: (_) {
+ final deleteReasonController = useTextEditingController();
+ return AlertDialog(
+ title: Text("Delete Message"),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "Are you sure you want to delete this message? This can not be reversed.",
+ ),
+ SizedBox(height: 12),
+ FormTextInput(
+ required: false,
+ capitalize: true,
+ controller: deleteReasonController,
+ title: "Reason for deletion (optional)",
+ ),
+ ],
),
actions: [
TextButton(
@@ -150,418 +106,263 @@ class RoomChat extends HookConsumerWidget {
child: Text("Cancel"),
),
TextButton(
- onPressed: () {
- room.roomData.client.reportEvent(
- room.roomData.id,
- message.id,
+ onPressed: () async {
+ notifier.deleteMessage(
+ message,
+ reason: deleteReasonController.text,
);
Navigator.of(context).pop();
},
- child: Text("Report"),
+ child: Text("Delete"),
+ ),
+ ],
+ );
+ },
+ ),
+ ),
+ child: ListTile(leading: Icon(Icons.delete), title: Text("Delete")),
+ ),
+ PopupMenuItem(
+ onTap: () => showDialog(
+ context: context,
+ builder: (context) => HookBuilder(
+ builder: (_) {
+ final reasonController = useTextEditingController();
+ return AlertDialog(
+ title: Text("Report"),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "Report this event to your server administrators, who can take action like banning this server or room.",
+ ),
+
+ SizedBox(height: 12),
+ FormTextInput(
+ required: false,
+ capitalize: true,
+ controller: reasonController,
+ title: "Reason for report (optional)",
),
],
),
- ),
- child: ListTile(
- leading: Icon(Icons.report, color: danger),
- title: Text("Report", style: TextStyle(color: danger)),
- ),
- ),
- ];
-
- 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: Column(
- 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 index,
- TapUpDetails? details,
- }) => details?.globalPosition == null
- ? null
- : context.showContextMenu(
- globalPosition:
- details!.globalPosition,
- children: getMessageOptions(message),
- ),
- onMessageLongPress:
- (
- context,
- message, {
- required details,
- required index,
- }) => context.showContextMenu(
- globalPosition: details.globalPosition,
- children: getMessageOptions(message),
- ),
- onMessageTap:
- (
- context,
- message, {
- required details,
- required index,
- }) {
- if (message is ImageMessage) {
- showDialog(
- context: context,
- builder: (_) => Dialog(
- backgroundColor:
- Colors.transparent,
- insetPadding: EdgeInsets.all(64),
- child: InteractiveViewer(
- child: Image(
- image: CachedNetworkImage(
- message.source,
- ref.watch(
- CrossCacheController
- .provider,
- ),
- headers: room
- .roomData
- .client
- .headers,
- ),
- ),
- ),
- ),
- );
- }
- },
- builders: Builders(
- loadMoreBuilder: (_) => Loading(),
- chatAnimatedListBuilder: (_, itemBuilder) =>
- ChatAnimatedList(
- itemBuilder: itemBuilder,
- onEndReached: notifier.loadOlder,
- onStartReached: notifier.markRead,
- bottomPadding: 72,
- ),
- composerBuilder: (_) => ChatBox(
- relationType: relationType.value,
- relatedMessage: replyToMessage.value,
- onDismiss: () =>
- replyToMessage.value = null,
- room: room.roomData,
- ),
-
- // customMessageBuilder:
- // (
- // context,
- // message,
- // index, {
- // required bool isSentByMe,
- // MessageGroupStatus? groupStatus,
- // }) {
- // final poll =
- // message.metadata?["poll"]
- // as PollStartContent;
- // final responses =
- // (message.metadata?["responses"]
- // as Map<
- // String,
- // Set
- // >)
- // .values
- // .expand((set) => set)
- // .fold({}, (
- // acc,
- // value,
- // ) {
- // acc[value] =
- // (acc[value] ?? 0) + 1;
- // return acc;
- // });
-
- // return Column(
- // crossAxisAlignment:
- // CrossAxisAlignment.start,
- // spacing: 4,
- // children: [
- // TopWidget(
- // message,
- // headers: room
- // .roomData
- // .client
- // .headers,
- // groupStatus: groupStatus,
- // ),
-
- // // TODO: Make this actually work
- // DynamicPolls(
- // startDate: DateTime.now(),
- // endDate: DateTime.now(),
- // private:
- // poll.kind ==
- // PollKind.undisclosed,
- // allowReselection: true,
- // backgroundDecoration:
- // BoxDecoration(
- // borderRadius:
- // BorderRadius.all(
- // Radius.circular(16),
- // ),
- // border: Border.all(
- // color: theme
- // .colorScheme
- // .primaryContainer,
- // width: 4,
- // ),
- // ),
- // allStyle: Styles(
- // titleStyle: TitleStyle(
- // style: theme
- // .textTheme
- // .headlineSmall,
- // ),
- // optionStyle: OptionStyle(
- // fillColor: theme
- // .colorScheme
- // .primaryContainer,
- // selectedBorderColor: theme
- // .colorScheme
- // .primary,
- // borderColor: theme
- // .colorScheme
- // .primary,
- // unselectedBorderColor:
- // Colors.transparent,
- // textSelectColor: theme
- // .colorScheme
- // .primary,
- // ),
- // ),
- // onOptionSelected:
- // (int index) {},
- // title: poll.question.mText,
- // options: poll.answers
- // .map(
- // (option) => option.mText,
- // )
- // .toList(),
- // ),
- // ],
- // );
- // },
- textMessageBuilder:
- (
- context,
- message,
- index, {
- required bool isSentByMe,
- MessageGroupStatus? groupStatus,
- }) => FlyerChatTextMessage(
- customWidget: Column(
- crossAxisAlignment:
- CrossAxisAlignment.start,
- children: [
- Html(
- (message.metadata?["formatted"]
- as String)
- .replaceAllMapped(
- RegExp(
- "(]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)",
- caseSensitive: false,
- ),
- (m) {
- // If it's already an tag, leave it unchanged
- if (m.group(1) !=
- null) {
- return m.group(1)!;
- }
-
- // Otherwise, wrap the bare URL
- final url = m.group(2)!;
- return "$url";
- },
- )
- .replaceAll("\n", "
"),
- client: room.roomData.client,
- ),
- if (message.editedAt != null)
- Text(
- "(edited)",
- style: theme
- .textTheme
- .labelSmall,
- ),
- ],
- ),
- 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,
- ),
- customImageProvider:
- CachedNetworkImage(
- message.source,
- ref.watch(
- CrossCacheController.provider,
- ),
- headers: room
- .roomData
- .client
- .headers,
- ),
- errorBuilder:
- (context, error, stackTrace) =>
- Center(
- child: Text(
- "Image Failed to Load",
- style: TextStyle(
- color: Theme.of(
- context,
- ).colorScheme.error,
- ),
- ),
- ),
- message: message,
- index: index,
- ),
- fileMessageBuilder:
- (
- _,
- message,
- index, {
- required bool isSentByMe,
- MessageGroupStatus? groupStatus,
- }) => InkWell(
- onTap: () => showDialog(
- context: context,
- builder: (_) => Dialog(
- child: Text(
- "TODO: Download Attachments", // TODO
- ),
- ),
- ),
- 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,
- }) => Text(
- "${message.authorId} sent ${message.metadata?["eventType"]}",
- style: theme.textTheme.labelSmall
- ?.copyWith(color: Colors.grey),
- ),
- ),
- resolveUser: notifier.resolveUser,
- chatController: controller,
- ),
- ),
- ),
- ],
+ actions: [
+ TextButton(
+ onPressed: Navigator.of(context).pop,
+ child: Text("Cancel"),
),
- ),
+ TextButton(
+ onPressed: () {
+ if (room.metadata == null) return;
+ client.reportEvent(
+ ReportRequest(
+ roomId: room.metadata!.id,
+ eventId: message.id,
+ reason: reasonController.text.isEmpty
+ ? null
+ : reasonController.text,
+ ),
+ );
+ Navigator.of(context).pop();
+ },
+ child: Text("Report"),
+ ),
+ ],
+ );
+ },
+ ),
+ ),
+ child: ListTile(
+ leading: Icon(Icons.report, color: danger),
+ title: Text("Report", style: TextStyle(color: danger)),
+ ),
+ ),
+ ];
+ }
- if (memberListOpened.value == true && showMembersByDefault)
- MemberList(room.roomData),
- ],
- ),
+ final chatTheme = ChatTheme.fromThemeData(theme).copyWith(
+ colors: ChatColors.fromThemeData(theme).copyWith(
+ primary: theme.colorScheme.primaryContainer,
+ onPrimary: theme.colorScheme.onPrimaryContainer,
+ ),
+ );
- endDrawer: showMembersByDefault
- ? null
- : MemberList(room.roomData),
- );
- },
- );
+ 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: Column(
+ children: [
+ Expanded(
+ child: ref
+ .watch(controllerProvider)
+ .betterWhen(
+ data: (controller) => Chat(
+ currentUserId: userId,
+ theme: chatTheme,
+ onMessageSecondaryTap:
+ (
+ context,
+ message, {
+ required index,
+ TapUpDetails? details,
+ }) => details?.globalPosition == null
+ ? null
+ : context.showContextMenu(
+ globalPosition: details!.globalPosition,
+ children: getMessageOptions(message),
+ ),
+ onMessageLongPress:
+ (
+ context,
+ message, {
+ required details,
+ required index,
+ }) => context.showContextMenu(
+ globalPosition: details.globalPosition,
+ children: getMessageOptions(message),
+ ),
+ builders: Builders(
+ loadMoreBuilder: (_) => Loading(),
+
+ chatAnimatedListBuilder: (_, itemBuilder) =>
+ ChatAnimatedList(
+ itemBuilder: itemBuilder,
+ onEndReached: room.hasMore
+ ? notifier.loadOlder
+ : null,
+ onStartReached: () => client.markRead(room),
+ bottomPadding: 72,
+ ),
+
+ composerBuilder: (_) => ChatBox(
+ relationType: relationType.value,
+ relatedMessage: replyToMessage.value,
+ onDismiss: () => replyToMessage.value = null,
+ room: room,
+ ),
+
+ textMessageBuilder:
+ (
+ context,
+ message,
+ index, {
+ required bool isSentByMe,
+ MessageGroupStatus? groupStatus,
+ }) => TextMessageWrapper(
+ room: room,
+ message,
+ content: message.text,
+ groupStatus: groupStatus,
+ onTapReply: notifier.scrollToMessage,
+ updateMessage: controller.updateMessage,
+ isSentByMe: isSentByMe,
+ ),
+
+ imageMessageBuilder:
+ (
+ context,
+ message,
+ index, {
+ required bool isSentByMe,
+ MessageGroupStatus? groupStatus,
+ }) => TextMessageWrapper(
+ message,
+ room: room,
+ content: message.text,
+ groupStatus: groupStatus,
+ onTapReply: notifier.scrollToMessage,
+ updateMessage: controller.updateMessage,
+ isSentByMe: isSentByMe,
+ extra: ExpandableImageMessage(
+ message,
+ index: index,
+ ),
+ ),
+
+ fileMessageBuilder:
+ (
+ _,
+ message,
+ index, {
+ required bool isSentByMe,
+ MessageGroupStatus? groupStatus,
+ }) => MessageWrapper(
+ message,
+ InkWell(
+ onTap: () => showDialog(
+ context: context,
+ builder: (_) => Dialog(
+ child: Text(
+ "TODO: Download Attachments",
+ ),
+ ),
+ ),
+ child: FlyerChatFileMessage(
+ topWidget: ReplyWidget(
+ room: room,
+ message,
+ onTapReply: notifier.scrollToMessage,
+ groupStatus: groupStatus,
+ ),
+ message: message,
+ index: index,
+ ),
+ ),
+ groupStatus,
+ ),
+
+ systemMessageBuilder:
+ (
+ _,
+ message,
+ index, {
+ required bool isSentByMe,
+ MessageGroupStatus? groupStatus,
+ }) => FlyerChatSystemMessage(
+ message: message,
+ index: index,
+ ),
+
+ unsupportedMessageBuilder:
+ (
+ _,
+ message,
+ index, {
+ required bool isSentByMe,
+ MessageGroupStatus? groupStatus,
+ }) => Text(
+ "${message.authorId} sent ${message.metadata?["eventType"]}",
+ style: theme.textTheme.labelSmall?.copyWith(
+ color: Colors.grey,
+ ),
+ ),
+ ),
+ resolveUser: notifier.resolveUser,
+ chatController: controller,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ if (memberListOpened.value == true && showMembersByDefault)
+ MemberList(room),
+ ],
+ ),
+
+ endDrawer: showMembersByDefault ? null : MemberList(room),
+ );
}
}
diff --git a/lib/widgets/chat_page/room_menu.dart b/lib/widgets/chat_page/room_menu.dart
index ea123fd..2687bc8 100644
--- a/lib/widgets/chat_page/room_menu.dart
+++ b/lib/widgets/chat_page/room_menu.dart
@@ -1,38 +1,33 @@
+import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/material.dart";
-import "package:flutter/services.dart";
-import "package:flutter_hooks/flutter_hooks.dart";
-import "package:matrix/matrix.dart";
-import "package:nexus/helpers/extensions/room_to_children.dart";
-import "package:nexus/widgets/form_text_input.dart";
+import "package:flutter_riverpod/flutter_riverpod.dart";
+import "package:nexus/controllers/client_controller.dart";
+import "package:nexus/models/room.dart";
-class RoomMenu extends StatelessWidget {
+class RoomMenu extends ConsumerWidget {
final Room room;
- const RoomMenu(this.room, {super.key});
+ final IList children;
+ const RoomMenu(this.room, {this.children = const IList.empty(), super.key});
@override
- Widget build(BuildContext context) {
+ Widget build(BuildContext context, WidgetRef ref) {
final danger = Theme.of(context).colorScheme.error;
-
- void markRead(String roomId) async {
- for (final child in await room.getAllChildren()) {
- await child.roomData.setReadMarker(
- child.roomData.lastEvent?.eventId,
- mRead: child.roomData.lastEvent?.eventId,
- );
- }
- }
+ final client = ref.watch(ClientController.provider.notifier);
return PopupMenuButton(
itemBuilder: (_) => [
+ // PopupMenuItem(
+ // onTap: () async {
+ // final link = await room.matrixToInviteLink();
+ // await Clipboard.setData(ClipboardData(text: link.toString()));
+ // },
+ // child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")),
+ // ),
PopupMenuItem(
onTap: () async {
- final link = await room.matrixToInviteLink();
- await Clipboard.setData(ClipboardData(text: link.toString()));
+ await client.markRead(room);
+ await Future.wait(children.map((child) => client.markRead(child)));
},
- child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")),
- ),
- PopupMenuItem(
- onTap: () => markRead(room.id),
child: ListTile(
leading: Icon(Icons.check),
title: Text("Mark as Read"),
@@ -44,7 +39,7 @@ class RoomMenu extends StatelessWidget {
builder: (context) => AlertDialog(
title: Text("Leave Room"),
content: Text(
- "Are you sure you want to leave \"${room.getLocalizedDisplayname()}\"?",
+ "Are you sure you want to leave \"${room.metadata?.name ?? "Unnamed Room"}\"?",
),
actions: [
TextButton(
@@ -54,10 +49,13 @@ class RoomMenu extends StatelessWidget {
TextButton(
onPressed: () async {
Navigator.of(context).pop();
- final snackbar = ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text("Leaving room...")));
- await room.leave();
+ final snackbar = ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text("Leaving room..."),
+ duration: Duration(days: 1),
+ ),
+ );
+ await client.leaveRoom(room);
snackbar.close();
},
child: Text("Leave"),
@@ -70,53 +68,53 @@ class RoomMenu extends StatelessWidget {
title: Text("Leave", style: TextStyle(color: danger)),
),
),
- PopupMenuItem(
- onTap: () => showDialog(
- context: context,
- builder: (context) => HookBuilder(
- builder: (_) {
- final reasonController = useTextEditingController();
- return AlertDialog(
- title: Text("Report"),
- content: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- "Report this room to your server administrators, who can take action like banning this room.",
- ),
+ // PopupMenuItem(
+ // onTap: () => showDialog(
+ // context: context,
+ // builder: (context) => HookBuilder(
+ // builder: (_) {
+ // final reasonController = useTextEditingController();
+ // return AlertDialog(
+ // title: Text("Report"),
+ // content: Column(
+ // mainAxisSize: MainAxisSize.min,
+ // crossAxisAlignment: CrossAxisAlignment.start,
+ // children: [
+ // Text(
+ // "Report this room to your server administrators, who can take action like banning this room.",
+ // ),
- SizedBox(height: 12),
- FormTextInput(
- required: false,
- capitalize: true,
- controller: reasonController,
- title: "Reason for report (optional)",
- ),
- ],
- ),
- actions: [
- TextButton(
- onPressed: Navigator.of(context).pop,
- child: Text("Cancel"),
- ),
- TextButton(
- onPressed: () {
- room.client.reportRoom(room.id, reasonController.text);
- Navigator.of(context).pop();
- },
- child: Text("Report"),
- ),
- ],
- );
- },
- ),
- ),
- child: ListTile(
- leading: Icon(Icons.report, color: danger),
- title: Text("Report", style: TextStyle(color: danger)),
- ),
- ),
+ // SizedBox(height: 12),
+ // FormTextInput(
+ // required: false,
+ // capitalize: true,
+ // controller: reasonController,
+ // title: "Reason for report (optional)",
+ // ),
+ // ],
+ // ),
+ // actions: [
+ // TextButton(
+ // onPressed: Navigator.of(context).pop,
+ // child: Text("Cancel"),
+ // ),
+ // TextButton(
+ // onPressed: () {
+ // room.client.reportRoom(room.id, reasonController.text);
+ // Navigator.of(context).pop();
+ // },
+ // child: Text("Report"),
+ // ),
+ // ],
+ // );
+ // },
+ // ),
+ // ),
+ // child: ListTile(
+ // leading: Icon(Icons.report, color: danger),
+ // title: Text("Report", style: TextStyle(color: danger)),
+ // ),
+ // ),
],
);
}
diff --git a/lib/widgets/chat_page/sidebar.dart b/lib/widgets/chat_page/sidebar.dart
index a5e1425..4642a58 100644
--- a/lib/widgets/chat_page/sidebar.dart
+++ b/lib/widgets/chat_page/sidebar.dart
@@ -1,4 +1,3 @@
-import "package:collection/collection.dart";
import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
@@ -6,8 +5,6 @@ import "package:nexus/controllers/client_controller.dart";
import "package:nexus/controllers/key_controller.dart";
import "package:nexus/controllers/selected_space_controller.dart";
import "package:nexus/controllers/spaces_controller.dart";
-import "package:nexus/helpers/extensions/better_when.dart";
-import "package:nexus/helpers/extensions/get_headers.dart";
import "package:nexus/helpers/extensions/join_room_with_snackbars.dart";
import "package:nexus/pages/settings_page.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
@@ -22,228 +19,212 @@ class Sidebar extends HookConsumerWidget {
final selectedSpaceProvider = KeyController.provider(
KeyController.spaceKey,
);
- final selectedSpace = ref.watch(selectedSpaceProvider);
- final selectedSpaceNotifier = ref.watch(selectedSpaceProvider.notifier);
+ final selectedSpaceId = ref.watch(selectedSpaceProvider);
+ final selectedSpaceIdNotifier = ref.watch(selectedSpaceProvider.notifier);
final selectedRoomController = KeyController.provider(
KeyController.roomKey,
);
- final selectedRoom = ref.watch(selectedRoomController);
- final selectedRoomNotifier = ref.watch(selectedRoomController.notifier);
+ final selectedRoomId = ref.watch(selectedRoomController);
+ final selectedRoomIdNotifier = ref.watch(selectedRoomController.notifier);
+
+ final spaces = ref.watch(SpacesController.provider);
+ final indexOfSelected = spaces.indexWhere(
+ (space) => space.id == selectedSpaceId,
+ );
+ final selectedIndex = indexOfSelected == -1 ? 0 : indexOfSelected;
+
+ final selectedSpace = ref.watch(SelectedSpaceController.provider);
+
+ final indexOfSelectedRoom = selectedSpace.children.indexWhere(
+ (room) => room.metadata?.id == selectedRoomId,
+ );
+ final selectedRoomIndex = indexOfSelectedRoom == -1
+ ? selectedSpace.children.isEmpty
+ ? null
+ : 0
+ : indexOfSelectedRoom;
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) {
- final indexOfSelected = spaces.indexWhere(
- (space) => space.id == selectedSpace,
- );
- final selectedIndex = indexOfSelected == -1
- ? 0
- : indexOfSelected;
-
- return NavigationRail(
- scrollable: true,
- onDestinationSelected: (value) {
- selectedSpaceNotifier.set(spaces[value].id);
- selectedRoomNotifier.set(
- spaces[value].children.firstOrNull?.roomData.id,
- );
- },
- destinations: spaces
- .map(
- (space) => NavigationRailDestination(
- icon: AvatarOrHash(
- space.avatar,
- fallback: space.icon == null
- ? null
- : Icon(space.icon),
- space.title,
- headers: space.client.headers,
- hasBadge:
- space.children.firstWhereOrNull(
- (room) => room.roomData.hasNewMessages,
- ) !=
- null,
- ),
- label: Text(space.title),
- padding: EdgeInsets.only(top: 4),
- ),
- )
- .toList(),
- selectedIndex: selectedIndex,
- trailingAtBottom: true,
- trailing: Padding(
- padding: EdgeInsets.symmetric(vertical: 16),
- child: Column(
- spacing: 8,
- children: [
- PopupMenuButton(
- itemBuilder: (_) => [
- PopupMenuItem(
- onTap: () => showDialog(
- context: context,
- builder: (alertContext) => HookBuilder(
- builder: (_) {
- final roomAlias =
- useTextEditingController();
- return AlertDialog(
- title: Text("Join a Room"),
- content: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment:
- CrossAxisAlignment.start,
- children: [
- Text(
- "Enter the room alias, ID, or a Matrix.to link.",
- ),
- SizedBox(height: 12),
- FormTextInput(
- required: false,
- capitalize: true,
- controller: roomAlias,
- title: "#room:server",
- ),
- ],
- ),
- actions: [
- TextButton(
- onPressed: Navigator.of(
- context,
- ).pop,
- child: Text("Cancel"),
- ),
- TextButton(
- onPressed: () async {
- Navigator.of(alertContext).pop();
-
- final client = await ref.watch(
- ClientController
- .provider
- .future,
- );
- if (context.mounted) {
- client.joinRoomWithSnackBars(
- context,
- roomAlias.text,
- ref,
- );
- }
- },
- child: Text("Join"),
- ),
- ],
- );
- },
- ),
- ),
- child: ListTile(
- title: Text(
- "Join an existing room (or space)",
- ),
- leading: Icon(Icons.numbers),
- ),
- ),
- PopupMenuItem(
- onTap: () {},
- child: ListTile(
- title: Text("Create a new room"),
- leading: Icon(Icons.add),
- ),
- ),
- ],
- icon: Icon(Icons.add),
- ),
- IconButton(
- onPressed: () => showDialog(
- context: context,
- builder: (context) =>
- AlertDialog(title: Text("To-do")),
- ),
- icon: Icon(Icons.explore),
- ),
- IconButton(
- onPressed: () => Navigator.of(context).push(
- MaterialPageRoute(builder: (_) => SettingsPage()),
- ),
- icon: Icon(Icons.settings),
- ),
- ],
+ NavigationRail(
+ scrollable: true,
+ onDestinationSelected: (value) {
+ selectedSpaceIdNotifier.set(spaces[value].id);
+ selectedRoomIdNotifier.set(
+ spaces[value].children.firstOrNull?.metadata?.id,
+ );
+ },
+ destinations: spaces
+ .map(
+ (space) => NavigationRailDestination(
+ icon: AvatarOrHash(
+ space.room?.metadata?.avatar,
+ fallback: space.icon == null ? null : Icon(space.icon),
+ space.title,
+ hasBadge: space.children.any(
+ (room) => room.metadata?.unreadMessages != 0,
+ ),
+ badgeNumber: space.children.fold(
+ 0,
+ (previousValue, room) =>
+ previousValue +
+ (room.metadata?.unreadNotifications ?? 0),
),
),
- );
- },
- ),
- Expanded(
- child: ref
- .watch(SelectedSpaceController.provider)
- .betterWhen(
- data: (space) {
- final indexOfSelected = space.children.indexWhere(
- (room) => room.roomData.id == selectedRoom,
- );
- final selectedIndex = indexOfSelected == -1
- ? space.children.isEmpty
- ? null
- : 0
- : indexOfSelected;
-
- return Scaffold(
- backgroundColor: Colors.transparent,
- appBar: AppBar(
- leading: AvatarOrHash(
- space.avatar,
- fallback: space.icon == null
- ? null
- : Icon(space.icon),
- space.title,
- headers: space.client.headers,
- ),
- title: Text(
- space.title,
- overflow: TextOverflow.ellipsis,
- ),
- backgroundColor: Colors.transparent,
- actions: [
- if (space.roomData != null) RoomMenu(space.roomData!),
- ],
- ),
- body: NavigationRail(
- scrollable: true,
- backgroundColor: Colors.transparent,
- extended: true,
- selectedIndex: selectedIndex,
- destinations: space.children
- .map(
- (room) => NavigationRailDestination(
- label: Text(room.title),
- icon: AvatarOrHash(
- hasBadge: room.roomData.hasNewMessages,
- room.avatar,
- room.title,
- fallback: selectedSpace == "dms"
- ? null
- : Icon(Icons.numbers),
- headers: space.client.headers,
+ label: Text(space.title),
+ padding: EdgeInsets.only(top: 4),
+ ),
+ )
+ .toList(),
+ selectedIndex: selectedIndex,
+ trailingAtBottom: true,
+ trailing: Padding(
+ padding: EdgeInsets.symmetric(vertical: 16),
+ child: Column(
+ spacing: 8,
+ children: [
+ PopupMenuButton(
+ itemBuilder: (_) => [
+ PopupMenuItem(
+ onTap: () => showDialog(
+ context: context,
+ builder: (alertContext) => HookBuilder(
+ builder: (_) {
+ final roomAlias = useTextEditingController();
+ return AlertDialog(
+ title: Text("Join a Room"),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "Enter the room alias, ID, or a Matrix.to link.",
+ ),
+ SizedBox(height: 12),
+ FormTextInput(
+ required: false,
+ capitalize: true,
+ controller: roomAlias,
+ title: "#room:server",
+ ),
+ ],
),
- ),
- )
- .toList(),
- onDestinationSelected: (value) => selectedRoomNotifier
- .set(space.children[value].roomData.id),
+ actions: [
+ TextButton(
+ onPressed: Navigator.of(context).pop,
+ child: Text("Cancel"),
+ ),
+ TextButton(
+ onPressed: () async {
+ Navigator.of(alertContext).pop();
+
+ final client = ref.watch(
+ ClientController.provider.notifier,
+ );
+ if (context.mounted) {
+ client.joinRoomWithSnackBars(
+ context,
+ roomAlias.text,
+ ref,
+ );
+ }
+ },
+ child: Text("Join"),
+ ),
+ ],
+ );
+ },
+ ),
+ ),
+ child: ListTile(
+ title: Text("Join an existing room (or space)"),
+ leading: Icon(Icons.numbers),
+ ),
),
- );
- },
+ PopupMenuItem(
+ onTap: () {},
+ child: ListTile(
+ title: Text("Create a new room"),
+ leading: Icon(Icons.add),
+ ),
+ ),
+ ],
+ icon: Icon(Icons.add),
+ ),
+ IconButton(
+ tooltip: "Explore other rooms",
+ onPressed: () => showDialog(
+ context: context,
+ builder: (context) => AlertDialog(title: Text("To-do")),
+ ),
+ icon: Icon(Icons.explore),
+ ),
+ IconButton(
+ tooltip: "Open settings",
+ onPressed: () => Navigator.of(
+ context,
+ ).push(MaterialPageRoute(builder: (_) => SettingsPage())),
+ icon: Icon(Icons.settings),
+ ),
+ ],
+ ),
+ ),
+ ),
+ Expanded(
+ child: Scaffold(
+ backgroundColor: Colors.transparent,
+ appBar: AppBar(
+ leading: AvatarOrHash(
+ selectedSpace.room?.metadata?.avatar,
+ fallback: selectedSpace.icon == null
+ ? null
+ : Icon(selectedSpace.icon),
+
+ selectedSpace.title,
),
+ title: Text(
+ selectedSpace.title,
+ overflow: TextOverflow.ellipsis,
+ ),
+ backgroundColor: Colors.transparent,
+ actions: [
+ if (selectedSpace.room != null)
+ RoomMenu(
+ selectedSpace.room!,
+ children: selectedSpace.children,
+ ),
+ ],
+ ),
+ body: NavigationRail(
+ scrollable: true,
+ backgroundColor: Colors.transparent,
+ extended: true,
+ selectedIndex: selectedRoomIndex,
+ destinations: selectedSpace.children
+ .map(
+ (room) => NavigationRailDestination(
+ label: Text(room.metadata?.name ?? "Unnamed Room"),
+ icon: AvatarOrHash(
+ room.metadata?.avatar,
+ hasBadge: room.metadata?.unreadMessages != 0,
+ badgeNumber: room.metadata?.unreadNotifications ?? 0,
+ room.metadata?.name ?? "Unnamed Room",
+ fallback: selectedSpaceId == "dms"
+ ? null
+ : Icon(Icons.numbers),
+ // space.client.headers,
+ ),
+ ),
+ )
+ .toList(),
+ onDestinationSelected: (value) => selectedRoomIdNotifier.set(
+ selectedSpace.children[value].metadata?.id,
+ ),
+ ),
+ ),
),
],
),
diff --git a/lib/widgets/chat_page/text_message_wrapper.dart b/lib/widgets/chat_page/text_message_wrapper.dart
new file mode 100644
index 0000000..9734a34
--- /dev/null
+++ b/lib/widgets/chat_page/text_message_wrapper.dart
@@ -0,0 +1,114 @@
+import "package:flutter/material.dart";
+import "package:flutter_chat_core/flutter_chat_core.dart";
+import "package:flutter_link_previewer/flutter_link_previewer.dart";
+import "package:nexus/models/room.dart";
+import "package:nexus/widgets/chat_page/html/html.dart";
+import "package:nexus/widgets/chat_page/message_wrapper.dart";
+import "package:nexus/widgets/chat_page/reply_widget.dart";
+
+class TextMessageWrapper extends StatelessWidget {
+ final Message message;
+ final String? content;
+ final Room room;
+ final MessageGroupStatus? groupStatus;
+ final Future Function(Message oldMessage, Message newMessage)
+ updateMessage;
+ final bool isSentByMe;
+ final Widget? extra;
+ final OnTapReply onTapReply;
+
+ const TextMessageWrapper(
+ this.message, {
+ this.content,
+ this.onTapReply,
+ required this.room,
+ required this.updateMessage,
+ required this.groupStatus,
+ required this.isSentByMe,
+ this.extra,
+ super.key,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
+ final textMessage = message is TextMessage ? message as TextMessage : null;
+
+ return MessageWrapper(
+ message,
+ ClipRRect(
+ borderRadius: BorderRadius.all(Radius.circular(8)),
+ child: Container(
+ padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12),
+ decoration: BoxDecoration(
+ color: isSentByMe
+ ? colorScheme.primaryContainer
+ : colorScheme.surfaceContainer,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ ReplyWidget(
+ message,
+ room: room,
+ groupStatus: groupStatus,
+ onTapReply: onTapReply,
+ ),
+ if (content != null)
+ Html(
+ textStyle: message.metadata?["big"] == true
+ ? TextStyle(fontSize: 32)
+ : null,
+ content!
+ .replaceAllMapped(
+ RegExp(
+ "(]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)",
+ caseSensitive: false,
+ ),
+ (m) {
+ // If it's already an tag, leave it unchanged
+ if (m.group(1) != null) {
+ return m.group(1)!;
+ }
+
+ // Otherwise, wrap the bare URL
+ final url = m.group(2)!;
+ return "$url";
+ },
+ )
+ .replaceAll("\n", "
"),
+ ),
+ if (textMessage?.editedAt != null)
+ Text("(edited)", style: theme.textTheme.labelSmall),
+ if (textMessage != null)
+ LinkPreview(
+ text: textMessage.text,
+ backgroundColor: isSentByMe
+ ? colorScheme.inversePrimary
+ : colorScheme.surfaceContainerLow,
+ outsidePadding: EdgeInsets.only(top: 4),
+ insidePadding: EdgeInsets.symmetric(
+ vertical: 8,
+ horizontal: 16,
+ ),
+ linkPreviewData: message.metadata?["linkPreviewData"],
+ onLinkPreviewDataFetched: (linkPreviewData) => updateMessage(
+ message,
+ message.copyWith(
+ metadata: {
+ ...(message.metadata ?? {}),
+ "linkPreviewData": linkPreviewData,
+ },
+ ),
+ ),
+ ),
+ if (extra != null) extra!,
+ ],
+ ),
+ ),
+ ),
+ groupStatus,
+ );
+ }
+}
diff --git a/lib/widgets/chat_page/top_widget.dart b/lib/widgets/chat_page/top_widget.dart
deleted file mode 100644
index 7831cb9..0000000
--- a/lib/widgets/chat_page/top_widget.dart
+++ /dev/null
@@ -1,123 +0,0 @@
-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/widgets/chat_page/html/quoted.dart";
-
-class TopWidget extends ConsumerWidget {
- final Message message;
- final bool alwaysShow;
- final Map headers;
- final MessageGroupStatus? groupStatus;
- const TopWidget(
- this.message, {
- required this.headers,
- required this.groupStatus,
- this.alwaysShow = false,
- super.key,
- });
-
- @override
- Widget build(BuildContext context, WidgetRef ref) => Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Builder(
- builder: (_) {
- final replyMessage = message.metadata?["reply"] as TextMessage?;
-
- if (replyMessage == null) return SizedBox.shrink();
- final smallerText = message is TextMessage
- ? replyMessage.text.substring(
- 0,
- min(
- max(
- max(
- (message as TextMessage).text.length - 20,
- message.metadata?["displayName"].length,
- ),
- 5,
- ),
- replyMessage.text.length,
- ),
- )
- : null;
- final replyText =
- (smallerText == null ||
- smallerText.length == replyMessage.text.length)
- ? replyMessage.text
- : "$smallerText...";
-
- return Padding(
- padding: EdgeInsets.only(bottom: 12),
- child: InkWell(
- onTap: () => showDialog(
- context: context,
- builder: (_) => Dialog(
- child: Text("TODO: Scroll to original message"),
- ), // TODO
- ),
- child: Quoted(
- 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,
- ),
- ),
- ],
- ),
- ),
- ),
- );
- },
- ),
- if (alwaysShow ||
- groupStatus?.isFirst != false ||
- message.metadata?["reply"] != null)
- InkWell(
- onTap: () => showDialog(
- context: context,
- builder: (_) =>
- Dialog(child: Text("TODO: Show user profile")), // TODO
- ),
- 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),
- ],
- );
-}
diff --git a/lib/widgets/form_text_input.dart b/lib/widgets/form_text_input.dart
index 492439b..21b2e5c 100644
--- a/lib/widgets/form_text_input.dart
+++ b/lib/widgets/form_text_input.dart
@@ -20,11 +20,13 @@ class FormTextInput extends StatelessWidget {
final Widget? trailing;
final InputBorder? border;
final List? formatters;
+ final bool autofocus;
const FormTextInput({
super.key,
this.border,
this.controller,
+ this.autofocus = false,
this.title,
this.obscure = false,
this.readOnly = false,
@@ -45,6 +47,7 @@ class FormTextInput extends StatelessWidget {
@override
Widget build(BuildContext context) => TextFormField(
+ autofocus: autofocus,
controller: controller,
keyboardType: keyboardType,
readOnly: readOnly,
diff --git a/lib/widgets/loading.dart b/lib/widgets/loading.dart
index aadc43c..9bb2858 100644
--- a/lib/widgets/loading.dart
+++ b/lib/widgets/loading.dart
@@ -1,13 +1,14 @@
import "package:flutter/material.dart";
class Loading extends StatelessWidget {
- const Loading({super.key});
+ final double? height;
+ const Loading({this.height, super.key});
@override
- Widget build(BuildContext context) => const Center(
- child: Padding(
- padding: EdgeInsets.all(16),
- child: CircularProgressIndicator(),
- ),
- );
+ Widget build(BuildContext context) => Center(
+ child: Padding(
+ padding: EdgeInsets.all(16),
+ child: SizedBox(height: height, child: CircularProgressIndicator()),
+ ),
+ );
}
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
index dffacff..f70fb6e 100644
--- a/linux/flutter/generated_plugin_registrant.cc
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -8,7 +8,6 @@
#include
#include
-#include
#include
#include
#include
@@ -21,9 +20,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
- g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
- fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
- flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
index 1cac43c..78dcf40 100644
--- a/linux/flutter/generated_plugins.cmake
+++ b/linux/flutter/generated_plugins.cmake
@@ -5,7 +5,6 @@
list(APPEND FLUTTER_PLUGIN_LIST
dynamic_system_colors
file_selector_linux
- flutter_secure_storage_linux
screen_retriever_linux
url_launcher_linux
window_manager
@@ -13,7 +12,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
- flutter_vodozemac
)
set(PLUGIN_BUNDLED_LIBRARIES)
diff --git a/nix/fake-rustup.sh b/nix/fake-rustup.sh
deleted file mode 100644
index 7884c05..0000000
--- a/nix/fake-rustup.sh
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/usr/bin/env bash
-# Fake rustup for nix-managed Rust toolchains
-
-case "$1" in
- run)
- if [[ "$2" == "stable" ]]; then
- shift 2
- if [[ $# -eq 0 ]]; then
- echo "fake rustup: no command given" >&2
- exit 1
- fi
- exec "$@"
- exit 0
- fi
- ;;
-
- toolchain)
- if [[ "$2" == "list" ]]; then
- echo "stable (default)"
- exit 0
- fi
- ;;
-
- target)
- if [[ "$2" == "list" && "$3" == "--toolchain" && "$4" == "stable" && "$5" == "--installed" ]]; then
- echo "x86_64-unknown-linux-gnu"
- exit 0
- fi
- ;;
-esac
-
-echo "fake rustup: the command:" >&2
-echo " rustup $*" >&2
-echo "…is not mocked yet" >&2
-exit 1
\ No newline at end of file
diff --git a/pubspec.lock b/pubspec.lock
index 01d55a5..da5de89 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -73,14 +73,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.13.0"
- base58check:
- dependency: transitive
- description:
- name: base58check
- sha256: "6c300dfc33e598d2fe26319e13f6243fea81eaf8204cb4c6b69ef20a625319a5"
- url: "https://pub.dev"
- source: hosted
- version: "2.0.0"
blurhash_dart:
dependency: transitive
description:
@@ -105,14 +97,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.3"
- build_cli_annotations:
- dependency: transitive
- description:
- name: build_cli_annotations
- sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95
- url: "https://pub.dev"
- source: hosted
- version: "2.1.1"
build_config:
dependency: transitive
description:
@@ -153,14 +137,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.12.1"
- canonical_json:
- dependency: transitive
- description:
- name: canonical_json
- sha256: d6be1dd66b420c6ac9f42e3693e09edf4ff6edfee26cb4c28c1c019fdb8c0c15
- url: "https://pub.dev"
- source: hosted
- version: "1.1.2"
characters:
dependency: transitive
description:
@@ -217,6 +193,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
+ code_assets:
+ dependency: "direct main"
+ description:
+ name: code_assets
+ sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
code_builder:
dependency: transitive
description:
@@ -394,13 +378,21 @@ packages:
source: hosted
version: "11.1.0"
ffi:
- dependency: transitive
+ dependency: "direct main"
description:
name: ffi
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
url: "https://pub.dev"
source: hosted
version: "2.1.5"
+ ffigen:
+ dependency: "direct main"
+ description:
+ name: ffigen
+ sha256: b7803707faeec4ce3c1b0c2274906504b796e3b70ad573577e72333bd1c9b3ba
+ url: "https://pub.dev"
+ source: hosted
+ version: "20.1.1"
file:
dependency: transitive
description:
@@ -517,14 +509,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
- flutter_math_fork:
- dependency: transitive
- description:
- name: flutter_math_fork
- sha256: "6d5f2f1aa57ae539ffb0a04bb39d2da67af74601d685a161aff7ce5bda5fa407"
- url: "https://pub.dev"
- source: hosted
- version: "0.7.4"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@@ -541,62 +525,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.0"
- flutter_rust_bridge:
- dependency: transitive
- description:
- name: flutter_rust_bridge
- sha256: "37ef40bc6f863652e865f0b2563ea07f0d3c58d8efad803cc01933a4b2ee067e"
- url: "https://pub.dev"
- source: hosted
- version: "2.11.1"
- flutter_secure_storage:
- dependency: "direct main"
- description:
- name: flutter_secure_storage
- sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40
- url: "https://pub.dev"
- source: hosted
- version: "10.0.0"
- flutter_secure_storage_darwin:
- dependency: transitive
- description:
- name: flutter_secure_storage_darwin
- sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3"
- url: "https://pub.dev"
- source: hosted
- version: "0.2.0"
- flutter_secure_storage_linux:
- dependency: transitive
- description:
- name: flutter_secure_storage_linux
- sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda"
- url: "https://pub.dev"
- source: hosted
- version: "3.0.0"
- flutter_secure_storage_platform_interface:
- dependency: transitive
- description:
- name: flutter_secure_storage_platform_interface
- sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6"
- url: "https://pub.dev"
- source: hosted
- version: "2.0.1"
- flutter_secure_storage_web:
- dependency: transitive
- description:
- name: flutter_secure_storage_web
- sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3"
- url: "https://pub.dev"
- source: hosted
- version: "2.1.0"
- flutter_secure_storage_windows:
- dependency: transitive
- description:
- name: flutter_secure_storage_windows
- sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613"
- url: "https://pub.dev"
- source: hosted
- version: "4.1.0"
flutter_svg:
dependency: "direct main"
description:
@@ -610,14 +538,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
- flutter_vodozemac:
- dependency: "direct main"
- description:
- name: flutter_vodozemac
- sha256: "16d4b44dd338689441fe42a80d0184e5c864e9563823de9e7e6371620d2c0590"
- url: "https://pub.dev"
- source: hosted
- version: "0.4.1"
flutter_web_plugins:
dependency: transitive
description: flutter
@@ -663,15 +583,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
- flyer_chat_text_message:
- dependency: "direct main"
- description:
- path: "packages/flyer_chat_text_message"
- ref: HEAD
- resolved-ref: "03be67c8c81c8f637672ee03dd8f082d2c223627"
- url: "https://github.com/Henry-Hiles/flutter_chat_ui"
- source: git
- version: "2.6.0"
freezed:
dependency: "direct dev"
description:
@@ -712,14 +623,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.3"
- gpt_markdown:
- dependency: transitive
- description:
- name: gpt_markdown
- sha256: "9b88dfaffea644070b648c204ca4a55745a49f4ad0b58ed0ab70913ad593c7a1"
- url: "https://pub.dev"
- source: hosted
- version: "1.1.5"
graphs:
dependency: transitive
description:
@@ -728,6 +631,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
+ hooks:
+ dependency: "direct main"
+ description:
+ name: hooks
+ sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
hooks_riverpod:
dependency: "direct main"
description:
@@ -744,14 +655,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.15.6"
- html_unescape:
- dependency: transitive
- description:
- name: html_unescape
- sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3"
- url: "https://pub.dev"
- source: hosted
- version: "2.0.0"
http:
dependency: transitive
description:
@@ -936,14 +839,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
- markdown:
- dependency: transitive
- description:
- name: markdown
- sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
- url: "https://pub.dev"
- source: hosted
- version: "7.3.0"
matcher:
dependency: transitive
description:
@@ -960,22 +855,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.11.1"
- matrix:
- dependency: "direct main"
- description:
- name: matrix
- sha256: fb116ee89f6871441f22f76a988db15cfcfb6dfac97e3e2d654c240080015707
- url: "https://pub.dev"
- source: hosted
- version: "4.1.0"
- mention_tag_text_field:
- dependency: "direct main"
- description:
- name: mention_tag_text_field
- sha256: ba7b9d8003e0f340a65c6dcdb7770f4340f653ae1612a9e31e11d12f7f1dd80f
- url: "https://pub.dev"
- source: hosted
- version: "0.0.9"
meta:
dependency: transitive
description:
@@ -1160,14 +1039,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
- random_string:
+ quiver:
dependency: transitive
description:
- name: random_string
- sha256: "03b52435aae8cbdd1056cf91bfc5bf845e9706724dd35ae2e99fa14a1ef79d02"
+ name: quiver
+ sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
url: "https://pub.dev"
source: hosted
- version: "2.3.1"
+ version: "3.2.2"
riverpod:
dependency: transitive
description:
@@ -1248,14 +1127,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.26.3"
- sdp_transform:
- dependency: transitive
- description:
- name: sdp_transform
- sha256: "73e412a5279a5c2de74001535208e20fff88f225c9a4571af0f7146202755e45"
- url: "https://pub.dev"
- source: hosted
- version: "0.3.2"
sembast:
dependency: transitive
description:
@@ -1357,14 +1228,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
- slugify:
- dependency: transitive
- description:
- name: slugify
- sha256: b272501565cb28050cac2d96b7bf28a2d24c8dae359280361d124f3093d337c3
- url: "https://pub.dev"
- source: hosted
- version: "2.0.0"
source_gen:
dependency: "direct overridden"
description:
@@ -1405,30 +1268,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.1"
- sqflite_common:
- dependency: transitive
- description:
- name: sqflite_common
- sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
- url: "https://pub.dev"
- source: hosted
- version: "2.5.6"
- sqflite_common_ffi:
- dependency: "direct main"
- description:
- name: sqflite_common_ffi
- sha256: "8d7b8749a516cbf6e9057f9b480b716ad14fc4f3d3873ca6938919cc626d9025"
- url: "https://pub.dev"
- source: hosted
- version: "2.3.7+1"
- sqlite3:
- dependency: transitive
- description:
- name: sqlite3
- sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2"
- url: "https://pub.dev"
- source: hosted
- version: "2.9.4"
stack_trace:
dependency: transitive
description:
@@ -1517,14 +1356,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.0+1"
- tuple:
- dependency: transitive
- description:
- name: tuple
- sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
- url: "https://pub.dev"
- source: hosted
- version: "2.0.2"
typed_data:
dependency: transitive
description:
@@ -1549,14 +1380,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.1"
- unorm_dart:
- dependency: transitive
- description:
- name: unorm_dart
- sha256: "5b35bff83fce4d76467641438f9e867dc9bcfdb8c1694854f230579d68cd8f4b"
- url: "https://pub.dev"
- source: hosted
- version: "0.2.0"
url_launcher:
dependency: "direct main"
description:
@@ -1669,15 +1492,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "15.0.2"
- vodozemac:
- dependency: "direct main"
- description:
- path: dart
- ref: "krille/use-specced-olm-session-config"
- resolved-ref: "8770e0555b1bb692e3e1a43a7726b27eae285b20"
- url: "https://github.com/famedly/dart-vodozemac"
- source: git
- version: "0.4.0"
watcher:
dependency: transitive
description:
@@ -1718,14 +1532,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.1"
- webrtc_interface:
- dependency: transitive
- description:
- name: webrtc_interface
- sha256: "2e604a31703ad26781782fb14fa8a4ee621154ee2c513d2b9938e486fa695233"
- url: "https://pub.dev"
- source: hosted
- version: "1.3.0"
win32:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index e733f2e..3c0198d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -12,11 +12,6 @@ environment:
sdk: "^3.9.2"
dependency_overrides:
- vodozemac:
- git:
- url: https://github.com/famedly/dart-vodozemac
- ref: krille/use-specced-olm-session-config
- path: dart
analyzer: ^8.4.0
source_gen: ^4.0.2
flutter_hooks: ^0.21.2
@@ -47,10 +42,6 @@ dependencies:
flyer_chat_image_message: ^2.2.2
flyer_chat_system_message: ^2.1.13
flyer_chat_file_message: ^2.3.1
- flyer_chat_text_message:
- git:
- url: https://github.com/Henry-Hiles/flutter_chat_ui
- path: packages/flyer_chat_text_message
flutter_chat_ui:
git:
url: https://github.com/Henry-Hiles/flutter_chat_ui
@@ -59,21 +50,19 @@ dependencies:
git:
url: https://github.com/Henry-Hiles/flutter_chat_ui
path: packages/flutter_link_previewer
- matrix: ^4.1.0
- sqflite_common_ffi: ^2.3.6
color_hash: ^1.0.1
- flutter_vodozemac: ^0.4.1
flutter_widget_from_html_core: ^0.17.0
flutter_svg: ^2.2.2
json_annotation: ^4.9.0
- vodozemac: ^0.4.0
shared_preferences: ^2.5.3
- mention_tag_text_field: ^0.0.9
fluttertagger: ^2.3.1
- flutter_secure_storage: ^10.0.0
dynamic_polls: ^0.0.6
flutter_hooks: ^0.21.3+1
cross_cache: ^1.1.0
+ ffi: ^2.1.5
+ hooks: ^1.0.0
+ code_assets: ^1.0.0
+ ffigen: ^20.1.1
dev_dependencies:
build_runner: ^2.4.11
diff --git a/scripts/generate.dart b/scripts/generate.dart
new file mode 100644
index 0000000..b240d98
--- /dev/null
+++ b/scripts/generate.dart
@@ -0,0 +1,45 @@
+import "dart:io";
+import "package:ffigen/ffigen.dart";
+import "package:path/path.dart";
+
+void main(List args) async {
+ final repoDir = Directory.fromUri(
+ Platform.script.resolve("../src/gomuks/source"),
+ );
+ if (await repoDir.exists()) await repoDir.delete(recursive: true);
+ await repoDir.create(recursive: true);
+
+ print("Cloning Gomuks repository...");
+ final cloneResult = await Process.run("git", [
+ "clone",
+ "--depth",
+ "1",
+ "https://mau.dev/gomuks/gomuks",
+ repoDir.path,
+ ]);
+
+ if (cloneResult.exitCode != 0) {
+ throw Exception(
+ "Failed to clone Gomuks repository: \n${cloneResult.stderr}",
+ );
+ }
+
+ print("Generating FFI Bindings...");
+
+ final libclangPath = Platform.environment["LIBCLANG_PATH"];
+ FfiGenerator(
+ output: Output(
+ dartFile: Platform.script.resolve("../lib/src/third_party/gomuks.g.dart"),
+ ),
+ headers: Headers(
+ entryPoints: [File(join(repoDir.path, "pkg", "ffi", "gomuksffi.h")).uri],
+ compilerOptions: ["--no-warnings"],
+ ),
+ functions: Functions.includeAll,
+ ).generate(
+ libclangDylib: libclangPath == null
+ ? null
+ : Uri.file(join(libclangPath, "libclang.so")),
+ );
+ print("Done!");
+}
diff --git a/scripts/generate.sh b/scripts/generate.sh
new file mode 100755
index 0000000..6076ab8
--- /dev/null
+++ b/scripts/generate.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+pushd "$(dirname "$(readlink -f "$0")")"/.. > /dev/null || exit
+
+mkdir -p build
+touch build/lock
+dart scripts/generate.dart
+rm build/lock
+
+popd > /dev/null || exit
\ No newline at end of file
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index b12edca..55fb066 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -8,7 +8,6 @@
#include
#include
-#include
#include
#include
#include
@@ -19,8 +18,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
- FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
- registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index 3c6fdca..9333a2f 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -5,7 +5,6 @@
list(APPEND FLUTTER_PLUGIN_LIST
dynamic_system_colors
file_selector_windows
- flutter_secure_storage_windows
screen_retriever_windows
url_launcher_windows
window_manager
@@ -13,7 +12,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
- flutter_vodozemac
)
set(PLUGIN_BUNDLED_LIBRARIES)