diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 0000000..c8099d1 --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,46 @@ +name: "Build Windows Version" + +on: + workflow_dispatch: + +jobs: + build-windows: + runs-on: "windows-latest" + + steps: + - name: "Checkout repository" + uses: "actions/checkout@v4" + + - name: "Set up Flutter" + uses: "subosito/flutter-action@v2" + + - name: "Set up Rust" + uses: "dtolnay/rust-toolchain@stable" + with: + targets: "x86_64-pc-windows-msvc" + + - name: "Install Flutter dependencies" + run: flutter pub get + + - name: "Run build_runner & build Windows EXE" + run: | + flutter pub run build_runner build --delete-conflicting-outputs + flutter build windows --release + + - name: "Upload exe zip" + uses: "actions/upload-artifact@v4" + with: + name: "windows-portable" + path: "build/windows/x64/runner/Release/" + + - name: "Install Inno Setup" + run: choco install innosetup -y + + - name: "Build Inno Setup installer" + run: iscc windows/installer.iss + + - name: "Upload installer artifact" + uses: "actions/upload-artifact@v4" + with: + name: "windows-installer" + path: "windows/dist/Nexus-Setup.exe" diff --git a/.vscode/settings.json b/.vscode/settings.json index 30f4254..25ea52b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,9 @@ { - "cSpell.words": ["Appbar", "Displayname", "prefs"] + "cSpell.words": [ + "Appbar", + "Displayname", + "Homeserver", + "prefs", + "vodozemac" + ] } diff --git a/README.md b/README.md index 670e20e..f67c963 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S - [ ] Platform Support - [x] Linux - - [x] Windows (untested, if you are interested in helping to test, open an issue) + - [x] Windows - [ ] MacOS - [ ] Android - [ ] iOS @@ -28,7 +28,7 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S - [x] Rooms / Spaces - [x] Displaying and choosing - [x] Reading, showing unread - - [ ] Mark as read button on rooms and spaces + - [x] Mark as read button on rooms and spaces - [ ] Searching - [ ] Creating (Rooms, Spaces, and DMs) - [ ] Joining @@ -38,6 +38,8 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S - [x] Leaving - [x] Subspaces - [x] Messages + - [x] Encryption + - [ ] Restoring crypto identity from passphrase/key or verification - [x] Sending - [x] Plain text - [x] HTML/Markdown @@ -48,7 +50,6 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S - [x] Rooms - [ ] Custom emojis/stickers - [ ] GIFs, maybe through Tenor or something - - [ ] Encrypted messages - [x] Recieving - [x] Plain text - [x] HTML @@ -57,8 +58,10 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S - [ ] Jump to original message - [x] Edits - [x] Attachments + - [x] Blurhashing - [ ] Downloading attachments - [ ] Opening attachments in their own view + - [ ] Polls - [x] Mentions - [x] Users - [x] Rooms @@ -66,11 +69,10 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S - [x] Matrix URIs - [x] Matrix.to links - [x] Custom emojis/stickers - - [ ] Encrypted messages - [x] History loading - [x] Backwards - [ ] Forwards - - [ ] Editing + - [x] Editing - [x] Deleting - [ ] Reactions: Waiting on https://github.com/flyerhq/flutter_chat_ui/pull/838 - [ ] Pins @@ -87,6 +89,7 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S - [ ] Spam filtering - [ ] Settings - [ ] Light/Dark mode + - [ ] Show media by default - [ ] Dynamic Theming - [ ] Devices - [ ] Viewing devices @@ -103,18 +106,43 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S ## Development -Fork and clone the project, then: +First, clone and open the repo: + +```sh +git clone https://git.federated.nexus/Henry-Hiles/nexus +cd nexus +``` + +### Prerequisites + +#### Linux - With Nix: Either use direnv, or `nix flake develop` - Without Nix: Install Flutter, Rust, the libsecret dev package for your distro (must be in `PKG_CONFIG_PATH`), and sqlite (must be in `LD_LIBRARY_PATH`). +#### Windows / MacOS + +I don't really know. You will need Flutter and Rust, and otherwise I guess just keep installing stuff until there aren't any errors. + +### + +Get dependencies: + +```sh +flutter pub get +``` + Build generated files, and watch for new changes: ```sh flutter pub run build_runner watch --delete-conflicting-outputs ``` -Run `flutter run` to run the app. +Run the app: + +```sh +flutter run +``` ## Community diff --git a/flake.lock b/flake.lock index b627cd3..7826732 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1759362264, - "narHash": "sha256-wfG0S7pltlYyZTM+qqlhJ7GMw2fTF4mLKCIVhLii/4M=", + "lastModified": 1767609335, + "narHash": "sha256-feveD98mQpptwrAEggBQKJTYbvwwglSbOv53uCfH9PY=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "758cf7296bee11f1706a574c77d072b8a7baa881", + "rev": "250481aafeb741edfe23d29195671c19b36b6dca", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1759381078, - "narHash": "sha256-gTrEEp5gEspIcCOx9PD8kMaF1iEmfBcTbO0Jag2QhQs=", + "lastModified": 1767640445, + "narHash": "sha256-UWYqmD7JFBEDBHWYcqE6s6c77pWdcU/i+bwD6XxMb8A=", "owner": "nixos", "repo": "nixpkgs", - "rev": "7df7ff7d8e00218376575f0acdcc5d66741351ee", + "rev": "9f0c42f8bc7151b8e7e5840fb3bd454ad850d8c5", "type": "github" }, "original": { @@ -36,11 +36,11 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1754788789, - "narHash": "sha256-x2rJ+Ovzq0sCMpgfgGaaqgBSwY+LST+WbZ6TytnT9Rk=", + "lastModified": 1765674936, + "narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "a73b9c743612e4244d865a2fdee11865283c04e6", + "rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85", "type": "github" }, "original": { diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart index 1a88526..9f69e8f 100644 --- a/lib/controllers/client_controller.dart +++ b/lib/controllers/client_controller.dart @@ -1,8 +1,10 @@ import "dart:convert"; import "dart:io"; import "package:flutter/foundation.dart"; +import "package:matrix/encryption.dart"; import "package:nexus/controllers/database_controller.dart"; -import "package:flutter_vodozemac/flutter_vodozemac.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"; @@ -13,23 +15,27 @@ class ClientController extends AsyncNotifier { bool updateShouldNotify( AsyncValue previous, AsyncValue next, - ) => previous.hasValue != next.hasValue; + ) => + previous.hasValue != next.hasValue || + previous.value?.accessToken != next.value?.accessToken; static const sessionBackupKey = "sessionBackup"; @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: init, + vodozemacInit: fl_vod.init, ), ); diff --git a/lib/controllers/events_controller.dart b/lib/controllers/events_controller.dart index 37b9ff2..e7a192d 100644 --- a/lib/controllers/events_controller.dart +++ b/lib/controllers/events_controller.dart @@ -1,30 +1,18 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:matrix/matrix.dart"; -import "package:nexus/controllers/from_controller.dart"; -class EventsController extends AsyncNotifier { +class EventsController extends AsyncNotifier { EventsController(this.room); final Room room; @override - Future build({String? from}) async { - final response = await room.client.getRoomEvents( - room.id, - Direction.b, - from: from, - limit: 32, - ); - if (ref.mounted) { - ref.watch(FromController.provider(room).notifier).set(response.end); - } - return response; + Future build({String? from}) => room.getTimeline(); + + Future prev() async { + final timeline = await future; + await timeline.requestHistory(); } - Future prev() async => - build(from: ref.read(FromController.provider(room))); - static final provider = AsyncNotifierProvider.autoDispose - .family( - EventsController.new, - ); + .family(EventsController.new); } diff --git a/lib/controllers/from_controller.dart b/lib/controllers/from_controller.dart deleted file mode 100644 index 54c850a..0000000 --- a/lib/controllers/from_controller.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:matrix/matrix.dart"; - -class FromController extends Notifier { - FromController(_); - @override - String? build() => null; - - void set(String? value) => state = value; - - static final provider = - NotifierProvider.family( - FromController.new, - ); -} diff --git a/lib/controllers/members_controller.dart b/lib/controllers/members_controller.dart index fae5433..df15c1c 100644 --- a/lib/controllers/members_controller.dart +++ b/lib/controllers/members_controller.dart @@ -15,8 +15,8 @@ class MembersController extends AsyncNotifier> { [], ); - static final provider = - AsyncNotifierProvider.family, Room>( + static final provider = AsyncNotifierProvider.family + .autoDispose, Room>( MembersController.new, ); } diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index 1a1be39..e9bd7ba 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -40,7 +40,16 @@ class RoomChatController extends AsyncNotifier { if (oldMessage == null || message == null) return; return await updateMessage( oldMessage, - message.copyWith(id: oldMessage.id), + message.copyWith( + id: oldMessage.id, + replyToMessageId: oldMessage.replyToMessageId, + metadata: { + ...(oldMessage.metadata ?? {}), + ...((message.metadata ?? {}).filterMap( + (key, value) => value == null ? null : MapEntry(key, value), + )), + }, + ), ); } if (message != null) { @@ -51,7 +60,7 @@ class RoomChatController extends AsyncNotifier { ); return InMemoryChatController( - messages: await response.chunk.toMessages(room), + messages: await response.events.toMessages(room), ); } @@ -76,14 +85,22 @@ class RoomChatController extends AsyncNotifier { } Future loadOlder() async { + final currentEvents = await future; + await ref.watch(EventsController.provider(room).notifier).prev(); + final newEvents = await ref.watch(EventsController.provider(room).future); + final controller = await future; - final response = await ref - .watch(EventsController.provider(room).notifier) - .prev(); - - final messages = await response.chunk.toMessages(room); - - await controller.insertAllMessages(messages, index: 0); + await controller.insertAllMessages( + await newEvents.events + .where( + (event) => !currentEvents.messages.any( + (existingEvent) => existingEvent.id == event.eventId, + ), + ) + .toList() + .toMessages(room), + index: 0, + ); ref.notifyListeners(); } diff --git a/lib/controllers/secure_storage_controller.dart b/lib/controllers/secure_storage_controller.dart index 8a579f5..4a5781b 100644 --- a/lib/controllers/secure_storage_controller.dart +++ b/lib/controllers/secure_storage_controller.dart @@ -1,26 +1,19 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:simple_secure_storage/simple_secure_storage.dart"; +import "package:flutter_secure_storage/flutter_secure_storage.dart"; -class SecureStorageController extends AsyncNotifier { +class SecureStorageController extends Notifier { @override - Future build() => SimpleSecureStorage.initialize(); + FlutterSecureStorage build() => FlutterSecureStorage(); - Future get(String key) async { - await future; - return SimpleSecureStorage.read(key); - } + Future get(String key) => state.read(key: key); - Future set(String key, String value) async { - await future; - return SimpleSecureStorage.write(key, value); - } + Future set(String key, String value) => + state.write(key: key, value: value); - Future clear() async { - await future; - return SimpleSecureStorage.clear(); - } + Future clear() => state.deleteAll(); - static final provider = AsyncNotifierProvider( - SecureStorageController.new, - ); + static final provider = + NotifierProvider( + SecureStorageController.new, + ); } diff --git a/lib/controllers/spaces_controller.dart b/lib/controllers/spaces_controller.dart index 3501de6..408dc00 100644 --- a/lib/controllers/spaces_controller.dart +++ b/lib/controllers/spaces_controller.dart @@ -64,7 +64,7 @@ class SpacesController extends AsyncNotifier> { avatar: space.avatar, id: space.roomData.id, roomData: space.roomData, - children: IList(await space.roomData.getAllChildren(client)), + children: IList(await space.roomData.getAllChildren()), ), ), )), diff --git a/lib/helpers/extensions/event_to_message.dart b/lib/helpers/extensions/event_to_message.dart index c003180..2481388 100644 --- a/lib/helpers/extensions/event_to_message.dart +++ b/lib/helpers/extensions/event_to_message.dart @@ -1,5 +1,4 @@ import "package:collection/collection.dart"; -import "package:flutter/foundation.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:matrix/matrix.dart"; @@ -9,7 +8,6 @@ extension EventToMessage on Event { 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); @@ -82,6 +80,7 @@ extension EventToMessage on Event { source: (await getAttachmentUri()).toString(), replyToMessageId: replyId, deliveredAt: originServerTs, + blurhash: (event.content["info"] as Map?)?["xyz.amorgan.blurhash"], ), MessageTypes.Audio => Message.audio( metadata: metadata, @@ -121,7 +120,9 @@ extension EventToMessage on Event { ), EventTypes.Redaction => null, _ => - kDebugMode + // Turn this on for debugging purposes + false + // ignore: dead_code ? Message.unsupported( metadata: metadata, id: eventId, diff --git a/lib/helpers/extensions/room_to_children.dart b/lib/helpers/extensions/room_to_children.dart index afdc99e..d115f9a 100644 --- a/lib/helpers/extensions/room_to_children.dart +++ b/lib/helpers/extensions/room_to_children.dart @@ -5,7 +5,7 @@ import "package:nexus/helpers/extensions/get_full_room.dart"; import "package:nexus/models/full_room.dart"; extension RoomToChildren on Room { - Future> getAllChildren(Client client) async { + Future> getAllChildren() async { final direct = await Future.wait( spaceChildren .map( @@ -19,7 +19,7 @@ extension RoomToChildren on Room { return (await Future.wait( direct.map( (child) async => child.roomData.isSpace - ? await child.roomData.getAllChildren(client) + ? await child.roomData.getAllChildren() : [child], ), )).expand((list) => list).toIList(); diff --git a/lib/helpers/extensions/scheme_to_theme.dart b/lib/helpers/extensions/scheme_to_theme.dart index e238cf9..aff5d52 100644 --- a/lib/helpers/extensions/scheme_to_theme.dart +++ b/lib/helpers/extensions/scheme_to_theme.dart @@ -7,6 +7,10 @@ extension SchemeToTheme on ColorScheme { titleSpacing: 0, backgroundColor: surfaceContainerLow, ), + textTheme: ThemeData( + fontFamilyFallback: ["sans"], + brightness: brightness, + ).textTheme, inputDecorationTheme: const InputDecorationTheme( border: OutlineInputBorder(), ), diff --git a/lib/main.dart b/lib/main.dart index 8cf4365..9e829a7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +import "dart:io"; + import "package:flutter/foundation.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client_controller.dart"; @@ -34,6 +36,7 @@ New Value: ${newValue is AsyncData ? newValue.value : newValue} void showError(Object error, [StackTrace? stackTrace]) { if (error.toString().contains("DioException")) 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()); @@ -57,11 +60,15 @@ void main() async { WindowOptions(titleBarStyle: TitleBarStyle.hidden), ); + if (Platform.isLinux) { + setWindowMinSize(const Size.square(500)); + } else { + await windowManager.setMinimumSize(Size.square(500)); + } + FlutterError.onError = (FlutterErrorDetails details) => showError(details.exception.toString(), details.stack); - setWindowMinSize(const Size.square(500)); - runApp( ProviderScope( observers: [ diff --git a/lib/widgets/appbar.dart b/lib/widgets/appbar.dart index 3ecaa1d..fa2088d 100644 --- a/lib/widgets/appbar.dart +++ b/lib/widgets/appbar.dart @@ -1,5 +1,6 @@ import "dart:io"; import "package:flutter/material.dart"; +import "package:window_manager/window_manager.dart"; class Appbar extends StatelessWidget implements PreferredSizeWidget { final Widget? leading; @@ -7,6 +8,7 @@ class Appbar extends StatelessWidget implements PreferredSizeWidget { final Color? backgroundColor; final double? scrolledUnderElevation; final List actions; + const Appbar({ super.key, this.title, @@ -17,19 +19,42 @@ class Appbar extends StatelessWidget implements PreferredSizeWidget { }); @override - Size get preferredSize => AppBar().preferredSize; + Size get preferredSize => const Size.fromHeight(kToolbarHeight); @override - AppBar build(BuildContext context) => AppBar( - leading: leading, - backgroundColor: backgroundColor, - scrolledUnderElevation: scrolledUnderElevation, - actionsPadding: EdgeInsets.symmetric(horizontal: 8), - title: title, - actions: [ - ...actions, - if (!(Platform.isAndroid || Platform.isIOS)) - IconButton(onPressed: () => exit(0), icon: Icon(Icons.close)), - ], - ); + Widget build(BuildContext context) { + Future maximize() async { + final isMaximized = await windowManager.isMaximized(); + + if (isMaximized) { + return windowManager.unmaximize(); + } + + return windowManager.maximize(); + } + + return GestureDetector( + behavior: HitTestBehavior.translucent, + onDoubleTap: maximize, + onPanStart: (_) => windowManager.startDragging(), + child: AppBar( + leading: leading, + backgroundColor: backgroundColor, + scrolledUnderElevation: scrolledUnderElevation, + actionsPadding: const EdgeInsets.symmetric(horizontal: 8), + title: title, + actions: [ + ...actions, + if (!(Platform.isAndroid || Platform.isIOS)) ...[ + if (!Platform.isLinux) + IconButton( + onPressed: maximize, + icon: const Icon(Icons.fullscreen), + ), + IconButton(onPressed: () => exit(0), icon: const Icon(Icons.close)), + ], + ], + ), + ); + } } diff --git a/lib/widgets/avatar_or_hash.dart b/lib/widgets/avatar_or_hash.dart index 41bd002..a077810 100644 --- a/lib/widgets/avatar_or_hash.dart +++ b/lib/widgets/avatar_or_hash.dart @@ -30,8 +30,8 @@ class AvatarOrHash extends StatelessWidget { child: Center( child: Badge( isLabelVisible: hasBadge, - smallSize: 8, - backgroundColor: Theme.of(context).colorScheme.onPrimaryContainer, + smallSize: 12, + backgroundColor: Theme.of(context).colorScheme.primary, child: ClipRRect( borderRadius: BorderRadius.all(Radius.circular(4)), child: SizedBox( diff --git a/lib/widgets/chat_page/chat_box.dart b/lib/widgets/chat_page/chat_box.dart index 016e3a7..8fd5acb 100644 --- a/lib/widgets/chat_page/chat_box.dart +++ b/lib/widgets/chat_page/chat_box.dart @@ -35,12 +35,17 @@ class ChatBox extends HookConsumerWidget { relatedMessage is TextMessage && controller.value.text.isEmpty) { final text = (relatedMessage as TextMessage).text; - controller.value.text = relatedMessage?.replyToMessageId == null + final splitText = relatedMessage?.replyToMessageId == null ? text : text.split("\n\n").sublist(1).join("\n\n"); + final notEmpty = splitText.isEmpty ? text : splitText; + controller.value.text = notEmpty.startsWith("* ") + ? notEmpty.substring(2) + : notEmpty; } void send() { + if (controller.value.text.trim().isEmpty) return; ref .watch(RoomChatController.provider(room).notifier) .send( @@ -119,11 +124,7 @@ class ChatBox extends HookConsumerWidget { triggerCharacter.value = newTriggerCharacter; query.value = newQuery; }, - triggerCharacterAndStyles: { - "@": style, - "#": style, - ":": style, - }, + triggerCharacterAndStyles: {"@": style, "#": style}, builder: (context, key) => TextFormField( enabled: room.canSendDefaultMessages, maxLines: 12, diff --git a/lib/widgets/chat_page/html/html.dart b/lib/widgets/chat_page/html/html.dart index ce5318a..04a5a1b 100644 --- a/lib/widgets/chat_page/html/html.dart +++ b/lib/widgets/chat_page/html/html.dart @@ -142,8 +142,6 @@ class Html extends ConsumerWidget { "data-mx-bg-color" => MapEntry("background-color", value), - "edited" => MapEntry("display", "block"), - _ => null, }, ) diff --git a/lib/widgets/chat_page/mention_overlay.dart b/lib/widgets/chat_page/mention_overlay.dart index 6558a9d..f6c7fe3 100644 --- a/lib/widgets/chat_page/mention_overlay.dart +++ b/lib/widgets/chat_page/mention_overlay.dart @@ -106,7 +106,7 @@ class MentionOverlay extends ConsumerWidget { title: Text(room.title), subtitle: room.roomData.topic.isEmpty ? null - : Text(room.roomData.topic), + : Text(room.roomData.topic, maxLines: 1), onTap: () => addTag( id: "[#${room.roomData.getLocalizedDisplayname()}](https://matrix.to/#/${room.roomData.id})", name: diff --git a/lib/widgets/chat_page/room_appbar.dart b/lib/widgets/chat_page/room_appbar.dart index 17696dd..b36a3ad 100644 --- a/lib/widgets/chat_page/room_appbar.dart +++ b/lib/widgets/chat_page/room_appbar.dart @@ -36,7 +36,7 @@ class RoomAppbar extends StatelessWidget implements PreferredSizeWidget { title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(room.title, overflow: TextOverflow.ellipsis), + Text(room.title, overflow: TextOverflow.ellipsis, maxLines: 1), if (room.roomData.topic.isNotEmpty) Text( room.roomData.topic, diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart index 0190e64..28df7a4 100644 --- a/lib/widgets/chat_page/room_chat.dart +++ b/lib/widgets/chat_page/room_chat.dart @@ -67,7 +67,9 @@ class RoomChat extends HookConsumerWidget { title: Text("Reply"), ), ), - if (message.authorId == room.roomData.client.userID) + // 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; @@ -199,12 +201,15 @@ class RoomChat extends HookConsumerWidget { ( context, message, { - required details, required index, - }) => context.showContextMenu( - globalPosition: details.globalPosition, - children: getMessageOptions(message), - ), + TapUpDetails? details, + }) => details?.globalPosition == null + ? null + : context.showContextMenu( + globalPosition: + details!.globalPosition, + children: getMessageOptions(message), + ), onMessageLongPress: ( context, @@ -239,22 +244,41 @@ class RoomChat extends HookConsumerWidget { required bool isSentByMe, MessageGroupStatus? groupStatus, }) => FlyerChatTextMessage( - customWidget: Html( - (message.metadata?["formatted"] + customWidget: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Html( + (message.metadata?["formatted"] as String) .replaceAllMapped( RegExp( - regexLink, + "(]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)", caseSensitive: false, ), - (m) => - "${m.group(0)!}", + (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", "
") + - ((message.editedAt != null) - ? "(edited)" - : ""), - client: room.roomData.client, + .replaceAll("\n", "
"), + client: room.roomData.client, + ), + if (message.editedAt != null) + Text( + "(edited)", + style: theme + .textTheme + .labelSmall, + ), + ], ), topWidget: TopWidget( message, diff --git a/lib/widgets/chat_page/room_menu.dart b/lib/widgets/chat_page/room_menu.dart index c5fa322..9dee21d 100644 --- a/lib/widgets/chat_page/room_menu.dart +++ b/lib/widgets/chat_page/room_menu.dart @@ -1,7 +1,8 @@ -import "package:clipboard/clipboard.dart"; import "package:flutter/material.dart"; +import "package:flutter/services.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:matrix/matrix.dart"; +import "package:nexus/helpers/extensions/room_to_children.dart"; import "package:nexus/widgets/form_text_input.dart"; class RoomMenu extends StatelessWidget { @@ -12,15 +13,31 @@ class RoomMenu extends StatelessWidget { Widget build(BuildContext context) { final danger = Theme.of(context).colorScheme.error; + void markRead(String roomId) async { + for (final child in await room.getAllChildren()) { + await child.roomData.setReadMarker( + child.roomData.lastEvent?.eventId, + mRead: child.roomData.lastEvent?.eventId, + ); + } + } + return PopupMenuButton( itemBuilder: (_) => [ PopupMenuItem( onTap: () async { final link = await room.matrixToInviteLink(); - await FlutterClipboard.copy(link.toString()); + 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"), + ), + ), PopupMenuItem( onTap: () => showDialog( context: context, diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index fd8ccf3..dffacff 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,10 +8,9 @@ #include #include +#include #include -#include #include -#include #include #include @@ -22,18 +21,15 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); - g_autoptr(FlPluginRegistrar) simple_secure_storage_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "SimpleSecureStorageLinuxPlugin"); - simple_secure_storage_linux_plugin_register_with_registrar(simple_secure_storage_linux_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); - g_autoptr(FlPluginRegistrar) webcrypto_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "WebcryptoPlugin"); - webcrypto_plugin_register_with_registrar(webcrypto_registrar); g_autoptr(FlPluginRegistrar) window_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); window_manager_plugin_register_with_registrar(window_manager_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 8d79b66..1cac43c 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,10 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_system_colors file_selector_linux + flutter_secure_storage_linux screen_retriever_linux - simple_secure_storage_linux url_launcher_linux - webcrypto window_manager window_size ) diff --git a/pubspec.lock b/pubspec.lock index 871bb6f..c24a860 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -193,14 +193,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.2" - clipboard: - dependency: "direct main" - description: - name: clipboard - sha256: "619f4e9e946cfd637ac994f49af356bb590ab88b0c4aded03204ee566fd69d9e" - url: "https://pub.dev" - source: hosted - version: "3.0.8" clock: dependency: transitive description: @@ -213,10 +205,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" url: "https://pub.dev" source: hosted - version: "4.11.0" + version: "4.11.1" collection: dependency: "direct main" description: @@ -301,10 +293,10 @@ packages: dependency: transitive description: name: custom_lint_visitor - sha256: "91f2a81e9f0abb4b9f3bb529f78b6227ce6050300d1ae5b1e2c69c66c7a566d8" + sha256: e466d17856197cf9bce7ca03804d784fddab809db7bda787f3d2799ac89faadd url: "https://pub.dev" source: hosted - version: "1.0.0+8.4.0" + version: "1.0.0+9.0.0" dart_style: dependency: transitive description: @@ -373,10 +365,10 @@ packages: dependency: transitive description: name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" file: dependency: transitive description: @@ -451,10 +443,10 @@ packages: description: path: "packages/flutter_chat_ui" ref: HEAD - resolved-ref: "6cfbadbf364251dd3c6a986e20c9d97636ad3412" + resolved-ref: "03be67c8c81c8f637672ee03dd8f082d2c223627" url: "https://github.com/Henry-Hiles/flutter_chat_ui" source: git - version: "2.9.1" + version: "2.11.1" flutter_hooks: dependency: "direct main" description: @@ -476,10 +468,10 @@ packages: description: path: "packages/flutter_link_previewer" ref: HEAD - resolved-ref: "6cfbadbf364251dd3c6a986e20c9d97636ad3412" + resolved-ref: "03be67c8c81c8f637672ee03dd8f082d2c223627" url: "https://github.com/Henry-Hiles/flutter_chat_ui" source: git - version: "4.1.2" + version: "4.2.0" flutter_lints: dependency: "direct dev" description: @@ -525,6 +517,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40 + url: "https://pub.dev" + source: hosted + version: "10.0.0" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" + url: "https://pub.dev" + source: hosted + version: "4.1.0" flutter_svg: dependency: "direct main" description: @@ -596,18 +636,18 @@ packages: description: path: "packages/flyer_chat_text_message" ref: HEAD - resolved-ref: "6cfbadbf364251dd3c6a986e20c9d97636ad3412" + resolved-ref: "03be67c8c81c8f637672ee03dd8f082d2c223627" url: "https://github.com/Henry-Hiles/flutter_chat_ui" source: git - version: "2.5.2" + version: "2.6.0" freezed: dependency: "direct dev" description: name: freezed - sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77" + sha256: "03dd9b7423ff0e31b7e01b2204593e5e1ac5ee553b6ea9d8184dff4a26b9fb07" url: "https://pub.dev" source: hosted - version: "3.2.3" + version: "3.2.4" freezed_annotation: dependency: "direct main" description: @@ -700,10 +740,10 @@ packages: dependency: transitive description: name: idb_shim - sha256: "071f3b05032fa62e60ca15db9939f8afbaf403b37e67747ac88f858c3e999228" + sha256: b26b2ad126be411d0072d1dfc4d97ebe02121a863e4eadc635b511b9bc138489 url: "https://pub.dev" source: hosted - version: "2.6.7+1" + version: "2.7.1+2" image: dependency: transitive description: @@ -816,14 +856,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.11.3" - just_throttle_it: - dependency: transitive - description: - name: just_throttle_it - sha256: af2d0c1e5c7f4e0bef79a55edf3d74c180908253f89203467bc432730f5fac5b - url: "https://pub.dev" - source: hosted - version: "3.0.1" leak_tracker: dependency: transitive description: @@ -908,10 +940,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -1164,10 +1196,10 @@ packages: dependency: transitive description: name: scrollview_observer - sha256: c2f713509f18f88f637b2084b47a90c91fb1ef066d5d82d2cf3194d8509dc6ab + sha256: "6e40ced415145c449a691d892157a3b854b751f024aed20d9aebda04c21444a3" url: "https://pub.dev" source: hosted - version: "1.26.2" + version: "1.26.3" sdp_transform: dependency: transitive description: @@ -1180,18 +1212,10 @@ packages: dependency: transitive description: name: sembast - sha256: c8063c3146c3c8d5f5b04230de7682c768440a575fbda2634f14d22f263197c3 + sha256: "139cf71496105de32e7a08a4e3a1ead0f81c4a616ec9703ed07e8f0d10cdd505" url: "https://pub.dev" source: hosted - version: "3.8.5+2" - sembast_web: - dependency: transitive - description: - name: sembast_web - sha256: "0362c7c241ad6546d3e27b4cfffaae505e5a9661e238dbcdd176756cc960fe7a" - url: "https://pub.dev" - source: hosted - version: "2.4.2" + version: "3.8.6" shared_preferences: dependency: "direct main" description: @@ -1280,62 +1304,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" - simple_secure_storage: - dependency: "direct main" - description: - name: simple_secure_storage - sha256: ca823a355bb7bb0e9b969876508e7d3a5dc0d1fb2dcb681c85b6e315f1e876e9 - url: "https://pub.dev" - source: hosted - version: "0.3.7" - simple_secure_storage_android: - dependency: transitive - description: - name: simple_secure_storage_android - sha256: "50fb27267755843af039da116d0e545f313ae329ef8838101880802259e0f741" - url: "https://pub.dev" - source: hosted - version: "0.3.1" - simple_secure_storage_darwin: - dependency: transitive - description: - name: simple_secure_storage_darwin - sha256: "8bd2ffcc62b478957ce20046bb96618b91a11e74af5d9fe2b4b229117bad18a7" - url: "https://pub.dev" - source: hosted - version: "0.2.2" - simple_secure_storage_linux: - dependency: transitive - description: - name: simple_secure_storage_linux - sha256: a7b7dccfaf496c27f882c26634ac083f2f545c0a4ca0818534c6261205a83686 - url: "https://pub.dev" - source: hosted - version: "0.2.5" - simple_secure_storage_platform_interface: - dependency: transitive - description: - name: simple_secure_storage_platform_interface - sha256: "04fd4ce4c2b97c01a12eba46f51e3075a793d11f13340d06a64eb9b45a463ca5" - url: "https://pub.dev" - source: hosted - version: "0.2.3" - simple_secure_storage_web: - dependency: transitive - description: - name: simple_secure_storage_web - sha256: "63a3474a9931ab2587e01d22e7e95c0b7cc31338c0fafed5db9d1d798d1d3e0e" - url: "https://pub.dev" - source: hosted - version: "0.2.3" - simple_secure_storage_windows: - dependency: transitive - description: - name: simple_secure_storage_windows - sha256: cf31d2a97c26cf854aeb3c9774cd253f6600fb3fdfc6d807d480afae678cef10 - url: "https://pub.dev" - source: hosted - version: "0.3.2" sky_engine: dependency: transitive description: flutter @@ -1401,10 +1369,10 @@ packages: dependency: "direct main" description: name: sqflite_common_ffi - sha256: "9faa2fedc5385ef238ce772589f7718c24cdddd27419b609bb9c6f703ea27988" + sha256: "8d7b8749a516cbf6e9057f9b480b716ad14fc4f3d3873ca6938919cc626d9025" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7+1" sqlite3: dependency: transitive description: @@ -1473,26 +1441,26 @@ packages: dependency: transitive description: name: test - sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" url: "https://pub.dev" source: hosted - version: "1.26.2" + version: "1.26.3" test_api: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" test_core: dependency: transitive description: name: test_core - sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" url: "https://pub.dev" source: hosted - version: "0.6.11" + version: "0.6.12" thumbhash: dependency: transitive description: @@ -1640,19 +1608,20 @@ packages: vodozemac: dependency: "direct main" description: - name: vodozemac - sha256: "39144e20740807731871c9248d811ed5a037b21d0aa9ffcfa630954de74139d9" - url: "https://pub.dev" - source: hosted + 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: name: watcher - sha256: f52385d4f73589977c80797e60fe51014f7f2b957b5e9a62c3f6ada439889249 + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" web: dependency: transitive description: @@ -1677,14 +1646,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" - webcrypto: - dependency: transitive - description: - name: webcrypto - sha256: "6b43001c4110856ff7fa5e5e65e7b2d44bec1d8b54a4d84d5fa2c7622267c5c1" - url: "https://pub.dev" - source: hosted - version: "0.6.0" webkit_inspection_protocol: dependency: transitive description: @@ -1759,5 +1720,5 @@ packages: source: hosted version: "2.2.3" sdks: - dart: ">=3.9.2 <4.0.0" + dart: ">=3.10.0 <4.0.0" flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 9551407..2e3ddfd 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 @@ -60,13 +65,12 @@ dependencies: flutter_vodozemac: ^0.4.1 flutter_widget_from_html_core: ^0.17.0 flutter_svg: ^2.2.2 - simple_secure_storage: ^0.3.6 json_annotation: ^4.9.0 vodozemac: ^0.4.0 - clipboard: ^3.0.8 shared_preferences: ^2.5.3 mention_tag_text_field: ^0.0.9 fluttertagger: ^2.3.1 + flutter_secure_storage: ^10.0.0 dev_dependencies: build_runner: ^2.4.11 @@ -83,4 +87,6 @@ flutter_launcher_icons: image_path: assets/icon.png adaptive_icon_background: "#000000" adaptive_icon_foreground: assets/foreground.png - remove_alpha_ios: true \ No newline at end of file + remove_alpha_ios: true + windows: + generate: true \ No newline at end of file diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 9d7af86..b12edca 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,9 @@ #include #include +#include #include -#include #include -#include #include #include @@ -20,14 +19,12 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); - SimpleSecureStorageWindowsPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("SimpleSecureStorageWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); - WebcryptoPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("WebcryptoPlugin")); WindowManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowManagerPlugin")); WindowSizePluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index dcf3309..3c6fdca 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,10 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_system_colors file_selector_windows + flutter_secure_storage_windows screen_retriever_windows - simple_secure_storage_windows url_launcher_windows - webcrypto window_manager window_size ) diff --git a/windows/installer.iss b/windows/installer.iss new file mode 100644 index 0000000..c5004c3 --- /dev/null +++ b/windows/installer.iss @@ -0,0 +1,17 @@ +[Setup] +AppName=Nexus +AppVersion=1.0.0 +DefaultDirName={pf}\Nexus +DefaultGroupName=Nexus +OutputDir=dist +OutputBaseFilename=Nexus-Setup +Compression=lzma +SolidCompression=yes +ArchitecturesInstallIn64BitMode=x64 + +[Files] +Source: "..\build\windows\x64\runner\Release\*"; DestDir: "{app}"; Flags: recursesubdirs ignoreversion + +[Icons] +Name: "{group}\Nexus"; Filename: "{app}\nexus.exe" +Name: "{commondesktop}\Nexus"; Filename: "{app}\nexus.exe" \ No newline at end of file diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index c04e20c..e3c83c9 100644 Binary files a/windows/runner/resources/app_icon.ico and b/windows/runner/resources/app_icon.ico differ