Compare commits
6 commits
feb3d97af5
...
fb3b19a27f
| Author | SHA1 | Date | |
|---|---|---|---|
|
fb3b19a27f |
|||
|
bd1d5ea745 |
|||
|
1ca802c78b |
|||
|
7b9eda2d36 |
|||
|
63eb001c09 |
|||
|
00c3503c1f |
14 changed files with 203 additions and 153 deletions
11
DEVELOPMENT.md
Normal file
11
DEVELOPMENT.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
## Build instructions
|
||||
|
||||
CBuild instructions can be found in [README.md](./README.md#build-it-yourself).
|
||||
|
||||
## Updating Gomuks
|
||||
|
||||
You can run the following command to update the Gomuks submodule:
|
||||
|
||||
```sh
|
||||
git submodule update --remote
|
||||
```
|
||||
|
|
@ -53,7 +53,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
|
|||
- [x] HTML/Markdown
|
||||
- [x] Replies
|
||||
- [x] Choose ping on/off
|
||||
- [ ] Per message profiles
|
||||
- [x] Per message profiles
|
||||
- [ ] Attachments
|
||||
- [ ] Commands with [MSC4391](https://github.com/matrix-org/matrix-spec-proposals/pull/4391)
|
||||
- [x] Mentions
|
||||
|
|
@ -120,6 +120,9 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
|
|||
- [ ] Align your message bubbles to left or right
|
||||
- [ ] Show media by default
|
||||
- [ ] Dynamic Theming
|
||||
- [ ] Personas
|
||||
- [ ] Setting per-message profiles for users (MSC4461)
|
||||
- [ ] Explain how to send messages using a certain PMP
|
||||
- [ ] Devices
|
||||
- [ ] Viewing devices
|
||||
- [ ] Verifying devices
|
||||
|
|
@ -218,6 +221,8 @@ Run the app:
|
|||
flutter run
|
||||
```
|
||||
|
||||
Development instructions can be found in [DEVELOPMENT.md](./DEVELOPMENT.md).
|
||||
|
||||
## Community
|
||||
|
||||
Join the [Nexus Client Matrix Room](https://matrix.to/#/#nexus:federated.nexus) for questions or help with developing or using Nexus Client.
|
||||
|
|
|
|||
2
gomuks
2
gomuks
|
|
@ -1 +1 @@
|
|||
Subproject commit daa0ba028e7d89ba9fc7580fc8099348e6145cb3
|
||||
Subproject commit fbe6fdbaabeaae46fb4c03e0d18260941f235d49
|
||||
|
|
@ -9,7 +9,7 @@ import "package:flutter/foundation.dart";
|
|||
import "package:nexus/controllers/account_data_controller.dart";
|
||||
import "package:nexus/controllers/client_state_controller.dart";
|
||||
import "package:nexus/controllers/init_complete_controller.dart";
|
||||
import "package:nexus/controllers/new_events_controller.dart";
|
||||
import "package:nexus/controllers/room_chat_controller.dart";
|
||||
import "package:nexus/controllers/rooms_controller.dart";
|
||||
import "package:nexus/controllers/space_edges_controller.dart";
|
||||
import "package:nexus/controllers/sync_status_controller.dart";
|
||||
|
|
@ -82,11 +82,10 @@ class ClientController extends AsyncNotifier<int> {
|
|||
final event = Event.fromJson(decodedMuksEvent["event"]);
|
||||
|
||||
if (event.type == "m.room.message") {
|
||||
ref
|
||||
.watch(
|
||||
NewEventsController.provider(event.roomId).notifier,
|
||||
)
|
||||
.add(IList([event]));
|
||||
final provider = RoomChatController.provider(event.roomId);
|
||||
if (ref.exists(provider)) {
|
||||
ref.watch(provider.notifier).addEvent(event);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "sync_complete":
|
||||
|
|
@ -127,9 +126,9 @@ class ClientController extends AsyncNotifier<int> {
|
|||
}
|
||||
debugPrint("Finished handling $muksEventType...");
|
||||
} catch (error, stackTrace) {
|
||||
debugPrintStack(stackTrace: stackTrace, label: error.toString());
|
||||
debugger();
|
||||
showError(error, stackTrace);
|
||||
debugPrintStack(stackTrace: stackTrace, label: error.toString());
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/models/event.dart";
|
||||
|
||||
class NewEventsController extends Notifier<IList<Event>> {
|
||||
final String roomId;
|
||||
NewEventsController(this.roomId);
|
||||
|
||||
@override
|
||||
IList<Event> build() => const IList.empty();
|
||||
|
||||
void add(IList<Event> newEvents) => state = newEvents;
|
||||
|
||||
static final provider = NotifierProvider.autoDispose
|
||||
.family<NewEventsController, IList<Event>, String>(
|
||||
NewEventsController.new,
|
||||
);
|
||||
}
|
||||
|
|
@ -7,11 +7,11 @@ import "package:fluttertagger/fluttertagger.dart";
|
|||
import "package:nexus/controllers/client_controller.dart";
|
||||
import "package:nexus/controllers/message_controller.dart";
|
||||
import "package:nexus/controllers/messages_controller.dart";
|
||||
import "package:nexus/controllers/new_events_controller.dart";
|
||||
import "package:nexus/controllers/rooms_controller.dart";
|
||||
import "package:nexus/controllers/selected_room_controller.dart";
|
||||
import "package:nexus/models/configs/messages_config.dart";
|
||||
import "package:nexus/models/configs/message_config.dart";
|
||||
import "package:nexus/models/event.dart";
|
||||
import "package:nexus/models/requests/get_related_events_request.dart";
|
||||
import "package:nexus/models/requests/get_room_state_request.dart";
|
||||
import "package:nexus/models/requests/paginate_request.dart";
|
||||
|
|
@ -77,106 +77,6 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
|
|||
);
|
||||
final controller = InMemoryChatController(messages: messages.toList());
|
||||
|
||||
ref.onDispose(
|
||||
ref.listen(NewEventsController.provider(roomId), (_, next) async {
|
||||
for (final event in next) {
|
||||
if (event.type == "m.reaction") {
|
||||
final message = controller.messages.firstWhereOrNull(
|
||||
(message) =>
|
||||
message.id == event.content["m.relates_to"]?["event_id"],
|
||||
);
|
||||
final key = event.content["m.relates_to"]?["key"];
|
||||
if (message == null || key == null || !ref.mounted) return;
|
||||
|
||||
return await controller.updateMessage(
|
||||
message,
|
||||
message.copyWith(
|
||||
reactions: IMap(message.reactions)
|
||||
.update(
|
||||
key,
|
||||
(reactors) => [...reactors, event.authorId],
|
||||
ifAbsent: () => [event.authorId],
|
||||
)
|
||||
.unlock,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (event.type == "m.room.redaction") {
|
||||
final controller = await future;
|
||||
final redactsId = event.content["redacts"];
|
||||
final originalMessage = controller.messages.firstWhereOrNull(
|
||||
(message) => message.id == redactsId,
|
||||
);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
if (originalMessage != null) {
|
||||
return await controller.removeMessage(originalMessage);
|
||||
}
|
||||
|
||||
final redacts = ref
|
||||
.read(SelectedRoomController.provider)
|
||||
?.events
|
||||
.firstWhere((event) => event.eventId == redactsId);
|
||||
|
||||
if (redacts?.type == "m.reaction") {
|
||||
final message = controller.messages.firstWhereOrNull(
|
||||
(message) =>
|
||||
message.id == redacts!.content["m.relates_to"]?["event_id"],
|
||||
);
|
||||
final key = redacts!.content["m.relates_to"]?["key"];
|
||||
if (message == null || key == null || !ref.mounted) return;
|
||||
|
||||
return await controller.updateMessage(
|
||||
message,
|
||||
message.copyWith(
|
||||
reactions: IMap(message.reactions)
|
||||
.update(
|
||||
key,
|
||||
(reactors) =>
|
||||
IList(reactors).remove(redacts.authorId).unlock,
|
||||
)
|
||||
.where((_, value) => value.isNotEmpty)
|
||||
.unlock,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
final message = await ref.watch(
|
||||
MessageController.provider(
|
||||
MessageConfig(event: event, room: room!, includeEdits: true),
|
||||
).future,
|
||||
);
|
||||
if (event.relationType == "m.replace") {
|
||||
final controller = await future;
|
||||
final oldMessage = controller.messages.firstWhereOrNull(
|
||||
(element) => element.id == event.relatesTo,
|
||||
);
|
||||
if (oldMessage == null || message == null || !ref.mounted) return;
|
||||
|
||||
return await controller.updateMessage(
|
||||
oldMessage,
|
||||
message.copyWith(
|
||||
id: oldMessage.id,
|
||||
replyToMessageId: oldMessage.replyToMessageId,
|
||||
metadata: {
|
||||
...(oldMessage.metadata ?? {}),
|
||||
...(message.metadata ?? {})
|
||||
.toIMap()
|
||||
.where((key, value) => value != null)
|
||||
.unlock,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (message != null && ref.mounted) {
|
||||
await insertMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, weak: true).close,
|
||||
);
|
||||
|
||||
ref.onDispose(controller.dispose);
|
||||
|
||||
// While there are under 20 messages, try up to load more messages until theres no more or we have 20 messages.
|
||||
|
|
@ -187,6 +87,105 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
|
|||
return controller;
|
||||
}
|
||||
|
||||
Future<void> addEvent(Event event) async {
|
||||
final controller = await future;
|
||||
if (event.type == "m.reaction") {
|
||||
final message = controller.messages.firstWhereOrNull(
|
||||
(message) => message.id == event.content["m.relates_to"]?["event_id"],
|
||||
);
|
||||
final key = event.content["m.relates_to"]?["key"];
|
||||
if (message == null || key == null || !ref.mounted) return;
|
||||
|
||||
return await controller.updateMessage(
|
||||
message,
|
||||
message.copyWith(
|
||||
reactions: IMap(message.reactions)
|
||||
.update(
|
||||
key,
|
||||
(reactors) => [...reactors, event.authorId],
|
||||
ifAbsent: () => [event.authorId],
|
||||
)
|
||||
.unlock,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (event.type == "m.room.redaction") {
|
||||
final controller = await future;
|
||||
final redactsId = event.content["redacts"];
|
||||
final originalMessage = controller.messages.firstWhereOrNull(
|
||||
(message) => message.id == redactsId,
|
||||
);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
if (originalMessage != null) {
|
||||
return await controller.removeMessage(originalMessage);
|
||||
}
|
||||
|
||||
final redacts = ref
|
||||
.read(SelectedRoomController.provider)
|
||||
?.events
|
||||
.firstWhere((event) => event.eventId == redactsId);
|
||||
|
||||
if (redacts?.type == "m.reaction") {
|
||||
final message = controller.messages.firstWhereOrNull(
|
||||
(message) =>
|
||||
message.id == redacts!.content["m.relates_to"]?["event_id"],
|
||||
);
|
||||
final key = redacts!.content["m.relates_to"]?["key"];
|
||||
if (message == null || key == null || !ref.mounted) return;
|
||||
|
||||
return await controller.updateMessage(
|
||||
message,
|
||||
message.copyWith(
|
||||
reactions: IMap(message.reactions)
|
||||
.update(
|
||||
key,
|
||||
(reactors) => IList(reactors).remove(redacts.authorId).unlock,
|
||||
)
|
||||
.where((_, value) => value.isNotEmpty)
|
||||
.unlock,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
final message = await ref.watch(
|
||||
MessageController.provider(
|
||||
MessageConfig(
|
||||
event: event,
|
||||
room: ref.read(RoomsController.provider)[roomId]!,
|
||||
includeEdits: true,
|
||||
),
|
||||
).future,
|
||||
);
|
||||
if (event.relationType == "m.replace") {
|
||||
final controller = await future;
|
||||
final oldMessage = controller.messages.firstWhereOrNull(
|
||||
(element) => element.id == event.relatesTo,
|
||||
);
|
||||
if (oldMessage == null || message == null || !ref.mounted) return;
|
||||
|
||||
return await controller.updateMessage(
|
||||
oldMessage,
|
||||
message.copyWith(
|
||||
id: oldMessage.id,
|
||||
replyToMessageId: oldMessage.replyToMessageId,
|
||||
metadata: {
|
||||
...(oldMessage.metadata ?? {}),
|
||||
...(message.metadata ?? {})
|
||||
.toIMap()
|
||||
.where((key, value) => value != null)
|
||||
.unlock,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (message != null && ref.mounted) {
|
||||
await insertMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> insertMessage(Message message) async {
|
||||
final controller = await future;
|
||||
final oldMessage = message.metadata?["txnId"] == null
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import "package:collection/collection.dart";
|
|||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/controllers/client_state_controller.dart";
|
||||
import "package:nexus/controllers/new_events_controller.dart";
|
||||
import "package:nexus/controllers/room_chat_controller.dart";
|
||||
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
||||
import "package:nexus/models/read_receipt.dart";
|
||||
import "package:nexus/models/room.dart";
|
||||
|
|
@ -34,18 +34,20 @@ class RoomsController extends Notifier<IMap<String, Room>> {
|
|||
);
|
||||
|
||||
if (addToNewEvents) {
|
||||
ref
|
||||
.watch(NewEventsController.provider(roomId).notifier)
|
||||
.add(
|
||||
incoming.timeline
|
||||
final provider = RoomChatController.provider(roomId);
|
||||
if (ref.exists(provider)) {
|
||||
for (final event
|
||||
in incoming.timeline
|
||||
.map(
|
||||
(timelineTuple) => events?.firstWhereOrNull(
|
||||
(event) => timelineTuple.eventRowId == event.rowId,
|
||||
),
|
||||
)
|
||||
.nonNulls
|
||||
.toIList(),
|
||||
);
|
||||
.toIList()) {
|
||||
ref.read(provider.notifier).addEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return acc.add(
|
||||
|
|
|
|||
|
|
@ -62,12 +62,6 @@ class ChatBox extends HookConsumerWidget {
|
|||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
|
||||
final canSendMessages = ref.watch(
|
||||
PowerLevelController.provider(
|
||||
PowerLevelConfig(eventType: "m.room.message"),
|
||||
),
|
||||
);
|
||||
|
||||
return Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
|
|
@ -92,7 +86,12 @@ class ChatBox extends HookConsumerWidget {
|
|||
child: Row(
|
||||
spacing: 8,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: canSendMessages
|
||||
children:
|
||||
ref.watch(
|
||||
PowerLevelController.provider(
|
||||
PowerLevelConfig(eventType: "m.room.message"),
|
||||
),
|
||||
)
|
||||
? [
|
||||
EmojiPickerButton(
|
||||
context: context,
|
||||
|
|
@ -101,7 +100,6 @@ class ChatBox extends HookConsumerWidget {
|
|||
),
|
||||
PopupMenuButton(
|
||||
tooltip: "Add media",
|
||||
enabled: canSendMessages,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
|
|
@ -145,7 +143,6 @@ class ChatBox extends HookConsumerWidget {
|
|||
"#": style,
|
||||
},
|
||||
builder: (context, key) => TextFormField(
|
||||
enabled: canSendMessages,
|
||||
maxLines: 12,
|
||||
minLines: 1,
|
||||
autofocus: true,
|
||||
|
|
@ -164,7 +161,7 @@ class ChatBox extends HookConsumerWidget {
|
|||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: !canSendMessages ? null : send,
|
||||
onPressed: send,
|
||||
icon: Icon(Icons.send),
|
||||
tooltip: "Send message",
|
||||
),
|
||||
|
|
|
|||
|
|
@ -365,13 +365,15 @@ class RoomChat extends HookConsumerWidget {
|
|||
text, {
|
||||
required shouldMention,
|
||||
required tags,
|
||||
}) => notifier.send(
|
||||
text,
|
||||
tags: tags,
|
||||
relationType: relationType.value,
|
||||
shouldMention: shouldMention,
|
||||
relation: relatedMessage.value,
|
||||
),
|
||||
}) => notifier
|
||||
.send(
|
||||
text,
|
||||
tags: tags,
|
||||
relationType: relationType.value,
|
||||
shouldMention: shouldMention,
|
||||
relation: relatedMessage.value,
|
||||
)
|
||||
.onError(showError),
|
||||
relationType: relationType.value,
|
||||
relatedMessage: relatedMessage.value,
|
||||
onDismiss: () => relatedMessage.value = null,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ buildGoModule (finalAttrs: {
|
|||
|
||||
src = "${src}/gomuks";
|
||||
|
||||
vendorHash = "sha256-zBDfBZqUoHIfZ0AajZEvSBbskjpFB7yIsomt0KYDo7Y=";
|
||||
vendorHash = "sha256-9FsZkE8JmFDQyigQAaFn7i2OsH6Q8yFigPFt/l2FIoU=";
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
|
|
|
|||
48
pubspec.lock
48
pubspec.lock
|
|
@ -935,6 +935,54 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
permission_handler:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.0.1"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.1"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.4.7"
|
||||
permission_handler_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_html
|
||||
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3+5"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_platform_interface
|
||||
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
permission_handler_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_windows
|
||||
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ dependencies:
|
|||
emoji_text_field:
|
||||
git:
|
||||
url: https://github.com/Henry-Hiles/emoji_text_field
|
||||
permission_handler: ^12.0.1
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: 2.15.0
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include <dynamic_system_colors/dynamic_color_plugin_c_api.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
|
@ -17,6 +18,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
dynamic_system_colors
|
||||
file_selector_windows
|
||||
permission_handler_windows
|
||||
screen_retriever_windows
|
||||
url_launcher_windows
|
||||
window_manager
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue