tags,
+ })
+ onSend;
const ChatBox({
required this.relatedMessage,
required this.relationType,
required this.onDismiss,
- required this.room,
+ required this.onSend,
+ this.node,
super.key,
});
@@ -39,42 +46,28 @@ class ChatBox extends HookConsumerWidget {
}
void send() {
- if (controller.value.text.trim().isEmpty || room.metadata == null) return;
- ref
- .watch(RoomChatController.provider(room.metadata!.id).notifier)
- .send(
- controller.value.formattedText,
- shouldMention: shouldMention.value,
- relation: relatedMessage,
- relationType: relationType,
- tags: controller.value.tags,
- );
+ if (controller.value.text.isEmpty) return;
+ onSend(
+ controller.value.formattedText,
+ shouldMention: shouldMention.value,
+ tags: controller.value.tags.toIList(),
+ );
+
onDismiss();
controller.value.text = "";
}
- final node = useFocusNode(
- onKeyEvent: (_, event) {
- if (event is KeyDownEvent && !Platform.isAndroid && !Platform.isIOS) {
- if (event.logicalKey == LogicalKeyboardKey.enter &&
- !HardwareKeyboard.instance.isShiftPressed) {
- send();
- return KeyEventResult.handled;
- } else if (event.logicalKey == LogicalKeyboardKey.escape) {
- onDismiss();
- return KeyEventResult.handled;
- }
- }
-
- return KeyEventResult.ignored;
- },
- )..requestFocus();
-
final style = TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
);
+ final canSendMessages = ref.watch(
+ PowerLevelController.provider(
+ PowerLevelConfig(eventType: "m.room.message"),
+ ),
+ );
+
return Positioned(
bottom: 0,
left: 0,
@@ -87,7 +80,6 @@ class ChatBox extends HookConsumerWidget {
children: [
RelationPreview(
relatedMessage,
- room: room,
shouldMention: shouldMention.value,
toggleShouldMention: () =>
shouldMention.value = !shouldMention.value,
@@ -99,75 +91,92 @@ class ChatBox extends HookConsumerWidget {
padding: EdgeInsets.symmetric(horizontal: 8),
child: Row(
spacing: 8,
- children: [
- PopupMenuButton(
- tooltip: "Add media",
- itemBuilder: (context) => [
- PopupMenuItem(
- child: ListTile(
- title: Text("Camera"),
- leading: Icon(Icons.add_a_photo),
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: canSendMessages
+ ? [
+ EmojiPickerButton(
+ context: context,
+ onSelection: (_) => node?.requestFocus(),
+ controller: controller.value,
),
- ),
- PopupMenuItem(
- child: ListTile(
- title: Text("Gallery"),
- leading: Icon(Icons.add_photo_alternate),
+ PopupMenuButton(
+ tooltip: "Add media",
+ enabled: canSendMessages,
+ 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),
),
- ),
- PopupMenuItem(
- child: ListTile(
- title: Text("Files"),
- leading: Icon(Icons.attachment),
+ Expanded(
+ child: FlutterTagger(
+ triggerStrategy: TriggerStrategy.eager,
+ overlay: MentionOverlay(
+ query: query.value,
+ triggerCharacter: triggerCharacter.value,
+ addTag: ({required id, required name}) {
+ controller.value.addTag(id: id, name: name);
+ node?.requestFocus();
+ },
+ ),
+ controller: controller.value,
+ onSearch: (newQuery, newTriggerCharacter) {
+ triggerCharacter.value = newTriggerCharacter;
+ query.value = newQuery;
+ },
+ triggerCharacterAndStyles: {
+ "@": style,
+ "#": style,
+ },
+ builder: (context, key) => TextFormField(
+ enabled: canSendMessages,
+ maxLines: 12,
+ minLines: 1,
+ autofocus: true,
+ decoration: InputDecoration(
+ hintText: "Your message here...",
+ border: InputBorder.none,
+ ),
+ controller: controller.value,
+ key: key,
+ onFieldSubmitted: (_) => send(),
+ // Don't defocus on submit
+ onEditingComplete: () {},
+ textInputAction: TextInputAction.done,
+ focusNode: node,
+ ),
+ ),
),
- ),
- ],
- icon: Icon(Icons.add),
- // enabled: room.canSendDefaultMessages, TODO: Permissions check
- ),
- Expanded(
- child: FlutterTagger(
- triggerStrategy: TriggerStrategy.eager,
- overlay: MentionOverlay(
- room,
- query: query.value,
- triggerCharacter: triggerCharacter.value,
- addTag: ({required id, required name}) {
- controller.value.addTag(id: id, name: name);
- node.requestFocus();
- },
- ),
- controller: controller.value,
- onSearch: (newQuery, newTriggerCharacter) {
- triggerCharacter.value = newTriggerCharacter;
- query.value = newQuery;
- },
- triggerCharacterAndStyles: {"@": style, "#": style},
- builder: (context, key) => TextFormField(
- // enabled: room.canSendDefaultMessages,
- maxLines: 12,
- minLines: 1,
- decoration: InputDecoration(
- hintText:
- true // TODO: room.canSendDefaultMessages
- ? "Your message here..."
- : "You don't have permission to send messages in this room...",
- border: InputBorder.none,
+ IconButton(
+ onPressed: !canSendMessages ? null : send,
+ icon: Icon(Icons.send),
+ tooltip: "Send message",
),
- controller: controller.value,
- key: key,
- autofocus: true,
- focusNode: node,
- ),
- ),
- ),
- IconButton(
- onPressed: send,
- // onPressed: room.canSendDefaultMessages ? send : null,
- icon: Icon(Icons.send),
- tooltip: "Send message",
- ),
- ],
+ ]
+ : [
+ Padding(
+ padding: EdgeInsetsGeometry.all(8),
+ child: Text(
+ "You don't have permission to send messages in this room...",
+ ),
+ ),
+ ],
),
),
],
diff --git a/lib/widgets/chat_page/composer/mention_overlay.dart b/lib/widgets/chat_page/composer/mention_overlay.dart
index d95253d..b650421 100644
--- a/lib/widgets/chat_page/composer/mention_overlay.dart
+++ b/lib/widgets/chat_page/composer/mention_overlay.dart
@@ -1,19 +1,18 @@
import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
-import "package:nexus/controllers/members_controller.dart";
+import "package:nexus/controllers/members_by_type_controller.dart";
import "package:nexus/controllers/rooms_controller.dart";
+import "package:nexus/controllers/via_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
-import "package:nexus/models/room.dart";
+import "package:nexus/models/membership_status.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/widgets/loading.dart";
class MentionOverlay extends ConsumerWidget {
final String? triggerCharacter;
final String query;
- final Room room;
final void Function({required String id, required String name}) addTag;
- const MentionOverlay(
- this.room, {
+ const MentionOverlay({
required this.query,
required this.addTag,
required this.triggerCharacter,
@@ -34,7 +33,9 @@ class MentionOverlay extends ConsumerWidget {
child: switch (triggerCharacter) {
"@" =>
ref
- .watch(MembersController.provider(room))
+ .watch(
+ MembersByTypeController.provider(MembershipStatus.join),
+ )
.betterWhen(
data: (members) => ListView(
children:
@@ -62,7 +63,7 @@ class MentionOverlay extends ConsumerWidget {
title: Text(member.displayName),
subtitle: Text(member.userId),
onTap: () => addTag(
- id: "[@${member.displayName}](https://matrix.to/#/${member.userId})",
+ id: "[@${member.displayName}](matrix:u/${member.userId.substring(1)})",
name: member.userId
.substring(1)
.split(":")
@@ -78,33 +79,43 @@ class MentionOverlay extends ConsumerWidget {
(query.isEmpty
? rooms.values
: rooms.values.where(
- (room) => (room.metadata?.name ?? "Unnamed Room")
- .toLowerCase()
- .contains(query.toLowerCase()),
+ (room) =>
+ (room.metadata?.name ?? room.metadata!.id)
+ .toLowerCase()
+ .contains(query.toLowerCase()),
))
- .map(
- (room) => ListTile(
+ .map((room) {
+ final name =
+ room.metadata?.name ??
+ room.metadata!.canonicalAlias ??
+ room.metadata!.id;
+ return ListTile(
leading: AvatarOrHash(
room.metadata?.avatar,
- room.metadata?.name ?? "Unnamed Room",
+ name,
fallback: Icon(Icons.numbers),
),
- title: Text(room.metadata?.name ?? "Unnamed Room"),
+ title: Text(name),
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 ??
- "",
- ),
- ),
- )
+ onTap: () {
+ final vias = ref.watch(
+ ViaController.provider(room),
+ );
+ addTag(
+ id: "[#$name](matrix:roomid/${room.metadata?.id.substring(1)}$vias)",
+ name:
+ (room.metadata?.canonicalAlias ??
+ room.metadata?.id)
+ ?.substring(1)
+ .split(":")
+ .first ??
+ "",
+ );
+ },
+ );
+ })
.toList(),
),
diff --git a/lib/widgets/chat_page/composer/relation_preview.dart b/lib/widgets/chat_page/composer/relation_preview.dart
index 7fded20..c90b07b 100644
--- a/lib/widgets/chat_page/composer/relation_preview.dart
+++ b/lib/widgets/chat_page/composer/relation_preview.dart
@@ -2,7 +2,6 @@ import "package:flutter/material.dart";
import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/models/relation_type.dart";
-import "package:nexus/models/room.dart";
import "package:nexus/widgets/chat_page/lazy_loading/message_avatar.dart";
import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart";
@@ -12,11 +11,9 @@ class RelationPreview extends ConsumerWidget {
final VoidCallback onDismiss;
final bool shouldMention;
final VoidCallback toggleShouldMention;
- final Room room;
const RelationPreview(
this.relatedMessage, {
- required this.room,
required this.relationType,
required this.onDismiss,
required this.shouldMention,
@@ -35,27 +32,38 @@ class RelationPreview extends ConsumerWidget {
child: Row(
spacing: 8,
children: [
- SizedBox(width: 4),
if (relationType == RelationType.edit)
Text(
"Editing message:",
style: TextStyle(fontWeight: FontWeight.bold),
),
- MessageAvatar(relatedMessage!, room),
- MessageDisplayname(
- relatedMessage!,
- room,
- style: theme.textTheme.labelMedium?.copyWith(
- fontWeight: FontWeight.bold,
- ),
- ),
+
+ MessageAvatar(relatedMessage!),
+
Expanded(
- child: Text(
- relatedMessage?.metadata?["body"] ??
- relatedMessage?.metadata?["eventType"],
- overflow: TextOverflow.ellipsis,
- style: theme.textTheme.labelMedium,
- maxLines: 1,
+ child: Row(
+ spacing: 8,
+ children: [
+ Flexible(
+ child: MessageDisplayname(
+ relatedMessage!,
+ style: theme.textTheme.labelMedium?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ Expanded(
+ child: Text(
+ relatedMessage?.metadata?["body"] ??
+ relatedMessage?.metadata?["eventType"] ??
+ "",
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ softWrap: false,
+ style: theme.textTheme.labelMedium,
+ ),
+ ),
+ ],
),
),
@@ -70,11 +78,12 @@ class RelationPreview extends ConsumerWidget {
),
),
),
+
IconButton(
tooltip:
"Cancel ${relationType == RelationType.edit ? "edit" : "reply"}",
onPressed: onDismiss,
- icon: Icon(Icons.close),
+ icon: const Icon(Icons.close),
iconSize: 20,
),
],
diff --git a/lib/widgets/chat_page/emoji_picker_button.dart b/lib/widgets/chat_page/emoji_picker_button.dart
new file mode 100644
index 0000000..e8805ca
--- /dev/null
+++ b/lib/widgets/chat_page/emoji_picker_button.dart
@@ -0,0 +1,51 @@
+import "package:emoji_text_field/emoji_text_field.dart";
+import "package:flutter/material.dart";
+import "package:hooks_riverpod/hooks_riverpod.dart";
+import "package:nexus/controllers/emoji_controller.dart";
+
+class EmojiPickerButton extends HookConsumerWidget {
+ final TextEditingController? controller;
+ final void Function(String emoji)? onSelection;
+ final VoidCallback? onPressed;
+ final BuildContext context;
+ const EmojiPickerButton({
+ this.controller,
+ this.onPressed,
+ this.onSelection,
+ required this.context,
+ super.key,
+ });
+
+ @override
+ Widget build(_, WidgetRef ref) => IconButton(
+ onPressed: () async {
+ onPressed?.call();
+ final controller = this.controller ?? TextEditingController();
+
+ final emojis = await ref.watch(EmojiController.provider.future);
+ if (context.mounted) {
+ showModalBottomSheet(
+ context: context,
+ builder: (context) => EmojiKeyboardView(
+ config: EmojiViewConfig(
+ showRecentTab: false,
+ customCategories: emojis.$1.unlock,
+ customKeywords: emojis.$2.unlock,
+ backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
+ height: 600,
+ ),
+ textController: controller
+ ..addListener(() {
+ // Without this, there will sometimes be a debugLocked is not true error sometimes
+ Future.delayed(Duration.zero, () {
+ if (context.mounted) Navigator.of(context).pop();
+ });
+ onSelection?.call(controller.text);
+ }),
+ ),
+ );
+ }
+ },
+ icon: Icon(Icons.emoji_emotions),
+ );
+}
diff --git a/lib/widgets/chat_page/expandable_image.dart b/lib/widgets/chat_page/expandable_image.dart
new file mode 100644
index 0000000..ac5bbe1
--- /dev/null
+++ b/lib/widgets/chat_page/expandable_image.dart
@@ -0,0 +1,48 @@
+import "dart:math";
+import "package:cross_cache/cross_cache.dart";
+import "package:flutter/material.dart";
+import "package:hooks_riverpod/hooks_riverpod.dart";
+import "package:nexus/controllers/cross_cache_controller.dart";
+import "package:nexus/helpers/extensions/get_headers.dart";
+import "package:nexus/widgets/error_dialog.dart";
+
+class ExpandableImage extends ConsumerWidget {
+ final Widget child;
+ final String? source;
+ const ExpandableImage(this.source, {required this.child, super.key});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) => InkWell(
+ onTap: source == null
+ ? null
+ : () => 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,
+ errorBuilder: (_, error, stackTrace) => ErrorDialog(
+ "Loading failed for $source\nError: $error",
+ stackTrace,
+ ),
+ image: CachedNetworkImage(
+ source!,
+ ref.watch(CrossCacheController.provider),
+ headers: ref.headers,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ child: child,
+ );
+}
diff --git a/lib/widgets/chat_page/image_message.dart b/lib/widgets/chat_page/expandable_image_message.dart
similarity index 55%
rename from lib/widgets/chat_page/image_message.dart
rename to lib/widgets/chat_page/expandable_image_message.dart
index 103fdd2..f6e8a03 100644
--- a/lib/widgets/chat_page/image_message.dart
+++ b/lib/widgets/chat_page/expandable_image_message.dart
@@ -1,4 +1,3 @@
-import "dart:math";
import "package:cross_cache/cross_cache.dart";
import "package:flutter/material.dart";
import "package:flutter_chat_core/flutter_chat_core.dart";
@@ -6,6 +5,7 @@ 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";
+import "package:nexus/widgets/chat_page/expandable_image.dart";
class ExpandableImageMessage extends ConsumerWidget {
final ImageMessage message;
@@ -14,31 +14,8 @@ class ExpandableImageMessage extends ConsumerWidget {
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,
- ),
- ),
- ),
- ),
- ),
- ),
- ),
+ Widget build(BuildContext context, WidgetRef ref) => ExpandableImage(
+ message.source,
child: FlyerChatImageMessage(
customImageProvider: CachedNetworkImage(
message.source,
diff --git a/lib/widgets/chat_page/html/html.dart b/lib/widgets/chat_page/html/html.dart
index dcc1d49..fb533ad 100644
--- a/lib/widgets/chat_page/html/html.dart
+++ b/lib/widgets/chat_page/html/html.dart
@@ -1,12 +1,15 @@
+import "package:cross_cache/cross_cache.dart";
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:nexus/controllers/client_state_controller.dart";
+import "package:nexus/controllers/cross_cache_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/widgets/chat_page/expandable_image.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";
@@ -30,20 +33,29 @@ class Html extends ConsumerWidget {
return InlineCustomWidget(child: SpoilerText(text: element.text));
}
- final height = int.tryParse(element.attributes["height"] ?? "") ?? 300;
+ final height =
+ int.tryParse(element.attributes["height"] ?? "") ??
+ (element.attributes.keys.contains("data-mx-emoticon") ? 32 : null) ??
+ 300;
final width = int.tryParse(element.attributes["width"] ?? "");
+ final src = Uri.tryParse(element.attributes["src"] ?? "")
+ ?.mxcToHttps(
+ ref.watch(
+ ClientStateController.provider.select(
+ (value) => value?.homeserverUrl,
+ ),
+ ) ??
+ "",
+ )
+ .toString();
return switch (element.localName) {
"code" =>
element.parent?.localName == "pre"
- ? element.outerHtml.contains("
")
- ? Html(
- """${element.outerHtml.replaceAll("
", "\n")}""",
- )
- : CodeBlock(
- element.text,
- lang: element.className.replaceAll("language-", ""),
- )
+ ? CodeBlock(
+ element.text,
+ lang: element.className.replaceAll("language-", ""),
+ )
: null,
"blockquote" => Quoted(Html(element.innerHtml)),
@@ -51,39 +63,40 @@ class Html extends ConsumerWidget {
"a" =>
element.attributes["href"]?.mention == null
? null
- : InlineCustomWidget(child: MentionChip(element.text)),
+ : InlineCustomWidget(
+ child: MentionChip(element.attributes["href"]!),
+ ),
"img" =>
- element.attributes["src"] == null
+ src == null
? SizedBox.shrink()
: InlineCustomWidget(
alignment: PlaceholderAlignment.middle,
- child: Image.network(
- Uri.parse(element.attributes["src"]!)
- .mxcToHttps(
- ref.watch(
- ClientStateController.provider.select(
- (value) => value?.homeserverUrl,
- ),
- ) ??
- "",
- )
- .toString(),
- headers: ref.headers,
- errorBuilder: (_, error, _) => Text(
- "Image Failed to Load",
- style: TextStyle(
- color: Theme.of(context).colorScheme.error,
+ child: ExpandableImage(
+ src,
+ child: Image(
+ image: CachedNetworkImage(
+ src,
+ ref.watch(CrossCacheController.provider),
+ 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(),
),
- height: height.toDouble(),
- width: width?.toDouble(),
- loadingBuilder: (_, child, loadingProgress) =>
- loadingProgress == null
- ? child
- : CircularProgressIndicator(),
),
),
+
+ // Allowed elements list
("del" ||
"h1" ||
"h2" ||
diff --git a/lib/widgets/chat_page/html/mention_chip.dart b/lib/widgets/chat_page/html/mention_chip.dart
index c2b832d..575ad03 100644
--- a/lib/widgets/chat_page/html/mention_chip.dart
+++ b/lib/widgets/chat_page/html/mention_chip.dart
@@ -1,25 +1,44 @@
import "package:flutter/material.dart";
+import "package:flutter_riverpod/flutter_riverpod.dart";
+import "package:nexus/controllers/user_controller.dart";
import "package:nexus/helpers/extensions/link_to_mention.dart";
+import "package:nexus/helpers/extensions/show_user_popover.dart";
-class MentionChip extends StatelessWidget {
- final String label;
- const MentionChip(this.label, {super.key});
+class MentionChip extends ConsumerWidget {
+ final String content;
+ const MentionChip(this.content, {super.key});
@override
- Widget build(BuildContext context) => ActionChip(
- label: Text(
- label.mention ?? label,
- style: TextStyle(
- fontWeight: FontWeight.bold,
- color: Theme.of(context).colorScheme.onPrimary,
+ Widget build(BuildContext context, WidgetRef ref) {
+ final membership = content.mention!.startsWith("@") == true
+ ? ref
+ .watch(UserController.provider(content.mention!))
+ .whenOrNull(data: (data) => data)
+ : null;
+
+ return InkWell(
+ onTapUp: (details) {
+ content.mention;
+ if (membership != null) {
+ context.showUserPopover(
+ membership,
+ globalPosition: details.globalPosition,
+ );
+ }
+ },
+ child: IgnorePointer(
+ child: Chip(
+ label: Text(
+ (membership == null ? null : "@${membership.displayName}") ??
+ content.mention!,
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ color: Theme.of(context).colorScheme.onPrimary,
+ ),
+ ),
+ backgroundColor: Theme.of(context).colorScheme.primary,
+ ),
),
- ),
- backgroundColor: Theme.of(context).colorScheme.primary,
- onPressed: () => showDialog(
- context: context,
- builder: (_) => Dialog(
- child: Text("TODO: Open room or join room dialog, or user popover"),
- ),
- ),
- );
+ );
+ }
}
diff --git a/lib/widgets/chat_page/join_dialog.dart b/lib/widgets/chat_page/join_dialog.dart
new file mode 100644
index 0000000..e718200
--- /dev/null
+++ b/lib/widgets/chat_page/join_dialog.dart
@@ -0,0 +1,137 @@
+import "package:collection/collection.dart";
+import "package:fast_immutable_collections/fast_immutable_collections.dart";
+import "package:flutter/material.dart";
+import "package:flutter_hooks/flutter_hooks.dart";
+import "package:hooks_riverpod/hooks_riverpod.dart";
+import "package:nexus/controllers/client_controller.dart";
+import "package:nexus/controllers/key_controller.dart";
+import "package:nexus/controllers/spaces_controller.dart";
+import "package:nexus/helpers/extensions/link_to_mention.dart";
+import "package:nexus/models/requests/join_room_request.dart";
+import "package:nexus/widgets/form_text_input.dart";
+
+class JoinDialog extends HookWidget {
+ final WidgetRef ref;
+ const JoinDialog(this.ref, {super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final roomAlias = useTextEditingController();
+ return AlertDialog(
+ title: Text("Join a Room"),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text("Enter the room alias, Matrix URI, or 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(context).pop();
+
+ if (context.mounted) {
+ final roomIdOrAlias = roomAlias.text.mention ?? roomAlias.text;
+
+ final scaffoldMessenger = ScaffoldMessenger.of(context);
+
+ final snackbar = scaffoldMessenger.showSnackBar(
+ SnackBar(
+ content: Text("Joining room $roomIdOrAlias."),
+ duration: Duration(days: 999),
+ ),
+ );
+
+ try {
+ final id = await ref
+ .watch(ClientController.provider.notifier)
+ .joinRoom(
+ JoinRoomRequest(
+ roomIdOrAlias: roomIdOrAlias,
+ via: IList(
+ Uri.tryParse(
+ roomAlias.text.replaceAll("/#", ""),
+ )?.queryParametersAll["via"] ??
+ [],
+ ),
+ ),
+ );
+
+ snackbar.close();
+
+ scaffoldMessenger.showSnackBar(
+ SnackBar(
+ content: Text("Room $roomIdOrAlias successfully joined."),
+ action: SnackBarAction(
+ label: "Open",
+ onPressed: () async {
+ final spaces = ref.watch(SpacesController.provider);
+ final space = spaces.firstWhereOrNull(
+ (space) => space.id == id,
+ );
+
+ await ref
+ .watch(
+ KeyController.provider(
+ KeyController.spaceKey,
+ ).notifier,
+ )
+ .set(
+ space?.id ??
+ spaces
+ .firstWhere(
+ (space) => space.children.any(
+ (child) => child.metadata?.id == id,
+ ),
+ )
+ .id,
+ );
+
+ if (space == null) {
+ await ref
+ .watch(
+ KeyController.provider(
+ KeyController.roomKey,
+ ).notifier,
+ )
+ .set(id);
+ }
+ },
+ ),
+ ),
+ );
+ } catch (error) {
+ snackbar.close();
+ if (context.mounted) {
+ scaffoldMessenger.showSnackBar(
+ SnackBar(
+ backgroundColor: Theme.of(
+ context,
+ ).colorScheme.errorContainer,
+ content: Text(
+ error.toString(),
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.onErrorContainer,
+ ),
+ ),
+ ),
+ );
+ }
+ }
+ }
+ },
+ child: Text("Join"),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/widgets/chat_page/lazy_loading/message_avatar.dart b/lib/widgets/chat_page/lazy_loading/message_avatar.dart
index 71fcf84..dc8dfef 100644
--- a/lib/widgets/chat_page/lazy_loading/message_avatar.dart
+++ b/lib/widgets/chat_page/lazy_loading/message_avatar.dart
@@ -1,28 +1,30 @@
-import "package:flutter/widgets.dart";
+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/author_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
-import "package:nexus/models/configs/author_config.dart";
-import "package:nexus/models/room.dart";
+import "package:nexus/helpers/extensions/show_user_popover.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
class MessageAvatar extends ConsumerWidget {
final Message message;
- final Room room;
final double height;
- const MessageAvatar(this.message, this.room, {this.height = 16, super.key});
+ const MessageAvatar(this.message, {this.height = 16, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) => ref
- .watch(
- AuthorController.provider(AuthorConfig(room: room, message: message)),
- )
+ .watch(AuthorController.provider(message))
.betterWhen(
- data: (membership) => AvatarOrHash(
- membership.avatarUrl,
- membership.displayName,
- height: height,
+ data: (membership) => InkWell(
+ onTapUp: (details) => context.showUserPopover(
+ membership,
+ globalPosition: details.globalPosition,
+ ),
+ child: AvatarOrHash(
+ membership.avatarUrl,
+ membership.displayName,
+ height: height,
+ ),
),
loading: () =>
AvatarOrHash(null, message.authorId.substring(1), height: height),
diff --git a/lib/widgets/chat_page/lazy_loading/message_displayname.dart b/lib/widgets/chat_page/lazy_loading/message_displayname.dart
index 7c10df3..88d2fa6 100644
--- a/lib/widgets/chat_page/lazy_loading/message_displayname.dart
+++ b/lib/widgets/chat_page/lazy_loading/message_displayname.dart
@@ -1,27 +1,37 @@
-import "package:flutter/widgets.dart";
+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/author_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
-import "package:nexus/models/configs/author_config.dart";
-import "package:nexus/models/room.dart";
+import "package:nexus/helpers/extensions/show_user_popover.dart";
class MessageDisplayname extends ConsumerWidget {
final Message message;
- final Room room;
final TextStyle? style;
- const MessageDisplayname(this.message, this.room, {this.style, super.key});
+ final bool clickable;
+ const MessageDisplayname(
+ this.message, {
+ this.clickable = true,
+ this.style,
+ super.key,
+ });
@override
Widget build(BuildContext context, WidgetRef ref) => ref
- .watch(
- AuthorController.provider(AuthorConfig(room: room, message: message)),
- )
+ .watch(AuthorController.provider(message))
.betterWhen(
- data: (membership) => Text(
- "${membership.displayName} ${message.metadata?["pmp"] == null ? "" : "(via ${message.authorId})"}",
- style: style,
- overflow: TextOverflow.ellipsis,
+ data: (membership) => InkWell(
+ onTapUp: clickable
+ ? (details) => context.showUserPopover(
+ membership,
+ globalPosition: details.globalPosition,
+ )
+ : null,
+ child: Text(
+ "${membership.displayName}${message.metadata?["pmp"] == null ? "" : " (via ${message.authorId})"}",
+ style: style,
+ overflow: TextOverflow.ellipsis,
+ ),
),
loading: () => Text(""),
);
diff --git a/lib/widgets/chat_page/member_list.dart b/lib/widgets/chat_page/member_list.dart
index 8cdbbb9..8be1ddd 100644
--- a/lib/widgets/chat_page/member_list.dart
+++ b/lib/widgets/chat_page/member_list.dart
@@ -1,27 +1,31 @@
import "package:flutter/material.dart";
+import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
-import "package:nexus/controllers/members_controller.dart";
+import "package:nexus/controllers/members_by_type_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
-import "package:nexus/models/room.dart";
+import "package:nexus/helpers/extensions/show_user_popover.dart";
+import "package:nexus/models/membership_status.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
-class MemberList extends ConsumerWidget {
- final Room room;
- const MemberList(this.room, {super.key});
+class MemberList extends HookConsumerWidget {
+ const MemberList({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
- final membersProvider = ref.watch(MembersController.provider(room));
+ final status = useState(MembershipStatus.join);
+ final membersProvider = ref.watch(
+ MembersByTypeController.provider(status.value),
+ );
+
return Drawer(
shape: Border(),
child: Column(
+ spacing: 8,
children: [
AppBar(
scrolledUnderElevation: 0,
leading: Icon(Icons.people),
- title: Text(
- "Members ${membersProvider.when(data: (members) => "${members.length}", error: (_, _) => "", loading: () => "")}",
- ),
+ title: Text("Members"),
actionsPadding: EdgeInsets.only(right: 4),
actions: [
if (Scaffold.of(context).hasEndDrawer)
@@ -32,28 +36,50 @@ class MemberList extends ConsumerWidget {
),
],
),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ spacing: 8,
+ children: [
+ FilterChip(
+ label: Text("Joined"),
+ onSelected: (value) => status.value = MembershipStatus.join,
+ selected: status.value == MembershipStatus.join,
+ ),
+ FilterChip(
+ label: Text("Invited"),
+ onSelected: (value) => status.value = MembershipStatus.invite,
+ selected: status.value == MembershipStatus.invite,
+ ),
+ FilterChip(
+ label: Text("Banned"),
+ onSelected: (value) => status.value = MembershipStatus.ban,
+ selected: status.value == MembershipStatus.ban,
+ ),
+ ],
+ ),
membersProvider.betterWhen(
data: (members) => Expanded(
child: ListView(
children: members
.map(
- (member) => ListTile(
- onTap: () => showDialog(
- context: context,
- builder: (context) =>
- Dialog(child: Text("TODO: Open member popover")),
+ (member) => InkWell(
+ onTapUp: (details) => context.showUserPopover(
+ member,
+ globalPosition: details.globalPosition,
),
- leading: AvatarOrHash(
- member.avatarUrl,
- member.displayName,
- ),
- title: Text(
- member.displayName,
- overflow: TextOverflow.ellipsis,
- ),
- subtitle: Text(
- member.userId,
- overflow: TextOverflow.ellipsis,
+ child: ListTile(
+ leading: AvatarOrHash(
+ member.avatarUrl,
+ member.displayName,
+ ),
+ title: Text(
+ member.displayName,
+ overflow: TextOverflow.ellipsis,
+ ),
+ subtitle: Text(
+ member.userId,
+ overflow: TextOverflow.ellipsis,
+ ),
),
),
)
diff --git a/lib/widgets/chat_page/reply_widget.dart b/lib/widgets/chat_page/reply_widget.dart
index b9fa2e1..b999be4 100644
--- a/lib/widgets/chat_page/reply_widget.dart
+++ b/lib/widgets/chat_page/reply_widget.dart
@@ -3,10 +3,10 @@ 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/controllers/selected_room_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/models/configs/message_config.dart";
import "package:nexus/models/requests/get_event_request.dart";
-import "package:nexus/models/room.dart";
import "package:nexus/widgets/chat_page/html/quoted.dart";
import "package:nexus/widgets/chat_page/lazy_loading/message_avatar.dart";
import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart";
@@ -16,12 +16,10 @@ 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,
@@ -29,73 +27,75 @@ class ReplyWidget extends ConsumerWidget {
});
@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!,
+ Widget build(BuildContext context, WidgetRef ref) {
+ final room = ref.watch(SelectedRoomController.provider);
+ return message.replyToMessageId == null || room == 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();
- }
+ )
+ .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();
+ }
- return InkWell(
- onTap: () => onTapReply?.call(replyMessage),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- spacing: 8,
- children: [
- MessageAvatar(replyMessage, room),
- Flexible(
- child: MessageDisplayname(
- replyMessage,
- room,
- style: Theme.of(context)
- .textTheme
- .labelMedium
- ?.copyWith(
- fontWeight: FontWeight.bold,
- ),
+ return InkWell(
+ onTap: () => onTapReply?.call(replyMessage),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ spacing: 8,
+ children: [
+ MessageAvatar(replyMessage),
+ Flexible(
+ child: MessageDisplayname(
+ replyMessage,
+ clickable: false,
+ style: Theme.of(context)
+ .textTheme
+ .labelMedium
+ ?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
),
- ),
- Flexible(
- child: Text(
- replyMessage.metadata!["body"],
- overflow: TextOverflow.ellipsis,
- style: Theme.of(
- context,
- ).textTheme.labelMedium,
- maxLines: 1,
+ Flexible(
+ child: Text(
+ replyMessage.metadata!["body"],
+ 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 436bcb9..62e282d 100644
--- a/lib/widgets/chat_page/room_appbar.dart
+++ b/lib/widgets/chat_page/room_appbar.dart
@@ -1,20 +1,20 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/material.dart";
-import "package:nexus/models/room.dart";
+import "package:hooks_riverpod/hooks_riverpod.dart";
+import "package:nexus/controllers/selected_room_controller.dart";
import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
+import "package:nexus/widgets/chat_page/expandable_image.dart";
import "package:nexus/widgets/chat_page/room_menu.dart";
-class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
+class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget {
final bool isDesktop;
- final Room room;
- final void Function(BuildContext context) onOpenMemberList;
+ final void Function(BuildContext context)? onOpenMemberList;
final void Function(BuildContext context) onOpenDrawer;
- const RoomAppbar(
- this.room, {
+ const RoomAppbar({
required this.isDesktop,
- required this.onOpenMemberList,
required this.onOpenDrawer,
+ this.onOpenMemberList,
super.key,
});
@@ -22,47 +22,57 @@ class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
Size get preferredSize => AppBar().preferredSize;
@override
- Widget build(BuildContext context) => Appbar(
- leading: isDesktop
- ? AvatarOrHash(
- room.metadata?.avatar,
- room.metadata?.name ?? "Unnamed Rooms",
- height: 24,
- fallback: Icon(Icons.numbers),
- )
- : DrawerButton(onPressed: () => onOpenDrawer(context)),
- scrolledUnderElevation: 0,
- title: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- room.metadata?.name ?? "Unnamed Room",
- overflow: TextOverflow.ellipsis,
- maxLines: 1,
- ),
- if (room.metadata?.topic?.isNotEmpty == true)
- Text(
- room.metadata!.topic!,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: Theme.of(context).textTheme.labelMedium?.copyWith(
- color: Theme.of(context).colorScheme.onSurfaceVariant,
+ Widget build(BuildContext context, WidgetRef ref) {
+ final room = ref.watch(SelectedRoomController.provider);
+ return Appbar(
+ leading: isDesktop
+ ? room == null
+ ? null
+ : ExpandableImage(
+ room.metadata?.avatar?.toString(),
+ child: AvatarOrHash(
+ room.metadata?.avatar,
+ room.metadata?.name ?? "Unnamed Rooms",
+ height: 24,
+ fallback: Icon(Icons.numbers),
+ ),
+ )
+ : DrawerButton(onPressed: () => onOpenDrawer(context)),
+ scrolledUnderElevation: 0,
+ title: room == null
+ ? null
+ : Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ room.metadata?.name ?? "Unnamed Room",
+ overflow: TextOverflow.ellipsis,
+ maxLines: 1,
+ ),
+ if (room.metadata?.topic?.isNotEmpty == true)
+ Text(
+ room.metadata!.topic!,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: Theme.of(context).textTheme.labelMedium?.copyWith(
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
),
- ),
- ],
- ),
- actions: [
- 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),
- ].toIList(),
- );
+ actions: [
+ IconButton(
+ onPressed: null,
+ icon: Icon(Icons.push_pin),
+ tooltip: "Open pinned messages",
+ ),
+ IconButton(
+ onPressed: () => onOpenMemberList?.call(context),
+ tooltip: "Open member list",
+ icon: Icon(Icons.people),
+ ),
+ if (room != null) RoomMenu(room),
+ ].toIList(),
+ );
+ }
}
diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart
index 6b3839a..7fb3f8f 100644
--- a/lib/widgets/chat_page/room_chat.dart
+++ b/lib/widgets/chat_page/room_chat.dart
@@ -1,27 +1,34 @@
+import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/material.dart";
+import "package:flutter/services.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:flyer_chat_file_message/flyer_chat_file_message.dart";
import "package:flyer_chat_system_message/flyer_chat_system_message.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
+import "package:nexus/controllers/account_data_controller.dart";
import "package:nexus/controllers/client_controller.dart";
import "package:nexus/controllers/client_state_controller.dart";
+import "package:nexus/controllers/power_level_controller.dart";
import "package:nexus/controllers/selected_room_controller.dart";
import "package:nexus/controllers/room_chat_controller.dart";
+import "package:nexus/controllers/via_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/helpers/extensions/show_context_menu.dart";
+import "package:nexus/models/configs/power_level_config.dart";
import "package:nexus/models/relation_type.dart";
import "package:nexus/models/requests/report_request.dart";
import "package:nexus/widgets/chat_page/composer/chat_box.dart";
-import "package:nexus/widgets/chat_page/image_message.dart";
+import "package:nexus/widgets/chat_page/emoji_picker_button.dart";
+import "package:nexus/widgets/chat_page/expandable_image_message.dart";
import "package:nexus/widgets/chat_page/member_list.dart";
import "package:nexus/widgets/chat_page/wrappers/message_wrapper.dart";
import "package:nexus/widgets/chat_page/room_appbar.dart";
import "package:nexus/widgets/chat_page/wrappers/text_message_wrapper.dart";
import "package:nexus/widgets/chat_page/reply_widget.dart";
import "package:nexus/widgets/form_text_input.dart";
-// import "package:dynamic_polls/dynamic_polls.dart";
+import "package:nexus/main.dart";
class RoomChat extends HookConsumerWidget {
final bool isDesktop;
@@ -35,46 +42,138 @@ class RoomChat extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final client = ref.watch(ClientController.provider.notifier);
- final replyToMessage = useState(null);
+ final relatedMessage = 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 roomId = ref.watch(
+ SelectedRoomController.provider.select((value) => value?.metadata?.id),
+ );
final theme = Theme.of(context);
final danger = theme.colorScheme.error;
- if (room == null || userId == null || room.metadata?.id == null) {
- return Center(
- child: Text(
- "Nothing to see here...",
- style: theme.textTheme.headlineMedium,
+ if (roomId == null || userId == null) {
+ return Scaffold(
+ appBar: RoomAppbar(
+ isDesktop: isDesktop,
+ onOpenDrawer: (_) => Scaffold.of(context).openDrawer(),
+ onOpenMemberList: null,
+ ),
+ body: Center(
+ child: Text(
+ "Nothing to see here...",
+ style: theme.textTheme.headlineMedium,
+ ),
),
);
}
- final controllerProvider = RoomChatController.provider(room.metadata!.id);
+ final controllerProvider = RoomChatController.provider(roomId);
final notifier = ref.watch(controllerProvider.notifier);
+ final composerNode = useFocusNode(
+ onKeyEvent: (_, event) {
+ if (event is KeyDownEvent &&
+ event.logicalKey == LogicalKeyboardKey.escape) {
+ relatedMessage.value = null;
+ return KeyEventResult.handled;
+ }
+
+ return KeyEventResult.ignored;
+ },
+ );
+
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 (ref.watch(
+ PowerLevelController.provider(
+ PowerLevelConfig(eventType: "m.reaction"),
+ ),
+ ))
+ PopupMenuItem(
+ child: Row(
+ children: [
+ ...{
+ ...ref.watch(
+ AccountDataController.provider.select(
+ (value) => IList(
+ value["m.recent_emoji"]?.content["recent_emoji"] ??
+ [],
+ ).map((entry) => entry["emoji"]),
+ ),
+ ),
+ "👍",
+ "🤣",
+ "😭",
+ "🤔",
+ }
+ .toIList()
+ .sublist(0, 4)
+ .map(
+ (emoji) => IconButton(
+ onPressed: () async {
+ Navigator.of(context).pop();
+ await notifier
+ .sendReaction(emoji, message)
+ .onError(showError);
+ },
+ icon: Text(emoji),
+ ),
+ ),
+ EmojiPickerButton(
+ context: context,
+ onPressed: Navigator.of(context).pop,
+ onSelection: (emoji) =>
+ notifier.sendReaction(emoji, message).onError(showError),
+ ),
+ ],
+ ),
+ ),
+ if (ref.watch(
+ PowerLevelController.provider(
+ PowerLevelConfig(eventType: "m.room.message"),
+ ),
+ ))
+ PopupMenuItem(
+ onTap: () {
+ relatedMessage.value = message;
+ relationType.value = RelationType.reply;
+ composerNode.requestFocus();
+ },
+ child: ListTile(leading: Icon(Icons.reply), title: Text("Reply")),
+ ),
if (message is TextMessage && isSentByMe)
PopupMenuItem(
onTap: () {
- replyToMessage.value = message;
+ relatedMessage.value = message;
relationType.value = RelationType.edit;
+ composerNode.requestFocus();
},
child: ListTile(leading: Icon(Icons.edit), title: Text("Edit")),
),
- if (isSentByMe) // TODO: Or if user has permission to redact others' messages
+ PopupMenuItem(
+ onTap: () async {
+ final room = ref.watch(SelectedRoomController.provider);
+ if (room == null) return;
+
+ final vias = ref.watch(ViaController.provider(room));
+
+ await Clipboard.setData(
+ ClipboardData(
+ text:
+ "matrix:roomid/${room.metadata?.id.substring(1)}/e/${message.id}$vias)",
+ ),
+ );
+ },
+ child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")),
+ ),
+ if (ref.watch(
+ PowerLevelController.provider(
+ PowerLevelConfig(eventType: "m.room.redaction"),
+ ),
+ ))
PopupMenuItem(
onTap: () => showDialog(
context: context,
@@ -106,11 +205,13 @@ class RoomChat extends HookConsumerWidget {
),
TextButton(
onPressed: () async {
- notifier.deleteMessage(
- message,
- reason: deleteReasonController.text,
- );
Navigator.of(context).pop();
+ await notifier
+ .deleteMessage(
+ message,
+ reason: deleteReasonController.text,
+ )
+ .onError(showError);
},
child: Text("Delete"),
),
@@ -119,7 +220,10 @@ class RoomChat extends HookConsumerWidget {
},
),
),
- child: ListTile(leading: Icon(Icons.delete), title: Text("Delete")),
+ child: ListTile(
+ leading: Icon(Icons.delete, color: danger),
+ title: Text("Delete", style: TextStyle(color: danger)),
+ ),
),
PopupMenuItem(
onTap: () => showDialog(
@@ -153,10 +257,9 @@ class RoomChat extends HookConsumerWidget {
),
TextButton(
onPressed: () {
- if (room.metadata == null) return;
client.reportEvent(
ReportRequest(
- roomId: room.metadata!.id,
+ roomId: roomId,
eventId: message.id,
reason: reasonController.text.isEmpty
? null
@@ -189,7 +292,6 @@ class RoomChat extends HookConsumerWidget {
return Scaffold(
appBar: RoomAppbar(
- room,
isDesktop: isDesktop,
onOpenDrawer: (_) => Scaffold.of(context).openDrawer(),
onOpenMemberList: (thisContext) {
@@ -237,18 +339,42 @@ class RoomChat extends HookConsumerWidget {
chatAnimatedListBuilder: (_, itemBuilder) =>
ChatAnimatedList(
itemBuilder: itemBuilder,
- onEndReached: room.hasMore
+ onEndReached:
+ ref.watch(
+ SelectedRoomController.provider.select(
+ (room) => room?.hasMore == true,
+ ),
+ )
? notifier.loadOlder
: null,
- onStartReached: () => client.markRead(room),
+ onStartReached: () async {
+ final room = ref.watch(
+ SelectedRoomController.provider,
+ );
+ return room == null
+ ? null
+ : await client.markRead(room);
+ },
bottomPadding: 72,
),
composerBuilder: (_) => ChatBox(
+ node: composerNode,
+ onSend:
+ (
+ text, {
+ required shouldMention,
+ required tags,
+ }) => notifier.send(
+ text,
+ tags: tags,
+ relationType: relationType.value,
+ shouldMention: shouldMention,
+ relation: relatedMessage.value,
+ ),
relationType: relationType.value,
- relatedMessage: replyToMessage.value,
- onDismiss: () => replyToMessage.value = null,
- room: room,
+ relatedMessage: relatedMessage.value,
+ onDismiss: () => relatedMessage.value = null,
),
textMessageBuilder:
@@ -259,7 +385,6 @@ class RoomChat extends HookConsumerWidget {
required bool isSentByMe,
MessageGroupStatus? groupStatus,
}) => TextMessageWrapper(
- room: room,
message,
content: message.text,
groupStatus: groupStatus,
@@ -277,7 +402,6 @@ class RoomChat extends HookConsumerWidget {
MessageGroupStatus? groupStatus,
}) => TextMessageWrapper(
message,
- room: room,
content: message.text,
groupStatus: groupStatus,
onTapReply: notifier.scrollToMessage,
@@ -309,7 +433,6 @@ class RoomChat extends HookConsumerWidget {
),
child: FlyerChatFileMessage(
topWidget: ReplyWidget(
- room: room,
message,
onTapReply: notifier.scrollToMessage,
groupStatus: groupStatus,
@@ -319,7 +442,6 @@ class RoomChat extends HookConsumerWidget {
),
),
groupStatus,
- room,
),
systemMessageBuilder:
@@ -348,7 +470,7 @@ class RoomChat extends HookConsumerWidget {
),
),
),
- resolveUser: notifier.resolveUser,
+ resolveUser: (_) async => null,
chatController: controller,
),
),
@@ -358,11 +480,11 @@ class RoomChat extends HookConsumerWidget {
),
if (memberListOpened.value == true && showMembersByDefault)
- MemberList(room),
+ MemberList(),
],
),
- endDrawer: showMembersByDefault ? null : MemberList(room),
+ endDrawer: showMembersByDefault ? null : MemberList(),
);
}
}
diff --git a/lib/widgets/chat_page/room_menu.dart b/lib/widgets/chat_page/room_menu.dart
index 2687bc8..4405707 100644
--- a/lib/widgets/chat_page/room_menu.dart
+++ b/lib/widgets/chat_page/room_menu.dart
@@ -1,7 +1,9 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/material.dart";
+import "package:flutter/services.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_controller.dart";
+import "package:nexus/controllers/via_controller.dart";
import "package:nexus/models/room.dart";
class RoomMenu extends ConsumerWidget {
@@ -16,13 +18,6 @@ class RoomMenu extends ConsumerWidget {
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 {
await client.markRead(room);
@@ -33,6 +28,18 @@ class RoomMenu extends ConsumerWidget {
title: Text("Mark as Read"),
),
),
+ PopupMenuItem(
+ onTap: () async {
+ final vias = ref.watch(ViaController.provider(room));
+
+ await Clipboard.setData(
+ ClipboardData(
+ text: "matrix:roomid/${room.metadata?.id.substring(1)}$vias)",
+ ),
+ );
+ },
+ child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")),
+ ),
PopupMenuItem(
onTap: () => showDialog(
context: context,
diff --git a/lib/widgets/chat_page/sidebar.dart b/lib/widgets/chat_page/sidebar.dart
index 4642a58..f79c38f 100644
--- a/lib/widgets/chat_page/sidebar.dart
+++ b/lib/widgets/chat_page/sidebar.dart
@@ -1,18 +1,15 @@
import "package:flutter/material.dart";
-import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
-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/join_room_with_snackbars.dart";
-import "package:nexus/pages/settings_page.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
+import "package:nexus/widgets/chat_page/join_dialog.dart";
import "package:nexus/widgets/chat_page/room_menu.dart";
-import "package:nexus/widgets/form_text_input.dart";
class Sidebar extends HookConsumerWidget {
- const Sidebar({super.key});
+ final bool isDesktop;
+ const Sidebar({required this.isDesktop, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -91,53 +88,7 @@ class Sidebar extends HookConsumerWidget {
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 = ref.watch(
- ClientController.provider.notifier,
- );
- if (context.mounted) {
- client.joinRoomWithSnackBars(
- context,
- roomAlias.text,
- ref,
- );
- }
- },
- child: Text("Join"),
- ),
- ],
- );
- },
- ),
+ builder: (_) => JoinDialog(ref),
),
child: ListTile(
title: Text("Join an existing room (or space)"),
@@ -145,7 +96,7 @@ class Sidebar extends HookConsumerWidget {
),
),
PopupMenuItem(
- onTap: () {},
+ onTap: null,
child: ListTile(
title: Text("Create a new room"),
leading: Icon(Icons.add),
@@ -156,17 +107,15 @@ class Sidebar extends HookConsumerWidget {
),
IconButton(
tooltip: "Explore other rooms",
- onPressed: () => showDialog(
- context: context,
- builder: (context) => AlertDialog(title: Text("To-do")),
- ),
+ onPressed: null,
icon: Icon(Icons.explore),
),
IconButton(
tooltip: "Open settings",
- onPressed: () => Navigator.of(
- context,
- ).push(MaterialPageRoute(builder: (_) => SettingsPage())),
+ onPressed: null,
+ // () => Navigator.of(
+ // context,
+ // ).push(MaterialPageRoute(builder: (_) => SettingsPage())),
icon: Icon(Icons.settings),
),
],
@@ -220,9 +169,12 @@ class Sidebar extends HookConsumerWidget {
),
)
.toList(),
- onDestinationSelected: (value) => selectedRoomIdNotifier.set(
- selectedSpace.children[value].metadata?.id,
- ),
+ onDestinationSelected: (value) {
+ selectedRoomIdNotifier.set(
+ selectedSpace.children[value].metadata?.id,
+ );
+ if (!isDesktop) Navigator.of(context).pop();
+ },
),
),
),
diff --git a/lib/widgets/chat_page/user_popover.dart b/lib/widgets/chat_page/user_popover.dart
new file mode 100644
index 0000000..a9a4799
--- /dev/null
+++ b/lib/widgets/chat_page/user_popover.dart
@@ -0,0 +1,214 @@
+import "package:flutter/material.dart";
+import "package:flutter_hooks/flutter_hooks.dart";
+import "package:flutter_riverpod/flutter_riverpod.dart";
+import "package:intl/intl.dart";
+import "package:nexus/controllers/client_controller.dart";
+import "package:nexus/controllers/client_state_controller.dart";
+import "package:nexus/controllers/power_level_controller.dart";
+import "package:nexus/controllers/profile_controller.dart";
+import "package:nexus/controllers/selected_room_controller.dart";
+import "package:nexus/helpers/extensions/better_when.dart";
+import "package:nexus/models/configs/power_level_config.dart";
+import "package:nexus/models/membership.dart";
+import "package:nexus/models/membership_status.dart";
+import "package:nexus/models/requests/membership_action.dart";
+import "package:nexus/models/requests/set_membership_request.dart";
+import "package:nexus/widgets/avatar_or_hash.dart";
+import "package:nexus/main.dart";
+import "package:nexus/widgets/chat_page/expandable_image.dart";
+import "package:nexus/widgets/form_text_input.dart";
+
+class UserPopover extends ConsumerWidget {
+ final Membership member;
+ const UserPopover(this.member, {super.key});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final theme = Theme.of(context);
+ final textTheme = theme.textTheme;
+ final client = ref.watch(ClientController.provider.notifier);
+ final roomId = ref.watch(
+ SelectedRoomController.provider.select((room) => room?.metadata?.id),
+ );
+
+ void showMembershipDialog(MembershipAction action) => showDialog(
+ context: context,
+ builder: (context) => HookBuilder(
+ builder: (context) {
+ final actionReasonController = useTextEditingController();
+ return AlertDialog(
+ title: Text(
+ "${toBeginningOfSentenceCase(action.name)} ${member.userId}",
+ ),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "Are you sure you want to ${action.name} ${member.userId}?",
+ ),
+ SizedBox(height: 12),
+ FormTextInput(
+ required: false,
+ capitalize: true,
+ controller: actionReasonController,
+ title: "Reason for ${action.name} (optional)",
+ ),
+ ],
+ ),
+ actions: [
+ TextButton(
+ onPressed: Navigator.of(context).pop,
+ child: Text("Cancel"),
+ ),
+ TextButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ client
+ .setMembership(
+ SetMembershipRequest(
+ userId: member.userId,
+ roomId: roomId!,
+ action: action,
+ reason: actionReasonController.text,
+ ),
+ )
+ .onError(showError);
+ },
+ child: Text(toBeginningOfSentenceCase(action.name)),
+ ),
+ ],
+ );
+ },
+ ),
+ );
+
+ return Column(
+ spacing: 16,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Wrap(
+ alignment: WrapAlignment.center,
+ spacing: 16,
+ runSpacing: 8,
+ children: [
+ ExpandableImage(
+ member.avatarUrl?.toString(),
+ child: AvatarOrHash(
+ member.avatarUrl,
+ member.displayName,
+ height: 80,
+ ),
+ ),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SelectableText(
+ member.displayName,
+ style: textTheme.headlineSmall,
+ ),
+ SelectableText(member.userId, style: textTheme.titleSmall),
+ SizedBox(height: 4),
+ ref
+ .watch(ProfileController.provider(member.userId))
+ .betterWhen(
+ loading: SizedBox.shrink,
+ data: (profile) => Wrap(
+ spacing: 4,
+ children: [
+ for (final pronoun in profile.pronouns.where(
+ (pronoun) => pronoun.language == "en",
+ ))
+ Chip(
+ label: Text(pronoun.summary),
+ labelStyle: TextStyle(
+ color: theme.colorScheme.onPrimary,
+ ),
+ color: WidgetStatePropertyAll(
+ theme.colorScheme.primary,
+ ),
+ ),
+ if (profile.timezone != null)
+ Chip(
+ label: Text(profile.timezone!),
+ labelStyle: TextStyle(
+ color: theme.colorScheme.onPrimary,
+ ),
+ color: WidgetStatePropertyAll(
+ theme.colorScheme.primary,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ if (member.userId !=
+ ref.watch(ClientStateController.provider)?.userId &&
+ roomId != null)
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ FilledButton.icon(onPressed: null, label: Text("Message")),
+
+ if (ref.watch(
+ PowerLevelController.provider(
+ PowerLevelConfig(
+ eventType: "m.room.member",
+ action: MembershipAction.kick,
+ isStateEvent: true,
+ targetUser: member.userId,
+ ),
+ ),
+ ) &&
+ member.status == MembershipStatus.join ||
+ member.status == MembershipStatus.invite)
+ FilledButton.icon(
+ onPressed: () => showMembershipDialog(MembershipAction.kick),
+ label: Text("Kick"),
+ style: ButtonStyle(
+ backgroundColor: WidgetStatePropertyAll(
+ theme.colorScheme.error,
+ ),
+ foregroundColor: WidgetStatePropertyAll(
+ theme.colorScheme.onError,
+ ),
+ ),
+ ),
+ if (ref.watch(
+ PowerLevelController.provider(
+ PowerLevelConfig(
+ eventType: "m.room.member",
+ action: MembershipAction.ban,
+ isStateEvent: true,
+ targetUser: member.userId,
+ ),
+ ),
+ ))
+ ElevatedButton.icon(
+ onPressed: () => showMembershipDialog(
+ member.status == MembershipStatus.ban
+ ? MembershipAction.unban
+ : MembershipAction.ban,
+ ),
+ label: Text(
+ member.status == MembershipStatus.ban ? "Unban" : "Ban",
+ ),
+ style: ButtonStyle(
+ backgroundColor: WidgetStatePropertyAll(
+ theme.colorScheme.errorContainer,
+ ),
+ foregroundColor: WidgetStatePropertyAll(
+ theme.colorScheme.onErrorContainer,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/widgets/chat_page/wrappers/message_wrapper.dart b/lib/widgets/chat_page/wrappers/message_wrapper.dart
index 1be6c2b..9c70c27 100644
--- a/lib/widgets/chat_page/wrappers/message_wrapper.dart
+++ b/lib/widgets/chat_page/wrappers/message_wrapper.dart
@@ -1,59 +1,83 @@
import "package:flutter/material.dart";
import "package:flutter_chat_core/flutter_chat_core.dart";
-import "package:nexus/models/room.dart";
import "package:nexus/widgets/chat_page/lazy_loading/message_avatar.dart";
import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart";
+import "package:nexus/widgets/chat_page/wrappers/reaction_row.dart";
+import "package:timeago/timeago.dart";
class MessageWrapper extends StatelessWidget {
final Message message;
final Widget child;
- final Room room;
final MessageGroupStatus? groupStatus;
- const MessageWrapper(
- this.message,
- this.child,
- this.groupStatus,
- this.room, {
- super.key,
- });
+ 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
- ? MessageAvatar(message, room, height: 40)
- : SizedBox(width: 40),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- spacing: 4,
- children: [
- if (groupStatus?.isFirst != false)
- MessageDisplayname(
- message,
- room,
- style: Theme.of(context).textTheme.titleMedium?.copyWith(
- fontWeight: FontWeight.bold,
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final error = message.metadata?["error"];
+
+ return 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
+ ? MessageAvatar(message, height: 40)
+ : SizedBox(width: 40),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ spacing: 4,
+ children: [
+ if (groupStatus?.isFirst != false)
+ Row(
+ spacing: 4,
+ children: [
+ Flexible(
+ child: MessageDisplayname(
+ message,
+ style: theme.textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ if (message.deliveredAt != null &&
+ groupStatus?.isFirst != false)
+ Tooltip(
+ message: message.deliveredAt!.toString(),
+ child: Text(
+ format(message.deliveredAt!),
+ style: theme.textTheme.labelSmall?.copyWith(
+ color: Colors.grey,
+ ),
+ ),
+ ),
+ ],
),
- ),
- child,
- ],
+ child,
+ if (error != null && error != "not sent")
+ Text(
+ error,
+ style: theme.textTheme.labelSmall?.copyWith(
+ color: theme.colorScheme.error,
+ ),
+ ),
+ ReactionRow(message),
+ ],
+ ),
),
- ),
- ],
+ ],
+ ),
),
- ),
- );
+ );
+ }
}
diff --git a/lib/widgets/chat_page/wrappers/reaction_row.dart b/lib/widgets/chat_page/wrappers/reaction_row.dart
new file mode 100644
index 0000000..5e8fe86
--- /dev/null
+++ b/lib/widgets/chat_page/wrappers/reaction_row.dart
@@ -0,0 +1,116 @@
+import "package:cross_cache/cross_cache.dart";
+import "package:fast_immutable_collections/fast_immutable_collections.dart";
+import "package:flutter/material.dart";
+import "package:flutter_chat_core/flutter_chat_core.dart";
+import "package:flutter_hooks/flutter_hooks.dart";
+import "package:flutter_riverpod/flutter_riverpod.dart";
+import "package:nexus/controllers/client_state_controller.dart";
+import "package:nexus/controllers/cross_cache_controller.dart";
+import "package:nexus/controllers/room_chat_controller.dart";
+import "package:nexus/controllers/selected_room_controller.dart";
+import "package:nexus/helpers/extensions/get_headers.dart";
+import "package:nexus/helpers/extensions/mxc_to_https.dart";
+import "package:nexus/main.dart";
+
+class ReactionRow extends ConsumerWidget {
+ final Message message;
+ const ReactionRow(this.message, {super.key});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final clientState = ref.watch(ClientStateController.provider);
+
+ return Wrap(
+ spacing: 4,
+ runSpacing: 4,
+ children: clientState?.homeserverUrl == null || message.reactions == null
+ ? []
+ : message.reactions!
+ .mapTo(
+ (reaction, reactors) => HookBuilder(
+ builder: (context) {
+ final enabled = useState(true);
+ final selected = reactors.contains(clientState!.userId);
+ return Tooltip(
+ message: reactors.join(", "),
+ child: ChoiceChip(
+ showCheckmark: false,
+ selected: selected,
+ label: Row(
+ mainAxisSize: MainAxisSize.min,
+ spacing: 8,
+ children: [
+ Flexible(
+ child: reaction.startsWith("mxc://")
+ ? Image(
+ height: 20,
+ image: CachedNetworkImage(
+ headers: ref.headers,
+ Uri.parse(reaction)
+ .mxcToHttps(
+ clientState.homeserverUrl!,
+ )
+ .toString(),
+ ref.watch(
+ CrossCacheController.provider,
+ ),
+ ),
+ )
+ : Text(
+ reaction,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ Text(
+ reactors.length.toString(),
+ overflow: TextOverflow.ellipsis,
+ ),
+ ],
+ ),
+ onSelected: enabled.value
+ ? (value) async {
+ enabled.value = false;
+ try {
+ final roomId = ref.watch(
+ SelectedRoomController.provider.select(
+ (value) => value?.metadata?.id,
+ ),
+ );
+ if (roomId == null ||
+ clientState.userId == null) {
+ return;
+ }
+
+ final controller = ref.watch(
+ RoomChatController.provider(
+ roomId,
+ ).notifier,
+ );
+
+ if (selected) {
+ await controller
+ .removeReaction(
+ reaction,
+ message,
+ clientState.userId!,
+ )
+ .onError(showError);
+ } else {
+ await controller
+ .sendReaction(reaction, message)
+ .onError(showError);
+ }
+ } finally {
+ enabled.value = true;
+ }
+ }
+ : null,
+ ),
+ );
+ },
+ ),
+ )
+ .toList(),
+ );
+ }
+}
diff --git a/lib/widgets/chat_page/wrappers/text_message_wrapper.dart b/lib/widgets/chat_page/wrappers/text_message_wrapper.dart
index 41bc01e..8d7a625 100644
--- a/lib/widgets/chat_page/wrappers/text_message_wrapper.dart
+++ b/lib/widgets/chat_page/wrappers/text_message_wrapper.dart
@@ -1,15 +1,21 @@
+import "package:cross_cache/cross_cache.dart";
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:flutter_linkify/flutter_linkify.dart";
+import "package:flutter_riverpod/flutter_riverpod.dart";
+import "package:nexus/controllers/cross_cache_controller.dart";
+import "package:nexus/controllers/url_preview_controller.dart";
+import "package:nexus/helpers/extensions/better_when.dart";
+import "package:nexus/helpers/extensions/get_headers.dart";
+import "package:nexus/helpers/launch_helper.dart";
import "package:nexus/widgets/chat_page/html/html.dart";
import "package:nexus/widgets/chat_page/wrappers/message_wrapper.dart";
import "package:nexus/widgets/chat_page/reply_widget.dart";
-class TextMessageWrapper extends StatelessWidget {
+class TextMessageWrapper extends ConsumerWidget {
final Message message;
final String? content;
- final Room room;
final MessageGroupStatus? groupStatus;
final Future Function(Message oldMessage, Message newMessage)
updateMessage;
@@ -21,7 +27,6 @@ class TextMessageWrapper extends StatelessWidget {
this.message, {
this.content,
this.onTapReply,
- required this.room,
required this.updateMessage,
required this.groupStatus,
required this.isSentByMe,
@@ -30,11 +35,17 @@ class TextMessageWrapper extends StatelessWidget {
});
@override
- Widget build(BuildContext context) {
+ Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final textMessage = message is TextMessage ? message as TextMessage : null;
+ final link = textMessage == null
+ ? null
+ : RegExp(
+ r'''https?://[^\s"'<>]+''',
+ ).allMatches(textMessage.text).firstOrNull?.group(0);
+
return MessageWrapper(
message,
ClipRRect(
@@ -43,7 +54,9 @@ class TextMessageWrapper extends StatelessWidget {
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12),
decoration: BoxDecoration(
color: isSentByMe
- ? colorScheme.primaryContainer
+ ? (message.id.startsWith("~")
+ ? colorScheme.onPrimary
+ : colorScheme.primaryContainer)
: colorScheme.surfaceContainer,
),
child: Column(
@@ -51,65 +64,84 @@ class TextMessageWrapper extends StatelessWidget {
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)!;
- }
+ message.metadata?["format"] == "org.matrix.custom.html"
+ ? Html(
+ textStyle: message.metadata?["big"] == true
+ ? TextStyle(fontSize: 32)
+ : null,
+ content!.replaceAllMapped(
+ RegExp(
+ "(]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)",
+ caseSensitive: false,
+ dotAll: true,
+ ),
+ (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";
- },
+ // Otherwise, wrap the bare URL
+ final url = m.group(2)!;
+ return "$url";
+ },
+ ),
)
- .replaceAll("\n", "
"),
- ),
+ : Linkify(
+ text: content!,
+ options: LinkifyOptions(humanize: false),
+ onOpen: (link) => ref
+ .watch(LaunchHelper.provider)
+ .launchUrl(Uri.parse(link.url)),
+ linkStyle: TextStyle(
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ ),
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 (link != null)
+ ref
+ .watch(UrlPreviewController.provider(link))
+ .betterWhen(
+ loading: SizedBox.shrink,
+ data: (preview) => preview == null
+ ? SizedBox.shrink()
+ : LinkPreview(
+ onTap: (url) => ref
+ .watch(LaunchHelper.provider)
+ .launchUrl(Uri.parse(url)),
+ imageBuilder: (url) => Image(
+ image: CachedNetworkImage(
+ url,
+ ref.watch(CrossCacheController.provider),
+ headers: ref.headers,
+ ),
+ fit: BoxFit.cover,
+ errorBuilder: (_, _, _) => SizedBox.shrink(),
+ ),
+ text: link,
+ backgroundColor: isSentByMe
+ ? colorScheme.inversePrimary
+ : colorScheme.surfaceContainerLow,
+ outsidePadding: EdgeInsets.only(top: 4),
+ insidePadding: EdgeInsets.symmetric(
+ vertical: 8,
+ horizontal: 16,
+ ),
+ linkPreviewData: preview,
+ onLinkPreviewDataFetched: (_) => null,
+ ),
),
- ),
- ),
if (extra != null) extra!,
],
),
),
),
groupStatus,
- room,
);
}
}
diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt
index 2e0c766..fee47c5 100644
--- a/linux/CMakeLists.txt
+++ b/linux/CMakeLists.txt
@@ -7,7 +7,7 @@ project(runner LANGUAGES CXX)
set(BINARY_NAME "nexus")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
-set(APPLICATION_ID "nexus.federated.nexus")
+set(APPLICATION_ID "nexus.federated.Nexus")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
index f70fb6e..5485b95 100644
--- a/linux/flutter/generated_plugin_registrant.cc
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -11,7 +11,6 @@
#include
#include
#include
-#include
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dynamic_system_colors_registrar =
@@ -29,7 +28,4 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);
- g_autoptr(FlPluginRegistrar) window_size_registrar =
- fl_plugin_registry_get_registrar_for_plugin(registry, "WindowSizePlugin");
- window_size_plugin_register_with_registrar(window_size_registrar);
}
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
index 78dcf40..13ef2de 100644
--- a/linux/flutter/generated_plugins.cmake
+++ b/linux/flutter/generated_plugins.cmake
@@ -8,7 +8,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
screen_retriever_linux
url_launcher_linux
window_manager
- window_size
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
diff --git a/linux/nexus.federated.Nexus.desktop b/linux/nexus.federated.Nexus.desktop
new file mode 100644
index 0000000..d3fa575
--- /dev/null
+++ b/linux/nexus.federated.Nexus.desktop
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Name=Nexus
+GenericName=Matrix Client
+Comment=A simple and user-friendly Matrix client
+Exec=nexus
+Icon=nexus
+Terminal=false
+Type=Application
+Categories=Chat;Network;InstantMessaging;
\ No newline at end of file
diff --git a/linux/nix/devshell.nix b/linux/nix/devshell.nix
new file mode 100644
index 0000000..91ba95a
--- /dev/null
+++ b/linux/nix/devshell.nix
@@ -0,0 +1,41 @@
+{ pkgs, lib }:
+let
+ android = pkgs.androidenv.composeAndroidPackages {
+ toolsVersion = "26.1.1";
+ platformToolsVersion = "36.0.1";
+ buildToolsVersions = [
+ "35.0.0"
+ "36.0.0"
+ ];
+ cmakeVersions = [ "3.22.1" ];
+ platformVersions = [ "36" ];
+ abiVersions = [
+ "armeabi-v7a"
+ "arm64-v8a"
+ ];
+ includeNDK = true;
+ ndkVersions = [ "28.2.13676358" ];
+ };
+in
+pkgs.mkShell {
+ packages = with pkgs; [
+ go
+ git
+ jdk17
+ flutter
+ android.platform-tools
+ ];
+
+ env = rec {
+ LIBCLANG_PATH = lib.makeLibraryPath [ pkgs.libclang ];
+ LD_LIBRARY_PATH = "./build/native_assets/linux:${lib.makeLibraryPath [ pkgs.zlib ]}";
+ CPATH = lib.makeSearchPath "include" [ pkgs.glibc.dev ];
+
+ ANDROID_HOME = "${android.androidsdk}/libexec/android-sdk";
+ ANDROID_SDK_ROOT = ANDROID_HOME;
+ JAVA_HOME = pkgs.jdk17;
+
+ TOOLS = "${ANDROID_HOME}/build-tools/${"36.0.0"}";
+ GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${TOOLS}/aapt2";
+ };
+}
diff --git a/linux/nix/pkg/default.nix b/linux/nix/pkg/default.nix
new file mode 100644
index 0000000..adaeb15
--- /dev/null
+++ b/linux/nix/pkg/default.nix
@@ -0,0 +1,45 @@
+{
+ lib,
+ callPackage,
+ libclang,
+ flutter,
+ src,
+}:
+
+flutter.buildFlutterApplication {
+ pname = "nexus";
+ version = "0.1.0";
+ inherit src;
+
+ preBuild = ''
+ cp ${callPackage ./gomuks.nix { inherit src; }}/lib/* .
+ packageRunCustom nexus generate source/scripts test
+ packageRun build_runner build
+ '';
+
+ env.LIBCLANG_PATH = lib.makeLibraryPath [ libclang ];
+
+ autoPubspecLock = src + "/pubspec.lock";
+
+ gitHashes = {
+ window_size = "sha256-XelNtp7tpZ91QCEcvewVphNUtgQX7xrp5QP0oFo6DgM=";
+ dynamic_system_colors = "sha256-es6rjMK1drkqZBKYUP77yw/q5+0uLwWOEDOXRawy3Dc=";
+ flutter_chat_ui = "sha256-4fuag7lRH5cMBFD3fUzj2K541JwXLoz8HF/4OMr3uhk=";
+ flutter_link_previewer = "sha256-4fuag7lRH5cMBFD3fUzj2K541JwXLoz8HF/4OMr3uhk=";
+ emoji_text_field = "sha256-3TOys09EP2GRo6pUBGPXaqBlE39O2Cmwt42Hs1cTDKo=";
+ };
+
+ postInstall = ''
+ install -D assets/icon.svg $out/share/icons/hicolor/scalable/apps/nexus.svg
+ install -Dm755 linux/nexus.federated.Nexus.desktop -t $out/share/applications
+ wrapProgram $out/bin/nexus \
+ --suffix LD_LIBRARY_PATH : $out/app/nexus/lib
+ '';
+
+ meta = {
+ description = "A simple and user-friendly Matrix client";
+ mainProgram = "nexus";
+ platforms = lib.platforms.linux;
+ maintainers = with lib.maintainers; [ quadradical ];
+ };
+}
diff --git a/linux/nix/pkg/gomuks.nix b/linux/nix/pkg/gomuks.nix
new file mode 100644
index 0000000..1bc92bf
--- /dev/null
+++ b/linux/nix/pkg/gomuks.nix
@@ -0,0 +1,31 @@
+{
+ src,
+ buildGoModule,
+}:
+
+buildGoModule (finalAttrs: {
+ pname = "gomuks-ffi";
+ version = "submodule";
+
+ doCheck = false;
+
+ src = "${src}/gomuks";
+
+ vendorHash = "sha256-zBDfBZqUoHIfZ0AajZEvSBbskjpFB7yIsomt0KYDo7Y=";
+
+ buildPhase = ''
+ runHook preBuild
+
+ go build -buildmode=c-shared -o libgomuks.so -tags goolm,noheic ./pkg/ffi
+
+ runHook postBuild
+ '';
+
+ installPhase = ''
+ runHook preInstall
+
+ install -Dm0644 libgomuks.so -t $out/lib
+
+ runHook postInstall
+ '';
+})
diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc
index 58cd859..abf5dc5 100644
--- a/linux/runner/my_application.cc
+++ b/linux/runner/my_application.cc
@@ -43,6 +43,7 @@ static void my_application_activate(GApplication* application) {
}
}
#endif
+ gtk_widget_set_size_request(GTK_WIDGET(window), 250, -1);
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
diff --git a/nix/android.nix b/nix/android.nix
deleted file mode 100644
index f373968..0000000
--- a/nix/android.nix
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- androidenv,
-}:
-androidenv.composeAndroidPackages {
- toolsVersion = "26.1.1";
- platformToolsVersion = "36.0.1";
- buildToolsVersions = [
- "35.0.0"
- "36.0.0"
- ];
- cmakeVersions = [ "3.22.1" ];
- platformVersions = [ "36" ];
- abiVersions = [
- "armeabi-v7a"
- "arm64-v8a"
- ];
- includeNDK = true;
- ndkVersions = [ "27.0.12077973" ];
-
-}
diff --git a/pubspec.lock b/pubspec.lock
index af73796..984341b 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -348,11 +348,21 @@ packages:
dynamic_system_colors:
dependency: "direct main"
description:
- name: dynamic_system_colors
- sha256: "43794e658fa88cbdec9f397dd1afd2eb69b6c9717e99b93b16ba37c3aa3b3a8c"
- url: "https://pub.dev"
- source: hosted
+ path: "."
+ ref: HEAD
+ resolved-ref: "3b61760d5e0ac1229eefde5b61247947eede4110"
+ url: "https://github.com/hasali19/flutter_dynamic_system_colors"
+ source: git
version: "1.8.0"
+ emoji_text_field:
+ dependency: "direct main"
+ description:
+ path: "."
+ ref: HEAD
+ resolved-ref: "5f7baaf8a6f059ec3ab8ff0f5d02339b00bf6997"
+ url: "https://github.com/Henry-Hiles/emoji_text_field"
+ source: git
+ version: "1.0.0"
encrypt:
dependency: transitive
description:
@@ -465,11 +475,10 @@ packages:
flutter_chat_ui:
dependency: "direct main"
description:
- path: "packages/flutter_chat_ui"
- ref: HEAD
- resolved-ref: "03be67c8c81c8f637672ee03dd8f082d2c223627"
- url: "https://github.com/Henry-Hiles/flutter_chat_ui"
- source: git
+ name: flutter_chat_ui
+ sha256: cfbaac38f429beb33d9cc1ca920ae7ccbadbed282c99335d590d61306d3a3d0f
+ url: "https://pub.dev"
+ source: hosted
version: "2.11.1"
flutter_hooks:
dependency: "direct main"
@@ -490,12 +499,19 @@ packages:
flutter_link_previewer:
dependency: "direct main"
description:
- path: "packages/flutter_link_previewer"
- ref: HEAD
- resolved-ref: "03be67c8c81c8f637672ee03dd8f082d2c223627"
- url: "https://github.com/Henry-Hiles/flutter_chat_ui"
- source: git
+ name: flutter_link_previewer
+ sha256: "346f345064e65bc8bf739bccf19d6d6ca50f8183ffc52e452afa58c06ee2cbf7"
+ url: "https://pub.dev"
+ source: hosted
version: "4.2.0"
+ flutter_linkify:
+ dependency: "direct main"
+ description:
+ name: flutter_linkify
+ sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.0.0"
flutter_lints:
dependency: "direct dev"
description:
@@ -656,7 +672,7 @@ packages:
source: hosted
version: "0.15.6"
http:
- dependency: transitive
+ dependency: "direct main"
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
@@ -823,6 +839,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
+ linkify:
+ dependency: transitive
+ description:
+ name: linkify
+ sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.0.0"
lints:
dependency: transitive
description:
@@ -1356,6 +1380,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.0+1"
+ timeago:
+ dependency: "direct main"
+ description:
+ name: timeago
+ sha256: b05159406a97e1cbb2b9ee4faa9fb096fe0e2dfcd8b08fcd2a00553450d3422e
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.7.1"
typed_data:
dependency: transitive
description:
@@ -1548,15 +1580,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.5.1"
- window_size:
- dependency: "direct main"
- description:
- path: "plugins/window_size"
- ref: HEAD
- resolved-ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601
- url: "https://github.com/google/flutter-desktop-embedding"
- source: git
- version: "0.1.0"
xdg_directories:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index ec69ae9..dbed5c5 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: nexus
description: "Yet another Matrix client"
-version: 1.0.0
+version: 0.1.0
publish_to: none
flutter:
@@ -31,25 +31,17 @@ dependencies:
image_picker: ^1.1.2
file_picker: ^10.3.3
path: ^1.9.0
- dynamic_system_colors: ^1.8.0
+ dynamic_system_colors:
+ git:
+ url: https://github.com/hasali19/flutter_dynamic_system_colors
collection: ^1.19.1
window_manager: ^0.5.1
- window_size:
- git:
- url: https://github.com/google/flutter-desktop-embedding
- path: plugins/window_size
flutter_chat_core: ^2.0.0
flyer_chat_image_message: ^2.2.2
flyer_chat_system_message: ^2.1.13
flyer_chat_file_message: ^2.3.1
- flutter_chat_ui:
- git:
- url: https://github.com/Henry-Hiles/flutter_chat_ui
- path: packages/flutter_chat_ui
- flutter_link_previewer:
- git:
- url: https://github.com/Henry-Hiles/flutter_chat_ui
- path: packages/flutter_link_previewer
+ flutter_chat_ui: ^2.11.1
+ flutter_link_previewer: ^4.2.0
color_hash: ^1.0.1
flutter_widget_from_html_core: ^0.17.0
flutter_svg: ^2.2.2
@@ -63,6 +55,12 @@ dependencies:
hooks: ^1.0.0
code_assets: ^1.0.0
ffigen: ^20.1.1
+ timeago: ^3.7.1
+ http: ^1.6.0
+ flutter_linkify: ^6.0.0
+ emoji_text_field:
+ git:
+ url: https://github.com/Henry-Hiles/emoji_text_field
dev_dependencies:
build_runner: ^2.4.11
@@ -77,8 +75,9 @@ flutter_launcher_icons:
ios: true
android: true
image_path: assets/icon.png
- adaptive_icon_background: "#000000"
+ adaptive_icon_background: assets/background.png
adaptive_icon_foreground: assets/foreground.png
+ adaptive_icon_monochrome: assets/monochrome.png
remove_alpha_ios: true
windows:
generate: true
\ No newline at end of file
diff --git a/scripts/generate.dart b/scripts/generate.dart
index b240d98..446a469 100644
--- a/scripts/generate.dart
+++ b/scripts/generate.dart
@@ -3,26 +3,7 @@ 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}",
- );
- }
+ final repoDir = Directory.fromUri(Platform.script.resolve("../gomuks"));
print("Generating FFI Bindings...");
diff --git a/scripts/generate.sh b/scripts/generate.sh
deleted file mode 100755
index 6076ab8..0000000
--- a/scripts/generate.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/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 55fb066..bde1c28 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -11,7 +11,6 @@
#include
#include
#include
-#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
DynamicColorPluginCApiRegisterWithRegistrar(
@@ -24,6 +23,4 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
- WindowSizePluginRegisterWithRegistrar(
- registry->GetRegistrarForPlugin("WindowSizePlugin"));
}
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index 9333a2f..7b6b425 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -8,7 +8,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
screen_retriever_windows
url_launcher_windows
window_manager
- window_size
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc
index 24405eb..3583d23 100644
--- a/windows/runner/Runner.rc
+++ b/windows/runner/Runner.rc
@@ -89,11 +89,11 @@ BEGIN
BEGIN
BLOCK "040904e4"
BEGIN
- VALUE "CompanyName", "nexus.federated.nexus" "\0"
+ VALUE "CompanyName", "nexus.federated.Nexus" "\0"
VALUE "FileDescription", "nexus" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "nexus" "\0"
- VALUE "LegalCopyright", "Copyright (C) 2025 nexus.federated.nexus. All rights reserved." "\0"
+ VALUE "LegalCopyright", "Copyright (C) 2025 nexus.federated.Nexus. All rights reserved." "\0"
VALUE "OriginalFilename", "nexus.exe" "\0"
VALUE "ProductName", "nexus" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico
index e3c83c9..f8a91f7 100644
Binary files a/windows/runner/resources/app_icon.ico and b/windows/runner/resources/app_icon.ico differ