diff --git a/.gitignore b/.gitignore index 757aeea..747b159 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,7 @@ key.properties # Generated Files *.g.dart *.freezed.dart -/src/ +/rust/ # Devel Password password.txt \ No newline at end of file diff --git a/README.md b/README.md index 488b48d..5b603b1 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S ## Progress -- [x] Move from the Dart SDK to the Gomuks SDK with Dart bindings: https://git.federated.nexus/Henry-Hiles/nexus/pulls/2 +- [ ] Move from the Dart SDK to the Gomuks SDK with Dart bindings: https://git.federated.nexus/Henry-Hiles/nexus/pulls/2 - [ ] Platform Support - [x] Linux - [x] Windows diff --git a/analysis_options.yaml b/analysis_options.yaml index a8b1078..c2aaaa0 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,7 +1,6 @@ analyzer: errors: invalid_annotation_target: ignore - avoid_print: ignore exclude: - "build/**" - "**/*.g.dart" diff --git a/build.yaml b/build.yaml deleted file mode 100644 index 5d6aeda..0000000 --- a/build.yaml +++ /dev/null @@ -1,6 +0,0 @@ -targets: - $default: - builders: - json_serializable: - options: - field_rename: snake diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/flake.nix b/flake.nix index c4c5cc3..027db59 100644 --- a/flake.nix +++ b/flake.nix @@ -23,7 +23,6 @@ perSystem = { - lib, pkgs, system, ... @@ -33,32 +32,40 @@ _module.args.pkgs = import nixpkgs { inherit system; config = { - permittedInsecurePackages = [ "olm-3.2.16" ]; android_sdk.accept_license = true; allowUnfree = true; }; }; - devShells.default = pkgs.mkShell { - packages = with pkgs; [ - go - olm - git - clang - (flutter.override { extraPkgConfigPackages = [ pkgs.libsecret ]; }) + devShells.default = + let + # android = pkgs.callPackage ./nix/android.nix { }; + in + pkgs.mkShell { + packages = with pkgs; [ + # jdk17 + cargo + (flutter.override { extraPkgConfigPackages = [ pkgs.libsecret ]; }) - (pkgs.writeShellScriptBin "rustup" (builtins.readFile ./nix/fake-rustup.sh)) - ]; + # android.platform-tools + (pkgs.writeShellScriptBin "rustup" (builtins.readFile ./nix/fake-rustup.sh)) + ]; - env = { - LD_LIBRARY_PATH = "${ - pkgs.lib.makeLibraryPath ([ - pkgs.sqlite - ]) - }:./build/native_assets/linux"; - CPATH = lib.makeSearchPath "include" [ pkgs.glibc.dev ]; + env = rec { + LD_LIBRARY_PATH = "${ + pkgs.lib.makeLibraryPath ([ + pkgs.sqlite + ]) + }:./build/linux/x64/debug/plugins/flutter_vodozemac"; + + # 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/hook/build.dart b/hook/build.dart deleted file mode 100644 index 4cb2f91..0000000 --- a/hook/build.dart +++ /dev/null @@ -1,54 +0,0 @@ -import "dart:io"; -import "package:hooks/hooks.dart"; -import "package:code_assets/code_assets.dart"; - -Future main(List args) => build(args, (input, output) async { - final buildDir = input.packageRoot.resolve("src/"); - if (await File(buildDir.resolve("lock").toFilePath()).exists()) return; - - final targetOS = input.config.code.targetOS; - String libFileName; - switch (targetOS) { - case OS.linux: - libFileName = "libgomuks.so"; - break; - case OS.macOS: - libFileName = "libgomuks.dylib"; - break; - case OS.windows: - libFileName = "libgomuks.dll"; - break; - default: - throw UnsupportedError("Unsupported OS: $targetOS"); - } - - final gomuksBuildDir = buildDir.resolve("gomuks/"); - final libFile = gomuksBuildDir.resolve(libFileName); - - print("Building Gomuks shared library $libFileName from source..."); - final result = await Process.run("go", [ - "build", - "-o", - libFile.path, - "-buildmode=c-shared", - ], workingDirectory: gomuksBuildDir.resolve("source/pkg/ffi/").toFilePath()); - - if (result.exitCode != 0) { - throw Exception("Failed to build Gomuks shared library\n${result.stderr}"); - } - - final generatedFile = "src/third_party/gomuks.g.dart"; - print("Adding $libFileName as asset..."); - output - ..assets.code.add( - CodeAsset( - package: "nexus", - name: generatedFile, - linkMode: DynamicLoadingBundled(), - file: libFile, - ), - ) - ..dependencies.add(libFile) - ..dependencies.add(gomuksBuildDir); - print("Done!"); -}); diff --git a/lib/controllers/avatar_controller.dart b/lib/controllers/avatar_controller.dart new file mode 100644 index 0000000..1bb4c72 --- /dev/null +++ b/lib/controllers/avatar_controller.dart @@ -0,0 +1,17 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/controllers/client_controller.dart"; + +class AvatarController extends AsyncNotifier { + final String mxc; + AvatarController(this.mxc); + @override + Future build() async => Uri.parse(mxc).getThumbnailUri( + await ref.watch(ClientController.provider.future), + width: 24, + height: 24, + ); + + static final provider = AsyncNotifierProvider.family + .autoDispose(AvatarController.new); +} diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart index fd7600f..9f69e8f 100644 --- a/lib/controllers/client_controller.dart +++ b/lib/controllers/client_controller.dart @@ -1,207 +1,106 @@ -import "dart:developer"; -import "dart:ffi"; -import "dart:isolate"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:ffi/ffi.dart"; +import "dart:convert"; +import "dart:io"; import "package:flutter/foundation.dart"; -import "package:nexus/controllers/client_state_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"; -import "package:nexus/controllers/top_level_spaces_controller.dart"; -import "package:nexus/helpers/extensions/gomuks_buffer.dart"; -import "package:nexus/models/client_state.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/paginate.dart"; -import "package:nexus/models/requests/get_event_request.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/login_request.dart"; -import "package:nexus/models/profile.dart"; -import "package:nexus/models/requests/paginate_request.dart"; -import "package:nexus/models/requests/redact_event_request.dart"; -import "package:nexus/models/requests/report_request.dart"; -import "package:nexus/models/requests/send_message_request.dart"; -import "package:nexus/models/room.dart"; -import "package:nexus/models/sync_data.dart"; -import "package:nexus/models/sync_status.dart"; -import "package:nexus/src/third_party/gomuks.g.dart"; +import "package:matrix/encryption.dart"; +import "package:nexus/controllers/database_controller.dart"; +import "package:vodozemac/vodozemac.dart" as vod; +import "package:flutter_vodozemac/flutter_vodozemac.dart" as fl_vod; +import "package:matrix/matrix.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/secure_storage_controller.dart"; +import "package:nexus/models/session_backup.dart"; -class ClientController extends AsyncNotifier { +class ClientController extends AsyncNotifier { @override - Future build() async { - final handle = await Isolate.run(GomuksInit); + bool updateShouldNotify( + AsyncValue previous, + AsyncValue next, + ) => + previous.hasValue != next.hasValue || + previous.value?.accessToken != next.value?.accessToken; + static const sessionBackupKey = "sessionBackup"; - final callable = - NativeCallable< - Void Function(Pointer, Int64, GomuksOwnedBuffer) - >.listener(( - Pointer command, - int requestId, - GomuksOwnedBuffer data, - ) { - try { - final muksEventType = command.cast().toDartString(); - debugPrint("Handling $muksEventType..."); - final decodedMuksEvent = data.toJson(); - - switch (muksEventType) { - case "client_state": - ref - .watch(ClientStateController.provider.notifier) - .set(ClientState.fromJson(decodedMuksEvent)); - break; - case "sync_status": - ref - .watch(SyncStatusController.provider.notifier) - .set(SyncStatus.fromJson(decodedMuksEvent)); - break; - case "sync_complete": - final syncData = SyncData.fromJson(decodedMuksEvent); - final roomProvider = RoomsController.provider; - - if (syncData.clearState) ref.invalidate(roomProvider); - ref - .watch(roomProvider.notifier) - .update(syncData.rooms, syncData.leftRooms); - if (syncData.topLevelSpaces != null) { - ref - .watch(TopLevelSpacesController.provider.notifier) - .set(syncData.topLevelSpaces!); - } - if (syncData.spaceEdges != null) { - ref - .watch(SpaceEdgesController.provider.notifier) - .set(syncData.spaceEdges!); - } - - // ref - // .watch(SyncStatusController.provider.notifier) - // .set(SyncStatus.fromJson(decodedMuksEvent)); - break; - case "typing": - //TODO: IMPL - break; - default: - debugPrint("Unhandled event: $muksEventType"); - } - debugPrint("Finished handling $muksEventType..."); - } catch (error, stackTrace) { - debugger(); - debugPrintStack(stackTrace: stackTrace, label: error.toString()); - } - }); - - ref.onDispose(() => GomuksDestroy(handle)); - ref.onDispose(callable.close); - - final errorCode = GomuksStart(handle, callable.nativeFunction); - - if (errorCode == 0) return handle; - throw Exception("GomuksStart returned error code $errorCode"); - } - - Future _sendCommand( - String command, - Map data, - ) async { - final bufferPointer = data.toGomuksBufferPtr(); - final handle = await future; - final response = await Isolate.run( - () => GomuksSubmitCommand( - handle, - command.toNativeUtf8().cast(), - bufferPointer.ref, + @override + Future build() async { + if (!vod.isInitialized()) fl_vod.init(); + final client = Client( + "nexus", + logLevel: kReleaseMode ? Level.warning : Level.verbose, + importantStateEvents: {"im.ponies.room_emotes"}, + supportedLoginTypes: {AuthenticationTypes.password}, + verificationMethods: {KeyVerificationMethod.emoji}, + database: await MatrixSdkDatabase.init( + "nexus", + database: await ref.watch(DatabaseController.provider.future), + ), + nativeImplementations: NativeImplementationsIsolate( + compute, + vodozemacInit: fl_vod.init, ), ); - calloc.free(bufferPointer); + final backupJson = await ref + .watch(SecureStorageController.provider.notifier) + .get(sessionBackupKey); - final json = response.buf.toJson(); - if (json is String) throw json; - return json; + if (backupJson != null) { + final backup = SessionBackup.fromJson(json.decode(backupJson)); + + await client.init( + waitForFirstSync: false, + newToken: backup.accessToken, + newHomeserver: backup.homeserver, + newUserID: backup.userID, + newDeviceID: backup.deviceID, + newDeviceName: backup.deviceName, + ); + } + + return client; } - Future redactEvent(RedactEventRequest report) => - _sendCommand("redact_event", report.toJson()); - - Future sendMessage(SendMessageRequest request) => - _sendCommand("send_message", request.toJson()); - - Future verify(String recoveryKey) async { + Future setHomeserver(Uri homeserverUrl) async { + final client = await future; try { - await _sendCommand("verify", {"recovery_key": recoveryKey}); + await client.checkHomeserver(homeserverUrl); return true; - } catch (error) { + } catch (_) { return false; } } - Future leaveRoom(Room room) async { - if (room.metadata == null) return; - await _sendCommand("leave_room", {"room_id": room.metadata!.id}); - } - - Future> getRoomState(GetRoomStateRequest request) async { - final response = - (await _sendCommand("get_room_state", request.toJson())) as List; - return response.map((event) => Event.fromJson(event)).toIList(); - } - - Future?> getRelatedEvents( - GetRelatedEventsRequest request, - ) async { - final response = - (await _sendCommand("get_related_events", request.toJson())) as List?; - return response?.map((event) => Event.fromJson(event)).toIList(); - } - - Future getEvent(GetEventRequest request) async { - final json = await _sendCommand("get_event", request.toJson()); - - return json == null ? null : Event.fromJson(json); - } - - Future paginate(PaginateRequest request) async => - Paginate.fromJson(await _sendCommand("paginate", request.toJson())); - - Future getProfile(String userId) async => - Profile.fromJson(await _sendCommand("get_profile", {"user_id": userId})); - - Future reportEvent(ReportRequest report) => - _sendCommand("report_event", report.toJson()); - - Future markRead(Room room) async { - if (room.events.isEmpty || room.metadata == null) return; - await _sendCommand("mark_read", { - "room_id": room.metadata?.id, - "receipt_type": "m.read", - "event_id": room.events.last.eventId, - }); - } - - Future login(LoginRequest login) async { + Future login(String username, String password) async { + final client = await future; try { - await _sendCommand("login", login.toJson()); + final deviceName = "Nexus Client login on ${Platform.localHostname}"; + final details = await MatrixApi(homeserver: client.homeserver).login( + LoginType.mLoginPassword, + initialDeviceDisplayName: deviceName, + identifier: AuthenticationUserIdentifier(user: username), + password: password, + ); + await ref + .watch(SecureStorageController.provider.notifier) + .set( + sessionBackupKey, + json.encode( + SessionBackup( + accessToken: details.accessToken, + homeserver: client.homeserver!, + userID: details.userId, + deviceID: details.deviceId, + deviceName: deviceName, + ).toJson(), + ), + ); + ref.invalidateSelf(asReload: true); return true; - } catch (error) { + } catch (_) { return false; } } - Future discoverHomeserver(Uri homeserver) async { - try { - final response = await _sendCommand("discover_homeserver", { - "user_id": "@fakeuser:${homeserver.host}", - }); - return response["m.homeserver"]?["base_url"]; - } catch (error) { - return null; - } - } - - static final provider = AsyncNotifierProvider( + static final provider = AsyncNotifierProvider( ClientController.new, ); } diff --git a/lib/controllers/client_state_controller.dart b/lib/controllers/client_state_controller.dart deleted file mode 100644 index 998d4a1..0000000 --- a/lib/controllers/client_state_controller.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/models/client_state.dart"; - -class ClientStateController extends Notifier { - @override - Null build() => null; - - void set(ClientState newState) { - state = newState; - } - - static final provider = NotifierProvider( - ClientStateController.new, - ); -} diff --git a/lib/controllers/database_controller.dart b/lib/controllers/database_controller.dart new file mode 100644 index 0000000..706560b --- /dev/null +++ b/lib/controllers/database_controller.dart @@ -0,0 +1,18 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:path/path.dart"; +import "package:path_provider/path_provider.dart"; +import "package:sqflite_common_ffi/sqflite_ffi.dart"; + +class DatabaseController extends AsyncNotifier { + @override + Future build() async { + databaseFactory = databaseFactoryFfi; + return databaseFactoryFfi.openDatabase( + join((await getApplicationSupportDirectory()).path, "database.db"), + ); + } + + static final provider = AsyncNotifierProvider( + DatabaseController.new, + ); +} diff --git a/lib/controllers/events_controller.dart b/lib/controllers/events_controller.dart new file mode 100644 index 0000000..e7a192d --- /dev/null +++ b/lib/controllers/events_controller.dart @@ -0,0 +1,18 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:matrix/matrix.dart"; + +class EventsController extends AsyncNotifier { + EventsController(this.room); + final Room room; + + @override + Future build({String? from}) => room.getTimeline(); + + Future prev() async { + final timeline = await future; + await timeline.requestHistory(); + } + + static final provider = AsyncNotifierProvider.autoDispose + .family(EventsController.new); +} diff --git a/lib/controllers/header_controller.dart b/lib/controllers/header_controller.dart deleted file mode 100644 index ead8f0d..0000000 --- a/lib/controllers/header_controller.dart +++ /dev/null @@ -1,27 +0,0 @@ -import "dart:ffi"; -import "dart:isolate"; -import "package:ffi/ffi.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client_controller.dart"; -import "package:nexus/src/third_party/gomuks.g.dart"; - -class HeaderController extends AsyncNotifier> { - @override - Future> build() async { - final handle = await ref.watch(ClientController.provider.future); - final info = await Isolate.run(() => GomuksGetAccountInfo(handle)); - final headers = { - if (info.access_token != nullptr) - "authorization": - "Bearer ${info.access_token.cast().toDartString()}", - }; - - await Isolate.run(() => GomuksFreeAccountInfo(info)); - return headers; - } - - static final provider = - AsyncNotifierProvider>( - HeaderController.new, - ); -} diff --git a/lib/controllers/members_controller.dart b/lib/controllers/members_controller.dart index 2a250a2..df15c1c 100644 --- a/lib/controllers/members_controller.dart +++ b/lib/controllers/members_controller.dart @@ -1,27 +1,22 @@ -import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/room.dart"; +import "package:matrix/matrix.dart"; -class MembersController extends AsyncNotifier> { +class MembersController extends AsyncNotifier> { final Room room; MembersController(this.room); @override - Future> build() async => - (room.state["m.room.member"]?.values ?? []) - .map( - (eventRowId) => room.events.firstWhereOrNull( - (event) => event.rowId == eventRowId, - ), - ) - .nonNulls - .where((member) => member.content["membership"] == "join") - .toIList(); + Future> build() async => IList( + (await room.client.getMembersByRoom( + room.id, + notMembership: Membership.leave, + )) ?? + [], + ); static final provider = AsyncNotifierProvider.family - .autoDispose, Room>( + .autoDispose, Room>( MembersController.new, ); } diff --git a/lib/controllers/message_controller.dart b/lib/controllers/message_controller.dart deleted file mode 100644 index b695ec1..0000000 --- a/lib/controllers/message_controller.dart +++ /dev/null @@ -1,189 +0,0 @@ -import "package:flutter_chat_core/flutter_chat_core.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client_controller.dart"; -import "package:nexus/controllers/client_state_controller.dart"; -import "package:nexus/controllers/profile_controller.dart"; -import "package:nexus/helpers/extensions/mxc_to_https.dart"; -import "package:nexus/models/message_config.dart"; -import "package:nexus/models/requests/get_event_request.dart"; -import "package:nexus/models/requests/get_related_events_request.dart"; - -class MessageController extends AsyncNotifier { - final MessageConfig config; - MessageController(this.config); - - @override - Future build() async { - if (config.event.relationType == "m.replace" && !config.includeEdits) { - return null; - } - final client = ref.watch(ClientController.provider.notifier); - - final newEvents = await client.getRelatedEvents( - GetRelatedEventsRequest( - roomId: config.event.roomId, - eventId: config.event.eventId, - relationType: "m.replace", - ), - ); - if (!ref.mounted) return null; - final event = newEvents?.lastOrNull ?? config.event; - - final replyId = - config.event.content["m.relates_to"]?["m.in_reply_to"]?["event_id"]; - final replyEvent = replyId == null - ? null - : await client.getEvent( - GetEventRequest(roomId: config.event.roomId, eventId: replyId), - ); - - if (!ref.mounted) return null; - - final author = await ref.read( - ProfileController.provider(event.authorId).future, - ); - if (!ref.mounted) return null; - - final content = (event.decrypted ?? event.content); - final type = (config.event.decryptedType ?? config.event.type); - final newContent = content["m.new_content"] as Map?; - final metadata = { - "timelineId": event.timelineRowId, - "formatted": - newContent?["formatted_body"] ?? - newContent?["body"] ?? - content["formatted_body"] ?? - content["body"] ?? - "", - if (replyEvent != null) - "reply": await ref.read( - MessageController.provider( - MessageConfig(event: replyEvent, mustBeText: true), - ).future, - ), - "body": newContent?["body"] ?? content["body"], - "eventType": type, - "avatarUrl": author.avatarUrl, - "displayName": author.displayName ?? event.authorId, - "txnId": config.event.transactionId, - }; - - if (!ref.mounted) return null; - - final editedAt = event.relationType == "m.replace" ? event.timestamp : null; - - if ((event.redactedBy != null && !config.mustBeText) || - (!config.includeEdits && (config.event.relationType == "m.replace"))) { - return null; - } - - // TODO: Use server-generated preview if enabled - - // final match = Uri.tryParse( - // RegExp(regexLink, caseSensitive: false).firstMatch(body)?.group(0) ?? "", - // ); - - final asText = - Message.text( - metadata: metadata, - id: config.event.eventId, - authorId: event.authorId, - text: config.event.redactedBy == null - ? content["body"] ?? "" - : "This message has been deleted...", - replyToMessageId: replyId, - deliveredAt: config.event.timestamp, - editedAt: editedAt, - ) - as TextMessage; - - if (config.mustBeText) return asText; - - final homeserver = ref.read(ClientStateController.provider)?.homeserverUrl; - final source = homeserver == null || content["url"] == null - ? "null" - : Uri.parse(content["url"]).mxcToHttps(homeserver).toString(); - - return switch (type) { - "m.room.encrypted" => asText.copyWith( - text: "Unable to decrypt message.", - metadata: {...metadata, "formatted": "Unable to decrypt message."}, - ), - // "org.matrix.msc3381.poll.start" => Message.custom( - // metadata: { - // ...metadata, - // "poll": event.parsedPollEventContent.pollStartContent, - // "responses": event.getPollResponses(timeline), - // }, - // id: eventId, - // deliveredAt: originServerTs, - // authorId: senderId, - // ), - ("m.sticker" || "m.room.message") => switch (content["msgtype"]) { - ("m.sticker" || "m.image") => Message.image( - id: config.event.eventId, - metadata: metadata, - authorId: event.authorId, - text: event.localContent?.sanitizedHtml, - source: source, - replyToMessageId: replyId, - deliveredAt: config.event.timestamp, - blurhash: (content["info"] as Map?)?["xyz.amorgan.blurhash"], - ), - "m.audio" => Message.audio( - id: config.event.eventId, - metadata: metadata, - authorId: event.authorId, - text: content["body"], - replyToMessageId: replyId, - source: source, - deliveredAt: config.event.timestamp, - // TODO: See if we can figure out duration - duration: Duration(hours: 1), - ), - "m.file" => Message.file( - name: content["filename"].toString(), - metadata: metadata, - id: config.event.eventId, - authorId: event.authorId, - source: source, - replyToMessageId: replyId, - deliveredAt: config.event.timestamp, - ), - _ => asText, - }, - "m.room.member" => Message.system( - metadata: metadata, - id: config.event.eventId, - authorId: event.authorId, - deliveredAt: config.event.timestamp, - text: - "${content["displayname"] ?? event.stateKey} ${switch (content["membership"]) { - "invite" => "was invited to", - "join" => "joined", - "leave" => "left", - "knock" => "asked to join", - "ban" => "was banned from", - _ => "did something relating to", - }} the room.", - ), - "m.room.redaction" => null, - _ => - // Turn this on for debugging purposes - false - // ignore: dead_code - ? Message.unsupported( - metadata: metadata, - id: config.event.eventId, - authorId: event.authorId, - replyToMessageId: replyId, - ) - : null, - }; - } - - static final provider = AsyncNotifierProvider.family - .autoDispose( - MessageController.new, - ); -} diff --git a/lib/controllers/messages_controller.dart b/lib/controllers/messages_controller.dart deleted file mode 100644 index 3edc8ab..0000000 --- a/lib/controllers/messages_controller.dart +++ /dev/null @@ -1,25 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_chat_core/flutter_chat_core.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/message_controller.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/message_config.dart"; - -class MessagesController extends AsyncNotifier> { - final IList events; - MessagesController(this.events); - - @override - Future> build() async => (await Future.wait( - events.map( - (event) => ref.watch( - MessageController.provider(MessageConfig(event: event)).future, - ), - ), - )).nonNulls.toIList(); - - static final provider = AsyncNotifierProvider.family - .autoDispose, IList>( - MessagesController.new, - ); -} diff --git a/lib/controllers/multi_provider_controller.dart b/lib/controllers/multi_provider_controller.dart deleted file mode 100644 index e23ecaa..0000000 --- a/lib/controllers/multi_provider_controller.dart +++ /dev/null @@ -1,20 +0,0 @@ -import "dart:async"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; - -class MultiProviderController extends AsyncNotifier { - MultiProviderController(this.providers); - final IList providers; - - @override - FutureOr build() async => await Future.wait( - providers.map((provider) => ref.watch(provider.future)), - ); - - static final provider = - AsyncNotifierProvider.family< - MultiProviderController, - void, - IList - >(MultiProviderController.new); -} diff --git a/lib/controllers/new_events_controller.dart b/lib/controllers/new_events_controller.dart deleted file mode 100644 index 215ebd3..0000000 --- a/lib/controllers/new_events_controller.dart +++ /dev/null @@ -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> { - final String roomId; - NewEventsController(this.roomId); - - @override - IList build() => const IList.empty(); - - void add(IList newEvents) => state = newEvents; - - static final provider = NotifierProvider.autoDispose - .family, String>( - NewEventsController.new, - ); -} diff --git a/lib/controllers/profile_controller.dart b/lib/controllers/profile_controller.dart deleted file mode 100644 index e825593..0000000 --- a/lib/controllers/profile_controller.dart +++ /dev/null @@ -1,17 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client_controller.dart"; -import "package:nexus/models/profile.dart"; - -class ProfileController extends AsyncNotifier { - final String userId; - ProfileController(this.userId); - - @override - Future build() => - ref.watch(ClientController.provider.notifier).getProfile(userId); - - static final provider = - AsyncNotifierProvider.family( - ProfileController.new, - ); -} diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index 091cdc9..9175e36 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -1,135 +1,67 @@ import "package:collection/collection.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_core/flutter_chat_core.dart" as chat; import "package:flutter_riverpod/flutter_riverpod.dart"; -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/message_config.dart"; -import "package:nexus/models/requests/get_room_state_request.dart"; -import "package:nexus/models/requests/paginate_request.dart"; -import "package:nexus/models/requests/redact_event_request.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/controllers/avatar_controller.dart"; +import "package:nexus/controllers/events_controller.dart"; +import "package:nexus/helpers/extensions/event_to_message.dart"; +import "package:nexus/helpers/extensions/list_to_messages.dart"; +import "package:fluttertagger/fluttertagger.dart" as tagger; import "package:nexus/models/relation_type.dart"; -import "package:nexus/models/requests/send_message_request.dart"; -import "package:nexus/models/room.dart"; class RoomChatController extends AsyncNotifier { - final String roomId; - RoomChatController(this.roomId); + final Room room; + RoomChatController(this.room); @override Future build() async { - final client = ref.watch(ClientController.provider.notifier); - final room = ref.read(SelectedRoomController.provider); - if (room == null) return InMemoryChatController(); - - final messages = await ref.watch( - MessagesController.provider( - room.timeline - .map( - (timelineRowTuple) => room.events.firstWhereOrNull( - (event) => event.rowId == timelineRowTuple.eventRowId, - ), - ) - .nonNulls - .toIList(), - ).future, - ); - final controller = InMemoryChatController(messages: messages.toList()); + final timeline = await ref.watch(EventsController.provider(room).future); ref.onDispose( - ref.listen(NewEventsController.provider(roomId), (_, next) async { - final controller = await future; - for (final event in next) { - if (event.type == "m.room.redaction") { + room.client.onTimelineEvent.stream.listen((event) async { + if (event.roomId != room.id) return; + + if (event.type == EventTypes.Redaction) { + final controller = await future; + final message = controller.messages.firstWhereOrNull( + (message) => message.id == event.redacts, + ); + if (message == null) return; + + await controller.removeMessage(message); + } else { + final message = await event.toMessage(includeEdits: true, timeline); + if (event.relationshipType == RelationshipTypes.edit) { final controller = await future; - final message = controller.messages.firstWhereOrNull( - (message) => message.id == event.content["redacts"], + final oldMessage = controller.messages.firstWhereOrNull( + (element) => element.id == event.relationshipEventId, ); - if (message == null || !ref.mounted) return; - - await controller.removeMessage(message); - } else { - final message = await ref.watch( - MessageController.provider( - MessageConfig(event: event, includeEdits: true), - ).future, + if (oldMessage == null || message == null) return; + return await updateMessage( + oldMessage, + message.copyWith( + id: oldMessage.id, + replyToMessageId: oldMessage.replyToMessageId, + metadata: { + ...(oldMessage.metadata ?? {}), + ...((message.metadata ?? {}).filterMap( + (key, value) => value == null ? null : MapEntry(key, value), + )), + }, + ), ); - 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 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 && - !controller.messages.any( - (oldMessage) => oldMessage.id == message.id, - ) && - ref.mounted) { - await controller.insertMessage(message); - } + } + if (message != null) { + return await insertMessage(message); } } - }, weak: true).close, + }).cancel, ); - ref.onDispose(controller.dispose); - - if (messages.length < 20) await loadOlder(controller); - - final state = await client.getRoomState( - GetRoomStateRequest( - roomId: roomId, - fetchMembers: room.metadata?.hasMemberList == false, - includeMembers: true, - ), + return InMemoryChatController( + messages: await timeline.events.toMessages(room, timeline), ); - - ref - .watch(RoomsController.provider.notifier) - .update( - { - roomId: Room( - events: state, - state: state.fold( - const IMap.empty(), - (previousValue, stateEvent) => previousValue.add( - stateEvent.type, - (previousValue[stateEvent.type] ?? const IMap.empty()).addAll( - IMap({ - if (stateEvent.stateKey != null) - stateEvent.stateKey!: stateEvent.rowId, - }), - ), - ), - ), - ), - }.toIMap(), - const ISet.empty(), - ); - - return controller; } Future insertMessage(Message message) async { @@ -149,61 +81,35 @@ class RoomChatController extends AsyncNotifier { Future deleteMessage(Message message, {String? reason}) async { final controller = await future; await controller.removeMessage(message); - await ref - .watch(ClientController.provider.notifier) - .redactEvent( - RedactEventRequest( - eventId: message.id, - roomId: roomId, - reason: reason, - ), - ); + await room.redactEvent(message.id, reason: reason); } - Future loadOlder([InMemoryChatController? chatController]) async { - final controller = chatController ?? await future; - final client = ref.watch(ClientController.provider.notifier); + Future loadOlder() async { + final currentEvents = await future; + await ref.watch(EventsController.provider(room).notifier).prev(); + final timeline = await ref.watch(EventsController.provider(room).future); - final response = await client.paginate( - PaginateRequest( - roomId: roomId, - maxTimelineId: controller.messages.firstOrNull?.metadata?["timelineId"], - ), - ); - - ref - .watch(RoomsController.provider.notifier) - .update( - IMap({ - roomId: Room( - events: response.events.addAll(response.relatedEvents), - hasMore: response.hasMore, - timeline: response.events - .map( - (event) => TimelineRowTuple( - timelineRowId: event.timelineRowId, - eventRowId: event.rowId, - ), - ) - .toIList(), - ), - }), - const ISet.empty(), - ); - - final messages = await ref.watch( - MessagesController.provider(response.events.reversed).future, - ); + final controller = await future; await controller.insertAllMessages( - messages + await timeline.events .where( - (newMessage) => !controller.messages.any( - (message) => message.id == newMessage.id, + (event) => !currentEvents.messages.any( + (existingEvent) => existingEvent.id == event.eventId, ), ) - .toList(), + .toList() + .toMessages(room, timeline), index: 0, ); + ref.notifyListeners(); + } + + Future markRead() async { + if (!room.hasNewMessages) return; + final controller = await future; + final id = controller.messages.last.id; + + await room.setReadMarker(id, mRead: id); } Future updateMessage(Message message, Message newMessage) async => @@ -211,7 +117,7 @@ class RoomChatController extends AsyncNotifier { Future send( String message, { - required Iterable tags, + required Iterable tags, required RelationType relationType, Message? relation, }) async { @@ -227,42 +133,30 @@ class RoomChatController extends AsyncNotifier { ); } - final client = ref.watch(ClientController.provider.notifier); - client.sendMessage( - SendMessageRequest( - roomId: roomId, - mentions: Mentions( - userIds: [ - if (relation != null && relationType == RelationType.reply) - relation.authorId, - ].toIList(), - room: taggedMessage.contains("@room"), - ), - text: taggedMessage, - relation: relation == null - ? null - : Relation(eventId: relation.id, relationType: relationType), - ), + await room.sendTextEvent( + taggedMessage, + editEventId: relationType == RelationType.edit ? relation?.id : null, + inReplyTo: (relationType == RelationType.reply && relation != null) + ? await room.getEventById(relation.id) + : null, ); } Future resolveUser(String id) async { - final user = await ref - .watch(ClientController.provider.notifier) - .getProfile(id); + final user = await room.client.getUserProfile(id); return chat.User( id: id, - name: user.displayName, - // imageSource: user.avatarUrl == null - // ? null - // : (await ref.watch( - // AvatarController.provider(user.avatarUrl!.toString()).future, - // )).toString(), + name: user.displayname, + imageSource: user.avatarUrl == null + ? null + : (await ref.watch( + AvatarController.provider(user.avatarUrl!.toString()).future, + )).toString(), ); } static final provider = AsyncNotifierProvider.family - .autoDispose( + .autoDispose( RoomChatController.new, ); } diff --git a/lib/controllers/rooms_controller.dart b/lib/controllers/rooms_controller.dart index 0945644..864d656 100644 --- a/lib/controllers/rooms_controller.dart +++ b/lib/controllers/rooms_controller.dart @@ -1,83 +1,23 @@ -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/new_events_controller.dart"; -import "package:nexus/models/read_receipt.dart"; -import "package:nexus/models/room.dart"; +import "package:nexus/controllers/client_controller.dart"; +import "package:nexus/helpers/extensions/get_full_room.dart"; +import "package:nexus/models/full_room.dart"; -class RoomsController extends Notifier> { +class RoomsController extends AsyncNotifier> { @override - IMap build() => const IMap.empty(); + Future> build() async { + final client = await ref.watch(ClientController.provider.future); - void update(IMap rooms, ISet leftRooms) { - final merged = rooms.entries.fold(state, (acc, entry) { - final roomId = entry.key; - final incoming = entry.value; - final existing = acc[roomId]; - - final events = existing?.events.updateById( - incoming.events, - (item) => item.eventId, - ); - - ref - .watch(NewEventsController.provider(roomId).notifier) - .add( - incoming.timeline - .map( - (timelineTuple) => events?.firstWhereOrNull( - (event) => timelineTuple.eventRowId == event.rowId, - ), - ) - .nonNulls - .toIList(), - ); - - return acc.add( - roomId, - existing?.copyWith( - metadata: incoming.metadata ?? existing.metadata, - events: events!, - state: incoming.state.entries.fold( - existing.state, - (previousValue, event) => previousValue.add( - event.key, - (previousValue[event.key] ?? const IMap.empty()).addAll( - event.value, - ), - ), - ), - timeline: - (incoming.reset - ? incoming.timeline - : existing.timeline.updateById( - incoming.timeline, - (item) => item.timelineRowId, - )) - .sortedBy((element) => element.timelineRowId) - .toIList(), - receipts: incoming.receipts.entries.fold( - existing.receipts, - (receiptAcc, event) => receiptAcc.add( - event.key, - (receiptAcc[event.key] ?? IList()).addAll( - event.value, - ), - ), - ), - ) ?? - incoming, - ); - }); - - final prunedList = leftRooms.fold( - merged, - (acc, roomId) => acc.remove(roomId), + ref.onDispose( + client.onSync.stream.listen((_) => ref.invalidateSelf()).cancel, ); - state = prunedList; + + return IList(await Future.wait(client.rooms.map((room) => room.fullRoom))); } - static final provider = NotifierProvider>( - RoomsController.new, - ); + static final provider = + AsyncNotifierProvider>( + RoomsController.new, + ); } diff --git a/lib/controllers/selected_room_controller.dart b/lib/controllers/selected_room_controller.dart index ffba78c..cfeead6 100644 --- a/lib/controllers/selected_room_controller.dart +++ b/lib/controllers/selected_room_controller.dart @@ -2,23 +2,24 @@ import "package:collection/collection.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/key_controller.dart"; import "package:nexus/controllers/selected_space_controller.dart"; -import "package:nexus/models/room.dart"; +import "package:nexus/models/full_room.dart"; -class SelectedRoomController extends Notifier { +class SelectedRoomController extends AsyncNotifier { @override - Room? build() { - final space = ref.watch(SelectedSpaceController.provider); + Future build() async { + final space = await ref.watch(SelectedSpaceController.provider.future); final selectedRoomId = ref.watch( KeyController.provider(KeyController.roomKey), ); return space.children.firstWhereOrNull( - (room) => room.metadata?.id == selectedRoomId, + (room) => room.roomData.id == selectedRoomId, ) ?? space.children.firstOrNull; } - static final provider = NotifierProvider( - SelectedRoomController.new, - ); + static final provider = + AsyncNotifierProvider( + SelectedRoomController.new, + ); } diff --git a/lib/controllers/selected_space_controller.dart b/lib/controllers/selected_space_controller.dart index dbeb71f..75bf287 100644 --- a/lib/controllers/selected_space_controller.dart +++ b/lib/controllers/selected_space_controller.dart @@ -4,10 +4,12 @@ import "package:nexus/controllers/key_controller.dart"; import "package:nexus/controllers/spaces_controller.dart"; import "package:nexus/models/space.dart"; -class SelectedSpaceController extends Notifier { +class SelectedSpaceController extends AsyncNotifier { @override - Space build() { - final spaces = ref.watch(SpacesController.provider); + Future build() async { + final spaces = await ref.watch( + SpacesController.provider.selectAsync((data) => data), + ); final selectedSpaceId = ref.watch( KeyController.provider(KeyController.spaceKey), ); @@ -16,7 +18,7 @@ class SelectedSpaceController extends Notifier { spaces.first; } - static final provider = NotifierProvider( + static final provider = AsyncNotifierProvider( SelectedSpaceController.new, ); } diff --git a/lib/controllers/space_edges_controller.dart b/lib/controllers/space_edges_controller.dart deleted file mode 100644 index 0349f36..0000000 --- a/lib/controllers/space_edges_controller.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/models/space_edge.dart"; - -class SpaceEdgesController extends Notifier>> { - @override - IMap> build() => const IMap.empty(); - - void set(IMap> newEdges) => state = newEdges; - - static final provider = - NotifierProvider>>( - SpaceEdgesController.new, - ); -} diff --git a/lib/controllers/spaces_controller.dart b/lib/controllers/spaces_controller.dart index 292e323..408dc00 100644 --- a/lib/controllers/spaces_controller.dart +++ b/lib/controllers/spaces_controller.dart @@ -1,96 +1,77 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/rooms_controller.dart"; -import "package:nexus/controllers/top_level_spaces_controller.dart"; -import "package:nexus/controllers/space_edges_controller.dart"; +import "package:nexus/controllers/client_controller.dart"; +import "package:nexus/helpers/extensions/get_full_room.dart"; +import "package:nexus/helpers/extensions/room_to_children.dart"; import "package:nexus/models/space.dart"; -import "package:nexus/models/room.dart"; -import "package:nexus/models/space_edge.dart"; -class SpacesController extends Notifier> { +class SpacesController extends AsyncNotifier> { @override - IList build() { - final rooms = ref.watch(RoomsController.provider); - final topLevelSpaceIds = ref.watch(TopLevelSpacesController.provider); - final spaceEdges = ref.watch(SpaceEdgesController.provider); + Future> build() async { + final client = await ref.watch(ClientController.provider.future); - final childRoomsBySpaceId = IMap.fromEntries( - topLevelSpaceIds.map((spaceId) { - ISet walk(String currentId) { - final children = spaceEdges[currentId] ?? IList(); - - return children.fold>(const ISet.empty(), (acc, edge) { - final childId = edge.childId; - final isSpace = spaceEdges.containsKey(childId); - - return acc - .addAll(!isSpace ? ISet([childId]) : const ISet.empty()) - .addAll(isSpace ? walk(childId) : const ISet.empty()); - }); - } - - return MapEntry( - spaceId, - walk(spaceId).map((id) => rooms[id]).nonNulls.toIList(), - ); - }), + ref.onDispose( + client.onSync.stream.listen((_) => ref.invalidateSelf()).cancel, ); - final allNestedRoomIds = childRoomsBySpaceId.values - .expand((l) => l) - .map( - (room) => - rooms.entries.firstWhere((entry) => entry.value == room).key, - ) - .toISet(); - - final otherRooms = rooms.entries - .where( - (e) => - !allNestedRoomIds.contains(e.key) && - !topLevelSpaceIds.contains(e.key) && - !spaceEdges.containsKey(e.key), - ) - .map((e) => e.value); - - final homeRooms = otherRooms - .where((room) => room.metadata?.dmUserId == null) - .toIList(); - - final dmRooms = otherRooms - .where((room) => room.metadata?.dmUserId != null) - .toIList(); - - final topLevelSpacesList = topLevelSpaceIds - .map((id) { - final room = rooms[id]; - if (room == null) return null; - - final children = childRoomsBySpaceId[id] ?? IList(); - return Space( - id: id, - title: room.metadata?.name ?? "Unnamed Room", - room: room, - children: children, - ); - }) - .nonNulls - .toIList(); - - return [ - Space(id: "home", title: "Home", icon: Icons.home, children: homeRooms), - Space( - id: "dms", - title: "Direct Messages", - icon: Icons.people, - children: dmRooms, + final topLevel = IList( + await Future.wait( + client.rooms + .where((room) => !room.isDirectChat) + .where( + (room) => client.rooms + .where((room) => room.isSpace) + .every( + (match) => match.spaceChildren.every( + (child) => child.roomId != room.id, + ), + ), + ) + .map((room) => room.fullRoom), ), - ...topLevelSpacesList, - ].toIList(); + ); + + final topLevelSpaces = topLevel.where((r) => r.roomData.isSpace).toIList(); + final topLevelRooms = topLevel.where((r) => !r.roomData.isSpace).toIList(); + + return IList([ + Space( + client: client, + title: "Home", + id: "home", + children: topLevelRooms, + icon: Icons.home, + ), + Space( + client: client, + title: "Direct Messages", + id: "dms", + children: IList( + await Future.wait( + client.rooms + .where((room) => room.isDirectChat) + .map((room) => room.fullRoom), + ), + ), + icon: Icons.person, + ), + ...(await Future.wait( + topLevelSpaces.map( + (space) async => Space( + client: client, + title: space.title, + avatar: space.avatar, + id: space.roomData.id, + roomData: space.roomData, + children: IList(await space.roomData.getAllChildren()), + ), + ), + )), + ]); } - static final provider = NotifierProvider>( + static final provider = AsyncNotifierProvider>( SpacesController.new, ); } diff --git a/lib/controllers/sync_status_controller.dart b/lib/controllers/sync_status_controller.dart deleted file mode 100644 index fe65732..0000000 --- a/lib/controllers/sync_status_controller.dart +++ /dev/null @@ -1,13 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/models/sync_status.dart"; - -class SyncStatusController extends Notifier { - @override - Null build() => null; - - void set(SyncStatus newStatus) => state = newStatus; - - static final provider = NotifierProvider( - SyncStatusController.new, - ); -} diff --git a/lib/controllers/thumbnail_controller.dart b/lib/controllers/thumbnail_controller.dart new file mode 100644 index 0000000..4500523 --- /dev/null +++ b/lib/controllers/thumbnail_controller.dart @@ -0,0 +1,22 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/controllers/client_controller.dart"; +import "package:nexus/models/image_data.dart"; + +class ThumbnailController extends AsyncNotifier { + ThumbnailController(this.data); + final ImageData data; + + @override + Future build({String? from}) async { + final client = await ref.watch(ClientController.provider.future); + final uri = await Uri.tryParse(data.uri)?.getDownloadUri(client); + + return uri.toString(); + } + + static final provider = AsyncNotifierProvider.family + .autoDispose( + ThumbnailController.new, + ); +} diff --git a/lib/controllers/top_level_spaces_controller.dart b/lib/controllers/top_level_spaces_controller.dart deleted file mode 100644 index e1f9c88..0000000 --- a/lib/controllers/top_level_spaces_controller.dart +++ /dev/null @@ -1,14 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; - -class TopLevelSpacesController extends Notifier> { - @override - IList build() => const IList.empty(); - - void set(IList newSpaces) => state = newSpaces; - - static final provider = - NotifierProvider>( - TopLevelSpacesController.new, - ); -} diff --git a/lib/helpers/extensions/event_to_message.dart b/lib/helpers/extensions/event_to_message.dart new file mode 100644 index 0000000..5591952 --- /dev/null +++ b/lib/helpers/extensions/event_to_message.dart @@ -0,0 +1,145 @@ +import "package:collection/collection.dart"; +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:matrix/matrix.dart"; + +extension EventToMessage on Event { + Future toMessage( + Timeline timeline, { + bool mustBeText = false, + bool includeEdits = false, + }) async { + final replyId = inReplyToEventId(); + final newEvent = (unsigned?["m.relations"] as Map?)?["m.replace"]; + final event = newEvent == null ? this : Event.fromJson(newEvent, room); + + final replyEvent = replyId == null + ? null + : await room.getEventById(replyId); + + final sender = + await event.fetchSenderUser() ?? event.senderFromMemoryOrFallback; + final newContent = event.content["m.new_content"] as Map?; + final metadata = { + "formatted": + newContent?["formatted_body"] ?? + newContent?["body"] ?? + event.content["formatted_body"] ?? + event.content["body"] ?? + "", + "reply": await replyEvent?.toMessage(mustBeText: true, timeline), + "body": newContent?["body"] ?? event.content["body"], + "eventType": event.type, + "avatarUrl": sender.avatarUrl.toString(), + "displayName": sender.displayName ?? sender.id, + "txnId": transactionId, + }; + + final editedAt = event.relationshipType == RelationshipTypes.edit + ? event.originServerTs + : null; + + if ((redacted && !mustBeText) || + (!includeEdits && (relationshipType == RelationshipTypes.edit))) { + return null; + } + + // TODO: Use server-generated preview if enabled when https://github.com/famedly/matrix-dart-sdk/issues/2195 is fixed. + + // final match = Uri.tryParse( + // RegExp(regexLink, caseSensitive: false).firstMatch(body)?.group(0) ?? "", + // ); + + // final preview = match == null + // ? null + // : await room.client.getUrlPreview(match); + + final asText = + Message.text( + metadata: metadata, + id: eventId, + authorId: senderId, + text: redacted ? "This message has been deleted..." : event.body, + replyToMessageId: replyId, + deliveredAt: originServerTs, + editedAt: editedAt, + ) + as TextMessage; + + if (mustBeText) return asText; + return switch (type) { + EventTypes.Encrypted => asText.copyWith( + text: "Unable to decrypt message.", + metadata: {...metadata, "formatted": "Unable to decrypt message."}, + ), + PollEventContent.startType => Message.custom( + metadata: { + ...metadata, + "poll": event.parsedPollEventContent.pollStartContent, + "responses": event.getPollResponses(timeline), + }, + id: eventId, + deliveredAt: originServerTs, + authorId: senderId, + ), + (EventTypes.Sticker || EventTypes.Message) => switch (messageType) { + (MessageTypes.Sticker || MessageTypes.Image) => Message.image( + metadata: metadata, + id: eventId, + authorId: senderId, + text: event.text, + source: (await getAttachmentUri()).toString(), + replyToMessageId: replyId, + deliveredAt: originServerTs, + blurhash: (event.content["info"] as Map?)?["xyz.amorgan.blurhash"], + ), + MessageTypes.Audio => Message.audio( + metadata: metadata, + id: eventId, + authorId: senderId, + text: event.text, + replyToMessageId: replyId, + source: (await event.getAttachmentUri()).toString(), + deliveredAt: originServerTs, + // TODO: See if we can figure out duration + duration: Duration(hours: 1), + ), + MessageTypes.File => Message.file( + name: event.content["filename"].toString(), + metadata: metadata, + id: eventId, + authorId: senderId, + source: (await event.getAttachmentUri()).toString(), + replyToMessageId: replyId, + deliveredAt: originServerTs, + ), + _ => asText, + }, + EventTypes.RoomMember => Message.system( + metadata: metadata, + id: eventId, + authorId: senderId, + text: + "${event.asUser.displayName ?? event.asUser.id} ${switch (Membership.values.firstWhereOrNull((membership) => membership.name == event.content["membership"])) { + Membership.invite => "was invited to", + Membership.join => "joined", + Membership.leave => "left", + Membership.knock => "asked to join", + Membership.ban => "was banned from", + _ => "did something relating to", + }} the room.", + ), + EventTypes.Redaction => null, + _ => + // Turn this on for debugging purposes + false + // ignore: dead_code + ? Message.unsupported( + metadata: metadata, + id: eventId, + authorId: senderId, + replyToMessageId: replyId, + ) + : null, + }; + } +} diff --git a/lib/helpers/extensions/get_full_room.dart b/lib/helpers/extensions/get_full_room.dart new file mode 100644 index 0000000..bbd0bc5 --- /dev/null +++ b/lib/helpers/extensions/get_full_room.dart @@ -0,0 +1,13 @@ +import "package:matrix/matrix.dart"; +import "package:nexus/models/full_room.dart"; + +extension GetFullRoom on Room { + Future get fullRoom async { + await loadHeroUsers(); + return FullRoom( + roomData: this, + title: getLocalizedDisplayname(), + avatar: await avatar?.getThumbnailUri(client, width: 24, height: 24), + ); + } +} diff --git a/lib/helpers/extensions/get_headers.dart b/lib/helpers/extensions/get_headers.dart index e1bb5f3..b8b1fde 100644 --- a/lib/helpers/extensions/get_headers.dart +++ b/lib/helpers/extensions/get_headers.dart @@ -1,7 +1,5 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/header_controller.dart"; +import "package:matrix/matrix.dart"; -extension GetHeaders on WidgetRef { - Map get headers => - watch(HeaderController.provider).requireValue; +extension GetHeaders on Client { + Map get headers => {"authorization": "Bearer $accessToken"}; } diff --git a/lib/helpers/extensions/gomuks_buffer.dart b/lib/helpers/extensions/gomuks_buffer.dart deleted file mode 100644 index 88cfd5a..0000000 --- a/lib/helpers/extensions/gomuks_buffer.dart +++ /dev/null @@ -1,36 +0,0 @@ -import "dart:convert"; -import "dart:ffi"; -import "dart:typed_data"; -import "package:ffi/ffi.dart"; -import "package:nexus/src/third_party/gomuks.g.dart"; - -extension GomuksOwnedBufferToX on GomuksOwnedBuffer { - Uint8List toBytes() { - try { - if (base == nullptr || length <= 0) return Uint8List(0); - return Uint8List.fromList(base.asTypedList(length)); - } finally { - calloc.free(base); - } - } - - dynamic toJson() => jsonDecode(utf8.decode(toBytes())); -} - -extension JsonToGomuksBuffer on Map { - Pointer toGomuksBufferPtr() { - final jsonString = json.encode(this); - final bytes = utf8.encode(jsonString); - - final dataPtr = calloc(bytes.length); - dataPtr.asTypedList(bytes.length).setAll(0, bytes); - - final ptr = calloc(); - - ptr.ref - ..base = dataPtr - ..length = bytes.length; - - return ptr; - } -} diff --git a/lib/helpers/extensions/join_room_with_snackbars.dart b/lib/helpers/extensions/join_room_with_snackbars.dart index df89740..0c7b1b3 100644 --- a/lib/helpers/extensions/join_room_with_snackbars.dart +++ b/lib/helpers/extensions/join_room_with_snackbars.dart @@ -1,87 +1,87 @@ import "package:collection/collection.dart"; import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/client_controller.dart"; +import "package:matrix/matrix.dart"; import "package:nexus/controllers/key_controller.dart"; import "package:nexus/controllers/spaces_controller.dart"; -extension JoinRoomWithSnackbars on ClientController { +extension JoinRoomWithSnackbars on Client { Future joinRoomWithSnackBars( BuildContext context, String roomAlias, WidgetRef ref, ) async { - // final parsed = roomAlias.parseIdentifierIntoParts(); - // final alias = parsed?.primaryIdentifier ?? roomAlias; + final parsed = roomAlias.parseIdentifierIntoParts(); + final alias = parsed?.primaryIdentifier ?? roomAlias; - // final scaffoldMessenger = ScaffoldMessenger.of(context); + final scaffoldMessenger = ScaffoldMessenger.of(context); - // final snackbar = scaffoldMessenger.showSnackBar( - // SnackBar( - // content: Text("Joining room $alias."), - // duration: Duration(days: 999), - // ), - // ); + final snackbar = scaffoldMessenger.showSnackBar( + SnackBar( + content: Text("Joining room $alias."), + duration: Duration(days: 999), + ), + ); - // try { - // final id = await joinRoom(alias, via: parsed?.via.toList()); + try { + final id = await joinRoom(alias, via: parsed?.via.toList()); - // snackbar.close(); + snackbar.close(); - // scaffoldMessenger.showSnackBar( - // SnackBar( - // content: Text("Room $alias successfully joined."), - // action: SnackBarAction( - // label: "Open", - // onPressed: () async { - // final spaces = await ref.refresh( - // SpacesController.provider.future, - // ); - // final space = spaces.firstWhereOrNull((space) => space.id == id); + scaffoldMessenger.showSnackBar( + SnackBar( + content: Text("Room $alias successfully joined."), + action: SnackBarAction( + label: "Open", + onPressed: () async { + final spaces = await ref.refresh( + SpacesController.provider.future, + ); + final space = spaces.firstWhereOrNull((space) => space.id == id); - // await ref - // .watch( - // KeyController.provider(KeyController.spaceKey).notifier, - // ) - // .set( - // space?.id ?? - // spaces - // .firstWhere( - // (space) => - // space.children.firstWhereOrNull( - // (child) => child.roomData.id == id, - // ) != - // null, - // ) - // .id, - // ); + await ref + .watch( + KeyController.provider(KeyController.spaceKey).notifier, + ) + .set( + space?.id ?? + spaces + .firstWhere( + (space) => + space.children.firstWhereOrNull( + (child) => child.roomData.id == id, + ) != + null, + ) + .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, - // ), - // ), - // ), - // ); - // } - // } + 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, + ), + ), + ), + ); + } + } } } diff --git a/lib/helpers/extensions/link_to_mention.dart b/lib/helpers/extensions/link_to_mention.dart deleted file mode 100644 index 33d2bd2..0000000 --- a/lib/helpers/extensions/link_to_mention.dart +++ /dev/null @@ -1,40 +0,0 @@ -extension LinkToMention on String { - /// Extracts a Matrix identifier from this string. - /// - /// Supports: - /// - https://matrix.to/#/... - /// - matrix:roomid/... - /// - matrix:r/... - /// - matrix:u/... - /// - /// Returns the decoded identifier (e.g. "#room:matrix.org") - /// or null if this is not a Matrix link. - String? get mention { - final trimmed = trim(); - - final matrixTo = RegExp( - r"^https?://matrix\.to/#/([^/?#]+)", - caseSensitive: false, - ); - - final matrixToMatch = matrixTo.firstMatch(trimmed); - if (matrixToMatch != null) { - return Uri.decodeComponent(matrixToMatch.group(1)!); - } - - if (trimmed.toLowerCase().startsWith("matrix:")) { - try { - final uri = Uri.parse(trimmed); - - if (uri.pathSegments.isNotEmpty) { - final identifier = uri.pathSegments.last; - if (identifier.isNotEmpty) { - return Uri.decodeComponent(identifier); - } - } - } catch (_) {} - } - - return null; - } -} diff --git a/lib/helpers/extensions/list_to_messages.dart b/lib/helpers/extensions/list_to_messages.dart new file mode 100644 index 0000000..edddb25 --- /dev/null +++ b/lib/helpers/extensions/list_to_messages.dart @@ -0,0 +1,10 @@ +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/helpers/extensions/event_to_message.dart"; + +extension ListToMessages on List { + Future> toMessages(Room room, Timeline timeline) async => + (await Future.wait( + map((event) => Event.fromMatrixEvent(event, room).toMessage(timeline)), + )).nonNulls.toList().reversed.toList(); +} diff --git a/lib/helpers/extensions/mxc_to_https.dart b/lib/helpers/extensions/mxc_to_https.dart deleted file mode 100644 index 468da12..0000000 --- a/lib/helpers/extensions/mxc_to_https.dart +++ /dev/null @@ -1,4 +0,0 @@ -extension MxcToHttps on Uri { - Uri mxcToHttps(String homeserver) => - Uri.parse("${homeserver}_matrix/client/v1/media/download/$host$path"); -} diff --git a/lib/helpers/extensions/room_to_children.dart b/lib/helpers/extensions/room_to_children.dart new file mode 100644 index 0000000..d115f9a --- /dev/null +++ b/lib/helpers/extensions/room_to_children.dart @@ -0,0 +1,27 @@ +import "package:collection/collection.dart"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/helpers/extensions/get_full_room.dart"; +import "package:nexus/models/full_room.dart"; + +extension RoomToChildren on Room { + Future> getAllChildren() async { + final direct = await Future.wait( + spaceChildren + .map( + (child) => client.rooms + .firstWhereOrNull((r) => r.id == child.roomId) + ?.fullRoom, + ) + .nonNulls, + ); + + return (await Future.wait( + direct.map( + (child) async => child.roomData.isSpace + ? await child.roomData.getAllChildren() + : [child], + ), + )).expand((list) => list).toIList(); + } +} diff --git a/lib/main.dart b/lib/main.dart index ab65e4b..9e829a7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,18 +1,15 @@ -import "dart:developer"; import "dart:io"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; + import "package:flutter/foundation.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client_controller.dart"; -import "package:nexus/controllers/client_state_controller.dart"; -import "package:nexus/controllers/header_controller.dart"; -import "package:nexus/controllers/multi_provider_controller.dart"; import "package:nexus/controllers/shared_prefs_controller.dart"; import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/scheme_to_theme.dart"; import "package:nexus/pages/chat_page.dart"; import "package:nexus/pages/login_page.dart"; -import "package:nexus/pages/verify_page.dart"; +import "package:nexus/pages/settings_page.dart"; +import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/loading.dart"; import "package:window_manager/window_manager.dart"; @@ -38,13 +35,11 @@ New Value: ${newValue is AsyncData ? newValue.value : newValue} void showError(Object error, [StackTrace? stackTrace]) { if (error.toString().contains("DioException")) return; - if (error.toString().contains("Invalid source")) return; if (error.toString().contains("UTF-16")) return; if (error.toString().contains("HTTP request failed")) return; if (error.toString().contains("Invalid image data")) return; debugPrintStack(stackTrace: stackTrace, label: error.toString()); - debugger(); if (navigatorKey.currentContext != null) { Future.delayed( Duration.zero, @@ -86,11 +81,11 @@ void main() async { ); } -class App extends StatelessWidget { +class App extends ConsumerWidget { const App({super.key}); @override - Widget build(BuildContext context) => DynamicColorBuilder( + Widget build(BuildContext context, WidgetRef ref) => DynamicColorBuilder( builder: (lightDynamic, darkDynamic) => MaterialApp( navigatorKey: navigatorKey, debugShowCheckedModeBanner: false, @@ -104,39 +99,42 @@ class App extends StatelessWidget { brightness: Brightness.dark, )) .theme, - home: Scaffold( - body: Consumer( - builder: (_, ref, _) => ref - .watch( - MultiProviderController.provider( - IListConst([ - SharedPrefsController.provider, - ClientController.provider, - HeaderController.provider, - ]), - ), - ) - .betterWhen( - data: (_) => Consumer( - builder: (_, ref, _) { - final clientState = ref.watch( - ClientStateController.provider, - ); - if (clientState == null || !clientState.isInitialized) { - return Loading(); - } - - if (!clientState.isLoggedIn) { - return LoginPage(); - } else if (!clientState.isVerified) { - return VerifyPage(); - } else { - return ChatPage(); - } - }, - ), - ), - ), + home: Builder( + builder: (context) => ref + .watch(SharedPrefsController.provider) + .betterWhen( + data: (_) => ref + .watch(ClientController.provider) + .betterWhen( + data: (client) => + client.accessToken == null ? LoginPage() : ChatPage(), + loading: () => Scaffold( + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 16, + children: [ + Text( + "Syncing...", + style: Theme.of(context).textTheme.headlineMedium, + ), + Loading(), + ], + ), + ), + appBar: Appbar( + actions: [ + IconButton( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => SettingsPage()), + ), + icon: Icon(Icons.settings), + ), + ], + ), + ), + ), + ), ), ), ); diff --git a/lib/models/client_state.dart b/lib/models/client_state.dart deleted file mode 100644 index 1e15136..0000000 --- a/lib/models/client_state.dart +++ /dev/null @@ -1,17 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "client_state.freezed.dart"; -part "client_state.g.dart"; - -@freezed -abstract class ClientState with _$ClientState { - const factory ClientState({ - required bool isInitialized, - required bool isLoggedIn, - required bool isVerified, - required String? userId, - required String? homeserverUrl, - }) = _ClientState; - - factory ClientState.fromJson(Map json) => - _$ClientStateFromJson(json); -} diff --git a/lib/models/epoch_date_time_converter.dart b/lib/models/epoch_date_time_converter.dart deleted file mode 100644 index c26d020..0000000 --- a/lib/models/epoch_date_time_converter.dart +++ /dev/null @@ -1,11 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; - -class EpochDateTimeConverter implements JsonConverter { - const EpochDateTimeConverter(); - - @override - DateTime fromJson(int json) => DateTime.fromMillisecondsSinceEpoch(json); - - @override - int toJson(DateTime object) => object.millisecondsSinceEpoch; -} diff --git a/lib/models/event.dart b/lib/models/event.dart deleted file mode 100644 index 623116b..0000000 --- a/lib/models/event.dart +++ /dev/null @@ -1,79 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/epoch_date_time_converter.dart"; -part "event.freezed.dart"; -part "event.g.dart"; - -@freezed -abstract class Event with _$Event { - const factory Event({ - @JsonKey(name: "rowid") required int rowId, - @JsonKey(name: "timeline_rowid") required int timelineRowId, - required String roomId, - required String eventId, - @JsonKey(name: "sender") required String authorId, - required String type, - String? stateKey, - @EpochDateTimeConverter() required DateTime timestamp, - required IMap content, - IMap? decrypted, - String? decryptedType, - @Default(IMap.empty()) IMap unsigned, - LocalContent? localContent, - String? transactionId, - String? redactedBy, - String? relatesTo, - String? relationType, - String? decryptionError, - String? sendError, - @Default(IMap.empty()) IMap reactions, - int? lastEditRowId, - @UnreadTypeConverter() UnreadType? unreadType, - }) = _Event; - - factory Event.fromJson(Map json) => _$EventFromJson(json); -} - -@freezed -abstract class LocalContent with _$LocalContent { - const factory LocalContent({ - String? sanitizedHtml, - bool? wasPlaintext, - bool? bigEmoji, - bool? hasMath, - bool? replyFallbackRemoved, - }) = _LocalContent; - - factory LocalContent.fromJson(Map json) => - _$LocalContentFromJson(json); -} - -class UnreadTypeConverter implements JsonConverter { - const UnreadTypeConverter(); - - @override - UnreadType? fromJson(int? json) => json == null ? null : UnreadType(json); - - @override - int? toJson(UnreadType? object) => object?.value; -} - -// I think this is correct but I'm not sure, its some type of bitmask. -@immutable -class UnreadType { - final int value; - - const UnreadType(this.value); - - static const none = UnreadType(0); - static const normal = UnreadType(1); - static const notify = UnreadType(2); - static const highlight = UnreadType(4); - static const sound = UnreadType(8); - - bool get isNone => value == 0; - bool get isNormal => (value & 1) != 0; - bool get shouldNotify => (value & 2) != 0; - bool get isHighlighted => (value & 4) != 0; - bool get playsSound => (value & 8) != 0; -} diff --git a/lib/models/full_room.dart b/lib/models/full_room.dart new file mode 100644 index 0000000..ee61da6 --- /dev/null +++ b/lib/models/full_room.dart @@ -0,0 +1,13 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:matrix/matrix.dart"; +part "full_room.freezed.dart"; + +@freezed +abstract class FullRoom with _$FullRoom { + const FullRoom._(); + const factory FullRoom({ + required Room roomData, + required String title, + required Uri? avatar, + }) = _FullRoom; +} diff --git a/lib/models/image_data.dart b/lib/models/image_data.dart new file mode 100644 index 0000000..e5bc57e --- /dev/null +++ b/lib/models/image_data.dart @@ -0,0 +1,11 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +part "image_data.freezed.dart"; + +@freezed +abstract class ImageData with _$ImageData { + const factory ImageData({ + required String uri, + required int? height, + required int? width, + }) = _ImageData; +} diff --git a/lib/models/lazy_load_summary.dart b/lib/models/lazy_load_summary.dart deleted file mode 100644 index 0cd250f..0000000 --- a/lib/models/lazy_load_summary.dart +++ /dev/null @@ -1,16 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -part "lazy_load_summary.freezed.dart"; -part "lazy_load_summary.g.dart"; - -@freezed -abstract class LazyLoadSummary with _$LazyLoadSummary { - const factory LazyLoadSummary({ - required IList? heroes, - required int? joinedMemberCount, - required int? invitedMemberCount, - }) = _LazyLoadSummary; - - factory LazyLoadSummary.fromJson(Map json) => - _$LazyLoadSummaryFromJson(json); -} diff --git a/lib/models/message_config.dart b/lib/models/message_config.dart deleted file mode 100644 index 4e5ff71..0000000 --- a/lib/models/message_config.dart +++ /dev/null @@ -1,16 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/event.dart"; -part "message_config.freezed.dart"; -part "message_config.g.dart"; - -@freezed -abstract class MessageConfig with _$MessageConfig { - const factory MessageConfig({ - @Default(false) bool mustBeText, - @Default(false) bool includeEdits, - required Event event, - }) = _MessageConfig; - - factory MessageConfig.fromJson(Map json) => - _$MessageConfigFromJson(json); -} diff --git a/lib/models/paginate.dart b/lib/models/paginate.dart deleted file mode 100644 index df0a0f6..0000000 --- a/lib/models/paginate.dart +++ /dev/null @@ -1,17 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/event.dart"; -part "paginate.freezed.dart"; -part "paginate.g.dart"; - -@freezed -abstract class Paginate with _$Paginate { - const factory Paginate({ - required IList events, - required IList relatedEvents, - required bool hasMore, - }) = _Paginate; - - factory Paginate.fromJson(Map json) => - _$PaginateFromJson(json); -} diff --git a/lib/models/profile.dart b/lib/models/profile.dart deleted file mode 100644 index d92b4f6..0000000 --- a/lib/models/profile.dart +++ /dev/null @@ -1,29 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -part "profile.freezed.dart"; -part "profile.g.dart"; - -@freezed -abstract class Profile with _$Profile { - const factory Profile({ - String? avatarUrl, - @JsonKey(name: "displayname") String? displayName, - @JsonKey(name: "us.cloke.msc4175.tz") String? timezone, - - @Default(IList.empty()) - @JsonKey(name: "io.fsky.nyx.pronouns") - IList pronouns, - }) = _Profile; - - factory Profile.fromJson(Map json) => - _$ProfileFromJson(json); -} - -@freezed -abstract class Pronoun with _$Pronoun { - const factory Pronoun({required String language, required String summary}) = - _Pronoun; - - factory Pronoun.fromJson(Map json) => - _$PronounFromJson(json); -} diff --git a/lib/models/read_receipt.dart b/lib/models/read_receipt.dart deleted file mode 100644 index d533e2d..0000000 --- a/lib/models/read_receipt.dart +++ /dev/null @@ -1,18 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/epoch_date_time_converter.dart"; -part "read_receipt.freezed.dart"; -part "read_receipt.g.dart"; - -@freezed -abstract class ReadReceipt with _$ReadReceipt { - const factory ReadReceipt({ - String? roomId, - required String userId, - String? threadId, - required String eventId, - @EpochDateTimeConverter() required DateTime timestamp, - }) = _ReadReceipt; - - factory ReadReceipt.fromJson(Map json) => - _$ReadReceiptFromJson(json); -} diff --git a/lib/models/requests/get_event_request.dart b/lib/models/requests/get_event_request.dart deleted file mode 100644 index 3812d50..0000000 --- a/lib/models/requests/get_event_request.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "get_event_request.freezed.dart"; -part "get_event_request.g.dart"; - -@freezed -abstract class GetEventRequest with _$GetEventRequest { - const factory GetEventRequest({ - required String roomId, - required String eventId, - @Default(false) bool unredact, - }) = _GetEventRequest; - - factory GetEventRequest.fromJson(Map json) => - _$GetEventRequestFromJson(json); -} diff --git a/lib/models/requests/get_related_events_request.dart b/lib/models/requests/get_related_events_request.dart deleted file mode 100644 index 7e2244f..0000000 --- a/lib/models/requests/get_related_events_request.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "get_related_events_request.freezed.dart"; -part "get_related_events_request.g.dart"; - -@freezed -abstract class GetRelatedEventsRequest with _$GetRelatedEventsRequest { - const factory GetRelatedEventsRequest({ - required String roomId, - required String eventId, - required String relationType, - }) = _GetRelatedEventsRequest; - - factory GetRelatedEventsRequest.fromJson(Map json) => - _$GetRelatedEventsRequestFromJson(json); -} diff --git a/lib/models/requests/get_room_state_request.dart b/lib/models/requests/get_room_state_request.dart deleted file mode 100644 index a154d5f..0000000 --- a/lib/models/requests/get_room_state_request.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "get_room_state_request.freezed.dart"; -part "get_room_state_request.g.dart"; - -@freezed -abstract class GetRoomStateRequest with _$GetRoomStateRequest { - const factory GetRoomStateRequest({ - required String roomId, - required bool fetchMembers, - @Default(false) bool includeMembers, - }) = _GetRoomStateRequest; - - factory GetRoomStateRequest.fromJson(Map json) => - _$GetRoomStateRequestFromJson(json); -} diff --git a/lib/models/requests/login_request.dart b/lib/models/requests/login_request.dart deleted file mode 100644 index b3704fa..0000000 --- a/lib/models/requests/login_request.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "login_request.freezed.dart"; -part "login_request.g.dart"; - -@freezed -abstract class LoginRequest with _$LoginRequest { - const factory LoginRequest({ - required String username, - required String password, - required String homeserverUrl, - }) = _LoginRequest; - - factory LoginRequest.fromJson(Map json) => - _$LoginRequestFromJson(json); -} diff --git a/lib/models/requests/paginate_request.dart b/lib/models/requests/paginate_request.dart deleted file mode 100644 index 44cf8ec..0000000 --- a/lib/models/requests/paginate_request.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "paginate_request.freezed.dart"; -part "paginate_request.g.dart"; - -@freezed -abstract class PaginateRequest with _$PaginateRequest { - const factory PaginateRequest({ - required String roomId, - required int? maxTimelineId, - @Default(20) int limit, - }) = _PaginateRequest; - - factory PaginateRequest.fromJson(Map json) => - _$PaginateRequestFromJson(json); -} diff --git a/lib/models/requests/redact_event_request.dart b/lib/models/requests/redact_event_request.dart deleted file mode 100644 index fed2255..0000000 --- a/lib/models/requests/redact_event_request.dart +++ /dev/null @@ -1,3 +0,0 @@ -import "package:nexus/models/requests/report_request.dart"; - -typedef RedactEventRequest = ReportRequest; diff --git a/lib/models/requests/report_request.dart b/lib/models/requests/report_request.dart deleted file mode 100644 index 749ad60..0000000 --- a/lib/models/requests/report_request.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "report_request.freezed.dart"; -part "report_request.g.dart"; - -@freezed -abstract class ReportRequest with _$ReportRequest { - const factory ReportRequest({ - required String roomId, - required String eventId, - String? reason, - }) = _ReportRequest; - - factory ReportRequest.fromJson(Map json) => - _$ReportRequestFromJson(json); -} diff --git a/lib/models/requests/send_message_request.dart b/lib/models/requests/send_message_request.dart deleted file mode 100644 index 883c585..0000000 --- a/lib/models/requests/send_message_request.dart +++ /dev/null @@ -1,54 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/relation_type.dart"; -part "send_message_request.freezed.dart"; -part "send_message_request.g.dart"; - -@freezed -abstract class SendMessageRequest with _$SendMessageRequest { - const factory SendMessageRequest({ - required String roomId, - required String text, - @Default(Mentions()) @JsonKey(name: "mentions") Mentions mentions, - @JsonKey(name: "relates_to") Relation? relation, - }) = _SendMessageRequest; - - factory SendMessageRequest.fromJson(Map json) => - _$SendMessageRequestFromJson(json); -} - -@freezed -abstract class Mentions with _$Mentions { - const factory Mentions({ - @Default(false) bool room, - @Default(IList.empty()) IList userIds, - }) = _Mentions; - - factory Mentions.fromJson(Map json) => - _$MentionsFromJson(json); -} - -@Freezed(toJson: false) -abstract class Relation with _$Relation { - const Relation._(); - - const factory Relation({ - required String eventId, - required RelationType relationType, - }) = _Relation; - - Map toJson() { - switch (relationType) { - case RelationType.reply: - return { - "m.in_reply_to": {"event_id": eventId}, - }; - - case RelationType.edit: - return {"rel_type": "m.replace", "event_id": eventId}; - } - } - - factory Relation.fromJson(Map json) => - _$RelationFromJson(json); -} diff --git a/lib/models/room.dart b/lib/models/room.dart deleted file mode 100644 index 3c3eec0..0000000 --- a/lib/models/room.dart +++ /dev/null @@ -1,36 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/read_receipt.dart"; -import "package:nexus/models/room_metadata.dart"; -part "room.freezed.dart"; -part "room.g.dart"; - -@freezed -abstract class Room with _$Room { - const factory Room({ - @JsonKey(name: "meta") RoomMetadata? metadata, - @Default(IList.empty()) IList timeline, - @Default(false) bool reset, - @Default(IMap.empty()) IMap> state, - // required IMap accountData, - @Default(IList.empty()) IList events, - @Default(IMap.empty()) IMap> receipts, - @Default(false) bool dismissNotifications, - @Default(true) bool hasMore, - // required IList notifications, - }) = _Room; - - factory Room.fromJson(Map json) => _$RoomFromJson(json); -} - -@freezed -abstract class TimelineRowTuple with _$TimelineRowTuple { - const factory TimelineRowTuple({ - @JsonKey(name: "timeline_rowid") required int timelineRowId, - @JsonKey(name: "event_rowid") int? eventRowId, - }) = _TimelineRowTuple; - - factory TimelineRowTuple.fromJson(Map json) => - _$TimelineRowTupleFromJson(json); -} diff --git a/lib/models/room_metadata.dart b/lib/models/room_metadata.dart deleted file mode 100644 index 7c16cae..0000000 --- a/lib/models/room_metadata.dart +++ /dev/null @@ -1,30 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/epoch_date_time_converter.dart"; -import "package:nexus/models/lazy_load_summary.dart"; -part "room_metadata.freezed.dart"; -part "room_metadata.g.dart"; - -@freezed -abstract class RoomMetadata with _$RoomMetadata { - const factory RoomMetadata({ - @JsonKey(name: "room_id") required String id, - - // required CreateEventContent creationContent, - // required TombstoneEventContent tombstoneEventContent, - String? name, - Uri? avatar, - String? dmUserId, - String? topic, - String? canonicalAlias, - LazyLoadSummary? lazyLoadSummary, - required bool hasMemberList, - @JsonKey(name: "preview_event_rowid") required int previewEventRowID, - @EpochDateTimeConverter() required DateTime sortingTimestamp, - required int unreadHighlights, - required int unreadNotifications, - required int unreadMessages, - }) = _RoomMetadata; - - factory RoomMetadata.fromJson(Map json) => - _$RoomMetadataFromJson(json); -} diff --git a/lib/models/session_backup.dart b/lib/models/session_backup.dart new file mode 100644 index 0000000..0245c7e --- /dev/null +++ b/lib/models/session_backup.dart @@ -0,0 +1,17 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +part "session_backup.freezed.dart"; +part "session_backup.g.dart"; + +@freezed +abstract class SessionBackup with _$SessionBackup { + const factory SessionBackup({ + required String accessToken, + required Uri homeserver, + required String userID, + required String deviceID, + required String deviceName, + }) = _SessionBackup; + + factory SessionBackup.fromJson(Map json) => + _$SessionBackupFromJson(json); +} diff --git a/lib/models/space.dart b/lib/models/space.dart index 631759a..d64dcc9 100644 --- a/lib/models/space.dart +++ b/lib/models/space.dart @@ -1,16 +1,20 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/widgets.dart"; import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/room.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/models/full_room.dart"; part "space.freezed.dart"; @freezed abstract class Space with _$Space { + const Space._(); const factory Space({ - required String id, required String title, + required String id, + required IList children, + required Client client, + Room? roomData, + Uri? avatar, IconData? icon, - Room? room, - required IList children, }) = _Space; } diff --git a/lib/models/space_edge.dart b/lib/models/space_edge.dart deleted file mode 100644 index 192af31..0000000 --- a/lib/models/space_edge.dart +++ /dev/null @@ -1,14 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "space_edge.freezed.dart"; -part "space_edge.g.dart"; - -@freezed -abstract class SpaceEdge with _$SpaceEdge { - const factory SpaceEdge({ - required String childId, - @Default(false) bool suggested, - }) = _SpaceEdge; - - factory SpaceEdge.fromJson(Map json) => - _$SpaceEdgeFromJson(json); -} diff --git a/lib/models/sync_data.dart b/lib/models/sync_data.dart deleted file mode 100644 index 0fc18ac..0000000 --- a/lib/models/sync_data.dart +++ /dev/null @@ -1,22 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/room.dart"; -import "package:nexus/models/space_edge.dart"; -part "sync_data.freezed.dart"; -part "sync_data.g.dart"; - -@freezed -abstract class SyncData with _$SyncData { - const factory SyncData({ - @Default(false) bool clearState, - // required IMap accountData, - @Default(IMap.empty()) IMap rooms, - @Default(ISet.empty()) ISet leftRooms, - // required IList invitedRooms, - IMap>? spaceEdges, - IList? topLevelSpaces, - }) = _SyncData; - - factory SyncData.fromJson(Map json) => - _$SyncDataFromJson(json); -} diff --git a/lib/models/sync_status.dart b/lib/models/sync_status.dart deleted file mode 100644 index 42c5f2a..0000000 --- a/lib/models/sync_status.dart +++ /dev/null @@ -1,18 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "sync_status.freezed.dart"; -part "sync_status.g.dart"; - -@freezed -abstract class SyncStatus with _$SyncStatus { - const factory SyncStatus({ - required SyncStatusType type, - String? error, - required int errorCount, - }) = _SyncStatus; - - factory SyncStatus.fromJson(Map json) => - _$SyncStatusFromJson(json); -} - -@JsonEnum(fieldRename: FieldRename.snake) -enum SyncStatusType { ok, waiting, erroring, permanentlyFailed } diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index ee2f4d0..c60cc45 100644 --- a/lib/pages/chat_page.dart +++ b/lib/pages/chat_page.dart @@ -1,13 +1,12 @@ import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/widgets/chat_page/sidebar.dart"; import "package:nexus/widgets/chat_page/room_chat.dart"; +import "package:nexus/widgets/chat_page/sidebar.dart"; -class ChatPage extends ConsumerWidget { +class ChatPage extends StatelessWidget { const ChatPage({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder( + Widget build(BuildContext context) => LayoutBuilder( builder: (context, constraints) { final isDesktop = constraints.maxWidth > 650; final showMembersByDefault = constraints.maxWidth > 1000; diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index b15f6d4..a31498c 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -5,7 +5,6 @@ import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/client_controller.dart"; import "package:nexus/helpers/launch_helper.dart"; import "package:nexus/models/homeserver.dart"; -import "package:nexus/models/requests/login_request.dart"; import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/divider_text.dart"; import "package:nexus/widgets/loading.dart"; @@ -16,25 +15,27 @@ class LoginPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final client = ref.watch(ClientController.provider.notifier); final isLoading = useState(false); - final homeserver = useState(null); + final allowLogin = useState(false); final launch = ref.watch(LaunchHelper.provider).launchUrl; - Future setHomeserver(Uri? newHomeserver) async { + Future setHomeserver(Uri? homeserver) async { isLoading.value = true; + final succeeded = homeserver == null + ? false + : await ref + .watch(ClientController.provider.notifier) + .setHomeserver( + homeserver.hasScheme + ? homeserver + : Uri.https(homeserver.path), + ); - homeserver.value = newHomeserver == null - ? null - : await client.discoverHomeserver( - newHomeserver.hasScheme - ? newHomeserver - : Uri.https(newHomeserver.path), - ); - - if (homeserver.value == null && context.mounted) { + if (succeeded) { + allowLogin.value = true; + } else if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( @@ -156,7 +157,7 @@ class LoginPage extends HookConsumerWidget { ), if (isLoading.value) Padding(padding: EdgeInsets.only(top: 32), child: Loading()) - else if (homeserver.value != null) ...[ + else if (allowLogin.value) ...[ DividerText("Then, sign in:"), SizedBox(height: 4), TextField( @@ -173,13 +174,9 @@ class LoginPage extends HookConsumerWidget { ElevatedButton( onPressed: () async { isLoading.value = true; - final succeeded = await client.login( - LoginRequest( - username: username.text, - password: password.text, - homeserverUrl: homeserver.value!, - ), - ); + final succeeded = await ref + .watch(ClientController.provider.notifier) + .login(username.text, password.text); if (!succeeded && context.mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/pages/verify_page.dart b/lib/pages/verify_page.dart deleted file mode 100644 index 1011f80..0000000 --- a/lib/pages/verify_page.dart +++ /dev/null @@ -1,82 +0,0 @@ -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/widgets/form_text_input.dart"; - -class VerifyPage extends HookConsumerWidget { - const VerifyPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final passphraseController = useTextEditingController(); - final isVerifying = useState(false); - return AlertDialog( - title: Text("Verify"), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Enter your recovery key or passphrase below to unlock encrypted messages.\nYour passphrase is usually not the same as your password.", - ), - SizedBox(height: 12), - FormTextInput( - required: false, - autofocus: true, - capitalize: true, - controller: passphraseController, - obscure: true, - title: "Recovery Key or Passphrase", - ), - ], - ), - actions: [ - TextButton( - onPressed: isVerifying.value - ? null - : () async { - final scaffoldMessenger = ScaffoldMessenger.of(context); - final snackbar = scaffoldMessenger.showSnackBar( - SnackBar( - content: Text( - "Attempting to verify with recovery key...", - ), - duration: Duration(days: 999), - ), - ); - - isVerifying.value = true; - - final success = await ref - .watch(ClientController.provider.notifier) - .verify(passphraseController.text); - - snackbar.close(); - if (!success) { - isVerifying.value = false; - if (context.mounted) { - scaffoldMessenger.showSnackBar( - SnackBar( - backgroundColor: Theme.of( - context, - ).colorScheme.errorContainer, - content: Text( - "Verification failed. Is your passphrase correct?", - style: TextStyle( - color: Theme.of( - context, - ).colorScheme.onErrorContainer, - ), - ), - ), - ); - } - } - }, - child: Text("Verify"), - ), - ], - ); - } -} diff --git a/lib/widgets/appbar.dart b/lib/widgets/appbar.dart index 00b0e4c..fa2088d 100644 --- a/lib/widgets/appbar.dart +++ b/lib/widgets/appbar.dart @@ -1,5 +1,4 @@ import "dart:io"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; import "package:window_manager/window_manager.dart"; @@ -8,7 +7,7 @@ class Appbar extends StatelessWidget implements PreferredSizeWidget { final Widget? title; final Color? backgroundColor; final double? scrolledUnderElevation; - final IList actions; + final List actions; const Appbar({ super.key, @@ -16,7 +15,7 @@ class Appbar extends StatelessWidget implements PreferredSizeWidget { this.backgroundColor, this.scrolledUnderElevation, this.leading, - this.actions = const IList.empty(), + this.actions = const [], }); @override diff --git a/lib/widgets/avatar_or_hash.dart b/lib/widgets/avatar_or_hash.dart index a47bbb5..a077810 100644 --- a/lib/widgets/avatar_or_hash.dart +++ b/lib/widgets/avatar_or_hash.dart @@ -1,34 +1,28 @@ import "package:color_hash/color_hash.dart"; -import "package:cross_cache/cross_cache.dart"; import "package:flutter/material.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/helpers/extensions/get_headers.dart"; -import "package:nexus/helpers/extensions/mxc_to_https.dart"; -class AvatarOrHash extends ConsumerWidget { +class AvatarOrHash extends StatelessWidget { final Uri? avatar; final String title; final Widget? fallback; final bool hasBadge; - final int badgeNumber; final double height; + final Map headers; const AvatarOrHash( this.avatar, this.title, { this.fallback, - this.badgeNumber = 0, this.hasBadge = false, this.height = 24, + required this.headers, super.key, }); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final box = ColoredBox( color: ColorHash(title).color, - child: Center(child: Text(title.isEmpty ? "" : title[0])), + child: Center(child: Text(title[0])), ); return SizedBox( width: height, @@ -36,7 +30,6 @@ class AvatarOrHash extends ConsumerWidget { child: Center( child: Badge( isLabelVisible: hasBadge, - label: badgeNumber != 0 ? Text(badgeNumber.toString()) : null, smallSize: 12, backgroundColor: Theme.of(context).colorScheme.primary, child: ClipRRect( @@ -46,21 +39,9 @@ class AvatarOrHash extends ConsumerWidget { height: height, child: avatar == null ? fallback ?? box - : Image( - image: CachedNetworkImage( - avatar! - .mxcToHttps( - ref.watch( - ClientStateController.provider.select( - (value) => value?.homeserverUrl, - ), - ) ?? - "", - ) - .toString(), - ref.watch(CrossCacheController.provider), - headers: ref.headers, - ), + : Image.network( + avatar.toString(), + headers: headers, fit: BoxFit.contain, errorBuilder: (_, _, _) => box, ), diff --git a/lib/widgets/chat_page/chat_box.dart b/lib/widgets/chat_page/chat_box.dart index 885ddd9..8fd5acb 100644 --- a/lib/widgets/chat_page/chat_box.dart +++ b/lib/widgets/chat_page/chat_box.dart @@ -5,9 +5,9 @@ import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:fluttertagger/fluttertagger.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:matrix/matrix.dart"; import "package:nexus/controllers/room_chat_controller.dart"; import "package:nexus/models/relation_type.dart"; -import "package:nexus/models/room.dart"; import "package:nexus/widgets/chat_page/mention_overlay.dart"; import "package:nexus/widgets/chat_page/relation_preview.dart"; @@ -45,9 +45,9 @@ class ChatBox extends HookConsumerWidget { } void send() { - if (controller.value.text.trim().isEmpty || room.metadata == null) return; + if (controller.value.text.trim().isEmpty) return; ref - .watch(RoomChatController.provider(room.metadata!.id).notifier) + .watch(RoomChatController.provider(room).notifier) .send( controller.value.formattedText, relation: relatedMessage, @@ -94,6 +94,7 @@ class ChatBox extends HookConsumerWidget { relatedMessage: relatedMessage, relationType: relationType, onDismiss: onDismiss, + room: room, ), Container( color: theme.colorScheme.surfaceContainerHighest, @@ -104,7 +105,7 @@ class ChatBox extends HookConsumerWidget { PopupMenuButton( itemBuilder: (context) => [], icon: Icon(Icons.add), - // enabled: room.canSendDefaultMessages, TODO: Permissions check + enabled: room.canSendDefaultMessages, ), Expanded( child: FlutterTagger( @@ -125,13 +126,13 @@ class ChatBox extends HookConsumerWidget { }, triggerCharacterAndStyles: {"@": style, "#": style}, builder: (context, key) => TextFormField( - // enabled: room.canSendDefaultMessages, + enabled: room.canSendDefaultMessages, maxLines: 12, minLines: 1, decoration: InputDecoration( - // hintText: room.canSendDefaultMessages - // ? "Your message here..." - // : "You don't have permission to send messages in this room...", + hintText: room.canSendDefaultMessages + ? "Your message here..." + : "You don't have permission to send messages in this room...", border: InputBorder.none, ), controller: controller.value, @@ -142,8 +143,7 @@ class ChatBox extends HookConsumerWidget { ), ), IconButton( - onPressed: send, - // onPressed: room.canSendDefaultMessages ? send : null, + onPressed: room.canSendDefaultMessages ? send : null, icon: Icon(Icons.send), ), ], diff --git a/lib/widgets/chat_page/html/html.dart b/lib/widgets/chat_page/html/html.dart index 18edf4a..04a5a1b 100644 --- a/lib/widgets/chat_page/html/html.dart +++ b/lib/widgets/chat_page/html/html.dart @@ -2,19 +2,21 @@ 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:matrix/matrix.dart"; +import "package:nexus/controllers/thumbnail_controller.dart"; import "package:nexus/helpers/extensions/get_headers.dart"; -import "package:nexus/helpers/extensions/link_to_mention.dart"; -import "package:nexus/helpers/extensions/mxc_to_https.dart"; import "package:nexus/helpers/launch_helper.dart"; +import "package:nexus/models/image_data.dart"; import "package:nexus/widgets/chat_page/html/mention_chip.dart"; import "package:nexus/widgets/chat_page/html/spoiler_text.dart"; import "package:nexus/widgets/chat_page/html/code_block.dart"; import "package:nexus/widgets/chat_page/html/quoted.dart"; +import "package:nexus/widgets/error_dialog.dart"; class Html extends ConsumerWidget { final String html; - const Html(this.html, {super.key}); + final Client client; + const Html(this.html, {required this.client, super.key}); @override Widget build(BuildContext context, WidgetRef ref) => HtmlWidget( @@ -36,43 +38,61 @@ class Html extends ConsumerWidget { ) : null, - "blockquote" => Quoted(Html(element.innerHtml)), + "blockquote" => Quoted(Html(element.innerHtml, client: client)), "a" => - element.attributes["href"]?.mention == null + element.attributes["href"]?.parseIdentifierIntoParts() == null ? null : InlineCustomWidget(child: MentionChip(element.text)), "img" => element.attributes["src"] == null ? null - : InlineCustomWidget( - child: Image.network( - Uri.parse(element.attributes["src"]!) - .mxcToHttps( - ref.watch( - ClientStateController.provider.select( - (value) => value?.homeserverUrl, + : Consumer( + builder: (_, ref, _) => ref + .watch( + ThumbnailController.provider( + ImageData( + uri: element.attributes["src"]!, + height: height, + width: width, + ), + ), + ) + .when( + data: (uri) { + if (uri == null) return SizedBox.shrink(); + + return InlineCustomWidget( + child: Image.network( + uri, + headers: client.headers, + errorBuilder: (_, error, _) => Text( + "Image Failed to Load", + style: TextStyle( + color: Theme.of(context).colorScheme.error, ), - ) ?? - "", - ) - .toString(), - headers: ref.headers, - errorBuilder: (_, error, _) => Text( - "Image Failed to Load", - style: TextStyle( - color: Theme.of(context).colorScheme.error, + ), + height: height.toDouble(), + width: width?.toDouble(), + loadingBuilder: (_, child, loadingProgress) => + loadingProgress == null + ? child + : CircularProgressIndicator(), + ), + ); + }, + error: ErrorDialog.new, + loading: () => InlineCustomWidget( + child: SizedBox( + width: width?.toDouble(), + height: height.toDouble(), + child: CircularProgressIndicator(), + ), + ), ), - ), - height: height.toDouble(), - width: width?.toDouble(), - loadingBuilder: (_, child, loadingProgress) => - loadingProgress == null - ? child - : CircularProgressIndicator(), - ), ), + ("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..e2c5003 100644 --- a/lib/widgets/chat_page/html/mention_chip.dart +++ b/lib/widgets/chat_page/html/mention_chip.dart @@ -1,5 +1,5 @@ import "package:flutter/material.dart"; -import "package:nexus/helpers/extensions/link_to_mention.dart"; +import "package:matrix/matrix.dart"; class MentionChip extends StatelessWidget { final String label; @@ -8,7 +8,7 @@ class MentionChip extends StatelessWidget { @override Widget build(BuildContext context) => ActionChip( label: Text( - label.mention ?? label, + label.parseIdentifierIntoParts()?.primaryIdentifier ?? label, style: TextStyle( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onPrimary, @@ -19,7 +19,7 @@ class MentionChip extends StatelessWidget { context: context, builder: (_) => Dialog( child: Text("TODO: Open room or join room dialog, or user popover"), - ), + ), // TODO ), ); } diff --git a/lib/widgets/chat_page/member_list.dart b/lib/widgets/chat_page/member_list.dart index 5e1f3bf..8130de8 100644 --- a/lib/widgets/chat_page/member_list.dart +++ b/lib/widgets/chat_page/member_list.dart @@ -1,8 +1,10 @@ import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/controllers/avatar_controller.dart"; import "package:nexus/controllers/members_controller.dart"; import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/models/room.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; class MemberList extends ConsumerWidget { @@ -20,7 +22,7 @@ class MemberList extends ConsumerWidget { AppBar( scrolledUnderElevation: 0, leading: Icon(Icons.people), - title: Text("Members (${members.length})"), + title: Text("Members"), actionsPadding: EdgeInsets.only(right: 4), actions: [ if (Scaffold.of(context).hasEndDrawer) @@ -30,27 +32,32 @@ class MemberList extends ConsumerWidget { ), ], ), - ...members.map( - (member) => ListTile( - onTap: () => showDialog( - context: context, - builder: (context) => - Dialog(child: Text("TODO: Open member popover")), + ...members + .where( + (membership) => + membership.content["membership"] == + Membership.join.name, + ) + .map( + (member) => ListTile( + onTap: () {}, + leading: AvatarOrHash( + ref + .watch( + AvatarController.provider( + member.content["avatar_url"].toString(), + ), + ) + .whenOrNull(data: (data) => data), + member.content["displayname"].toString(), + headers: room.client.headers, + ), + title: Text( + member.content["displayname"].toString(), + overflow: TextOverflow.ellipsis, + ), + ), ), - leading: AvatarOrHash( - Uri.tryParse(member.content["avatar_url"] ?? ""), - member.content["displayname"].toString(), - ), - title: Text( - member.content["displayname"].toString(), - overflow: TextOverflow.ellipsis, - ), - subtitle: Text( - member.authorId, - overflow: TextOverflow.ellipsis, - ), - ), - ), ], ), ), diff --git a/lib/widgets/chat_page/mention_overlay.dart b/lib/widgets/chat_page/mention_overlay.dart index b2f2d9d..f6c7fe3 100644 --- a/lib/widgets/chat_page/mention_overlay.dart +++ b/lib/widgets/chat_page/mention_overlay.dart @@ -1,9 +1,11 @@ import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/controllers/avatar_controller.dart"; import "package:nexus/controllers/members_controller.dart"; import "package:nexus/controllers/rooms_controller.dart"; import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/models/room.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/loading.dart"; @@ -21,101 +23,108 @@ class MentionOverlay extends ConsumerWidget { }); @override - Widget build(BuildContext context, WidgetRef ref) { - final rooms = ref.watch(RoomsController.provider); - - return Padding( - padding: EdgeInsets.all(8), - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(12)), - child: Container( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - padding: EdgeInsets.all(8), - child: switch (triggerCharacter) { - "@" => - ref - .watch(MembersController.provider(room)) - .betterWhen( - data: (members) => ListView( - children: - (query.isEmpty - ? members - : members.where( - (member) => - member.authorId - .toLowerCase() - .contains(query.toLowerCase()) || - (member.content["displayname"] - as String?) - ?.toLowerCase() - .contains( - query.toLowerCase(), - ) == - true, - )) - .map( - (member) => ListTile( - leading: AvatarOrHash( - Uri.tryParse( - member.content["avatar_url"] ?? "", - ), - member.content["displayname"] ?? "", - ), - title: Text( - member.content["displayname"] as String? ?? - member.authorId, - ), - onTap: () => addTag( - id: "[@${member.content["displayname"]}](https://matrix.to/#/${member.authorId})", - name: member.authorId - .substring(1) - .split(":") - .first, - ), + Widget build(BuildContext context, WidgetRef ref) => Padding( + padding: EdgeInsets.all(8), + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(12)), + child: Container( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + padding: EdgeInsets.all(8), + child: switch (triggerCharacter) { + "@" => + ref + .watch(MembersController.provider(room)) + .betterWhen( + data: (members) => ListView( + children: + (query.isEmpty + ? members + : members.where( + (member) => + member.senderId.toLowerCase().contains( + query.toLowerCase(), + ) || + (member.content["displayname"] + as String?) + ?.toLowerCase() + .contains( + query.toLowerCase(), + ) == + true, + )) + .map( + (member) => ListTile( + leading: AvatarOrHash( + ref + .watch( + AvatarController.provider( + member.content["avatar_url"] + .toString(), + ), + ) + .whenOrNull(data: (data) => data), + member.content["displayname"].toString(), + headers: room.client.headers, ), - ) - .toList(), - ), + title: Text( + member.content["displayname"] as String? ?? + member.senderId, + ), + onTap: () => addTag( + id: member.senderId, + name: member.senderId + .substring(1) + .split(":") + .first, + ), + ), + ) + .toList(), ), - "#" => ListView( - children: - (query.isEmpty - ? rooms.values - : rooms.values.where( - (room) => (room.metadata?.name ?? "Unnamed Room") - .toLowerCase() - .contains(query.toLowerCase()), - )) - .map( - (room) => ListTile( - leading: AvatarOrHash( - room.metadata?.avatar, - room.metadata?.name ?? "Unnamed Room", - fallback: Icon(Icons.numbers), - ), - title: Text(room.metadata?.name ?? "Unnamed Room"), - subtitle: room.metadata?.topic == null - ? null - : Text(room.metadata!.topic!, maxLines: 1), - onTap: () => addTag( - id: "[#${room.metadata?.name ?? "Unnamed Room"}](https://matrix.to/#/${room.metadata?.id})", - name: - (room.metadata?.canonicalAlias ?? - room.metadata?.id) - ?.substring(1) - .split(":") - .first ?? - "", - ), - ), - ) - .toList(), - ), - - _ => Loading(), - }, - ), + ), + "#" => + ref + .watch(RoomsController.provider) + .betterWhen( + data: (rooms) => ListView( + children: + (query.isEmpty + ? rooms + : rooms.where( + (room) => room.title.toLowerCase().contains( + query.toLowerCase(), + ), + )) + .map( + (room) => ListTile( + leading: AvatarOrHash( + room.avatar, + room.title, + fallback: Icon(Icons.numbers), + headers: room.roomData.client.headers, + ), + title: Text(room.title), + subtitle: room.roomData.topic.isEmpty + ? null + : Text(room.roomData.topic, maxLines: 1), + onTap: () => addTag( + id: "[#${room.roomData.getLocalizedDisplayname()}](https://matrix.to/#/${room.roomData.id})", + name: + (room.roomData.canonicalAlias.isEmpty + ? room.roomData.id + : room.roomData.canonicalAlias) + .substring(1) + .split(":") + .first, + ), + ), + ) + .toList(), + ), + ), + _ => Loading(), + }, ), - ); - } + ), + ); } diff --git a/lib/widgets/chat_page/relation_preview.dart b/lib/widgets/chat_page/relation_preview.dart index 9918b35..07bac4e 100644 --- a/lib/widgets/chat_page/relation_preview.dart +++ b/lib/widgets/chat_page/relation_preview.dart @@ -1,16 +1,22 @@ import "package:flutter/material.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/controllers/avatar_controller.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; import "package:nexus/models/relation_type.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; class RelationPreview extends ConsumerWidget { final Message? relatedMessage; final RelationType relationType; final VoidCallback onDismiss; + final Room room; const RelationPreview({ required this.relatedMessage, required this.relationType, required this.onDismiss, + required this.room, super.key, }); @@ -31,18 +37,18 @@ class RelationPreview extends ConsumerWidget { "Editing message:", style: TextStyle(fontWeight: FontWeight.bold), ), - // AvatarOrHash( - // ref - // .watch( - // AvatarController.provider( - // relatedMessage!.metadata!["avatarUrl"], - // ), - // ) - // .whenOrNull(data: (data) => data), - // relatedMessage!.metadata!["displayName"].toString(), - // headers: room.client.headers, - // height: 16, - // ), + AvatarOrHash( + ref + .watch( + AvatarController.provider( + relatedMessage!.metadata!["avatarUrl"], + ), + ) + .whenOrNull(data: (data) => data), + relatedMessage!.metadata!["displayName"].toString(), + headers: room.client.headers, + height: 16, + ), Text( relatedMessage!.metadata?["displayName"] ?? relatedMessage!.authorId, diff --git a/lib/widgets/chat_page/room_appbar.dart b/lib/widgets/chat_page/room_appbar.dart index 21aa4ae..b36a3ad 100644 --- a/lib/widgets/chat_page/room_appbar.dart +++ b/lib/widgets/chat_page/room_appbar.dart @@ -1,13 +1,13 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; -import "package:nexus/models/room.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; +import "package:nexus/models/full_room.dart"; import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/chat_page/room_menu.dart"; class RoomAppbar extends StatelessWidget implements PreferredSizeWidget { final bool isDesktop; - final Room room; + final FullRoom room; final void Function(BuildContext context) onOpenMemberList; final void Function(BuildContext context) onOpenDrawer; const RoomAppbar( @@ -25,24 +25,21 @@ class RoomAppbar extends StatelessWidget implements PreferredSizeWidget { Widget build(BuildContext context) => Appbar( leading: isDesktop ? AvatarOrHash( - room.metadata?.avatar, - room.metadata?.name ?? "Unnamed Rooms", + room.avatar, + room.title, height: 24, fallback: Icon(Icons.numbers), + headers: room.roomData.client.headers, ) : DrawerButton(onPressed: () => onOpenDrawer(context)), scrolledUnderElevation: 0, title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - room.metadata?.name ?? "Unnamed Room", - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - if (room.metadata?.topic?.isNotEmpty == true) + Text(room.title, overflow: TextOverflow.ellipsis, maxLines: 1), + if (room.roomData.topic.isNotEmpty) Text( - room.metadata!.topic!, + room.roomData.topic, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.labelMedium?.copyWith( @@ -57,7 +54,7 @@ class RoomAppbar extends StatelessWidget implements PreferredSizeWidget { onPressed: () => onOpenMemberList(context), icon: Icon(Icons.people), ), - RoomMenu(room), - ].toIList(), + RoomMenu(room.roomData), + ], ); } diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart index 6adc013..0d57663 100644 --- a/lib/widgets/chat_page/room_chat.dart +++ b/lib/widgets/chat_page/room_chat.dart @@ -9,8 +9,6 @@ import "package:flyer_chat_image_message/flyer_chat_image_message.dart"; import "package:flyer_chat_system_message/flyer_chat_system_message.dart"; import "package:flyer_chat_text_message/flyer_chat_text_message.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/client_controller.dart"; -import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/controllers/cross_cache_controller.dart"; import "package:nexus/controllers/selected_room_controller.dart"; import "package:nexus/controllers/room_chat_controller.dart"; @@ -18,7 +16,6 @@ import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/get_headers.dart"; import "package:nexus/helpers/extensions/show_context_menu.dart"; import "package:nexus/models/relation_type.dart"; -import "package:nexus/models/requests/report_request.dart"; import "package:nexus/widgets/chat_page/chat_box.dart"; import "package:nexus/widgets/chat_page/html/html.dart"; import "package:nexus/widgets/chat_page/member_list.dart"; @@ -27,6 +24,7 @@ import "package:nexus/widgets/chat_page/top_widget.dart"; import "package:nexus/widgets/form_text_input.dart"; import "package:nexus/widgets/loading.dart"; // import "package:dynamic_polls/dynamic_polls.dart"; +// import "package:matrix/matrix.dart"; class RoomChat extends HookConsumerWidget { final bool isDesktop; @@ -39,70 +37,112 @@ class RoomChat extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final client = ref.watch(ClientController.provider.notifier); final replyToMessage = useState(null); final memberListOpened = useState(showMembersByDefault); final relationType = useState(RelationType.reply); - final room = ref.watch(SelectedRoomController.provider); - final userId = ref.watch(ClientStateController.provider)?.userId; - final theme = Theme.of(context); final danger = theme.colorScheme.error; - if (room == null || userId == null || room.metadata?.id == null) { - return Center( - child: Text( - "Nothing to see here...", - style: theme.textTheme.headlineMedium, - ), - ); - } + return ref + .watch(SelectedRoomController.provider) + .betterWhen( + data: (room) { + if (room == null) { + return Center( + child: Text( + "Nothing to see here...", + style: theme.textTheme.headlineMedium, + ), + ); + } + final controllerProvider = RoomChatController.provider( + room.roomData, + ); + final notifier = ref.watch(controllerProvider.notifier); - final controllerProvider = RoomChatController.provider(room.metadata!.id); - final notifier = ref.watch(controllerProvider.notifier); - - List getMessageOptions(Message message) { - final isSentByMe = message.authorId == userId; - return [ - PopupMenuItem( - onTap: () { - replyToMessage.value = message; - relationType.value = RelationType.reply; - }, - child: ListTile(leading: Icon(Icons.reply), title: Text("Reply")), - ), - if (message is TextMessage && isSentByMe) - PopupMenuItem( - onTap: () { - replyToMessage.value = message; - relationType.value = RelationType.edit; - }, - child: ListTile(leading: Icon(Icons.edit), title: Text("Edit")), - ), - if (isSentByMe) // TODO: Or if user has permission to redact others' messages - PopupMenuItem( - onTap: () => showDialog( - context: context, - builder: (context) => HookBuilder( - builder: (_) { - final deleteReasonController = useTextEditingController(); - return AlertDialog( - title: Text("Delete Message"), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Are you sure you want to delete this message? This can not be reversed.", - ), - SizedBox(height: 12), - FormTextInput( - required: false, - capitalize: true, - controller: deleteReasonController, - title: "Reason for deletion (optional)", - ), - ], + List getMessageOptions(Message message) => [ + PopupMenuItem( + onTap: () { + replyToMessage.value = message; + relationType.value = RelationType.reply; + }, + child: ListTile( + leading: Icon(Icons.reply), + title: Text("Reply"), + ), + ), + // Should check if is state event (has state_key), if so, don't show edit option + if (message is TextMessage && + message.authorId == room.roomData.client.userID) + PopupMenuItem( + onTap: () { + replyToMessage.value = message; + relationType.value = RelationType.edit; + }, + child: ListTile( + leading: Icon(Icons.edit), + title: Text("Edit"), + ), + ), + if (message.authorId == room.roomData.client.userID || + room.roomData.canRedact) + PopupMenuItem( + onTap: () => showDialog( + context: context, + builder: (context) => HookBuilder( + builder: (_) { + final deleteReasonController = + useTextEditingController(); + return AlertDialog( + title: Text("Delete Message"), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Are you sure you want to delete this message? This can not be reversed.", + ), + SizedBox(height: 12), + FormTextInput( + required: false, + capitalize: true, + controller: deleteReasonController, + title: "Reason for deletion (optional)", + ), + ], + ), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: Text("Cancel"), + ), + TextButton( + onPressed: () async { + notifier.deleteMessage( + message, + reason: deleteReasonController.text, + ); + Navigator.of(context).pop(); + }, + child: Text("Delete"), + ), + ], + ); + }, + ), + ), + child: ListTile( + leading: Icon(Icons.delete), + title: Text("Delete"), + ), + ), + PopupMenuItem( + onTap: () => showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text("Report"), + content: Text( + "Report this message to your server administrators, who can take action like banning that user or blocking that server from federating.", ), actions: [ TextButton( @@ -110,436 +150,418 @@ class RoomChat extends HookConsumerWidget { child: Text("Cancel"), ), TextButton( - onPressed: () async { - notifier.deleteMessage( - message, - reason: deleteReasonController.text, + onPressed: () { + room.roomData.client.reportEvent( + room.roomData.id, + message.id, ); Navigator.of(context).pop(); }, - child: Text("Delete"), - ), - ], - ); - }, - ), - ), - child: ListTile(leading: Icon(Icons.delete), title: Text("Delete")), - ), - PopupMenuItem( - onTap: () => showDialog( - context: context, - builder: (context) => HookBuilder( - builder: (_) { - final reasonController = useTextEditingController(); - return AlertDialog( - title: Text("Report"), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Report this event to your server administrators, who can take action like banning this server or room.", - ), - - SizedBox(height: 12), - FormTextInput( - required: false, - capitalize: true, - controller: reasonController, - title: "Reason for report (optional)", + child: Text("Report"), ), ], ), - actions: [ - TextButton( - onPressed: Navigator.of(context).pop, - child: Text("Cancel"), - ), - TextButton( - onPressed: () { - if (room.metadata == null) return; - client.reportEvent( - ReportRequest( - roomId: room.metadata!.id, - eventId: message.id, - reason: reasonController.text.isEmpty - ? null - : reasonController.text, - ), - ); - Navigator.of(context).pop(); - }, - child: Text("Report"), - ), - ], - ); - }, - ), - ), - child: ListTile( - leading: Icon(Icons.report, color: danger), - title: Text("Report", style: TextStyle(color: danger)), - ), - ), - ]; - } - - return Scaffold( - appBar: RoomAppbar( - room, - isDesktop: isDesktop, - onOpenDrawer: (_) => Scaffold.of(context).openDrawer(), - onOpenMemberList: (thisContext) { - memberListOpened.value = !memberListOpened.value; - Scaffold.of(thisContext).openEndDrawer(); - }, - ), - body: Row( - children: [ - Expanded( - child: Column( - children: [ - Expanded( - child: ref - .watch(controllerProvider) - .betterWhen( - data: (controller) => Chat( - currentUserId: userId, - theme: ChatTheme.fromThemeData(theme).copyWith( - colors: ChatColors.fromThemeData(theme).copyWith( - primary: theme.colorScheme.primaryContainer, - onPrimary: theme.colorScheme.onPrimaryContainer, - ), - ), - onMessageSecondaryTap: - ( - context, - message, { - required index, - TapUpDetails? details, - }) => details?.globalPosition == null - ? null - : context.showContextMenu( - globalPosition: details!.globalPosition, - children: getMessageOptions(message), - ), - onMessageLongPress: - ( - context, - message, { - required details, - required index, - }) => context.showContextMenu( - globalPosition: details.globalPosition, - children: getMessageOptions(message), - ), - onMessageTap: - ( - context, - message, { - required details, - required index, - }) { - if (message is ImageMessage) { - showDialog( - context: context, - builder: (_) => Dialog( - backgroundColor: Colors.transparent, - insetPadding: EdgeInsets.all(64), - child: InteractiveViewer( - child: Image( - image: CachedNetworkImage( - message.source, - ref.watch( - CrossCacheController.provider, - ), - headers: ref.headers, - ), - ), - ), - ), - ); - } - }, - builders: Builders( - loadMoreBuilder: (_) => Loading(), - chatAnimatedListBuilder: (_, itemBuilder) => - ChatAnimatedList( - itemBuilder: itemBuilder, - onEndReached: room.hasMore - ? notifier.loadOlder - : null, - onStartReached: () => client.markRead(room), - bottomPadding: 72, - ), - composerBuilder: (_) => ChatBox( - relationType: relationType.value, - relatedMessage: replyToMessage.value, - onDismiss: () => replyToMessage.value = null, - room: room, - ), - - // TODO: Polls - // customMessageBuilder: - // ( - // context, - // message, - // index, { - // required bool isSentByMe, - // MessageGroupStatus? groupStatus, - // }) { - // final poll = - // message.metadata?["poll"] - // as PollStartContent; - // final responses = - // (message.metadata?["responses"] - // as Map< - // String, - // Set - // >) - // .values - // .expand((set) => set) - // .fold({}, ( - // acc, - // value, - // ) { - // acc[value] = - // (acc[value] ?? 0) + 1; - // return acc; - // }); - - // return Column( - // crossAxisAlignment: - // CrossAxisAlignment.start, - // spacing: 4, - // children: [ - // TopWidget( - // message, - // headers: room - // .roomData - // .client - // .headers, - // groupStatus: groupStatus, - // ), - - // DynamicPolls( - // startDate: DateTime.now(), - // endDate: DateTime.now(), - // private: - // poll.kind == - // PollKind.undisclosed, - // allowReselection: true, - // backgroundDecoration: - // BoxDecoration( - // borderRadius: - // BorderRadius.all( - // Radius.circular(16), - // ), - // border: Border.all( - // color: theme - // .colorScheme - // .primaryContainer, - // width: 4, - // ), - // ), - // allStyle: Styles( - // titleStyle: TitleStyle( - // style: theme - // .textTheme - // .headlineSmall, - // ), - // optionStyle: OptionStyle( - // fillColor: theme - // .colorScheme - // .primaryContainer, - // selectedBorderColor: theme - // .colorScheme - // .primary, - // borderColor: theme - // .colorScheme - // .primary, - // unselectedBorderColor: - // Colors.transparent, - // textSelectColor: theme - // .colorScheme - // .primary, - // ), - // ), - // onOptionSelected: - // (int index) {}, - // title: poll.question.mText, - // options: poll.answers - // .map( - // (option) => option.mText, - // ) - // .toList(), - // ), - // ], - // ); - // }, - textMessageBuilder: - ( - context, - message, - index, { - required bool isSentByMe, - MessageGroupStatus? groupStatus, - }) => FlyerChatTextMessage( - customWidget: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Html( - (message.metadata?["formatted"] - as String) - .replaceAllMapped( - RegExp( - "(]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)", - caseSensitive: false, - ), - (m) { - // If it's already an tag, leave it unchanged - if (m.group(1) != null) { - return m.group(1)!; - } - - // Otherwise, wrap the bare URL - final url = m.group(2)!; - return "$url"; - }, - ) - .replaceAll("\n", "
"), - ), - if (message.editedAt != null) - Text( - "(edited)", - style: theme.textTheme.labelSmall, - ), - ], - ), - topWidget: TopWidget( - message, - groupStatus: groupStatus, - ), - message: message, - showTime: true, - index: index, - ), - linkPreviewBuilder: (_, message, isSentByMe) => - LinkPreview( - text: message.text, - backgroundColor: isSentByMe - ? theme.colorScheme.inversePrimary - : theme.colorScheme.surfaceContainerLow, - insidePadding: EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, - ), - linkPreviewData: message.linkPreviewData, - onLinkPreviewDataFetched: (linkPreviewData) => - notifier.updateMessage( - message, - message.copyWith( - linkPreviewData: linkPreviewData, - ), - ), - ), - imageMessageBuilder: - ( - _, - message, - index, { - required bool isSentByMe, - MessageGroupStatus? groupStatus, - }) => FlyerChatImageMessage( - topWidget: TopWidget( - message, - groupStatus: groupStatus, - alwaysShow: true, - ), - customImageProvider: CachedNetworkImage( - message.source, - ref.watch(CrossCacheController.provider), - headers: ref.headers, - ), - errorBuilder: (context, error, stackTrace) => - Center( - child: Text( - "Image Failed to Load", - style: TextStyle( - color: Theme.of( - context, - ).colorScheme.error, - ), - ), - ), - message: message, - index: index, - ), - fileMessageBuilder: - ( - _, - message, - index, { - required bool isSentByMe, - MessageGroupStatus? groupStatus, - }) => InkWell( - onTap: () => showDialog( - context: context, - builder: (_) => Dialog( - child: Text("TODO: Download Attachments"), - ), - ), - child: FlyerChatFileMessage( - topWidget: TopWidget( - message, - groupStatus: groupStatus, - ), - message: message, - index: index, - ), - ), - systemMessageBuilder: - ( - _, - message, - index, { - required bool isSentByMe, - MessageGroupStatus? groupStatus, - }) => FlyerChatSystemMessage( - message: message, - index: index, - ), - unsupportedMessageBuilder: - ( - _, - message, - index, { - required bool isSentByMe, - MessageGroupStatus? groupStatus, - }) => Text( - "${message.authorId} sent ${message.metadata?["eventType"]}", - style: theme.textTheme.labelSmall?.copyWith( - color: Colors.grey, - ), - ), - ), - resolveUser: notifier.resolveUser, - chatController: controller, - ), - ), ), - ], - ), - ), + child: ListTile( + leading: Icon(Icons.report, color: danger), + title: Text("Report", style: TextStyle(color: danger)), + ), + ), + ]; - if (memberListOpened.value == true && showMembersByDefault) - MemberList(room), - ], - ), + return Scaffold( + appBar: RoomAppbar( + room, + isDesktop: isDesktop, + onOpenDrawer: (_) => Scaffold.of(context).openDrawer(), + onOpenMemberList: (thisContext) { + memberListOpened.value = !memberListOpened.value; + Scaffold.of(thisContext).openEndDrawer(); + }, + ), + body: Row( + children: [ + Expanded( + child: Column( + children: [ + Expanded( + child: ref + .watch(controllerProvider) + .betterWhen( + data: (controller) => Chat( + currentUserId: room.roomData.client.userID!, + theme: ChatTheme.fromThemeData(theme) + .copyWith( + colors: ChatColors.fromThemeData(theme) + .copyWith( + primary: theme + .colorScheme + .primaryContainer, + onPrimary: theme + .colorScheme + .onPrimaryContainer, + ), + ), + onMessageSecondaryTap: + ( + context, + message, { + required index, + TapUpDetails? details, + }) => details?.globalPosition == null + ? null + : context.showContextMenu( + globalPosition: + details!.globalPosition, + children: getMessageOptions(message), + ), + onMessageLongPress: + ( + context, + message, { + required details, + required index, + }) => context.showContextMenu( + globalPosition: details.globalPosition, + children: getMessageOptions(message), + ), + onMessageTap: + ( + context, + message, { + required details, + required index, + }) { + if (message is ImageMessage) { + showDialog( + context: context, + builder: (_) => Dialog( + backgroundColor: + Colors.transparent, + insetPadding: EdgeInsets.all(64), + child: InteractiveViewer( + child: Image( + image: CachedNetworkImage( + message.source, + ref.watch( + CrossCacheController + .provider, + ), + headers: room + .roomData + .client + .headers, + ), + ), + ), + ), + ); + } + }, + builders: Builders( + loadMoreBuilder: (_) => Loading(), + chatAnimatedListBuilder: (_, itemBuilder) => + ChatAnimatedList( + itemBuilder: itemBuilder, + onEndReached: notifier.loadOlder, + onStartReached: notifier.markRead, + bottomPadding: 72, + ), + composerBuilder: (_) => ChatBox( + relationType: relationType.value, + relatedMessage: replyToMessage.value, + onDismiss: () => + replyToMessage.value = null, + room: room.roomData, + ), - endDrawer: showMembersByDefault ? null : MemberList(room), - ); + // customMessageBuilder: + // ( + // context, + // message, + // index, { + // required bool isSentByMe, + // MessageGroupStatus? groupStatus, + // }) { + // final poll = + // message.metadata?["poll"] + // as PollStartContent; + // final responses = + // (message.metadata?["responses"] + // as Map< + // String, + // Set + // >) + // .values + // .expand((set) => set) + // .fold({}, ( + // acc, + // value, + // ) { + // acc[value] = + // (acc[value] ?? 0) + 1; + // return acc; + // }); + + // return Column( + // crossAxisAlignment: + // CrossAxisAlignment.start, + // spacing: 4, + // children: [ + // TopWidget( + // message, + // headers: room + // .roomData + // .client + // .headers, + // groupStatus: groupStatus, + // ), + + // // TODO: Make this actually work + // DynamicPolls( + // startDate: DateTime.now(), + // endDate: DateTime.now(), + // private: + // poll.kind == + // PollKind.undisclosed, + // allowReselection: true, + // backgroundDecoration: + // BoxDecoration( + // borderRadius: + // BorderRadius.all( + // Radius.circular(16), + // ), + // border: Border.all( + // color: theme + // .colorScheme + // .primaryContainer, + // width: 4, + // ), + // ), + // allStyle: Styles( + // titleStyle: TitleStyle( + // style: theme + // .textTheme + // .headlineSmall, + // ), + // optionStyle: OptionStyle( + // fillColor: theme + // .colorScheme + // .primaryContainer, + // selectedBorderColor: theme + // .colorScheme + // .primary, + // borderColor: theme + // .colorScheme + // .primary, + // unselectedBorderColor: + // Colors.transparent, + // textSelectColor: theme + // .colorScheme + // .primary, + // ), + // ), + // onOptionSelected: + // (int index) {}, + // title: poll.question.mText, + // options: poll.answers + // .map( + // (option) => option.mText, + // ) + // .toList(), + // ), + // ], + // ); + // }, + textMessageBuilder: + ( + context, + message, + index, { + required bool isSentByMe, + MessageGroupStatus? groupStatus, + }) => FlyerChatTextMessage( + customWidget: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Html( + (message.metadata?["formatted"] + as String) + .replaceAllMapped( + RegExp( + "(]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)", + caseSensitive: false, + ), + (m) { + // If it's already an tag, leave it unchanged + if (m.group(1) != + null) { + return m.group(1)!; + } + + // Otherwise, wrap the bare URL + final url = m.group(2)!; + return "$url"; + }, + ) + .replaceAll("\n", "
"), + client: room.roomData.client, + ), + if (message.editedAt != null) + Text( + "(edited)", + style: theme + .textTheme + .labelSmall, + ), + ], + ), + topWidget: TopWidget( + message, + headers: + room.roomData.client.headers, + groupStatus: groupStatus, + ), + message: message, + showTime: true, + index: index, + ), + linkPreviewBuilder: + (_, message, isSentByMe) => LinkPreview( + text: message.text, + backgroundColor: isSentByMe + ? theme.colorScheme.inversePrimary + : theme + .colorScheme + .surfaceContainerLow, + insidePadding: EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + linkPreviewData: + message.linkPreviewData, + onLinkPreviewDataFetched: + (linkPreviewData) => + notifier.updateMessage( + message, + message.copyWith( + linkPreviewData: + linkPreviewData, + ), + ), + ), + imageMessageBuilder: + ( + _, + message, + index, { + required bool isSentByMe, + MessageGroupStatus? groupStatus, + }) => FlyerChatImageMessage( + topWidget: TopWidget( + message, + headers: + room.roomData.client.headers, + groupStatus: groupStatus, + alwaysShow: true, + ), + customImageProvider: + CachedNetworkImage( + message.source, + ref.watch( + CrossCacheController.provider, + ), + headers: room + .roomData + .client + .headers, + ), + errorBuilder: + (context, error, stackTrace) => + Center( + child: Text( + "Image Failed to Load", + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.error, + ), + ), + ), + message: message, + index: index, + ), + fileMessageBuilder: + ( + _, + message, + index, { + required bool isSentByMe, + MessageGroupStatus? groupStatus, + }) => InkWell( + onTap: () => showDialog( + context: context, + builder: (_) => Dialog( + child: Text( + "TODO: Download Attachments", // TODO + ), + ), + ), + child: FlyerChatFileMessage( + topWidget: TopWidget( + message, + headers: + room.roomData.client.headers, + groupStatus: groupStatus, + ), + message: message, + index: index, + ), + ), + systemMessageBuilder: + ( + _, + message, + index, { + required bool isSentByMe, + MessageGroupStatus? groupStatus, + }) => FlyerChatSystemMessage( + message: message, + index: index, + ), + unsupportedMessageBuilder: + ( + _, + message, + index, { + required bool isSentByMe, + MessageGroupStatus? groupStatus, + }) => Text( + "${message.authorId} sent ${message.metadata?["eventType"]}", + style: theme.textTheme.labelSmall + ?.copyWith(color: Colors.grey), + ), + ), + resolveUser: notifier.resolveUser, + chatController: controller, + ), + ), + ), + ], + ), + ), + + if (memberListOpened.value == true && showMembersByDefault) + MemberList(room.roomData), + ], + ), + + endDrawer: showMembersByDefault + ? null + : MemberList(room.roomData), + ); + }, + ); } } diff --git a/lib/widgets/chat_page/room_menu.dart b/lib/widgets/chat_page/room_menu.dart index f14b8ca..ea123fd 100644 --- a/lib/widgets/chat_page/room_menu.dart +++ b/lib/widgets/chat_page/room_menu.dart @@ -1,36 +1,38 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; import "package:flutter/services.dart"; import "package:flutter_hooks/flutter_hooks.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client_controller.dart"; -import "package:nexus/models/room.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/helpers/extensions/room_to_children.dart"; import "package:nexus/widgets/form_text_input.dart"; -class RoomMenu extends ConsumerWidget { +class RoomMenu extends StatelessWidget { final Room room; - final IList children; - const RoomMenu(this.room, {this.children = const IList.empty(), super.key}); + const RoomMenu(this.room, {super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final danger = Theme.of(context).colorScheme.error; - final client = ref.watch(ClientController.provider.notifier); + + void markRead(String roomId) async { + for (final child in await room.getAllChildren()) { + await child.roomData.setReadMarker( + child.roomData.lastEvent?.eventId, + mRead: child.roomData.lastEvent?.eventId, + ); + } + } 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); - await Future.wait(children.map((child) => client.markRead(child))); + final link = await room.matrixToInviteLink(); + await Clipboard.setData(ClipboardData(text: link.toString())); }, + child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")), + ), + PopupMenuItem( + onTap: () => markRead(room.id), child: ListTile( leading: Icon(Icons.check), title: Text("Mark as Read"), @@ -42,7 +44,7 @@ class RoomMenu extends ConsumerWidget { builder: (context) => AlertDialog( title: Text("Leave Room"), content: Text( - "Are you sure you want to leave \"${room.metadata?.name ?? "Unnamed Room"}\"?", + "Are you sure you want to leave \"${room.getLocalizedDisplayname()}\"?", ), actions: [ TextButton( @@ -52,13 +54,10 @@ class RoomMenu extends ConsumerWidget { TextButton( onPressed: () async { Navigator.of(context).pop(); - final snackbar = ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Leaving room..."), - duration: Duration(days: 1), - ), - ); - await client.leaveRoom(room); + final snackbar = ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text("Leaving room..."))); + await room.leave(); snackbar.close(); }, child: Text("Leave"), @@ -71,53 +70,53 @@ class RoomMenu extends ConsumerWidget { title: Text("Leave", style: TextStyle(color: danger)), ), ), - // PopupMenuItem( - // onTap: () => showDialog( - // context: context, - // builder: (context) => HookBuilder( - // builder: (_) { - // final reasonController = useTextEditingController(); - // return AlertDialog( - // title: Text("Report"), - // content: Column( - // mainAxisSize: MainAxisSize.min, - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Text( - // "Report this room to your server administrators, who can take action like banning this room.", - // ), + PopupMenuItem( + onTap: () => showDialog( + context: context, + builder: (context) => HookBuilder( + builder: (_) { + final reasonController = useTextEditingController(); + return AlertDialog( + title: Text("Report"), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Report this room to your server administrators, who can take action like banning this room.", + ), - // SizedBox(height: 12), - // FormTextInput( - // required: false, - // capitalize: true, - // controller: reasonController, - // title: "Reason for report (optional)", - // ), - // ], - // ), - // actions: [ - // TextButton( - // onPressed: Navigator.of(context).pop, - // child: Text("Cancel"), - // ), - // TextButton( - // onPressed: () { - // room.client.reportRoom(room.id, reasonController.text); - // Navigator.of(context).pop(); - // }, - // child: Text("Report"), - // ), - // ], - // ); - // }, - // ), - // ), - // child: ListTile( - // leading: Icon(Icons.report, color: danger), - // title: Text("Report", style: TextStyle(color: danger)), - // ), - // ), + SizedBox(height: 12), + FormTextInput( + required: false, + capitalize: true, + controller: reasonController, + title: "Reason for report (optional)", + ), + ], + ), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: Text("Cancel"), + ), + TextButton( + onPressed: () { + room.client.reportRoom(room.id, reasonController.text); + Navigator.of(context).pop(); + }, + child: Text("Report"), + ), + ], + ); + }, + ), + ), + child: ListTile( + leading: Icon(Icons.report, color: danger), + title: Text("Report", style: TextStyle(color: danger)), + ), + ), ], ); } diff --git a/lib/widgets/chat_page/sidebar.dart b/lib/widgets/chat_page/sidebar.dart index 341dd60..a5e1425 100644 --- a/lib/widgets/chat_page/sidebar.dart +++ b/lib/widgets/chat_page/sidebar.dart @@ -1,3 +1,4 @@ +import "package:collection/collection.dart"; import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; @@ -5,6 +6,8 @@ import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/key_controller.dart"; import "package:nexus/controllers/selected_space_controller.dart"; import "package:nexus/controllers/spaces_controller.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; import "package:nexus/helpers/extensions/join_room_with_snackbars.dart"; import "package:nexus/pages/settings_page.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; @@ -19,210 +22,228 @@ class Sidebar extends HookConsumerWidget { final selectedSpaceProvider = KeyController.provider( KeyController.spaceKey, ); - final selectedSpaceId = ref.watch(selectedSpaceProvider); - final selectedSpaceIdNotifier = ref.watch(selectedSpaceProvider.notifier); + final selectedSpace = ref.watch(selectedSpaceProvider); + final selectedSpaceNotifier = ref.watch(selectedSpaceProvider.notifier); final selectedRoomController = KeyController.provider( KeyController.roomKey, ); - final selectedRoomId = ref.watch(selectedRoomController); - final selectedRoomIdNotifier = ref.watch(selectedRoomController.notifier); - - final spaces = ref.watch(SpacesController.provider); - final indexOfSelected = spaces.indexWhere( - (space) => space.id == selectedSpaceId, - ); - final selectedIndex = indexOfSelected == -1 ? 0 : indexOfSelected; - - final selectedSpace = ref.watch(SelectedSpaceController.provider); - - final indexOfSelectedRoom = selectedSpace.children.indexWhere( - (room) => room.metadata?.id == selectedRoomId, - ); - final selectedRoomIndex = indexOfSelectedRoom == -1 - ? selectedSpace.children.isEmpty - ? null - : 0 - : indexOfSelectedRoom; + final selectedRoom = ref.watch(selectedRoomController); + final selectedRoomNotifier = ref.watch(selectedRoomController.notifier); return Drawer( shape: Border(), child: Row( children: [ - NavigationRail( - scrollable: true, - onDestinationSelected: (value) { - selectedSpaceIdNotifier.set(spaces[value].id); - selectedRoomIdNotifier.set( - spaces[value].children.firstOrNull?.metadata?.id, - ); - }, - destinations: spaces - .map( - (space) => NavigationRailDestination( - icon: AvatarOrHash( - space.room?.metadata?.avatar, - fallback: space.icon == null ? null : Icon(space.icon), - space.title, - hasBadge: space.children.any( - (room) => room.metadata?.unreadMessages != 0, - ), - badgeNumber: space.children.fold( - 0, - (previousValue, room) => - previousValue + - (room.metadata?.unreadNotifications ?? 0), - ), - ), - label: Text(space.title), - padding: EdgeInsets.only(top: 4), - ), - ) - .toList(), - selectedIndex: selectedIndex, - trailingAtBottom: true, - trailing: Padding( - padding: EdgeInsets.symmetric(vertical: 16), - child: Column( - spacing: 8, - children: [ - PopupMenuButton( - itemBuilder: (_) => [ - PopupMenuItem( - onTap: () => showDialog( - context: context, - builder: (alertContext) => HookBuilder( - builder: (_) { - final roomAlias = useTextEditingController(); - return AlertDialog( - title: Text("Join a Room"), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Enter the room alias, ID, or a Matrix.to link.", - ), - SizedBox(height: 12), - FormTextInput( - required: false, - capitalize: true, - controller: roomAlias, - title: "#room:server", - ), - ], - ), - actions: [ - TextButton( - onPressed: Navigator.of(context).pop, - child: Text("Cancel"), - ), - TextButton( - onPressed: () async { - Navigator.of(alertContext).pop(); + ref + .watch(SpacesController.provider) + .when( + loading: SizedBox.shrink, + error: (error, stack) { + debugPrintStack(label: error.toString(), stackTrace: stack); + throw error; + }, + data: (spaces) { + final indexOfSelected = spaces.indexWhere( + (space) => space.id == selectedSpace, + ); + final selectedIndex = indexOfSelected == -1 + ? 0 + : indexOfSelected; - final client = ref.watch( - ClientController.provider.notifier, - ); - if (context.mounted) { - client.joinRoomWithSnackBars( - context, - roomAlias.text, - ref, - ); - } - }, - child: Text("Join"), - ), - ], - ); - }, + return NavigationRail( + scrollable: true, + onDestinationSelected: (value) { + selectedSpaceNotifier.set(spaces[value].id); + selectedRoomNotifier.set( + spaces[value].children.firstOrNull?.roomData.id, + ); + }, + destinations: spaces + .map( + (space) => NavigationRailDestination( + icon: AvatarOrHash( + space.avatar, + fallback: space.icon == null + ? null + : Icon(space.icon), + space.title, + headers: space.client.headers, + hasBadge: + space.children.firstWhereOrNull( + (room) => room.roomData.hasNewMessages, + ) != + null, + ), + label: Text(space.title), + padding: EdgeInsets.only(top: 4), ), - ), - child: ListTile( - title: Text("Join an existing room (or space)"), - leading: Icon(Icons.numbers), - ), - ), - PopupMenuItem( - onTap: () {}, - child: ListTile( - title: Text("Create a new room"), - leading: Icon(Icons.add), - ), - ), - ], - icon: Icon(Icons.add), - ), - IconButton( - onPressed: () => showDialog( - context: context, - builder: (context) => AlertDialog(title: Text("To-do")), - ), - icon: Icon(Icons.explore), - ), - IconButton( - onPressed: () => Navigator.of( - context, - ).push(MaterialPageRoute(builder: (_) => SettingsPage())), - icon: Icon(Icons.settings), - ), - ], - ), - ), - ), - Expanded( - child: Scaffold( - backgroundColor: Colors.transparent, - appBar: AppBar( - leading: AvatarOrHash( - selectedSpace.room?.metadata?.avatar, - fallback: selectedSpace.icon == null - ? null - : Icon(selectedSpace.icon), + ) + .toList(), + selectedIndex: selectedIndex, + trailingAtBottom: true, + trailing: Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Column( + spacing: 8, + children: [ + PopupMenuButton( + itemBuilder: (_) => [ + PopupMenuItem( + onTap: () => showDialog( + context: context, + builder: (alertContext) => HookBuilder( + builder: (_) { + final roomAlias = + useTextEditingController(); + return AlertDialog( + title: Text("Join a Room"), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Enter the room alias, ID, or a Matrix.to link.", + ), + SizedBox(height: 12), + FormTextInput( + required: false, + capitalize: true, + controller: roomAlias, + title: "#room:server", + ), + ], + ), + actions: [ + TextButton( + onPressed: Navigator.of( + context, + ).pop, + child: Text("Cancel"), + ), + TextButton( + onPressed: () async { + Navigator.of(alertContext).pop(); - selectedSpace.title, - ), - title: Text( - selectedSpace.title, - overflow: TextOverflow.ellipsis, - ), - backgroundColor: Colors.transparent, - actions: [ - if (selectedSpace.room != null) - RoomMenu( - selectedSpace.room!, - children: selectedSpace.children, - ), - ], - ), - body: NavigationRail( - scrollable: true, - backgroundColor: Colors.transparent, - extended: true, - selectedIndex: selectedRoomIndex, - destinations: selectedSpace.children - .map( - (room) => NavigationRailDestination( - label: Text(room.metadata?.name ?? "Unnamed Room"), - icon: AvatarOrHash( - room.metadata?.avatar, - hasBadge: room.metadata?.unreadMessages != 0, - badgeNumber: room.metadata?.unreadNotifications ?? 0, - room.metadata?.name ?? "Unnamed Room", - fallback: selectedSpaceId == "dms" - ? null - : Icon(Icons.numbers), - // space.client.headers, - ), + final client = await ref.watch( + ClientController + .provider + .future, + ); + if (context.mounted) { + client.joinRoomWithSnackBars( + context, + roomAlias.text, + ref, + ); + } + }, + child: Text("Join"), + ), + ], + ); + }, + ), + ), + child: ListTile( + title: Text( + "Join an existing room (or space)", + ), + leading: Icon(Icons.numbers), + ), + ), + PopupMenuItem( + onTap: () {}, + child: ListTile( + title: Text("Create a new room"), + leading: Icon(Icons.add), + ), + ), + ], + icon: Icon(Icons.add), + ), + IconButton( + onPressed: () => showDialog( + context: context, + builder: (context) => + AlertDialog(title: Text("To-do")), + ), + icon: Icon(Icons.explore), + ), + IconButton( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => SettingsPage()), + ), + icon: Icon(Icons.settings), + ), + ], ), - ) - .toList(), - onDestinationSelected: (value) => selectedRoomIdNotifier.set( - selectedSpace.children[value].metadata?.id, - ), + ), + ); + }, ), - ), + Expanded( + child: ref + .watch(SelectedSpaceController.provider) + .betterWhen( + data: (space) { + final indexOfSelected = space.children.indexWhere( + (room) => room.roomData.id == selectedRoom, + ); + final selectedIndex = indexOfSelected == -1 + ? space.children.isEmpty + ? null + : 0 + : indexOfSelected; + + return Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + leading: AvatarOrHash( + space.avatar, + fallback: space.icon == null + ? null + : Icon(space.icon), + space.title, + headers: space.client.headers, + ), + title: Text( + space.title, + overflow: TextOverflow.ellipsis, + ), + backgroundColor: Colors.transparent, + actions: [ + if (space.roomData != null) RoomMenu(space.roomData!), + ], + ), + body: NavigationRail( + scrollable: true, + backgroundColor: Colors.transparent, + extended: true, + selectedIndex: selectedIndex, + destinations: space.children + .map( + (room) => NavigationRailDestination( + label: Text(room.title), + icon: AvatarOrHash( + hasBadge: room.roomData.hasNewMessages, + room.avatar, + room.title, + fallback: selectedSpace == "dms" + ? null + : Icon(Icons.numbers), + headers: space.client.headers, + ), + ), + ) + .toList(), + onDestinationSelected: (value) => selectedRoomNotifier + .set(space.children[value].roomData.id), + ), + ); + }, + ), ), ], ), diff --git a/lib/widgets/chat_page/top_widget.dart b/lib/widgets/chat_page/top_widget.dart index cfba6fc..7831cb9 100644 --- a/lib/widgets/chat_page/top_widget.dart +++ b/lib/widgets/chat_page/top_widget.dart @@ -1,16 +1,18 @@ import "dart:math"; import "package:flutter/material.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:flutter_chat_ui/flutter_chat_ui.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/chat_page/html/quoted.dart"; class TopWidget extends ConsumerWidget { final Message message; final bool alwaysShow; + final Map headers; final MessageGroupStatus? groupStatus; const TopWidget( this.message, { + required this.headers, required this.groupStatus, this.alwaysShow = false, super.key, @@ -60,10 +62,10 @@ class TopWidget extends ConsumerWidget { mainAxisSize: MainAxisSize.min, spacing: 8, children: [ - AvatarOrHash( - Uri.tryParse(replyMessage.metadata?["avatarUrl"] ?? ""), - replyMessage.metadata?["displayName"] ?? "", - height: 16, + Avatar( + userId: replyMessage.authorId, + headers: headers, + size: 16, ), Flexible( child: Text( @@ -102,10 +104,7 @@ class TopWidget extends ConsumerWidget { mainAxisSize: MainAxisSize.min, spacing: 8, children: [ - AvatarOrHash( - Uri.parse(message.metadata?["avatarUrl"] ?? ""), - message.metadata?["displayName"] ?? "", - ), + Avatar(userId: message.authorId, headers: headers), Flexible( child: Text( message.metadata?["displayName"] ?? message.authorId, diff --git a/lib/widgets/form_text_input.dart b/lib/widgets/form_text_input.dart index 21b2e5c..492439b 100644 --- a/lib/widgets/form_text_input.dart +++ b/lib/widgets/form_text_input.dart @@ -20,13 +20,11 @@ class FormTextInput extends StatelessWidget { final Widget? trailing; final InputBorder? border; final List? formatters; - final bool autofocus; const FormTextInput({ super.key, this.border, this.controller, - this.autofocus = false, this.title, this.obscure = false, this.readOnly = false, @@ -47,7 +45,6 @@ class FormTextInput extends StatelessWidget { @override Widget build(BuildContext context) => TextFormField( - autofocus: autofocus, controller: controller, keyboardType: keyboardType, readOnly: readOnly, diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 8b658f4..1cac43c 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -13,6 +13,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_vodozemac ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/pubspec.lock b/pubspec.lock index 222e779..01d55a5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -73,6 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + base58check: + dependency: transitive + description: + name: base58check + sha256: "6c300dfc33e598d2fe26319e13f6243fea81eaf8204cb4c6b69ef20a625319a5" + url: "https://pub.dev" + source: hosted + version: "2.0.0" blurhash_dart: dependency: transitive description: @@ -97,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.3" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95 + url: "https://pub.dev" + source: hosted + version: "2.1.1" build_config: dependency: transitive description: @@ -137,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.12.1" + canonical_json: + dependency: transitive + description: + name: canonical_json + sha256: d6be1dd66b420c6ac9f42e3693e09edf4ff6edfee26cb4c28c1c019fdb8c0c15 + url: "https://pub.dev" + source: hosted + version: "1.1.2" characters: dependency: transitive description: @@ -193,14 +217,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" - code_assets: - dependency: "direct main" - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" code_builder: dependency: transitive description: @@ -378,21 +394,13 @@ packages: source: hosted version: "11.1.0" ffi: - dependency: "direct main" + dependency: transitive description: name: ffi sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c url: "https://pub.dev" source: hosted version: "2.1.5" - ffigen: - dependency: "direct main" - description: - name: ffigen - sha256: b7803707faeec4ce3c1b0c2274906504b796e3b70ad573577e72333bd1c9b3ba - url: "https://pub.dev" - source: hosted - version: "20.1.1" file: dependency: transitive description: @@ -533,6 +541,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + flutter_rust_bridge: + dependency: transitive + description: + name: flutter_rust_bridge + sha256: "37ef40bc6f863652e865f0b2563ea07f0d3c58d8efad803cc01933a4b2ee067e" + url: "https://pub.dev" + source: hosted + version: "2.11.1" flutter_secure_storage: dependency: "direct main" description: @@ -594,6 +610,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_vodozemac: + dependency: "direct main" + description: + name: flutter_vodozemac + sha256: "16d4b44dd338689441fe42a80d0184e5c864e9563823de9e7e6371620d2c0590" + url: "https://pub.dev" + source: hosted + version: "0.4.1" flutter_web_plugins: dependency: transitive description: flutter @@ -704,14 +728,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" - hooks: - dependency: "direct main" - description: - name: hooks - sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7" - url: "https://pub.dev" - source: hosted - version: "1.0.0" hooks_riverpod: dependency: "direct main" description: @@ -728,6 +744,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.6" + html_unescape: + dependency: transitive + description: + name: html_unescape + sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3" + url: "https://pub.dev" + source: hosted + version: "2.0.0" http: dependency: transitive description: @@ -912,6 +936,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + markdown: + dependency: transitive + description: + name: markdown + sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" + url: "https://pub.dev" + source: hosted + version: "7.3.0" matcher: dependency: transitive description: @@ -928,6 +960,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.11.1" + matrix: + dependency: "direct main" + description: + name: matrix + sha256: fb116ee89f6871441f22f76a988db15cfcfb6dfac97e3e2d654c240080015707 + url: "https://pub.dev" + source: hosted + version: "4.1.0" mention_tag_text_field: dependency: "direct main" description: @@ -1120,14 +1160,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - quiver: + random_string: dependency: transitive description: - name: quiver - sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + name: random_string + sha256: "03b52435aae8cbdd1056cf91bfc5bf845e9706724dd35ae2e99fa14a1ef79d02" url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "2.3.1" riverpod: dependency: transitive description: @@ -1208,6 +1248,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.26.3" + sdp_transform: + dependency: transitive + description: + name: sdp_transform + sha256: "73e412a5279a5c2de74001535208e20fff88f225c9a4571af0f7146202755e45" + url: "https://pub.dev" + source: hosted + version: "0.3.2" sembast: dependency: transitive description: @@ -1309,6 +1357,14 @@ packages: description: flutter source: sdk version: "0.0.0" + slugify: + dependency: transitive + description: + name: slugify + sha256: b272501565cb28050cac2d96b7bf28a2d24c8dae359280361d124f3093d337c3 + url: "https://pub.dev" + source: hosted + version: "2.0.0" source_gen: dependency: "direct overridden" description: @@ -1349,6 +1405,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_common_ffi: + dependency: "direct main" + description: + name: sqflite_common_ffi + sha256: "8d7b8749a516cbf6e9057f9b480b716ad14fc4f3d3873ca6938919cc626d9025" + url: "https://pub.dev" + source: hosted + version: "2.3.7+1" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2" + url: "https://pub.dev" + source: hosted + version: "2.9.4" stack_trace: dependency: transitive description: @@ -1469,6 +1549,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + unorm_dart: + dependency: transitive + description: + name: unorm_dart + sha256: "5b35bff83fce4d76467641438f9e867dc9bcfdb8c1694854f230579d68cd8f4b" + url: "https://pub.dev" + source: hosted + version: "0.2.0" url_launcher: dependency: "direct main" description: @@ -1581,6 +1669,15 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + vodozemac: + dependency: "direct main" + description: + path: dart + ref: "krille/use-specced-olm-session-config" + resolved-ref: "8770e0555b1bb692e3e1a43a7726b27eae285b20" + url: "https://github.com/famedly/dart-vodozemac" + source: git + version: "0.4.0" watcher: dependency: transitive description: @@ -1621,6 +1718,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + webrtc_interface: + dependency: transitive + description: + name: webrtc_interface + sha256: "2e604a31703ad26781782fb14fa8a4ee621154ee2c513d2b9938e486fa695233" + url: "https://pub.dev" + source: hosted + version: "1.3.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7893653..e733f2e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,11 @@ environment: sdk: "^3.9.2" dependency_overrides: + vodozemac: + git: + url: https://github.com/famedly/dart-vodozemac + ref: krille/use-specced-olm-session-config + path: dart analyzer: ^8.4.0 source_gen: ^4.0.2 flutter_hooks: ^0.21.2 @@ -54,10 +59,14 @@ dependencies: git: url: https://github.com/Henry-Hiles/flutter_chat_ui path: packages/flutter_link_previewer + matrix: ^4.1.0 + sqflite_common_ffi: ^2.3.6 color_hash: ^1.0.1 + flutter_vodozemac: ^0.4.1 flutter_widget_from_html_core: ^0.17.0 flutter_svg: ^2.2.2 json_annotation: ^4.9.0 + vodozemac: ^0.4.0 shared_preferences: ^2.5.3 mention_tag_text_field: ^0.0.9 fluttertagger: ^2.3.1 @@ -65,10 +74,6 @@ dependencies: dynamic_polls: ^0.0.6 flutter_hooks: ^0.21.3+1 cross_cache: ^1.1.0 - ffi: ^2.1.5 - hooks: ^1.0.0 - code_assets: ^1.0.0 - ffigen: ^20.1.1 dev_dependencies: build_runner: ^2.4.11 diff --git a/scripts/generate.dart b/scripts/generate.dart deleted file mode 100644 index 9806603..0000000 --- a/scripts/generate.dart +++ /dev/null @@ -1,39 +0,0 @@ -import "dart:io"; -import "package:ffigen/ffigen.dart"; -import "package:path/path.dart"; - -void main(List args) async { - final repoDir = Directory.fromUri( - Platform.script.resolve("../src/gomuks/source"), - ); - if (await repoDir.exists()) await repoDir.delete(recursive: true); - await repoDir.create(recursive: true); - - print("Cloning Gomuks repository..."); - final cloneResult = await Process.run("git", [ - "clone", - "--depth", - "1", - "https://mau.dev/gomuks/gomuks", - repoDir.path, - ]); - - if (cloneResult.exitCode != 0) { - throw Exception( - "Failed to clone Gomuks repository: \n${cloneResult.stderr}", - ); - } - - print("Generating FFI Bindings..."); - FfiGenerator( - output: Output( - dartFile: Platform.script.resolve("../lib/src/third_party/gomuks.g.dart"), - ), - headers: Headers( - entryPoints: [File(join(repoDir.path, "pkg", "ffi", "gomuksffi.h")).uri], - compilerOptions: ["--no-warnings"], - ), - functions: Functions.includeAll, - ).generate(); - print("Done!"); -} diff --git a/scripts/generate.sh b/scripts/generate.sh deleted file mode 100755 index faafd29..0000000 --- a/scripts/generate.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -pushd "$(dirname "$(readlink -f "$0")")"/.. || exit - -mkdir -p build -touch build/lock -dart scripts/generate.dart -rm build/lock - -popd || exit \ No newline at end of file diff --git a/src/gomuks/libgomuks.h b/src/gomuks/libgomuks.h deleted file mode 100644 index 962d281..0000000 --- a/src/gomuks/libgomuks.h +++ /dev/null @@ -1,105 +0,0 @@ -/* Code generated by cmd/cgo; DO NOT EDIT. */ - -/* package go.mau.fi/gomuks/pkg/ffi */ - - -#line 1 "cgo-builtin-export-prolog" - -#include - -#ifndef GO_CGO_EXPORT_PROLOGUE_H -#define GO_CGO_EXPORT_PROLOGUE_H - -#ifndef GO_CGO_GOSTRING_TYPEDEF -typedef struct { const char *p; ptrdiff_t n; } _GoString_; -extern size_t _GoStringLen(_GoString_ s); -extern const char *_GoStringPtr(_GoString_ s); -#endif - -#endif - -/* Start of preamble from import "C" comments. */ - - -#line 9 "ffi.go" - -#include "gomuksffi.h" -#include - -static inline void _gomuks_callEventCallback(EventCallback cb, const char *command, int64_t request_id, GomuksOwnedBuffer data) { - cb(command, request_id, data); -} - -#line 1 "cgo-generated-wrapper" - - -/* End of preamble from import "C" comments. */ - - -/* Start of boilerplate cgo prologue. */ -#line 1 "cgo-gcc-export-header-prolog" - -#ifndef GO_CGO_PROLOGUE_H -#define GO_CGO_PROLOGUE_H - -typedef signed char GoInt8; -typedef unsigned char GoUint8; -typedef short GoInt16; -typedef unsigned short GoUint16; -typedef int GoInt32; -typedef unsigned int GoUint32; -typedef long long GoInt64; -typedef unsigned long long GoUint64; -typedef GoInt64 GoInt; -typedef GoUint64 GoUint; -typedef size_t GoUintptr; -typedef float GoFloat32; -typedef double GoFloat64; -#ifdef _MSC_VER -#if !defined(__cplusplus) || _MSVC_LANG <= 201402L -#include -typedef _Fcomplex GoComplex64; -typedef _Dcomplex GoComplex128; -#else -#include -typedef std::complex GoComplex64; -typedef std::complex GoComplex128; -#endif -#else -typedef float _Complex GoComplex64; -typedef double _Complex GoComplex128; -#endif - -/* - static assertion to make sure the file is being used on architecture - at least with matching size of GoInt. -*/ -typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1]; - -#ifndef GO_CGO_GOSTRING_TYPEDEF -typedef _GoString_ GoString; -#endif -typedef void *GoMap; -typedef void *GoChan; -typedef struct { void *t; void *v; } GoInterface; -typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; - -#endif - -/* End of boilerplate cgo prologue. */ - -#ifdef __cplusplus -extern "C" { -#endif - -extern GomuksHandle GomuksInit(void); -extern int GomuksStart(GomuksHandle handle, EventCallback callback); -extern void GomuksDestroy(GomuksHandle handle); -extern GomuksResponse GomuksSubmitCommand(GomuksHandle handle, char* command, GomuksBorrowedBuffer data); -extern GomuksAccountInfo GomuksGetAccountInfo(GomuksHandle handle); -extern void GomuksFreeAccountInfo(GomuksAccountInfo info); -extern void GomuksFreeBuffer(GomuksOwnedBuffer buf); - -#ifdef __cplusplus -} -#endif diff --git a/src/gomuks/libgomuks.so b/src/gomuks/libgomuks.so deleted file mode 100644 index c4987c5..0000000 Binary files a/src/gomuks/libgomuks.so and /dev/null differ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 8967b80..3c6fdca 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -13,6 +13,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_vodozemac ) set(PLUGIN_BUNDLED_LIBRARIES)