wip mentions

This commit is contained in:
Henry Hiles 2025-12-07 10:53:49 -05:00
commit 541933a939
No known key found for this signature in database
11 changed files with 145 additions and 86 deletions

View file

@ -10,6 +10,11 @@ import "package:nexus/controllers/secure_storage_controller.dart";
import "package:nexus/models/session_backup.dart"; import "package:nexus/models/session_backup.dart";
class ClientController extends AsyncNotifier<Client> { class ClientController extends AsyncNotifier<Client> {
@override
bool updateShouldNotify(
AsyncValue<Client> previous,
AsyncValue<Client> next,
) => previous.hasValue != next.hasValue;
static const sessionBackupKey = "sessionBackup"; static const sessionBackupKey = "sessionBackup";
@override @override
@ -81,7 +86,7 @@ class ClientController extends AsyncNotifier<Client> {
).toJson(), ).toJson(),
), ),
); );
ref.invalidateSelf(); ref.invalidateSelf(asReload: true);
return true; return true;
} catch (_) { } catch (_) {
return false; return false;

View file

@ -92,11 +92,18 @@ class RoomChatController extends AsyncNotifier<ChatController> {
Future<void> updateMessage(Message message, Message newMessage) async => Future<void> updateMessage(Message message, Message newMessage) async =>
(await future).updateMessage(message, newMessage); (await future).updateMessage(message, newMessage);
Future<void> send(String message, {Message? replyTo}) async => Future<void> send(Message message, {Message? replyTo}) async {
final controller = await future;
controller.insertMessage(message);
if (message is TextMessage) {
await room.sendTextEvent( await room.sendTextEvent(
message, message.text,
inReplyTo: replyTo == null ? null : await room.getEventById(replyTo.id), inReplyTo: replyTo == null ? null : await room.getEventById(replyTo.id),
); );
}
// TODO: Handle other types of message
}
Future<chat.User> resolveUser(String id) async { Future<chat.User> resolveUser(String id) async {
final user = await room.client.getUserProfile(id); final user = await room.client.getUserProfile(id);

View file

@ -97,7 +97,7 @@ extension EventToMessage on Event {
id: eventId, id: eventId,
authorId: senderId, authorId: senderId,
text: text:
"${content["displayname"]} ${switch (Membership.values.firstWhereOrNull((membership) => membership.name == content["membership"])) { "${sender.displayName} ${switch (Membership.values.firstWhereOrNull((membership) => membership.name == content["membership"])) {
Membership.invite => "was invited to", Membership.invite => "was invited to",
Membership.join => "joined", Membership.join => "joined",
Membership.leave => "left", Membership.leave => "left",

View file

@ -1,3 +1,4 @@
import "package:flutter/foundation.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_controller.dart";
import "package:nexus/controllers/shared_prefs_controller.dart"; import "package:nexus/controllers/shared_prefs_controller.dart";
@ -13,7 +14,23 @@ import "package:window_size/window_size.dart";
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
final class Logger extends ProviderObserver {
@override
void didUpdateProvider(
ProviderObserverContext context,
Object? previousValue,
Object? newValue,
) {
print('''{
"provider": "${context.provider}",
"changed": ${previousValue != newValue}
"type": ${previousValue.runtimeType}
}''');
}
}
void showError(Object error, [StackTrace? stackTrace]) { void showError(Object error, [StackTrace? stackTrace]) {
if (error.toString().contains("ParentDataWidget")) return;
if (error.toString().contains("DioException")) return; if (error.toString().contains("DioException")) return;
if (error.toString().contains("UTF-16")) return; if (error.toString().contains("UTF-16")) return;
@ -43,7 +60,9 @@ void main() async {
setWindowMinSize(const Size.square(500)); setWindowMinSize(const Size.square(500));
runApp(ProviderScope(child: const App())); runApp(
ProviderScope(observers: [if (kDebugMode) Logger()], child: const App()),
);
} }
class App extends ConsumerWidget { class App extends ConsumerWidget {

View file

@ -10,14 +10,4 @@ abstract class FullRoom with _$FullRoom {
required String title, required String title,
required Uri? avatar, required Uri? avatar,
}) = _FullRoom; }) = _FullRoom;
@override
bool operator ==(Object other) =>
other.runtimeType == runtimeType &&
other is FullRoom &&
other.avatar == avatar &&
other.title == title;
@override
int get hashCode => Object.hash(runtimeType, title, avatar);
} }

View file

@ -17,16 +17,4 @@ abstract class Space with _$Space {
Uri? avatar, Uri? avatar,
Icon? icon, Icon? icon,
}) = _Space; }) = _Space;
@override
bool operator ==(Object other) =>
other.runtimeType == runtimeType &&
other is Space &&
other.title == title &&
other.id == id &&
other.icon == icon &&
other.avatar == avatar;
@override
int get hashCode => Object.hash(runtimeType, title, id, icon, avatar);
} }

View file

@ -1,64 +1,101 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_chat_ui/flutter_chat_ui.dart"; import "package:flutter_chat_ui/flutter_chat_ui.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:fluttertagger/fluttertagger.dart";
import "package:matrix/matrix.dart";
import "package:nexus/helpers/extensions/get_headers.dart";
import "package:nexus/widgets/form_text_input.dart";
class ChatBox extends StatelessWidget { class ChatBox extends HookWidget {
final Message? replyToMessage; final Message? replyToMessage;
final VoidCallback onDismiss; final VoidCallback onDismiss;
final Map<String, String> headers; final Room room;
const ChatBox({ const ChatBox({
required this.replyToMessage, required this.replyToMessage,
required this.onDismiss, required this.onDismiss,
required this.headers, required this.room,
super.key, super.key,
}); });
@override @override
Widget build(BuildContext context) => Composer( Widget build(BuildContext context) {
sigmaX: 0, final theme = Theme.of(context);
sigmaY: 0, final controller = useRef(FlutterTaggerController());
sendIconColor: Theme.of(context).colorScheme.primary, final trigger = useState<String?>(null);
sendOnEnter: true, final style = TextStyle(
topWidget: replyToMessage == null color: theme.colorScheme.primary,
? null fontWeight: FontWeight.bold,
: ColoredBox( );
color: Theme.of(context).colorScheme.surfaceContainer,
child: Padding( return Column(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), mainAxisAlignment: MainAxisAlignment.end,
child: Row( children: [
spacing: 8, FlutterTagger(
children: [ overlay: SizedBox(),
Avatar( controller: controller.value,
userId: replyToMessage!.authorId, onSearch: (query, triggerCharacter) {
headers: headers, triggerCharacter == "#";
size: 16, if (controller.value.tags.isEmpty)
), controller.value.addTag(id: "id", name: "name");
Text( },
replyToMessage!.metadata?["displayName"] ?? triggerCharacterAndStyles: {"@": style, "#": style},
replyToMessage!.authorId, builder: (context, key) => TextFormField(controller: controller.value, key: key,autofocus: true,onFieldSubmitted: (_) {
style: Theme.of(context).textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.bold, },)
), // Composer(
), // textEditingController: controller.value,
Expanded( // key: key,
child: (replyToMessage is TextMessage) // sigmaY: 0,
? Text( // sendIconColor: theme.colorScheme.primary,
(replyToMessage as TextMessage).text, // sendOnEnter: true,
overflow: TextOverflow.ellipsis, // topWidget: replyToMessage == null
style: Theme.of(context).textTheme.labelMedium, // ? null
maxLines: 1, // : ColoredBox(
) // color: theme.colorScheme.surfaceContainer,
: SizedBox(), // child: Padding(
), // padding: EdgeInsets.symmetric(
IconButton( // horizontal: 16,
onPressed: onDismiss, // vertical: 4,
icon: Icon(Icons.close), // ),
iconSize: 20, // child: Row(
), // spacing: 8,
], // children: [
), // Avatar(
), // userId: replyToMessage!.authorId,
), // headers: room.client.headers,
autofocus: true, // size: 16,
); // ),
// Text(
// replyToMessage!.metadata?["displayName"] ??
// replyToMessage!.authorId,
// style: theme.textTheme.labelMedium?.copyWith(
// fontWeight: FontWeight.bold,
// ),
// ),
// Expanded(
// child: (replyToMessage is TextMessage)
// ? Text(
// (replyToMessage as TextMessage).text,
// overflow: TextOverflow.ellipsis,
// style: theme.textTheme.labelMedium,
// maxLines: 1,
// )
// : SizedBox(),
// ),
// IconButton(
// onPressed: onDismiss,
// icon: Icon(Icons.close),
// iconSize: 20,
// ),
// ],
// ),
// ),
// ),
// autofocus: true,
// ),
),
],
);
}
} }

View file

@ -85,7 +85,9 @@ class Html extends ConsumerWidget {
headers: client.headers, headers: client.headers,
errorBuilder: (_, error, _) => Text( errorBuilder: (_, error, _) => Text(
"Image Failed to Load", "Image Failed to Load",
style: TextStyle(color: Colors.red), style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
), ),
height: height.toDouble(), height: height.toDouble(),
width: width?.toDouble(), width: width?.toDouble(),

View file

@ -177,7 +177,7 @@ class RoomChat extends HookConsumerWidget {
composerBuilder: (_) => ChatBox( composerBuilder: (_) => ChatBox(
replyToMessage: replyToMessage.value, replyToMessage: replyToMessage.value,
onDismiss: () => replyToMessage.value = null, onDismiss: () => replyToMessage.value = null,
headers: room.roomData.client.headers, room: room.roomData,
), ),
textMessageBuilder: textMessageBuilder:
( (
@ -296,13 +296,6 @@ class RoomChat extends HookConsumerWidget {
), ),
), ),
), ),
onMessageSend: (message) {
notifier.send(
message,
replyTo: replyToMessage.value,
);
replyToMessage.value = null;
},
resolveUser: notifier.resolveUser, resolveUser: notifier.resolveUser,
chatController: controller, chatController: controller,
), ),

View file

@ -575,6 +575,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.17.0" version: "0.17.0"
fluttertagger:
dependency: "direct main"
description:
name: fluttertagger
sha256: "3df0132bdd431a7279da78ea70500ea1e767fa093f43f32785b757c10c6a0fcc"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
flyer_chat_file_message: flyer_chat_file_message:
dependency: "direct main" dependency: "direct main"
description: description:
@ -912,6 +920,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" version: "3.0.2"
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: meta:
dependency: transitive dependency: transitive
description: description:

View file

@ -65,6 +65,8 @@ dependencies:
vodozemac: ^0.4.0 vodozemac: ^0.4.0
clipboard: ^2.0.2 clipboard: ^2.0.2
shared_preferences: ^2.5.3 shared_preferences: ^2.5.3
mention_tag_text_field: ^0.0.9
fluttertagger: ^2.3.1
dev_dependencies: dev_dependencies:
build_runner: ^2.4.11 build_runner: ^2.4.11