From 8a64b3630c8e7c5161292757ee8e74c6bc10cebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sun, 19 Oct 2025 07:25:02 +0200 Subject: [PATCH 01/21] fix: Null pointer crash in chat list item --- lib/pages/chat_list/chat_list_item.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 52f1be868..73000ccd9 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -222,7 +222,7 @@ class ChatListItem extends StatelessWidget { children: [ if (typingText.isEmpty && ownMessage && - room.lastEvent!.status.isSending) ...[ + room.lastEvent?.status.isSending == true) ...[ const SizedBox( width: 16, height: 16, From 50995e57ff7eb9d5c587e5722100f2ad20e42156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sun, 19 Oct 2025 07:25:15 +0200 Subject: [PATCH 02/21] chore: Follow up encryption button design --- lib/pages/chat/encryption_button.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/pages/chat/encryption_button.dart b/lib/pages/chat/encryption_button.dart index 3bc556b4a..a18e10609 100644 --- a/lib/pages/chat/encryption_button.dart +++ b/lib/pages/chat/encryption_button.dart @@ -21,6 +21,7 @@ class EncryptionButton extends StatelessWidget { .stream .where((s) => s.deviceLists != null), builder: (context, snapshot) { + final shouldBeEncrypted = room.joinRules != JoinRules.public; return FutureBuilder( future: room.encrypted ? room.calcEncryptionHealthState() @@ -46,9 +47,13 @@ class EncryptionButton extends StatelessWidget { ), ), child: Icon( - room.encrypted ? Icons.lock_outlined : Icons.lock_open_outlined, + room.encrypted + ? Icons.lock_outlined + : Icons.no_encryption_outlined, size: 20, - color: theme.colorScheme.onSurface, + color: (shouldBeEncrypted && !room.encrypted) + ? theme.colorScheme.error + : theme.colorScheme.onSurface, ), ), onPressed: () => context.go('/rooms/${room.id}/encryption'), From 7ff6d7b4013e6f4287998ff7230cfc4833182808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sun, 19 Oct 2025 07:39:21 +0200 Subject: [PATCH 03/21] chore: Adjust encryption page design --- .../chat_encryption_settings_view.dart | 127 +++++++++--------- 1 file changed, 64 insertions(+), 63 deletions(-) diff --git a/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart b/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart index f6adffd58..754178c25 100644 --- a/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart +++ b/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart @@ -9,6 +9,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart'; import 'package:fluffychat/utils/beautify_string_extension.dart'; +import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; class ChatEncryptionSettingsView extends StatelessWidget { @@ -58,7 +59,6 @@ class ChatEncryptionSettingsView extends StatelessWidget { size: 128, color: theme.colorScheme.onInverseSurface, ), - const Divider(), if (room.isDirectChat) Padding( padding: const EdgeInsets.all(16.0), @@ -107,72 +107,73 @@ class ChatEncryptionSettingsView extends StatelessWidget { shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: deviceKeys.length, - itemBuilder: (BuildContext context, int i) => - SwitchListTile( - value: !deviceKeys[i].blocked, - activeThumbColor: deviceKeys[i].verified - ? Colors.green - : Colors.orange, - onChanged: (_) => - controller.toggleDeviceKey(deviceKeys[i]), - title: Row( - children: [ - Icon( - deviceKeys[i].verified - ? Icons.verified_outlined - : deviceKeys[i].blocked - ? Icons.block_outlined - : Icons.info_outlined, - color: deviceKeys[i].verified - ? Colors.green - : deviceKeys[i].blocked - ? Colors.red - : Colors.orange, - size: 20, - ), - const SizedBox(width: 4), - Text( - deviceKeys[i].deviceId ?? - L10n.of(context).unknownDevice, - ), - const SizedBox(width: 4), - Flexible( - fit: FlexFit.loose, - child: Material( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, + itemBuilder: (BuildContext context, int i) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (i == 0 || + deviceKeys[i].userId != + deviceKeys[i - 1].userId) ...[ + const Divider(), + FutureBuilder( + future: room.client + .getUserProfile(deviceKeys[i].userId), + builder: (context, snapshot) { + final displayname = + snapshot.data?.displayname ?? + deviceKeys[i].userId.localpart ?? + deviceKeys[i].userId; + return ListTile( + leading: Avatar( + name: displayname, + mxContent: snapshot.data?.avatarUrl, ), - side: BorderSide( - color: theme.colorScheme.primary, - ), - ), - color: theme.colorScheme.primaryContainer, - child: Padding( - padding: const EdgeInsets.all(4.0), - child: Text( - deviceKeys[i].userId, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: theme.colorScheme.primary, - fontSize: 12, - fontStyle: FontStyle.italic, - ), - ), - ), - ), + title: Text(displayname), + subtitle: Text(deviceKeys[i].userId), + ); + }, ), ], - ), - subtitle: Text( - deviceKeys[i].ed25519Key?.beautified ?? - L10n.of(context).unknownEncryptionAlgorithm, - style: TextStyle( - fontFamily: 'RobotoMono', - color: theme.colorScheme.secondary, + SwitchListTile( + value: !deviceKeys[i].blocked, + activeThumbColor: deviceKeys[i].verified + ? Colors.green + : Colors.orange, + onChanged: (_) => + controller.toggleDeviceKey(deviceKeys[i]), + title: Row( + children: [ + Text( + deviceKeys[i].verified + ? L10n.of(context).verified + : deviceKeys[i].blocked + ? L10n.of(context).blocked + : L10n.of(context).unverified, + style: TextStyle( + color: deviceKeys[i].verified + ? Colors.green + : deviceKeys[i].blocked + ? Colors.red + : Colors.orange, + ), + ), + const Text(' | ID: '), + Text( + deviceKeys[i].deviceId ?? + L10n.of(context).unknownDevice, + ), + ], + ), + subtitle: Text( + deviceKeys[i].ed25519Key?.beautified ?? + L10n.of(context).unknownEncryptionAlgorithm, + style: TextStyle( + fontFamily: 'RobotoMono', + color: theme.colorScheme.secondary, + fontSize: 11, + ), + ), ), - ), + ], ), ); }, From cf64812ffdba3d5ffdd7a91b9a924e8538f09d38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 21:10:07 +0000 Subject: [PATCH 04/21] build: (deps): bump animations from 2.0.11 to 2.1.0 Bumps [animations](https://github.com/flutter/packages/tree/main/packages) from 2.0.11 to 2.1.0. - [Commits](https://github.com/flutter/packages/commits/animations-v2.1.0/packages) --- updated-dependencies: - dependency-name: animations dependency-version: 2.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index cc62d28de..2b58d45d7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: "direct main" description: name: animations - sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb + sha256: a8031b276f0a7986ac907195f10ca7cd04ecf2a8a566bd6dbe03018a9b02b427 url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.1.0" ansicolor: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e1687cb5a..bb9add613 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,7 @@ environment: sdk: ">=3.0.0 <4.0.0" dependencies: - animations: ^2.0.11 + animations: ^2.1.0 app_links: ^6.4.1 archive: ^4.0.7 async: ^2.11.0 From 1cf422f45d31eeb3a29ac2ae1bfb28df57fdb393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sun, 19 Oct 2025 07:54:39 +0200 Subject: [PATCH 05/21] ci: Build only for arm64 --- .github/workflows/integrate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integrate.yaml b/.github/workflows/integrate.yaml index a93d4c70a..38ba8e788 100644 --- a/.github/workflows/integrate.yaml +++ b/.github/workflows/integrate.yaml @@ -45,7 +45,7 @@ jobs: cache: true - uses: moonrepo/setup-rust@v1 - run: flutter pub get - - run: flutter build apk --debug + - run: flutter build apk --debug --target-platform android-arm64 build_debug_web: runs-on: ubuntu-latest From f2207e0d48d50dc829ae220e8464bba393cbecdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sun, 19 Oct 2025 09:19:38 +0200 Subject: [PATCH 06/21] build: (deps): bump unifiedpush_ui from 0.1.0 to 0.2.0 Bumps unifiedpush_ui from 0.1.0 to 0.2.0. --- updated-dependencies: - dependency-name: unifiedpush_ui dependency-version: 0.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- lib/utils/background_push.dart | 9 +++++++-- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index eaea22d99..d25a7a68a 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -363,8 +363,13 @@ class BackgroundPush { } Future setupUp() async { - await UnifiedPushUi(matrix!.context, ["default"], UPFunctions()) - .registerAppWithDialog(); + await UnifiedPushUi( + context: matrix!.context, + instances: ["default"], + unifiedPushFunctions: UPFunctions(), + showNoDistribDialog: false, + onNoDistribDialogDismissed: () {}, // TODO: Implement me + ).registerAppWithDialog(); } Future _newUpEndpoint(PushEndpoint newPushEndpoint, String i) async { diff --git a/pubspec.lock b/pubspec.lock index 8e53a3c02..8ce8b3a64 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1967,10 +1967,10 @@ packages: dependency: "direct main" description: name: unifiedpush_ui - sha256: cf86f0214f37debd41f25c0425c8489df85e27f9f8784fed571eb7a86d39ba11 + sha256: "1b36b2aa0bc6b61577e2661c1183bd3442969ecf77b4c78174796d324f66dd1d" url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.2.0" universal_html: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 488558f8f..cb3339447 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -76,7 +76,7 @@ dependencies: swipe_to_action: ^0.3.0 tor_detector_web: ^1.1.0 unifiedpush: ^6.2.0 - unifiedpush_ui: ^0.1.0 + unifiedpush_ui: ^0.2.0 universal_html: ^2.2.4 url_launcher: ^6.3.2 video_compress: ^3.1.4 From bedfde7c10d61b8344b89c912671f4a512b2665d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sun, 19 Oct 2025 09:20:13 +0200 Subject: [PATCH 07/21] build: Update desktop plugins --- linux/flutter/generated_plugin_registrant.cc | 12 ++++++++++++ linux/flutter/generated_plugins.cmake | 3 +++ macos/Flutter/GeneratedPluginRegistrant.swift | 6 ++++++ windows/flutter/generated_plugin_registrant.cc | 9 +++++++++ windows/flutter/generated_plugins.cmake | 3 +++ 5 files changed, 33 insertions(+) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index f6178cd62..18556ce65 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -15,8 +15,11 @@ #include #include #include +#include #include #include +#include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { @@ -47,12 +50,21 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) record_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); record_linux_plugin_register_with_registrar(record_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) sqlcipher_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); sqlite3_flutter_libs_plugin_register_with_registrar(sqlcipher_flutter_libs_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); g_autoptr(FlPluginRegistrar) window_to_front_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "WindowToFrontPlugin"); window_to_front_plugin_register_with_registrar(window_to_front_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 9d145db6f..caa16a27f 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -12,8 +12,11 @@ list(APPEND FLUTTER_PLUGIN_LIST gtk handy_window record_linux + screen_retriever_linux sqlcipher_flutter_libs url_launcher_linux + webcrypto + window_manager window_to_front ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index dd7fa9ed5..57246fde3 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -23,6 +23,7 @@ import just_audio import package_info_plus import path_provider_foundation import record_macos +import screen_retriever_macos import share_plus import shared_preferences_foundation import sqlcipher_flutter_libs @@ -30,6 +31,8 @@ import url_launcher_macos import video_compress import video_player_avfoundation import wakelock_plus +import webcrypto +import window_manager import window_to_front func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { @@ -51,6 +54,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin")) + ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) @@ -58,5 +62,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) + WebcryptoPlugin.register(with: registry.registrar(forPlugin: "WebcryptoPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin")) } diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index d083bd166..80985b1f1 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -16,9 +16,12 @@ #include #include #include +#include #include #include #include +#include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -42,12 +45,18 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); RecordWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); + ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WebcryptoPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WebcryptoPlugin")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); WindowToFrontPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowToFrontPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 3276ee890..382699d2c 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -13,9 +13,12 @@ list(APPEND FLUTTER_PLUGIN_LIST geolocator_windows permission_handler_windows record_windows + screen_retriever_windows share_plus sqlcipher_flutter_libs url_launcher_windows + webcrypto + window_manager window_to_front ) From 2538f31351cf0fbd94d1cf6583bcc07f9241de5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sun, 19 Oct 2025 12:10:14 +0200 Subject: [PATCH 08/21] design: Improved spaces UX --- lib/l10n/intl_en.arb | 7 +- lib/pages/chat_list/chat_list_item.dart | 42 +- lib/pages/chat_list/space_view.dart | 484 ++++++++++++------ lib/pages/chat_list/unread_bubble.dart | 56 ++ .../adaptive_dialogs/public_room_dialog.dart | 4 +- lib/widgets/avatar.dart | 10 +- 6 files changed, 410 insertions(+), 193 deletions(-) create mode 100644 lib/pages/chat_list/unread_bubble.dart diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 1d9e551ad..f08cb8fa0 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -3378,5 +3378,10 @@ "noMessagesYet": "No messages yet", "longPressToRecordVoiceMessage": "Long press to record voice message.", "pause": "Pause", - "resume": "Resume" + "resume": "Resume", + "newSubSpace": "New sub space", + "moveToDifferentSpace": "Move to different space", + "moveUp": "Move up", + "moveDown": "Move down", + "removeFromSpaceDescription": "The chat will be removed from the space but still appear in your chat list." } diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 73000ccd9..6a3ea9582 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -4,6 +4,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pages/chat_list/unread_bubble.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/room_status_extension.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; @@ -46,11 +47,6 @@ class ChatListItem extends StatelessWidget { final unread = room.isUnread; final directChatMatrixId = room.directChatMatrixID; final isDirectChat = directChatMatrixId != null; - final unreadBubbleSize = unread || room.hasNewMessages - ? room.notificationCount > 0 - ? 20.0 - : 14.0 - : 0.0; final hasNotifications = room.notificationCount > 0; final backgroundColor = activeChat ? theme.colorScheme.secondaryContainer : null; @@ -318,41 +314,7 @@ class ChatListItem extends StatelessWidget { ), ), const SizedBox(width: 8), - AnimatedContainer( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(horizontal: 7), - height: unreadBubbleSize, - width: !hasNotifications && !unread && !room.hasNewMessages - ? 0 - : (unreadBubbleSize - 9) * - room.notificationCount.toString().length + - 9, - decoration: BoxDecoration( - color: room.highlightCount > 0 - ? theme.colorScheme.error - : hasNotifications || room.markedUnread - ? theme.colorScheme.primary - : theme.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(7), - ), - child: hasNotifications - ? Text( - room.notificationCount.toString(), - style: TextStyle( - color: room.highlightCount > 0 - ? theme.colorScheme.onError - : hasNotifications - ? theme.colorScheme.onPrimary - : theme.colorScheme.onPrimaryContainer, - fontSize: 13, - fontWeight: FontWeight.w500, - ), - textAlign: TextAlign.center, - ) - : const SizedBox.shrink(), - ), + UnreadBubble(room: room), ], ), onTap: onTap, diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 33ea75344..d9f47938c 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; @@ -8,20 +10,31 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; -import 'package:fluffychat/pages/chat_list/search_title.dart'; +import 'package:fluffychat/pages/chat_list/unread_bubble.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/stream_extension.dart'; +import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/public_room_dialog.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import 'package:fluffychat/widgets/hover_builder.dart'; import 'package:fluffychat/widgets/matrix.dart'; enum AddRoomType { chat, subspace } +enum SpaceChildAction { edit, moveToSpace, removeFromSpace } + +enum SpaceActions { + settings, + invite, + members, + leave, +} + class SpaceView extends StatefulWidget { final String spaceId; final void Function() onBack; @@ -58,9 +71,28 @@ class _SpaceViewState extends State { } void _loadHierarchy() async { - final room = Matrix.of(context).client.getRoomById(widget.spaceId); + final matrix = Matrix.of(context); + final room = matrix.client.getRoomById(widget.spaceId); if (room == null) return; + final cacheKey = 'spaces_history_cache${room.id}'; + if (_discoveredChildren.isEmpty) { + final cachedChildren = matrix.store.getStringList(cacheKey); + if (cachedChildren != null) { + try { + _discoveredChildren.addAll( + cachedChildren.map( + (jsonString) => + SpaceRoomsChunk$2.fromJson(jsonDecode(jsonString)), + ), + ); + } catch (e, s) { + Logs().e('Unable to json decode spaces hierarchy cache!', e, s); + matrix.store.remove(cacheKey); + } + } + } + setState(() { _isLoading = true; }); @@ -74,16 +106,25 @@ class _SpaceViewState extends State { ); if (!mounted) return; setState(() { + if (_nextBatch == null) _discoveredChildren.clear(); _nextBatch = hierarchy.nextBatch; if (hierarchy.nextBatch == null) { _noMoreRooms = true; } _discoveredChildren.addAll( - hierarchy.rooms - .where((c) => room.client.getRoomById(c.roomId) == null), + hierarchy.rooms.where((room) => room.roomId != widget.spaceId), ); _isLoading = false; }); + + if (_nextBatch == null) { + matrix.store.setStringList( + cacheKey, + _discoveredChildren + .map((child) => jsonEncode(child.toJson())) + .toList(), + ); + } } catch (e, s) { Logs().w('Unable to load hierarchy', e, s); if (!mounted) return; @@ -111,9 +152,7 @@ class _SpaceViewState extends State { ), ); if (mounted && joined == true) { - setState(() { - _discoveredChildren.remove(item); - }); + setState(() {}); } } @@ -129,6 +168,10 @@ class _SpaceViewState extends State { await space?.postLoad(); context.push('/rooms/${widget.spaceId}/invite'); break; + case SpaceActions.members: + await space?.postLoad(); + context.push('/rooms/${widget.spaceId}/details/members'); + break; case SpaceActions.leave: final confirmed = await showOkCancelAlertDialog( context: context, @@ -151,27 +194,11 @@ class _SpaceViewState extends State { } } - void _addChatOrSubspace() async { - final roomType = await showModalActionPopup( - context: context, - title: L10n.of(context).addChatOrSubSpace, - actions: [ - AdaptiveModalAction( - value: AddRoomType.subspace, - label: L10n.of(context).createNewSpace, - ), - AdaptiveModalAction( - value: AddRoomType.chat, - label: L10n.of(context).createGroup, - ), - ], - ); - if (roomType == null) return; - + void _addChatOrSubspace(AddRoomType roomType) async { final names = await showTextInputDialog( context: context, title: roomType == AddRoomType.subspace - ? L10n.of(context).createNewSpace + ? L10n.of(context).newSubSpace : L10n.of(context).createGroup, hintText: roomType == AddRoomType.subspace ? L10n.of(context).spaceName @@ -196,29 +223,169 @@ class _SpaceViewState extends State { late final String roomId; final activeSpace = client.getRoomById(widget.spaceId)!; await activeSpace.postLoad(); + final isPublicSpace = activeSpace.joinRules == JoinRules.public; if (roomType == AddRoomType.subspace) { roomId = await client.createSpace( name: names, - visibility: activeSpace.joinRules == JoinRules.public - ? sdk.Visibility.public - : sdk.Visibility.private, + visibility: + isPublicSpace ? sdk.Visibility.public : sdk.Visibility.private, ); } else { roomId = await client.createGroupChat( + enableEncryption: !isPublicSpace, groupName: names, - preset: activeSpace.joinRules == JoinRules.public + preset: isPublicSpace ? CreateRoomPreset.publicChat : CreateRoomPreset.privateChat, - visibility: activeSpace.joinRules == JoinRules.public - ? sdk.Visibility.public - : sdk.Visibility.private, + visibility: + isPublicSpace ? sdk.Visibility.public : sdk.Visibility.private, + initialState: isPublicSpace + ? null + : [ + StateEvent( + content: { + 'join_rule': 'restricted', + 'allow': [ + { + 'room_id': widget.spaceId, + 'type': 'm.room_membership', + }, + ], + }, + type: EventTypes.RoomJoinRules, + ), + ], ); } await activeSpace.setSpaceChild(roomId); }, ); if (result.error != null) return; + setState(() { + _nextBatch = null; + _discoveredChildren.clear(); + }); + _loadHierarchy(); + } + + void _showSpaceChildEditMenu(BuildContext posContext, String roomId) async { + final overlay = + Overlay.of(posContext).context.findRenderObject() as RenderBox; + + final button = posContext.findRenderObject() as RenderBox; + + final position = RelativeRect.fromRect( + Rect.fromPoints( + button.localToGlobal(const Offset(0, -65), ancestor: overlay), + button.localToGlobal( + button.size.bottomRight(Offset.zero) + const Offset(-50, 0), + ancestor: overlay, + ), + ), + Offset.zero & overlay.size, + ); + + final action = await showMenu( + context: posContext, + position: position, + items: [ + PopupMenuItem( + value: SpaceChildAction.moveToSpace, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.move_down_outlined), + const SizedBox(width: 12), + Text(L10n.of(context).moveToDifferentSpace), + ], + ), + ), + PopupMenuItem( + value: SpaceChildAction.edit, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.edit_outlined), + const SizedBox(width: 12), + Text(L10n.of(context).edit), + ], + ), + ), + PopupMenuItem( + value: SpaceChildAction.removeFromSpace, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.group_remove_outlined), + const SizedBox(width: 12), + Text(L10n.of(context).removeFromSpace), + ], + ), + ), + ], + ); + if (action == null) return; + if (!mounted) return; + final space = Matrix.of(context).client.getRoomById(widget.spaceId); + if (space == null) return; + switch (action) { + case SpaceChildAction.edit: + context.push('/rooms/${widget.spaceId}/details'); + case SpaceChildAction.moveToSpace: + final spacesWithPowerLevels = space.client.rooms + .where( + (room) => + room.isSpace && + room.canChangeStateEvent(EventTypes.SpaceChild) && + room.id != widget.spaceId, + ) + .toList(); + final newSpace = await showModalActionPopup( + context: context, + title: L10n.of(context).space, + actions: spacesWithPowerLevels + .map( + (space) => AdaptiveModalAction( + value: space, + label: space + .getLocalizedDisplayname(MatrixLocals(L10n.of(context))), + ), + ) + .toList(), + ); + if (newSpace == null) return; + final result = await showFutureLoadingDialog( + context: context, + future: () async { + await newSpace.setSpaceChild(newSpace.id); + await space.removeSpaceChild(roomId); + }, + ); + if (result.isError) return; + if (!mounted) return; + _nextBatch = null; + _loadHierarchy(); + return; + + case SpaceChildAction.removeFromSpace: + final consent = await showOkCancelAlertDialog( + context: context, + title: L10n.of(context).removeFromSpace, + message: L10n.of(context).removeFromSpaceDescription, + ); + if (consent != OkCancelResult.ok) return; + if (!mounted) return; + final result = await showFutureLoadingDialog( + context: context, + future: () => space.removeSpaceChild(roomId), + ); + if (result.isError) return; + if (!mounted) return; + _nextBatch = null; + _loadHierarchy(); + return; + } } @override @@ -228,6 +395,11 @@ class _SpaceViewState extends State { final room = Matrix.of(context).client.getRoomById(widget.spaceId); final displayname = room?.getLocalizedDisplayname() ?? L10n.of(context).nothingFound; + const avatarSize = Avatar.defaultSize / 1.5; + final isAdmin = room?.canChangeStateEvent( + EventTypes.SpaceChild, + ) == + true; return Scaffold( appBar: AppBar( leading: FluffyThemes.isColumnMode(context) @@ -242,6 +414,7 @@ class _SpaceViewState extends State { title: ListTile( contentPadding: EdgeInsets.zero, leading: Avatar( + size: avatarSize, mxContent: room?.avatar, name: displayname, border: BorderSide(width: 1, color: theme.dividerColor), @@ -252,18 +425,38 @@ class _SpaceViewState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, ), - subtitle: room == null - ? null - : Text( - L10n.of(context).countChatsAndCountParticipants( - room.spaceChildren.length, - room.summary.mJoinedMemberCount ?? 1, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), ), actions: [ + if (isAdmin) + PopupMenuButton( + icon: const Icon(Icons.add_outlined), + onSelected: _addChatOrSubspace, + tooltip: L10n.of(context).addChatOrSubSpace, + itemBuilder: (context) => [ + PopupMenuItem( + value: AddRoomType.chat, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.group_add_outlined), + const SizedBox(width: 12), + Text(L10n.of(context).newGroup), + ], + ), + ), + PopupMenuItem( + value: AddRoomType.subspace, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.workspaces_outlined), + const SizedBox(width: 12), + Text(L10n.of(context).newSubSpace), + ], + ), + ), + ], + ), PopupMenuButton( useRootNavigator: true, onSelected: _onSpaceAction, @@ -290,6 +483,21 @@ class _SpaceViewState extends State { ], ), ), + PopupMenuItem( + value: SpaceActions.members, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.group_outlined), + const SizedBox(width: 12), + Text( + L10n.of(context).countParticipants( + room?.summary.mJoinedMemberCount ?? 1, + ), + ), + ], + ), + ), PopupMenuItem( value: SpaceActions.leave, child: Row( @@ -305,16 +513,6 @@ class _SpaceViewState extends State { ), ], ), - floatingActionButton: room?.canChangeStateEvent( - EventTypes.SpaceChild, - ) == - true - ? FloatingActionButton.extended( - onPressed: _addChatOrSubspace, - label: Text(L10n.of(context).group), - icon: const Icon(Icons.group_add_outlined), - ) - : null, body: room == null ? const Center( child: Icon( @@ -332,9 +530,11 @@ class _SpaceViewState extends State { .whereType() .toSet(); - final joinedRooms = room.client.rooms - .where((room) => childrenIds.remove(room.id)) - .toList(); + final joinedRooms = Map.fromEntries( + room.client.rooms + .where((room) => childrenIds.remove(room.id)) + .map((room) => MapEntry(room.id, room)), + ); final joinedParents = room.spaceParents .map((parent) { @@ -349,7 +549,6 @@ class _SpaceViewState extends State { slivers: [ SliverAppBar( floating: true, - toolbarHeight: 72, scrolledUnderElevation: 0, backgroundColor: Colors.transparent, automaticallyImplyLeading: false, @@ -359,11 +558,6 @@ class _SpaceViewState extends State { textInputAction: TextInputAction.search, decoration: InputDecoration( filled: true, - fillColor: theme.colorScheme.secondaryContainer, - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(99), - ), contentPadding: EdgeInsets.zero, hintText: L10n.of(context).search, hintStyle: TextStyle( @@ -423,42 +617,11 @@ class _SpaceViewState extends State { }, ), SliverList.builder( - itemCount: joinedRooms.length, + itemCount: _discoveredChildren.length + 1, itemBuilder: (context, i) { - final joinedRoom = joinedRooms[i]; - return ChatListItem( - joinedRoom, - filter: filter, - onTap: () => widget.onChatTab(joinedRoom), - onLongPress: (context) => widget.onChatContext( - joinedRoom, - context, - ), - activeChat: widget.activeChat == joinedRoom.id, - ); - }, - ), - SliverList.builder( - itemCount: _discoveredChildren.length + 2, - itemBuilder: (context, i) { - if (i == 0) { - return SearchTitle( - title: L10n.of(context).discover, - icon: const Icon(Icons.explore_outlined), - ); - } - i--; if (i == _discoveredChildren.length) { if (_noMoreRooms) { - return Padding( - padding: const EdgeInsets.all(12.0), - child: Center( - child: Text( - L10n.of(context).noMoreChatsFound, - style: const TextStyle(fontSize: 13), - ), - ), - ); + return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.symmetric( @@ -468,11 +631,7 @@ class _SpaceViewState extends State { child: TextButton( onPressed: _isLoading ? null : _loadHierarchy, child: _isLoading - ? LinearProgressIndicator( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, - ), - ) + ? const CircularProgressIndicator.adaptive() : Text(L10n.of(context).loadMore), ), ); @@ -484,6 +643,7 @@ class _SpaceViewState extends State { if (!displayname.toLowerCase().contains(filter)) { return const SizedBox.shrink(); } + final joinedRoom = joinedRooms[item.roomId]; return Padding( padding: const EdgeInsets.symmetric( horizontal: 8, @@ -493,51 +653,83 @@ class _SpaceViewState extends State { borderRadius: BorderRadius.circular(AppConfig.borderRadius), clipBehavior: Clip.hardEdge, - child: ListTile( - visualDensity: - const VisualDensity(vertical: -0.5), - contentPadding: - const EdgeInsets.symmetric(horizontal: 8), - onTap: () => _joinChildRoom(item), - leading: Avatar( - mxContent: item.avatarUrl, - name: displayname, - borderRadius: item.roomType == 'm.space' - ? BorderRadius.circular( - AppConfig.borderRadius / 2, - ) + color: joinedRoom != null && + widget.activeChat == joinedRoom.id + ? theme.colorScheme.secondaryContainer + : Colors.transparent, + child: HoverBuilder( + builder: (context, hovered) => ListTile( + visualDensity: + const VisualDensity(vertical: -0.5), + contentPadding: + const EdgeInsets.symmetric(horizontal: 8), + onTap: joinedRoom != null + ? () => widget.onChatTab(joinedRoom) + : () => _joinChildRoom(item), + onLongPress: isAdmin + ? () => _showSpaceChildEditMenu( + context, + item.roomId, + ) : null, - ), - title: Row( - children: [ - Expanded( - child: Text( - displayname, - maxLines: 1, - overflow: TextOverflow.ellipsis, + leading: hovered && isAdmin + ? SizedBox.square( + dimension: avatarSize, + child: IconButton( + splashRadius: avatarSize, + iconSize: 14, + style: IconButton.styleFrom( + foregroundColor: theme.colorScheme + .onTertiaryContainer, + backgroundColor: theme + .colorScheme.tertiaryContainer, + ), + onPressed: () => + _showSpaceChildEditMenu( + context, + item.roomId, + ), + icon: const Icon(Icons.edit_outlined), + ), + ) + : Avatar( + size: avatarSize, + mxContent: item.avatarUrl, + name: '#', + backgroundColor: + theme.colorScheme.surfaceContainer, + textColor: item.name?.darkColor ?? + theme.colorScheme.onSurface, + border: item.roomType == 'm.space' + ? BorderSide( + color: theme.colorScheme + .surfaceContainerHighest, + ) + : null, + borderRadius: item.roomType == 'm.space' + ? BorderRadius.circular( + AppConfig.borderRadius / 4, + ) + : null, + ), + title: Row( + children: [ + Expanded( + child: Opacity( + opacity: joinedRoom == null ? 0.5 : 1, + child: Text( + displayname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), ), - ), - Text( - item.numJoinedMembers.toString(), - style: TextStyle( - fontSize: 13, - color: theme.textTheme.bodyMedium!.color, - ), - ), - const SizedBox(width: 4), - const Icon( - Icons.people_outlined, - size: 14, - ), - ], - ), - subtitle: Text( - item.topic ?? - L10n.of(context).countParticipants( - item.numJoinedMembers, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + if (joinedRoom != null) + UnreadBubble(room: joinedRoom) + else + const Icon(Icons.chevron_right_outlined), + ], + ), ), ), ), @@ -552,9 +744,3 @@ class _SpaceViewState extends State { ); } } - -enum SpaceActions { - settings, - invite, - leave, -} diff --git a/lib/pages/chat_list/unread_bubble.dart b/lib/pages/chat_list/unread_bubble.dart new file mode 100644 index 000000000..964094f97 --- /dev/null +++ b/lib/pages/chat_list/unread_bubble.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/config/themes.dart'; + +class UnreadBubble extends StatelessWidget { + final Room room; + const UnreadBubble({required this.room, super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final unread = room.isUnread; + final hasNotifications = room.notificationCount > 0; + final unreadBubbleSize = unread || room.hasNewMessages + ? room.notificationCount > 0 + ? 20.0 + : 14.0 + : 0.0; + return AnimatedContainer( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(horizontal: 7), + height: unreadBubbleSize, + width: !hasNotifications && !unread && !room.hasNewMessages + ? 0 + : (unreadBubbleSize - 9) * room.notificationCount.toString().length + + 9, + decoration: BoxDecoration( + color: room.highlightCount > 0 + ? theme.colorScheme.error + : hasNotifications || room.markedUnread + ? theme.colorScheme.primary + : theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(7), + ), + child: hasNotifications + ? Text( + room.notificationCount.toString(), + style: TextStyle( + color: room.highlightCount > 0 + ? theme.colorScheme.onError + : hasNotifications + ? theme.colorScheme.onPrimary + : theme.colorScheme.onPrimaryContainer, + fontSize: 13, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ) + : const SizedBox.shrink(), + ); + } +} diff --git a/lib/widgets/adaptive_dialogs/public_room_dialog.dart b/lib/widgets/adaptive_dialogs/public_room_dialog.dart index 09e125613..add44c89d 100644 --- a/lib/widgets/adaptive_dialogs/public_room_dialog.dart +++ b/lib/widgets/adaptive_dialogs/public_room_dialog.dart @@ -30,7 +30,9 @@ class PublicRoomDialog extends StatelessWidget { final result = await showFutureLoadingDialog( context: context, future: () async { - if (chunk != null && client.getRoomById(chunk.roomId) != null) { + if (chunk != null && + client.getRoomById(chunk.roomId) != null && + client.getRoomById(chunk.roomId)?.membership != Membership.leave) { return chunk.roomId; } final roomId = chunk != null && knock diff --git a/lib/widgets/avatar.dart b/lib/widgets/avatar.dart index 31a523fa3..fdecdb5d7 100644 --- a/lib/widgets/avatar.dart +++ b/lib/widgets/avatar.dart @@ -18,6 +18,8 @@ class Avatar extends StatelessWidget { final BorderRadius? borderRadius; final IconData? icon; final BorderSide? border; + final Color? backgroundColor; + final Color? textColor; const Avatar({ this.mxContent, @@ -30,6 +32,8 @@ class Avatar extends StatelessWidget { this.borderRadius, this.border, this.icon, + this.backgroundColor, + this.textColor, super.key, }); @@ -71,14 +75,16 @@ class Avatar extends StatelessWidget { height: size, placeholder: (_) => noPic ? Container( - decoration: BoxDecoration(color: name?.lightColorAvatar), + decoration: BoxDecoration( + color: backgroundColor ?? name?.lightColorAvatar, + ), alignment: Alignment.center, child: Text( fallbackLetters, textAlign: TextAlign.center, style: TextStyle( fontFamily: 'RobotoMono', - color: Colors.white, + color: textColor ?? Colors.white, fontWeight: FontWeight.bold, fontSize: (size / 2.5).roundToDouble(), ), From 7fa7941f84f18033897f80a601f3a563c553aaa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sun, 19 Oct 2025 13:00:31 +0200 Subject: [PATCH 09/21] build: Update macOS Podfile --- macos/Podfile.lock | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 80bbc5087..8507a72ef 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -42,6 +42,8 @@ PODS: - FlutterMacOS - record_macos (1.1.0): - FlutterMacOS + - screen_retriever_macos (0.0.1): + - FlutterMacOS - share_plus (0.0.1): - FlutterMacOS - shared_preferences_foundation (0.0.1): @@ -64,7 +66,12 @@ PODS: - FlutterMacOS - wakelock_plus (0.0.1): - FlutterMacOS + - webcrypto (0.1.1): + - Flutter + - FlutterMacOS - WebRTC-SDK (137.7151.04) + - window_manager (0.5.0): + - FlutterMacOS - window_to_front (0.0.1): - FlutterMacOS @@ -89,6 +96,7 @@ DEPENDENCIES: - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`) + - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqlcipher_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlcipher_flutter_libs/macos`) @@ -96,6 +104,8 @@ DEPENDENCIES: - video_compress (from `Flutter/ephemeral/.symlinks/plugins/video_compress/macos`) - video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) + - webcrypto (from `Flutter/ephemeral/.symlinks/plugins/webcrypto/darwin`) + - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) - window_to_front (from `Flutter/ephemeral/.symlinks/plugins/window_to_front/macos`) SPEC REPOS: @@ -144,6 +154,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin record_macos: :path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos + screen_retriever_macos: + :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos share_plus: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: @@ -158,6 +170,10 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin wakelock_plus: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos + webcrypto: + :path: Flutter/ephemeral/.symlinks/plugins/webcrypto/darwin + window_manager: + :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos window_to_front: :path: Flutter/ephemeral/.symlinks/plugins/window_to_front/macos @@ -182,6 +198,7 @@ SPEC CHECKSUMS: package_info_plus: f0052d280d17aa382b932f399edf32507174e870 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 record_macos: 43194b6c06ca6f8fa132e2acea72b202b92a0f5b + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 SQLCipher: eb79c64049cb002b4e9fcb30edb7979bf4706dfc @@ -190,7 +207,9 @@ SPEC CHECKSUMS: video_compress: 752b161da855df2492dd1a8fa899743cc8fe9534 video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b + webcrypto: a5f5eb3e375cf0a99993e207e97cdcab5c94ce2e WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e + window_manager: b729e31d38fb04905235df9ea896128991cad99e window_to_front: 9e76fd432e36700a197dac86a0011e49c89abe0a PODFILE CHECKSUM: d0975b16fbdecb73b109d8fbc88aa77ffe4c7a8d From 765bff9e32af7cbe42867243618155872d16ae34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sun, 19 Oct 2025 13:00:47 +0200 Subject: [PATCH 10/21] chore: Follow up spaces UX --- lib/l10n/intl_en.arb | 11 +++- lib/pages/chat_list/chat_list_body.dart | 3 -- lib/pages/chat_list/chat_list_item.dart | 6 +-- lib/pages/chat_list/space_view.dart | 71 ++----------------------- lib/widgets/navigation_rail.dart | 23 +++----- 5 files changed, 25 insertions(+), 89 deletions(-) diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index f08cb8fa0..c89fd55ef 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -3383,5 +3383,14 @@ "moveToDifferentSpace": "Move to different space", "moveUp": "Move up", "moveDown": "Move down", - "removeFromSpaceDescription": "The chat will be removed from the space but still appear in your chat list." + "removeFromSpaceDescription": "The chat will be removed from the space but still appear in your chat list.", + "countChats": "{chats} chats", + "@countChats": { + "type": "String", + "placeholders": { + "chats": { + "type": "int" + } + } + } } diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index 248ba22d0..a789f3586 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -36,10 +36,7 @@ class ChatListViewBody extends StatelessWidget { spaceId: activeSpace, onBack: controller.clearActiveSpace, onChatTab: (room) => controller.onChatTap(room), - onChatContext: (room, context) => - controller.chatContextAction(room, context), activeChat: controller.activeChat, - toParentSpace: controller.setActiveSpace, ); } final spaces = client.rooms.where((r) => r.isSpace); diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 6a3ea9582..2fcb251a1 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -242,10 +242,8 @@ class ChatListItem extends StatelessWidget { Expanded( child: room.isSpace && room.membership == Membership.join ? Text( - L10n.of(context).countChatsAndCountParticipants( - room.spaceChildren.length, - (room.summary.mJoinedMemberCount ?? 1), - ), + L10n.of(context) + .countChats(room.spaceChildren.length), style: TextStyle(color: theme.colorScheme.outline), ) : typingText.isNotEmpty diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index d9f47938c..a16220c4e 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -38,9 +38,7 @@ enum SpaceActions { class SpaceView extends StatefulWidget { final String spaceId; final void Function() onBack; - final void Function(String spaceId) toParentSpace; final void Function(Room room) onChatTab; - final void Function(Room room, BuildContext context) onChatContext; final String? activeChat; const SpaceView({ @@ -48,8 +46,6 @@ class SpaceView extends StatefulWidget { required this.onBack, required this.onChatTab, required this.activeChat, - required this.toParentSpace, - required this.onChatContext, super.key, }); @@ -525,25 +521,6 @@ class _SpaceViewState extends State { .where((s) => s.hasRoomUpdate) .rateLimit(const Duration(seconds: 1)), builder: (context, snapshot) { - final childrenIds = room.spaceChildren - .map((c) => c.roomId) - .whereType() - .toSet(); - - final joinedRooms = Map.fromEntries( - room.client.rooms - .where((room) => childrenIds.remove(room.id)) - .map((room) => MapEntry(room.id, room)), - ); - - final joinedParents = room.spaceParents - .map((parent) { - final roomId = parent.roomId; - if (roomId == null) return null; - return room.client.getRoomById(roomId); - }) - .whereType() - .toList(); final filter = _filterController.text.trim().toLowerCase(); return CustomScrollView( slivers: [ @@ -575,47 +552,6 @@ class _SpaceViewState extends State { ), ), ), - SliverList.builder( - itemCount: joinedParents.length, - itemBuilder: (context, i) { - final displayname = - joinedParents[i].getLocalizedDisplayname(); - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 1, - ), - child: Material( - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - clipBehavior: Clip.hardEdge, - child: ListTile( - minVerticalPadding: 0, - leading: Icon( - Icons.adaptive.arrow_back_outlined, - size: 16, - ), - title: Row( - children: [ - Avatar( - mxContent: joinedParents[i].avatar, - name: displayname, - size: Avatar.defaultSize / 2, - borderRadius: BorderRadius.circular( - AppConfig.borderRadius / 4, - ), - ), - const SizedBox(width: 8), - Expanded(child: Text(displayname)), - ], - ), - onTap: () => - widget.toParentSpace(joinedParents[i].id), - ), - ), - ); - }, - ), SliverList.builder( itemCount: _discoveredChildren.length + 1, itemBuilder: (context, i) { @@ -643,7 +579,10 @@ class _SpaceViewState extends State { if (!displayname.toLowerCase().contains(filter)) { return const SizedBox.shrink(); } - final joinedRoom = joinedRooms[item.roomId]; + var joinedRoom = room.client.getRoomById(item.roomId); + if (joinedRoom?.membership == Membership.leave) { + joinedRoom = null; + } return Padding( padding: const EdgeInsets.symmetric( horizontal: 8, @@ -664,7 +603,7 @@ class _SpaceViewState extends State { contentPadding: const EdgeInsets.symmetric(horizontal: 8), onTap: joinedRoom != null - ? () => widget.onChatTab(joinedRoom) + ? () => widget.onChatTab(joinedRoom!) : () => _joinChildRoom(item), onLongPress: isAdmin ? () => _showSpaceChildEditMenu( diff --git a/lib/widgets/navigation_rail.dart b/lib/widgets/navigation_rail.dart index d27d84912..7cb4be43b 100644 --- a/lib/widgets/navigation_rail.dart +++ b/lib/widgets/navigation_rail.dart @@ -43,15 +43,8 @@ class SpacesNavigationRail extends StatelessWidget { .where((s) => s.hasRoomUpdate) .rateLimit(const Duration(seconds: 1)), builder: (context, _) { - final allSpaces = client.rooms.where((room) => room.isSpace); - final rootSpaces = allSpaces - .where( - (space) => !allSpaces.any( - (parentSpace) => parentSpace.spaceChildren - .any((child) => child.roomId == space.id), - ), - ) - .toList(); + final allSpaces = + client.rooms.where((room) => room.isSpace).toList(); return SizedBox( width: FluffyThemes.isColumnMode(context) @@ -62,7 +55,7 @@ class SpacesNavigationRail extends StatelessWidget { Expanded( child: ListView.builder( scrollDirection: Axis.vertical, - itemCount: rootSpaces.length + 2, + itemCount: allSpaces.length + 2, itemBuilder: (context, i) { if (i == 0) { return NaviRailItem( @@ -81,7 +74,7 @@ class SpacesNavigationRail extends StatelessWidget { ); } i--; - if (i == rootSpaces.length) { + if (i == allSpaces.length) { return NaviRailItem( isSelected: false, onTap: () => context.go('/rooms/newspace'), @@ -92,9 +85,9 @@ class SpacesNavigationRail extends StatelessWidget { toolTip: L10n.of(context).createNewSpace, ); } - final space = rootSpaces[i]; + final space = allSpaces[i]; final displayname = - rootSpaces[i].getLocalizedDisplayname( + allSpaces[i].getLocalizedDisplayname( MatrixLocals(L10n.of(context)), ); final spaceChildrenIds = @@ -102,11 +95,11 @@ class SpacesNavigationRail extends StatelessWidget { return NaviRailItem( toolTip: displayname, isSelected: activeSpaceId == space.id, - onTap: () => onGoToSpaceId(rootSpaces[i].id), + onTap: () => onGoToSpaceId(allSpaces[i].id), unreadBadgeFilter: (room) => spaceChildrenIds.contains(room.id), icon: Avatar( - mxContent: rootSpaces[i].avatar, + mxContent: allSpaces[i].avatar, name: displayname, border: BorderSide( width: 1, From 70c5f03e8ddfa3cc8bb8cfa32ac3219f6d615481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sun, 19 Oct 2025 13:16:39 +0200 Subject: [PATCH 11/21] chore: Better routing to space id --- lib/config/routes.dart | 2 ++ lib/pages/chat_list/chat_list.dart | 3 +++ lib/widgets/adaptive_dialogs/public_room_dialog.dart | 2 ++ 3 files changed, 7 insertions(+) diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 9871a013f..8f92d4c6d 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -114,6 +114,7 @@ abstract class AppRoutes { ? TwoColumnLayout( mainView: ChatList( activeChat: state.pathParameters['roomid'], + activeSpace: state.uri.queryParameters['spaceId'], displayNavigationRail: state.path?.startsWith('/rooms/settings') != true, ), @@ -132,6 +133,7 @@ abstract class AppRoutes { ? const EmptyPage() : ChatList( activeChat: state.pathParameters['roomid'], + activeSpace: state.uri.queryParameters['spaceId'], ), ), routes: [ diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index ac3df32ae..8bb8680af 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -72,11 +72,13 @@ extension LocalizedActiveFilter on ActiveFilter { class ChatList extends StatefulWidget { static BuildContext? contextForVoip; final String? activeChat; + final String? activeSpace; final bool displayNavigationRail; const ChatList({ super.key, required this.activeChat, + this.activeSpace, this.displayNavigationRail = false, }); @@ -400,6 +402,7 @@ class ChatListController extends State @override void initState() { _initReceiveSharingIntent(); + _activeSpaceId = widget.activeSpace; scrollController.addListener(_onScroll); _waitForFirstSync(); diff --git a/lib/widgets/adaptive_dialogs/public_room_dialog.dart b/lib/widgets/adaptive_dialogs/public_room_dialog.dart index add44c89d..6b11d45c9 100644 --- a/lib/widgets/adaptive_dialogs/public_room_dialog.dart +++ b/lib/widgets/adaptive_dialogs/public_room_dialog.dart @@ -66,6 +66,8 @@ class PublicRoomDialog extends StatelessWidget { if (chunk?.roomType != 'm.space' && !client.getRoomById(result.result!)!.isSpace) { context.go('/rooms/$roomId'); + } else { + context.go('/rooms?spaceId=$roomId'); } return; } From 9eb1f4fc1e7c3e0a3323560ef0f655206d8717db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sun, 19 Oct 2025 14:22:42 +0200 Subject: [PATCH 12/21] fix: Cupertino text dialogs --- .../adaptive_dialogs/dialog_text_field.dart | 27 ++-- .../show_text_input_dialog.dart | 135 +++++++++--------- 2 files changed, 81 insertions(+), 81 deletions(-) diff --git a/lib/widgets/adaptive_dialogs/dialog_text_field.dart b/lib/widgets/adaptive_dialogs/dialog_text_field.dart index c80147878..0d801bd4d 100644 --- a/lib/widgets/adaptive_dialogs/dialog_text_field.dart +++ b/lib/widgets/adaptive_dialogs/dialog_text_field.dart @@ -64,20 +64,23 @@ class DialogTextField extends StatelessWidget { ); case TargetPlatform.iOS: case TargetPlatform.macOS: + final placeholder = labelText ?? hintText; return Column( - mainAxisSize: MainAxisSize.min, children: [ - CupertinoTextField( - controller: controller, - obscureText: obscureText, - minLines: minLines, - maxLines: maxLines, - maxLength: maxLength, - keyboardType: keyboardType, - autocorrect: autocorrect, - prefix: prefixText != null ? Text(prefixText) : null, - suffix: suffixText != null ? Text(suffixText) : null, - placeholder: labelText ?? hintText, + SizedBox( + height: placeholder == null ? null : ((maxLines ?? 1) + 1) * 20, + child: CupertinoTextField( + controller: controller, + obscureText: obscureText, + minLines: minLines, + maxLines: maxLines, + maxLength: maxLength, + keyboardType: keyboardType, + autocorrect: autocorrect, + prefix: prefixText != null ? Text(prefixText) : null, + suffix: suffixText != null ? Text(suffixText) : null, + placeholder: placeholder, + ), ), if (errorText != null) Text( diff --git a/lib/widgets/adaptive_dialogs/show_text_input_dialog.dart b/lib/widgets/adaptive_dialogs/show_text_input_dialog.dart index 37d4fdee0..32fb5795c 100644 --- a/lib/widgets/adaptive_dialogs/show_text_input_dialog.dart +++ b/lib/widgets/adaptive_dialogs/show_text_input_dialog.dart @@ -34,76 +34,73 @@ Future showTextInputDialog({ useRootNavigator: useRootNavigator, builder: (context) { final error = ValueNotifier(null); - return ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 512), - child: AlertDialog.adaptive( - title: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 256), - child: Text(title), - ), - content: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 256), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (message != null) - SelectableLinkify( - text: message, - textScaleFactor: MediaQuery.textScalerOf(context).scale(1), - linkStyle: TextStyle( - color: Theme.of(context).colorScheme.primary, - decorationColor: Theme.of(context).colorScheme.primary, - ), - options: const LinkifyOptions(humanize: false), - onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), - ), - const SizedBox(height: 16), - ValueListenableBuilder( - valueListenable: error, - builder: (context, error, _) { - return DialogTextField( - hintText: hintText, - errorText: error, - labelText: labelText, - controller: controller, - initialText: initialText, - prefixText: prefixText, - suffixText: suffixText, - minLines: minLines, - maxLines: maxLines, - maxLength: maxLength, - keyboardType: keyboardType, - ); - }, - ), - ], - ), - ), - actions: [ - AdaptiveDialogAction( - onPressed: () => Navigator.of(context).pop(null), - child: Text(cancelLabel ?? L10n.of(context).cancel), - ), - AdaptiveDialogAction( - onPressed: () { - final input = controller.text; - final errorText = validator?.call(input); - if (errorText != null) { - error.value = errorText; - return; - } - Navigator.of(context).pop(input); - }, - autofocus: true, - child: Text( - okLabel ?? L10n.of(context).ok, - style: isDestructive - ? TextStyle(color: Theme.of(context).colorScheme.error) - : null, - ), - ), - ], + return AlertDialog.adaptive( + title: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 256), + child: Text(title), ), + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 256), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (message != null) + SelectableLinkify( + text: message, + textScaleFactor: MediaQuery.textScalerOf(context).scale(1), + linkStyle: TextStyle( + color: Theme.of(context).colorScheme.primary, + decorationColor: Theme.of(context).colorScheme.primary, + ), + options: const LinkifyOptions(humanize: false), + onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), + ), + const SizedBox(height: 16), + ValueListenableBuilder( + valueListenable: error, + builder: (context, error, _) { + return DialogTextField( + hintText: hintText, + errorText: error, + labelText: labelText, + controller: controller, + initialText: initialText, + prefixText: prefixText, + suffixText: suffixText, + minLines: minLines, + maxLines: maxLines, + maxLength: maxLength, + keyboardType: keyboardType, + ); + }, + ), + ], + ), + ), + actions: [ + AdaptiveDialogAction( + onPressed: () => Navigator.of(context).pop(null), + child: Text(cancelLabel ?? L10n.of(context).cancel), + ), + AdaptiveDialogAction( + onPressed: () { + final input = controller.text; + final errorText = validator?.call(input); + if (errorText != null) { + error.value = errorText; + return; + } + Navigator.of(context).pop(input); + }, + autofocus: true, + child: Text( + okLabel ?? L10n.of(context).ok, + style: isDestructive + ? TextStyle(color: Theme.of(context).colorScheme.error) + : null, + ), + ), + ], ); }, ); From d6dcbe04216ba702302d17c7f3f087c071dfcd65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sun, 19 Oct 2025 14:23:05 +0200 Subject: [PATCH 13/21] feat: Add support for restricted join rule --- lib/l10n/intl_en.arb | 18 +++++++++++++ .../chat_access_settings_controller.dart | 27 ++++++++++++++++--- .../chat_access_settings_page.dart | 19 ++++++++++--- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index c89fd55ef..7a285f1ad 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -3392,5 +3392,23 @@ "type": "int" } } + }, + "spaceMemberOf": "Space member of {spaces}", + "@spaceMemberOf": { + "type": "String", + "placeholders": { + "spaces": { + "type": "String" + } + } + }, + "spaceMemberOfCanKnock": "Space member of {spaces} can knock", + "@spaceMemberOfCanKnock": { + "type": "String", + "placeholders": { + "spaces": { + "type": "String" + } + } } } diff --git a/lib/pages/chat_access_settings/chat_access_settings_controller.dart b/lib/pages/chat_access_settings/chat_access_settings_controller.dart index af48595ca..5c13623e5 100644 --- a/lib/pages/chat_access_settings/chat_access_settings_controller.dart +++ b/lib/pages/chat_access_settings/chat_access_settings_controller.dart @@ -26,6 +26,16 @@ class ChatAccessSettingsController extends State { bool historyVisibilityLoading = false; bool guestAccessLoading = false; Room get room => Matrix.of(context).client.getRoomById(widget.roomId)!; + Set get knownSpaceParents => { + ...room.client.rooms.where( + (space) => + space.isSpace && + space.spaceChildren.any((child) => child.roomId == room.id), + ), + ...room.spaceParents + .map((parent) => room.client.getRoomById(parent.roomId ?? '')) + .whereType(), + }; String get roomVersion => room @@ -46,9 +56,12 @@ class ChatAccessSettingsController extends State { joinRules.remove(JoinRules.knock); } - // Not yet supported in FluffyChat: - joinRules.remove(JoinRules.restricted); - joinRules.remove(JoinRules.knockRestricted); + if (knownSpaceParents.isEmpty) { + joinRules.remove(JoinRules.restricted); + if (roomVersionInt != null && roomVersionInt <= 6) { + joinRules.remove(JoinRules.knockRestricted); + } + } // If an unsupported join rule is the current join rule, display it: final currentJoinRule = room.joinRules; @@ -64,7 +77,13 @@ class ChatAccessSettingsController extends State { }); try { - await room.setJoinRules(newJoinRules); + await room.setJoinRules( + newJoinRules, + allowConditionRoomId: {JoinRules.restricted, JoinRules.knockRestricted} + .contains(newJoinRules) + ? knownSpaceParents.first.id + : null, + ); } catch (e, s) { Logs().w('Unable to change join rules', e, s); if (mounted) { diff --git a/lib/pages/chat_access_settings/chat_access_settings_page.dart b/lib/pages/chat_access_settings/chat_access_settings_page.dart index 5f679cffb..616d13934 100644 --- a/lib/pages/chat_access_settings/chat_access_settings_page.dart +++ b/lib/pages/chat_access_settings/chat_access_settings_page.dart @@ -88,7 +88,10 @@ class ChatAccessSettingsPageView extends StatelessWidget { enabled: !controller.joinRulesLoading && room.canChangeJoinRules, title: Text( - joinRule.localizedString(L10n.of(context)), + joinRule.localizedString( + L10n.of(context), + controller.knownSpaceParents, + ), ), value: joinRule, ), @@ -280,7 +283,7 @@ class _AliasListTile extends StatelessWidget { } extension JoinRulesDisplayString on JoinRules { - String localizedString(L10n l10n) { + String localizedString(L10n l10n, Set spaceParents) { switch (this) { case JoinRules.public: return l10n.anyoneCanJoin; @@ -291,9 +294,17 @@ extension JoinRulesDisplayString on JoinRules { case JoinRules.private: return l10n.noOneCanJoin; case JoinRules.restricted: - return l10n.restricted; + return l10n.spaceMemberOf( + spaceParents + .map((space) => space.getLocalizedDisplayname(MatrixLocals(l10n))) + .join(', '), + ); case JoinRules.knockRestricted: - return l10n.knockRestricted; + return l10n.spaceMemberOfCanKnock( + spaceParents + .map((space) => space.getLocalizedDisplayname(MatrixLocals(l10n))) + .join(', '), + ); } } } From 8bf46a38783c2000ad1cbcbfb6a7ab8757dcc1c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sun, 19 Oct 2025 14:38:19 +0200 Subject: [PATCH 14/21] chore: Correctly remove knockRestricted from possible join rules --- .../chat_access_settings_controller.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/pages/chat_access_settings/chat_access_settings_controller.dart b/lib/pages/chat_access_settings/chat_access_settings_controller.dart index 5c13623e5..b39b3daf1 100644 --- a/lib/pages/chat_access_settings/chat_access_settings_controller.dart +++ b/lib/pages/chat_access_settings/chat_access_settings_controller.dart @@ -54,13 +54,12 @@ class ChatAccessSettingsController extends State { // Knock is only supported for rooms up from version 7: if (roomVersionInt != null && roomVersionInt <= 6) { joinRules.remove(JoinRules.knock); + joinRules.remove(JoinRules.knockRestricted); } if (knownSpaceParents.isEmpty) { joinRules.remove(JoinRules.restricted); - if (roomVersionInt != null && roomVersionInt <= 6) { - joinRules.remove(JoinRules.knockRestricted); - } + joinRules.remove(JoinRules.knockRestricted); } // If an unsupported join rule is the current join rule, display it: From fdb5826d095b3dfcf32a380a4d6df6afbd6207b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sun, 19 Oct 2025 16:16:32 +0200 Subject: [PATCH 15/21] build: Remove dependency for telephony --- android/app/src/main/AndroidManifest.xml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 53286e1cf..7ff6ba64e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -17,7 +17,6 @@ - @@ -125,16 +124,6 @@ android:foregroundServiceType="camera|microphone|mediaProjection"> - - - - - - From 4f0ed3e93fdb595a841b972882da62a6fedf52aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sun, 19 Oct 2025 17:40:47 +0200 Subject: [PATCH 16/21] chore: Follow up notification actions --- lib/utils/push_helper.dart | 40 ++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/utils/push_helper.dart b/lib/utils/push_helper.dart index 6d75d568c..6a9dd6e4e 100644 --- a/lib/utils/push_helper.dart +++ b/lib/utils/push_helper.dart @@ -278,25 +278,27 @@ Future _tryPushHelper( importance: Importance.high, priority: Priority.max, groupKey: event.room.spaceParents.firstOrNull?.roomId ?? 'rooms', - actions: [ - AndroidNotificationAction( - FluffyChatNotificationActions.reply.name, - l10n.reply, - inputs: [ - AndroidNotificationActionInput( - label: l10n.writeAMessage, - ), - ], - cancelNotification: false, - allowGeneratedReplies: true, - semanticAction: SemanticAction.reply, - ), - AndroidNotificationAction( - FluffyChatNotificationActions.markAsRead.name, - l10n.markAsRead, - semanticAction: SemanticAction.markAsRead, - ), - ], + actions: event.type == EventTypes.RoomMember + ? null + : [ + AndroidNotificationAction( + FluffyChatNotificationActions.reply.name, + l10n.reply, + inputs: [ + AndroidNotificationActionInput( + label: l10n.writeAMessage, + ), + ], + cancelNotification: false, + allowGeneratedReplies: true, + semanticAction: SemanticAction.reply, + ), + AndroidNotificationAction( + FluffyChatNotificationActions.markAsRead.name, + l10n.markAsRead, + semanticAction: SemanticAction.markAsRead, + ), + ], ); const iOSPlatformChannelSpecifics = DarwinNotificationDetails(); final platformChannelSpecifics = NotificationDetails( From 4c357f6249d2543308b61acdc2049b65c6e8ce6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sun, 19 Oct 2025 19:36:47 +0200 Subject: [PATCH 17/21] refactor: Use AppSettings enum based configuration everywhere and fix load from json on web --- README.md | 58 +++++++- config.sample.json | 35 +++-- integration_test/app_test.dart | 3 +- lib/config/app_config.dart | 98 +++---------- lib/config/setting_keys.dart | 134 +++++++++++------- lib/config/themes.dart | 3 +- lib/main.dart | 15 +- lib/pages/chat/chat.dart | 29 ++-- lib/pages/chat/chat_input_row.dart | 11 +- lib/pages/chat/chat_view.dart | 4 +- lib/pages/chat/events/cute_events.dart | 4 +- lib/pages/chat/events/image_bubble.dart | 9 +- lib/pages/chat/events/message.dart | 7 +- lib/pages/chat/events/message_content.dart | 12 +- .../chat/events/message_download_content.dart | 7 +- lib/pages/chat/events/reply_content.dart | 4 +- lib/pages/chat/events/state_message.dart | 3 +- lib/pages/chat/events/video_player.dart | 9 +- lib/pages/chat/input_bar.dart | 3 +- lib/pages/chat/recording_view_model.dart | 15 +- lib/pages/chat_list/chat_list.dart | 11 +- lib/pages/chat_list/chat_list_body.dart | 9 +- lib/pages/chat_list/chat_list_view.dart | 4 +- .../homeserver_picker/homeserver_picker.dart | 5 +- .../homeserver_picker_view.dart | 3 +- lib/pages/settings/settings_view.dart | 4 +- .../settings_chat/settings_chat_view.dart | 29 +--- .../settings_security/settings_security.dart | 1 - .../settings_security_view.dart | 8 +- lib/pages/settings_style/settings_style.dart | 10 +- .../settings_style/settings_style_view.dart | 22 ++- lib/utils/background_push.dart | 25 ++-- lib/utils/client_manager.dart | 7 +- lib/utils/init_with_restore.dart | 5 +- .../filtered_timeline_extension.dart | 11 +- .../cipher.dart | 6 +- .../notification_background_handler.dart | 9 +- lib/utils/platform_infos.dart | 5 +- lib/utils/push_helper.dart | 6 +- lib/widgets/app_lock.dart | 3 +- lib/widgets/config_viewer.dart | 16 +-- lib/widgets/fluffy_chat_app.dart | 4 +- lib/widgets/layouts/login_scaffold.dart | 3 +- .../local_notifications_extension.dart | 6 +- lib/widgets/matrix.dart | 75 +--------- lib/widgets/settings_switch_list_tile.dart | 13 +- 46 files changed, 359 insertions(+), 404 deletions(-) diff --git a/README.md b/README.md index 884e220f8..e33536a27 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,63 @@ Please visit the website for installation instructions: # How to build -Please visit the [Wiki](https://github.com/krille-chan/fluffychat/wiki) for build instructions: +1. To build FluffyChat you need [Flutter](https://flutter.dev) and [Rust](https://www.rust-lang.org/tools/install) -- https://github.com/krille-chan/fluffychat/wiki/How-To-Build +2. Clone the repo: +``` +git clone https://github.com/krille-chan/fluffychat.git +cd fluffychat +``` +3. Choose your target platform below and enable support for it. +3.1 If you want, enable Googles Firebase Cloud Messaging: + +`git apply ./scripts/enable-android-google-services.patch` + +4. Debug with: `flutter run` + +### Android + +* Build with: `flutter build apk` + +### iOS / iPadOS + +* Have a Mac with Xcode installed, and set up for Xcode-managed app signing +* If you want automatic app installation to connected devices, make sure you have Apple Configurator installed, with the Automation Tools (`cfgutil`) enabled +* Set a few environment variables + * FLUFFYCHAT_NEW_TEAM: the Apple Developer team that your certificates should live under + * FLUFFYCHAT_NEW_GROUP: the group you want App IDs and such to live under (ie: com.example.fluffychat) + * FLUFFYCHAT_INSTALL_IPA: set to `1` if you want the IPA to be deployed to connected devices after building, otherwise unset +* Run `./scripts/build-ios.sh` + +### Web + +* Build with: +```bash +./scripts/prepare-web.sh # To install Vodozemac +flutter build web --release +``` + +* Optionally configure by serving a `config.json` at the same path as fluffychat. + An example can be found at `config.sample.json`. All values there are optional. + **Please only the values, you really need**. If you e.g. only want + to change the default homeserver, then only modify the `defaultHomeserver` key. + +### Desktop (Linux, Windows, macOS) + +* Enable Desktop support in Flutter: https://flutter.dev/desktop + +#### Install custom dependencies (Linux) + +```bash +sudo apt install libjsoncpp1 libsecret-1-dev libsecret-1-0 librhash0 libwebkit2gtk-4.0-dev +``` + +* Build with one of these: +```bash +flutter build linux --release +flutter build windows --release +flutter build macos --release +``` # Special thanks diff --git a/config.sample.json b/config.sample.json index ff37ec238..5aa0a4bb0 100644 --- a/config.sample.json +++ b/config.sample.json @@ -1,10 +1,29 @@ { - "application_name": "FluffyChat", - "application_welcome_message": null, - "default_homeserver": "matrix.org", - "web_base_url": "https://fluffychat.im/web", - "privacy_url": "https://fluffychat.im/en/privacy.html", - "render_html": false, - "hide_redacted_events": false, - "hide_unknown_events": false + "applicationName": "FluffyChat", + "defaultHomeserver": "matrix.org", + "privacyUrl": "https://github.com/krille-chan/fluffychat/blob/main/PRIVACY.md", + "audioRecordingNumChannels": 1, + "audioRecordingAutoGain": true, + "audioRecordingEchoCancel": false, + "audioRecordingNoiseSuppress": true, + "audioRecordingBitRate": 64000, + "audioRecordingSamplingRate": 44100, + "renderHtml": true, + "fontSizeFactor": 1, + "hideRedactedEvents": false, + "hideUnknownEvents": true, + "separateChatTypes": false, + "autoplayImages": true, + "sendTypingNotifications": true, + "sendPublicReadReceipts": true, + "swipeRightToLeftToReply": true, + "sendOnEnter": false, + "showPresences": true, + "displayNavigationRail": false, + "experimentalVoip": false, + "shareKeysWith": "all", + "noEncryptionWarningShown": false, + "displayChatDetailsColumn": false, + "colorSchemeSeedInt": 4283835834, + "enableSoftLogout": false } \ No newline at end of file diff --git a/integration_test/app_test.dart b/integration_test/app_test.dart index 4b49aac6b..3a0139365 100644 --- a/integration_test/app_test.dart +++ b/integration_test/app_test.dart @@ -1,4 +1,3 @@ -import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/pages/chat/chat_view.dart'; import 'package:fluffychat/pages/chat_list/chat_list_body.dart'; import 'package:fluffychat/pages/chat_list/search_title.dart'; @@ -25,7 +24,7 @@ void main() { () async { // this random dialog popping up is super hard to cover in tests SharedPreferences.setMockInitialValues({ - SettingKeys.showNoGoogle: false, + 'chat.fluffy.show_no_google': false, }); }, ); diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index a80d16e14..e6a4b6c0c 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -1,32 +1,25 @@ import 'dart:ui'; -import 'package:matrix/matrix.dart'; - abstract class AppConfig { - static String _applicationName = 'FluffyChat'; - - static String get applicationName => _applicationName; - static String? _applicationWelcomeMessage; - - static String? get applicationWelcomeMessage => _applicationWelcomeMessage; - static String _defaultHomeserver = 'matrix.org'; - - static String get defaultHomeserver => _defaultHomeserver; - static double fontSizeFactor = 1; - static const Color chatColor = primaryColor; - static Color? colorSchemeSeed = primaryColor; - static const double messageFontSize = 16.0; - static const bool allowOtherHomeservers = true; - static const bool enableRegistration = true; + // Const and final configuration values (immutable) static const Color primaryColor = Color(0xFF5625BA); static const Color primaryColorLight = Color(0xFFCCBDEA); static const Color secondaryColor = Color(0xFF41a2bc); - static String _privacyUrl = - 'https://github.com/krille-chan/fluffychat/blob/main/PRIVACY.md'; - static const Set defaultReactions = {'👍', '❤️', '😂', '😮', '😢'}; + static const Color chatColor = primaryColor; + static const double messageFontSize = 16.0; + static const bool allowOtherHomeservers = true; + static const bool enableRegistration = true; + static const bool hideTypingUsernames = false; + + static const String inviteLinkPrefix = 'https://matrix.to/#/'; + static const String deepLinkPrefix = 'im.fluffychat://chat/'; + static const String schemePrefix = 'matrix:'; + static const String pushNotificationsChannelId = 'fluffychat_push'; + static const String pushNotificationsAppId = 'chat.fluffy.fluffychat'; + static const double borderRadius = 18.0; + static const double columnWidth = 360.0; - static String get privacyUrl => _privacyUrl; static const String website = 'https://fluffychat.im'; static const String enablePushTutorial = 'https://github.com/krille-chan/fluffychat/wiki/Push-Notifications-without-Google-Services'; @@ -36,80 +29,25 @@ abstract class AppConfig { 'https://github.com/krille-chan/fluffychat/wiki/How-to-Find-Users-in-FluffyChat'; static const String appId = 'im.fluffychat.FluffyChat'; static const String appOpenUrlScheme = 'im.fluffychat'; - static String _webBaseUrl = 'https://fluffychat.im/web'; - static String get webBaseUrl => _webBaseUrl; static const String sourceCodeUrl = 'https://github.com/krille-chan/fluffychat'; static const String supportUrl = 'https://github.com/krille-chan/fluffychat/issues'; static const String changelogUrl = 'https://github.com/krille-chan/fluffychat/blob/main/CHANGELOG.md'; + + static const Set defaultReactions = {'👍', '❤️', '😂', '😮', '😢'}; + static final Uri newIssueUrl = Uri( scheme: 'https', host: 'github.com', path: '/krille-chan/fluffychat/issues/new', ); - static bool renderHtml = true; - static bool hideRedactedEvents = false; - static bool hideUnknownEvents = true; - static bool separateChatTypes = false; - static bool autoplayImages = true; - static bool sendTypingNotifications = true; - static bool sendPublicReadReceipts = true; - static bool swipeRightToLeftToReply = true; - static bool? sendOnEnter; - static bool showPresences = true; - static bool displayNavigationRail = false; - static bool experimentalVoip = false; - static const bool hideTypingUsernames = false; - static const String inviteLinkPrefix = 'https://matrix.to/#/'; - static const String deepLinkPrefix = 'im.fluffychat://chat/'; - static const String schemePrefix = 'matrix:'; - static const String pushNotificationsChannelId = 'fluffychat_push'; - static const String pushNotificationsAppId = 'chat.fluffy.fluffychat'; - static const double borderRadius = 18.0; - static const double columnWidth = 360.0; + static final Uri homeserverList = Uri( scheme: 'https', host: 'servers.joinmatrix.org', path: 'servers.json', ); - - static void loadFromJson(Map json) { - if (json['chat_color'] != null) { - try { - colorSchemeSeed = Color(json['chat_color']); - } catch (e) { - Logs().w( - 'Invalid color in config.json! Please make sure to define the color in this format: "0xffdd0000"', - e, - ); - } - } - if (json['application_name'] is String) { - _applicationName = json['application_name']; - } - if (json['application_welcome_message'] is String) { - _applicationWelcomeMessage = json['application_welcome_message']; - } - if (json['default_homeserver'] is String) { - _defaultHomeserver = json['default_homeserver']; - } - if (json['privacy_url'] is String) { - _privacyUrl = json['privacy_url']; - } - if (json['web_base_url'] is String) { - _webBaseUrl = json['web_base_url']; - } - if (json['render_html'] is bool) { - renderHtml = json['render_html']; - } - if (json['hide_redacted_events'] is bool) { - hideRedactedEvents = json['hide_redacted_events']; - } - if (json['hide_unknown_events'] is bool) { - hideUnknownEvents = json['hide_unknown_events']; - } - } } diff --git a/lib/config/setting_keys.dart b/lib/config/setting_keys.dart index 484e48bc0..084e24007 100644 --- a/lib/config/setting_keys.dart +++ b/lib/config/setting_keys.dart @@ -1,40 +1,12 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; + +import 'package:http/http.dart' as http; +import 'package:matrix/matrix_api_lite/utils/logs.dart'; import 'package:shared_preferences/shared_preferences.dart'; -abstract class SettingKeys { - static const String renderHtml = 'chat.fluffy.renderHtml'; - static const String hideRedactedEvents = 'chat.fluffy.hideRedactedEvents'; - static const String hideUnknownEvents = 'chat.fluffy.hideUnknownEvents'; - static const String hideUnimportantStateEvents = - 'chat.fluffy.hideUnimportantStateEvents'; - static const String separateChatTypes = 'chat.fluffy.separateChatTypes'; - static const String sentry = 'sentry'; - static const String theme = 'theme'; - static const String amoledEnabled = 'amoled_enabled'; - static const String codeLanguage = 'code_language'; - static const String showNoGoogle = 'chat.fluffy.show_no_google'; - static const String fontSizeFactor = 'chat.fluffy.font_size_factor'; - static const String showNoPid = 'chat.fluffy.show_no_pid'; - static const String databasePassword = 'database-password'; - static const String appLockKey = 'chat.fluffy.app_lock'; - static const String unifiedPushRegistered = - 'chat.fluffy.unifiedpush.registered'; - static const String unifiedPushEndpoint = 'chat.fluffy.unifiedpush.endpoint'; - static const String ownStatusMessage = 'chat.fluffy.status_msg'; - static const String dontAskForBootstrapKey = - 'chat.fluffychat.dont_ask_bootstrap'; - static const String autoplayImages = 'chat.fluffy.autoplay_images'; - static const String sendTypingNotifications = - 'chat.fluffy.send_typing_notifications'; - static const String sendPublicReadReceipts = - 'chat.fluffy.send_public_read_receipts'; - static const String sendOnEnter = 'chat.fluffy.send_on_enter'; - static const String swipeRightToLeftToReply = - 'chat.fluffy.swipeRightToLeftToReply'; - static const String experimentalVoip = 'chat.fluffy.experimental_voip'; - static const String showPresences = 'chat.fluffy.show_presences'; - static const String displayNavigationRail = - 'chat.fluffy.display_navigation_rail'; -} +import 'package:fluffychat/utils/platform_infos.dart'; enum AppSettings { textMessageMaxLength('textMessageMaxLength', 16384), @@ -44,6 +16,9 @@ enum AppSettings { audioRecordingNoiseSuppress('audioRecordingNoiseSuppress', true), audioRecordingBitRate('audioRecordingBitRate', 64000), audioRecordingSamplingRate('audioRecordingSamplingRate', 44100), + showNoGoogle('chat.fluffy.show_no_google', false), + unifiedPushRegistered('chat.fluffy.unifiedpush.registered', false), + unifiedPushEndpoint('chat.fluffy.unifiedpush.endpoint', ''), pushNotificationsGatewayUrl( 'pushNotificationsGatewayUrl', 'https://push.fluffychat.im/_matrix/push/v1/notify', @@ -52,6 +27,19 @@ enum AppSettings { 'pushNotificationsPusherFormat', 'event_id_only', ), + renderHtml('chat.fluffy.renderHtml', true), + fontSizeFactor('chat.fluffy.font_size_factor', 1.0), + hideRedactedEvents('chat.fluffy.hideRedactedEvents', false), + hideUnknownEvents('chat.fluffy.hideUnknownEvents', true), + separateChatTypes('chat.fluffy.separateChatTypes', false), + autoplayImages('chat.fluffy.autoplay_images', true), + sendTypingNotifications('chat.fluffy.send_typing_notifications', true), + sendPublicReadReceipts('chat.fluffy.send_public_read_receipts', true), + swipeRightToLeftToReply('chat.fluffy.swipeRightToLeftToReply', true), + sendOnEnter('chat.fluffy.send_on_enter', false), + showPresences('chat.fluffy.show_presences', true), + displayNavigationRail('chat.fluffy.display_navigation_rail', false), + experimentalVoip('chat.fluffy.experimental_voip', false), shareKeysWith('chat.fluffy.share_keys_with_2', 'all'), noEncryptionWarningShown( 'chat.fluffy.no_encryption_warning_shown', @@ -61,40 +49,88 @@ enum AppSettings { 'chat.fluffy.display_chat_details_column', false, ), + // AppConfig-mirrored settings + applicationName('chat.fluffy.application_name', 'FluffyChat'), + defaultHomeserver('chat.fluffy.default_homeserver', 'matrix.org'), + privacyUrl( + 'chat.fluffy.privacy_url', + 'https://github.com/krille-chan/fluffychat/blob/main/PRIVACY.md', + ), + // colorSchemeSeed stored as ARGB int + colorSchemeSeedInt( + 'chat.fluffy.color_scheme_seed', + 0xFF5625BA, + ), enableSoftLogout('chat.fluffy.enable_soft_logout', false); final String key; final T defaultValue; const AppSettings(this.key, this.defaultValue); + + static late final SharedPreferences store; + + static Future init({loadWebConfigFile = true}) async { + final store = AppSettings.store = await SharedPreferences.getInstance(); + + if (store.getBool(AppSettings.sendOnEnter.key) == null) { + await store.setBool(AppSettings.sendOnEnter.key, !PlatformInfos.isMobile); + } + + if (kIsWeb && loadWebConfigFile) { + try { + final configJsonString = + utf8.decode((await http.get(Uri.parse('config.json'))).bodyBytes); + final configJson = + json.decode(configJsonString) as Map; + for (final setting in AppSettings.values) { + if (store.get(setting.key) != null) continue; + final configValue = configJson[setting.key]; + if (configValue == null) continue; + if (configValue is bool) { + await store.setBool(setting.key, configValue); + } + if (configValue is String) { + await store.setString(setting.key, configValue); + } + if (configValue is int) { + await store.setInt(setting.key, configValue); + } + if (configValue is double) { + await store.setDouble(setting.key, configValue); + } + } + } on FormatException catch (_) { + Logs().v('[ConfigLoader] config.json not found'); + } catch (e) { + Logs().v('[ConfigLoader] config.json not found', e); + } + } + + return store; + } } extension AppSettingsBoolExtension on AppSettings { - bool getItem(SharedPreferences store) => store.getBool(key) ?? defaultValue; + bool get value => AppSettings.store.getBool(key) ?? defaultValue; - Future setItem(SharedPreferences store, bool value) => - store.setBool(key, value); + Future setItem(bool value) => AppSettings.store.setBool(key, value); } extension AppSettingsStringExtension on AppSettings { - String getItem(SharedPreferences store) => - store.getString(key) ?? defaultValue; + String get value => AppSettings.store.getString(key) ?? defaultValue; - Future setItem(SharedPreferences store, String value) => - store.setString(key, value); + Future setItem(String value) => AppSettings.store.setString(key, value); } extension AppSettingsIntExtension on AppSettings { - int getItem(SharedPreferences store) => store.getInt(key) ?? defaultValue; + int get value => AppSettings.store.getInt(key) ?? defaultValue; - Future setItem(SharedPreferences store, int value) => - store.setInt(key, value); + Future setItem(int value) => AppSettings.store.setInt(key, value); } extension AppSettingsDoubleExtension on AppSettings { - double getItem(SharedPreferences store) => - store.getDouble(key) ?? defaultValue; + double get value => AppSettings.store.getDouble(key) ?? defaultValue; - Future setItem(SharedPreferences store, double value) => - store.setDouble(key, value); + Future setItem(double value) => AppSettings.store.setDouble(key, value); } diff --git a/lib/config/themes.dart b/lib/config/themes.dart index e7e516008..b409dc247 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'app_config.dart'; abstract class FluffyThemes { @@ -45,7 +46,7 @@ abstract class FluffyThemes { ]) { final colorScheme = ColorScheme.fromSeed( brightness: brightness, - seedColor: seed ?? AppConfig.colorSchemeSeed ?? AppConfig.primaryColor, + seedColor: seed ?? Color(AppSettings.colorSchemeSeedInt.value), ); final isColumnMode = FluffyThemes.isColumnMode(context); return ThemeData( diff --git a/lib/main.dart b/lib/main.dart index f3be9dc52..9f9c350c2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,7 +6,6 @@ import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vod; import 'package:matrix/matrix.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'config/setting_keys.dart'; @@ -14,17 +13,17 @@ import 'utils/background_push.dart'; import 'widgets/fluffy_chat_app.dart'; void main() async { - Logs().i('Welcome to ${AppConfig.applicationName} <3'); - // Our background push shared isolate accesses flutter-internal things very early in the startup proccess // To make sure that the parts of flutter needed are started up already, we need to ensure that the // widget bindings are initialized already. WidgetsFlutterBinding.ensureInitialized(); + final store = await AppSettings.init(); + Logs().i('Welcome to ${AppSettings.applicationName.value} <3'); + await vod.init(wasmPath: './assets/assets/vodozemac/'); Logs().nativeColors = !PlatformInfos.isIOS; - final store = await SharedPreferences.getInstance(); final clients = await ClientManager.getClients(store: store); // If the app starts in detached mode, we assume that it is in @@ -44,14 +43,14 @@ void main() async { // To start the flutter engine afterwards we add an custom observer. WidgetsBinding.instance.addObserver(AppStarter(clients, store)); Logs().i( - '${AppConfig.applicationName} started in background-fetch mode. No GUI will be created unless the app is no longer detached.', + '${AppSettings.applicationName.value} started in background-fetch mode. No GUI will be created unless the app is no longer detached.', ); return; } // Started in foreground mode. Logs().i( - '${AppConfig.applicationName} started in foreground mode. Rendering GUI...', + '${AppSettings.applicationName.value} started in foreground mode. Rendering GUI...', ); await startGui(clients, store); } @@ -63,7 +62,7 @@ Future startGui(List clients, SharedPreferences store) async { if (PlatformInfos.isMobile) { try { pin = - await const FlutterSecureStorage().read(key: SettingKeys.appLockKey); + await const FlutterSecureStorage().read(key: 'chat.fluffy.app_lock'); } catch (e, s) { Logs().d('Unable to read PIN from Secure storage', e, s); } @@ -92,7 +91,7 @@ class AppStarter with WidgetsBindingObserver { if (state == AppLifecycleState.detached) return; Logs().i( - '${AppConfig.applicationName} switches from the detached background-fetch mode to ${state.name} mode. Rendering GUI...', + '${AppSettings.applicationName.value} switches from the detached background-fetch mode to ${state.name} mode. Rendering GUI...', ); // Switching to foreground mode needs to reenable send online sync presence. for (final client in clients) { diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 2490eea4c..125552c43 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -13,10 +13,8 @@ import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:universal_html/html.dart' as html; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; @@ -224,7 +222,7 @@ class ChatController extends State } void _loadDraft() async { - final prefs = await SharedPreferences.getInstance(); + final prefs = Matrix.of(context).store; final draft = prefs.getString('draft_$roomId'); if (draft != null && draft.isNotEmpty) { sendController.text = draft; @@ -274,7 +272,7 @@ class ChatController extends State KeyEventResult _customEnterKeyHandling(FocusNode node, KeyEvent evt) { if (!HardwareKeyboard.instance.isShiftPressed && evt.logicalKey.keyLabel == 'Enter' && - (AppConfig.sendOnEnter ?? !PlatformInfos.isMobile)) { + AppSettings.sendOnEnter.value) { if (evt is KeyDownEvent) { send(); } @@ -325,7 +323,7 @@ class ChatController extends State WidgetsBinding.instance.addPostFrameCallback(_shareItems); super.initState(); _displayChatDetailsColumn = ValueNotifier( - AppSettings.displayChatDetailsColumn.getItem(Matrix.of(context).store), + AppSettings.displayChatDetailsColumn.value, ); sendingClient = Matrix.of(context).client; @@ -365,7 +363,9 @@ class ChatController extends State var readMarkerEventIndex = readMarkerEventId.isEmpty ? -1 : timeline!.events - .filterByVisibleInGui(exceptionEventId: readMarkerEventId) + .filterByVisibleInGui( + exceptionEventId: readMarkerEventId, + ) .indexWhere((e) => e.eventId == readMarkerEventId); // Read marker is existing but not found in first events. Try a single @@ -373,7 +373,9 @@ class ChatController extends State if (readMarkerEventId.isNotEmpty && readMarkerEventIndex == -1) { await timeline?.requestHistory(historyCount: _loadHistoryCount); readMarkerEventIndex = timeline!.events - .filterByVisibleInGui(exceptionEventId: readMarkerEventId) + .filterByVisibleInGui( + exceptionEventId: readMarkerEventId, + ) .indexWhere((e) => e.eventId == readMarkerEventId); } @@ -492,7 +494,7 @@ class ChatController extends State _setReadMarkerFuture = timeline .setReadMarker( eventId: eventId, - public: AppConfig.sendPublicReadReceipts, + public: AppSettings.sendPublicReadReceipts.value, ) .then((_) { _setReadMarkerFuture = null; @@ -542,7 +544,7 @@ class ChatController extends State Future send() async { if (sendController.text.trim().isEmpty) return; _storeInputTimeoutTimer?.cancel(); - final prefs = await SharedPreferences.getInstance(); + final prefs = Matrix.of(context).store; prefs.remove('draft_$roomId'); var parseCommands = true; @@ -960,7 +962,9 @@ class ChatController extends State final eventIndex = foundEvent == null ? -1 : timeline!.events - .filterByVisibleInGui(exceptionEventId: eventId) + .filterByVisibleInGui( + exceptionEventId: eventId, + ) .indexOf(foundEvent); if (eventIndex == -1) { @@ -1203,7 +1207,7 @@ class ChatController extends State _storeInputTimeoutTimer?.cancel(); _storeInputTimeoutTimer = Timer(_storeInputTimeout, () async { - final prefs = await SharedPreferences.getInstance(); + final prefs = Matrix.of(context).store; await prefs.setString('draft_$roomId', text); }); if (text.endsWith(' ') && Matrix.of(context).hasComplexBundles) { @@ -1220,7 +1224,7 @@ class ChatController extends State } } } - if (AppConfig.sendTypingNotifications) { + if (AppSettings.sendTypingNotifications.value) { typingCoolDown?.cancel(); typingCoolDown = Timer(const Duration(seconds: 2), () { typingCoolDown = null; @@ -1307,7 +1311,6 @@ class ChatController extends State void toggleDisplayChatDetailsColumn() async { await AppSettings.displayChatDetailsColumn.setItem( - Matrix.of(context).store, !_displayChatDetailsColumn.value, ); _displayChatDetailsColumn.value = !_displayChatDetailsColumn.value; diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index ea42e92f6..7d520bac1 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:animations/animations.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/recording_input_row.dart'; import 'package:fluffychat/pages/chat/recording_view_model.dart'; @@ -297,10 +297,11 @@ class ChatInputRow extends StatelessWidget { maxLines: 8, autofocus: !PlatformInfos.isMobile, keyboardType: TextInputType.multiline, - textInputAction: AppConfig.sendOnEnter == true && - PlatformInfos.isMobile - ? TextInputAction.send - : null, + textInputAction: + AppSettings.sendOnEnter.value == true && + PlatformInfos.isMobile + ? TextInputAction.send + : null, onSubmitted: controller.onInputBarSubmitted, onSubmitImage: controller.sendImageFromClipBoard, focusNode: controller.inputFocus, diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index eb9b0e234..2d08b1a2c 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -6,7 +6,7 @@ import 'package:badges/badges.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/chat.dart'; @@ -119,7 +119,7 @@ class ChatView extends StatelessWidget { ]; } else if (!controller.room.isArchived) { return [ - if (AppConfig.experimentalVoip && + if (AppSettings.experimentalVoip.value && Matrix.of(context).voipPlugin != null && controller.room.isDirectChat) IconButton( diff --git a/lib/pages/chat/events/cute_events.dart b/lib/pages/chat/events/cute_events.dart index bb675e641..9d43021dc 100644 --- a/lib/pages/chat/events/cute_events.dart +++ b/lib/pages/chat/events/cute_events.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; class CuteContent extends StatefulWidget { @@ -21,7 +21,7 @@ class _CuteContentState extends State { @override void initState() { - if (AppConfig.autoplayImages && !_isOverlayShown) { + if (AppSettings.autoplayImages.value && !_isOverlayShown) { addOverlay(); } super.initState(); diff --git a/lib/pages/chat/events/image_bubble.dart b/lib/pages/chat/events/image_bubble.dart index ea0439bec..305b83ba8 100644 --- a/lib/pages/chat/events/image_bubble.dart +++ b/lib/pages/chat/events/image_bubble.dart @@ -4,6 +4,7 @@ import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/pages/image_viewer/image_viewer.dart'; import 'package:fluffychat/utils/file_description.dart'; import 'package:fluffychat/utils/url_launcher.dart'; @@ -139,14 +140,14 @@ class ImageBubble extends StatelessWidget { textScaleFactor: MediaQuery.textScalerOf(context).scale(1), style: TextStyle( color: textColor, - fontSize: - AppConfig.fontSizeFactor * AppConfig.messageFontSize, + fontSize: AppSettings.fontSizeFactor.value * + AppConfig.messageFontSize, ), options: const LinkifyOptions(humanize: false), linkStyle: TextStyle( color: linkColor, - fontSize: - AppConfig.fontSizeFactor * AppConfig.messageFontSize, + fontSize: AppSettings.fontSizeFactor.value * + AppConfig.messageFontSize, decoration: TextDecoration.underline, decorationColor: linkColor, ), diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index d0b4c025e..e9a8b3cc2 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -7,6 +7,7 @@ import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:matrix/matrix.dart'; import 'package:swipe_to_action/swipe_to_action.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/events/room_creation_state_event.dart'; @@ -204,7 +205,7 @@ class Message extends StatelessWidget { child: Icon(Icons.check_outlined), ), ), - direction: AppConfig.swipeRightToLeftToReply + direction: AppSettings.swipeRightToLeftToReply.value ? SwipeDirection.endToStart : SwipeDirection.startToEnd, onSwipe: (_) => onSwipe(), @@ -243,7 +244,7 @@ class Message extends StatelessWidget { child: Text( event.originServerTs.localizedTime(context), style: TextStyle( - fontSize: 12 * AppConfig.fontSizeFactor, + fontSize: 12 * AppSettings.fontSizeFactor.value, fontWeight: FontWeight.bold, color: theme.colorScheme.secondary, ), @@ -890,7 +891,7 @@ class Message extends StatelessWidget { child: Text( L10n.of(context).readUpToHere, style: TextStyle( - fontSize: 12 * AppConfig.fontSizeFactor, + fontSize: 12 * AppSettings.fontSizeFactor.value, ), ), ), diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 5876335eb..85aa05941 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/events/video_player.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; @@ -105,7 +106,8 @@ class MessageContent extends StatelessWidget { @override Widget build(BuildContext context) { - final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor; + final fontSize = + AppConfig.messageFontSize * AppSettings.fontSizeFactor.value; final buttonTextColor = textColor; switch (event.type) { case EventTypes.Message: @@ -255,7 +257,7 @@ class MessageContent extends StatelessWidget { }, ); } - var html = AppConfig.renderHtml && event.isRichMessage + var html = AppSettings.renderHtml.value && event.isRichMessage ? event.formattedText : event.body; if (event.messageType == MessageTypes.Emote) { @@ -274,14 +276,14 @@ class MessageContent extends StatelessWidget { html: html, textColor: textColor, room: event.room, - fontSize: AppConfig.fontSizeFactor * + fontSize: AppSettings.fontSizeFactor.value * AppConfig.messageFontSize * (bigEmotes ? 5 : 1), limitHeight: !selected, linkStyle: TextStyle( color: linkColor, - fontSize: - AppConfig.fontSizeFactor * AppConfig.messageFontSize, + fontSize: AppSettings.fontSizeFactor.value * + AppConfig.messageFontSize, decoration: TextDecoration.underline, decorationColor: linkColor, ), diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index f1b517d14..18b5c0b07 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -4,6 +4,7 @@ import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/utils/file_description.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/url_launcher.dart'; @@ -92,12 +93,14 @@ class MessageDownloadContent extends StatelessWidget { textScaleFactor: MediaQuery.textScalerOf(context).scale(1), style: TextStyle( color: textColor, - fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, + fontSize: AppSettings.fontSizeFactor.value * + AppConfig.messageFontSize, ), options: const LinkifyOptions(humanize: false), linkStyle: TextStyle( color: linkColor, - fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, + fontSize: AppSettings.fontSizeFactor.value * + AppConfig.messageFontSize, decoration: TextDecoration.underline, decorationColor: linkColor, ), diff --git a/lib/pages/chat/events/reply_content.dart b/lib/pages/chat/events/reply_content.dart index b3df56522..10e6c2351 100644 --- a/lib/pages/chat/events/reply_content.dart +++ b/lib/pages/chat/events/reply_content.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import '../../../config/app_config.dart'; @@ -30,7 +31,8 @@ class ReplyContent extends StatelessWidget { final timeline = this.timeline; final displayEvent = timeline != null ? replyEvent.getDisplayEvent(timeline) : replyEvent; - final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor; + final fontSize = + AppConfig.messageFontSize * AppSettings.fontSizeFactor.value; final color = theme.brightness == Brightness.dark ? theme.colorScheme.onTertiaryContainer : ownMessage diff --git a/lib/pages/chat/events/state_message.dart b/lib/pages/chat/events/state_message.dart index 7d6be428c..9ef9d4e7a 100644 --- a/lib/pages/chat/events/state_message.dart +++ b/lib/pages/chat/events/state_message.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; @@ -68,7 +69,7 @@ class StateMessage extends StatelessWidget { ), textAlign: TextAlign.center, style: TextStyle( - fontSize: 12 * AppConfig.fontSizeFactor, + fontSize: 12 * AppSettings.fontSizeFactor.value, decoration: event.redacted ? TextDecoration.lineThrough : null, diff --git a/lib/pages/chat/events/video_player.dart b/lib/pages/chat/events/video_player.dart index 6b31eb774..5635bd23f 100644 --- a/lib/pages/chat/events/video_player.dart +++ b/lib/pages/chat/events/video_player.dart @@ -6,6 +6,7 @@ import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/utils/file_description.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -136,14 +137,14 @@ class EventVideoPlayer extends StatelessWidget { textScaleFactor: MediaQuery.textScalerOf(context).scale(1), style: TextStyle( color: textColor, - fontSize: - AppConfig.fontSizeFactor * AppConfig.messageFontSize, + fontSize: AppSettings.fontSizeFactor.value * + AppConfig.messageFontSize, ), options: const LinkifyOptions(humanize: false), linkStyle: TextStyle( color: linkColor, - fontSize: - AppConfig.fontSizeFactor * AppConfig.messageFontSize, + fontSize: AppSettings.fontSizeFactor.value * + AppConfig.messageFontSize, decoration: TextDecoration.underline, decorationColor: linkColor, ), diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index cfb4bb16e..54915218d 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -415,8 +415,7 @@ class InputBar extends StatelessWidget { // it sets the types for the callback incorrectly onSubmitted!(text); }, - maxLength: - AppSettings.textMessageMaxLength.getItem(Matrix.of(context).store), + maxLength: AppSettings.textMessageMaxLength.value, decoration: decoration, onChanged: (text) { // fix for the library for now diff --git a/lib/pages/chat/recording_view_model.dart b/lib/pages/chat/recording_view_model.dart index f222f7c13..4c3471008 100644 --- a/lib/pages/chat/recording_view_model.dart +++ b/lib/pages/chat/recording_view_model.dart @@ -15,7 +15,6 @@ import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import 'events/audio_player.dart'; class RecordingViewModel extends StatefulWidget { @@ -62,8 +61,6 @@ class RecordingViewModelState extends State { } if (await AudioRecorder().hasPermission() == false) return; - final store = Matrix.of(context).store; - final audioRecorder = _audioRecorder ??= AudioRecorder(); setState(() {}); @@ -93,12 +90,12 @@ class RecordingViewModelState extends State { await audioRecorder.start( RecordConfig( - bitRate: AppSettings.audioRecordingBitRate.getItem(store), - sampleRate: AppSettings.audioRecordingSamplingRate.getItem(store), - numChannels: AppSettings.audioRecordingNumChannels.getItem(store), - autoGain: AppSettings.audioRecordingAutoGain.getItem(store), - echoCancel: AppSettings.audioRecordingEchoCancel.getItem(store), - noiseSuppress: AppSettings.audioRecordingNoiseSuppress.getItem(store), + bitRate: AppSettings.audioRecordingBitRate.value, + sampleRate: AppSettings.audioRecordingSamplingRate.value, + numChannels: AppSettings.audioRecordingNumChannels.value, + autoGain: AppSettings.audioRecordingAutoGain.value, + echoCancel: AppSettings.audioRecordingEchoCancel.value, + noiseSuppress: AppSettings.audioRecordingNoiseSuppress.value, encoder: codec, ), path: path ?? '', diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 8bb8680af..07c71db4d 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -12,7 +12,6 @@ import 'package:matrix/matrix.dart' as sdk; import 'package:matrix/matrix.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; @@ -94,9 +93,7 @@ class ChatListController extends State StreamSubscription? _intentUriStreamSubscription; - ActiveFilter activeFilter = AppConfig.separateChatTypes - ? ActiveFilter.messages - : ActiveFilter.allChats; + late ActiveFilter activeFilter; String? _activeSpaceId; String? get activeSpaceId => _activeSpaceId; @@ -401,6 +398,9 @@ class ChatListController extends State @override void initState() { + activeFilter = AppSettings.separateChatTypes.value + ? ActiveFilter.messages + : ActiveFilter.allChats; _initReceiveSharingIntent(); _activeSpaceId = widget.activeSpace; @@ -713,8 +713,7 @@ class ChatListController extends State context: context, ); if (result == OkCancelResult.ok) { - await Matrix.of(context).store.setBool(SettingKeys.showPresences, false); - AppConfig.showPresences = false; + AppSettings.showPresences.setItem(false); setState(() {}); } } diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index a789f3586..fe696b036 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; @@ -120,7 +120,8 @@ class ChatListViewBody extends StatelessWidget { ), ), ], - if (!controller.isSearchMode && AppConfig.showPresences) + if (!controller.isSearchMode && + AppSettings.showPresences.value) GestureDetector( onLongPress: () => controller.dismissStatusList(), child: StatusMessageList( @@ -155,14 +156,14 @@ class ChatListViewBody extends StatelessWidget { shrinkWrap: true, scrollDirection: Axis.horizontal, children: [ - if (AppConfig.separateChatTypes) + if (AppSettings.separateChatTypes.value) ActiveFilter.messages else ActiveFilter.allChats, ActiveFilter.groups, ActiveFilter.unread, if (spaceDelegateCandidates.isNotEmpty && - !AppConfig.displayNavigationRail && + !AppSettings.displayNavigationRail.value && !FluffyThemes.isColumnMode(context)) ActiveFilter.spaces, ] diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 35f763983..1b04bed8c 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; @@ -32,7 +32,7 @@ class ChatListView extends StatelessWidget { child: Row( children: [ if (FluffyThemes.isColumnMode(context) || - AppConfig.displayNavigationRail) ...[ + AppSettings.displayNavigationRail.value) ...[ SpacesNavigationRail( activeSpaceId: controller.activeSpaceId, onGoToChats: controller.clearActiveSpace, diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index b17a3bc06..4e96a7154 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -11,6 +11,7 @@ import 'package:universal_html/html.dart' as html; import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart'; import 'package:fluffychat/utils/file_selector.dart'; @@ -34,7 +35,7 @@ class HomeserverPickerController extends State { bool isLoading = false; final TextEditingController homeserverController = TextEditingController( - text: AppConfig.defaultHomeserver, + text: AppSettings.defaultHomeserver.value, ); String? error; @@ -211,7 +212,7 @@ class HomeserverPickerController extends State { case MoreLoginActions.importBackup: restoreBackup(); case MoreLoginActions.privacy: - launchUrlString(AppConfig.privacyUrl); + launchUrlString(AppSettings.privacyUrl.value); case MoreLoginActions.about: PlatformInfos.showDialog(context); } diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index 4161f1f9a..6beedc47d 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -5,6 +5,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; @@ -155,7 +156,7 @@ class HomeserverPickerView extends StatelessWidget { AppConfig.borderRadius, ), ), - hintText: AppConfig.defaultHomeserver, + hintText: AppSettings.defaultHomeserver.value, hintStyle: TextStyle( color: theme.colorScheme.surfaceTint, ), diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index 0a72704aa..aa9c3d50b 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; @@ -244,7 +244,7 @@ class SettingsView extends StatelessWidget { ListTile( leading: const Icon(Icons.privacy_tip_outlined), title: Text(L10n.of(context).privacy), - onTap: () => launchUrlString(AppConfig.privacyUrl), + onTap: () => launchUrlString(AppSettings.privacyUrl.value), ), ListTile( leading: const Icon(Icons.info_outline_rounded), diff --git a/lib/pages/settings_chat/settings_chat_view.dart b/lib/pages/settings_chat/settings_chat_view.dart index d3526a28e..b125cf734 100644 --- a/lib/pages/settings_chat/settings_chat_view.dart +++ b/lib/pages/settings_chat/settings_chat_view.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; @@ -34,41 +33,29 @@ class SettingsChatView extends StatelessWidget { SettingsSwitchListTile.adaptive( title: L10n.of(context).formattedMessages, subtitle: L10n.of(context).formattedMessagesDescription, - onChanged: (b) => AppConfig.renderHtml = b, - storeKey: SettingKeys.renderHtml, - defaultValue: AppConfig.renderHtml, + setting: AppSettings.renderHtml, ), SettingsSwitchListTile.adaptive( title: L10n.of(context).hideRedactedMessages, subtitle: L10n.of(context).hideRedactedMessagesBody, - onChanged: (b) => AppConfig.hideRedactedEvents = b, - storeKey: SettingKeys.hideRedactedEvents, - defaultValue: AppConfig.hideRedactedEvents, + setting: AppSettings.hideRedactedEvents, ), SettingsSwitchListTile.adaptive( title: L10n.of(context).hideInvalidOrUnknownMessageFormats, - onChanged: (b) => AppConfig.hideUnknownEvents = b, - storeKey: SettingKeys.hideUnknownEvents, - defaultValue: AppConfig.hideUnknownEvents, + setting: AppSettings.hideUnknownEvents, ), if (PlatformInfos.isMobile) SettingsSwitchListTile.adaptive( title: L10n.of(context).autoplayImages, - onChanged: (b) => AppConfig.autoplayImages = b, - storeKey: SettingKeys.autoplayImages, - defaultValue: AppConfig.autoplayImages, + setting: AppSettings.autoplayImages, ), SettingsSwitchListTile.adaptive( title: L10n.of(context).sendOnEnter, - onChanged: (b) => AppConfig.sendOnEnter = b, - storeKey: SettingKeys.sendOnEnter, - defaultValue: AppConfig.sendOnEnter ?? !PlatformInfos.isMobile, + setting: AppSettings.sendOnEnter, ), SettingsSwitchListTile.adaptive( title: L10n.of(context).swipeRightToLeftToReply, - onChanged: (b) => AppConfig.swipeRightToLeftToReply = b, - storeKey: SettingKeys.swipeRightToLeftToReply, - defaultValue: AppConfig.swipeRightToLeftToReply, + setting: AppSettings.swipeRightToLeftToReply, ), Divider(color: theme.dividerColor), ListTile( @@ -102,12 +89,10 @@ class SettingsChatView extends StatelessWidget { SettingsSwitchListTile.adaptive( title: L10n.of(context).experimentalVideoCalls, onChanged: (b) { - AppConfig.experimentalVoip = b; Matrix.of(context).createVoipPlugin(); return; }, - storeKey: SettingKeys.experimentalVoip, - defaultValue: AppConfig.experimentalVoip, + setting: AppSettings.experimentalVoip, ), ], ), diff --git a/lib/pages/settings_security/settings_security.dart b/lib/pages/settings_security/settings_security.dart index a645460c5..2ec3747c0 100644 --- a/lib/pages/settings_security/settings_security.dart +++ b/lib/pages/settings_security/settings_security.dart @@ -112,7 +112,6 @@ class SettingsSecurityController extends State { void changeShareKeysWith(ShareKeysWith? shareKeysWith) async { if (shareKeysWith == null) return; AppSettings.shareKeysWith.setItem( - Matrix.of(context).store, shareKeysWith.name, ); Matrix.of(context).client.shareKeysWith = shareKeysWith; diff --git a/lib/pages/settings_security/settings_security_view.dart b/lib/pages/settings_security/settings_security_view.dart index b181c9915..039fd97eb 100644 --- a/lib/pages/settings_security/settings_security_view.dart +++ b/lib/pages/settings_security/settings_security_view.dart @@ -63,16 +63,12 @@ class SettingsSecurityView extends StatelessWidget { title: L10n.of(context).sendTypingNotifications, subtitle: L10n.of(context).sendTypingNotificationsDescription, - onChanged: (b) => AppConfig.sendTypingNotifications = b, - storeKey: SettingKeys.sendTypingNotifications, - defaultValue: AppConfig.sendTypingNotifications, + setting: AppSettings.sendTypingNotifications, ), SettingsSwitchListTile.adaptive( title: L10n.of(context).sendReadReceipts, subtitle: L10n.of(context).sendReadReceiptsDescription, - onChanged: (b) => AppConfig.sendPublicReadReceipts = b, - storeKey: SettingKeys.sendPublicReadReceipts, - defaultValue: AppConfig.sendPublicReadReceipts, + setting: AppSettings.sendPublicReadReceipts, ), ListTile( trailing: const Icon(Icons.chevron_right_outlined), diff --git a/lib/pages/settings_style/settings_style.dart b/lib/pages/settings_style/settings_style.dart index 49b0b003c..968759ad4 100644 --- a/lib/pages/settings_style/settings_style.dart +++ b/lib/pages/settings_style/settings_style.dart @@ -18,7 +18,9 @@ class SettingsStyle extends StatefulWidget { class SettingsStyleController extends State { void setChatColor(Color? color) async { - AppConfig.colorSchemeSeed = color; + AppSettings.colorSchemeSeedInt.setItem( + color?.toARGB32() ?? AppSettings.colorSchemeSeedInt.defaultValue, + ); ThemeController.of(context).setPrimaryColor(color); } @@ -156,11 +158,7 @@ class SettingsStyleController extends State { } void changeFontSizeFactor(double d) { - setState(() => AppConfig.fontSizeFactor = d); - Matrix.of(context).store.setString( - SettingKeys.fontSizeFactor, - AppConfig.fontSizeFactor.toString(), - ); + AppSettings.fontSizeFactor.setItem(d); } @override diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index f4bbd7285..23dbe68cb 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -230,7 +230,7 @@ class SettingsStyleView extends StatelessWidget { style: TextStyle( color: theme.onBubbleColor, fontSize: AppConfig.messageFontSize * - AppConfig.fontSizeFactor, + AppSettings.fontSizeFactor.value, ), ), ), @@ -263,7 +263,7 @@ class SettingsStyleView extends StatelessWidget { style: TextStyle( color: theme.colorScheme.onSurface, fontSize: AppConfig.messageFontSize * - AppConfig.fontSizeFactor, + AppSettings.fontSizeFactor.value, ), ), ), @@ -325,13 +325,15 @@ class SettingsStyleView extends StatelessWidget { ), ListTile( title: Text(L10n.of(context).fontSize), - trailing: Text('× ${AppConfig.fontSizeFactor}'), + trailing: Text( + '× ${AppSettings.fontSizeFactor.value}', + ), ), Slider.adaptive( min: 0.5, max: 2.5, divisions: 20, - value: AppConfig.fontSizeFactor, + value: AppSettings.fontSizeFactor.value, semanticFormatterCallback: (d) => d.toString(), onChanged: controller.changeFontSizeFactor, ), @@ -349,21 +351,15 @@ class SettingsStyleView extends StatelessWidget { ), SettingsSwitchListTile.adaptive( title: L10n.of(context).presencesToggle, - onChanged: (b) => AppConfig.showPresences = b, - storeKey: SettingKeys.showPresences, - defaultValue: AppConfig.showPresences, + setting: AppSettings.showPresences, ), SettingsSwitchListTile.adaptive( title: L10n.of(context).separateChatTypes, - onChanged: (b) => AppConfig.separateChatTypes = b, - storeKey: SettingKeys.separateChatTypes, - defaultValue: AppConfig.separateChatTypes, + setting: AppSettings.separateChatTypes, ), SettingsSwitchListTile.adaptive( title: L10n.of(context).displayNavigationRail, - onChanged: (b) => AppConfig.displayNavigationRail = b, - storeKey: SettingKeys.displayNavigationRail, - defaultValue: AppConfig.displayNavigationRail, + setting: AppSettings.displayNavigationRail, ), ], ), diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index d25a7a68a..4fa5a52b4 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -217,8 +217,7 @@ class BackgroundPush { currentPushers.first.lang == 'en' && currentPushers.first.data.url.toString() == gatewayUrl && currentPushers.first.data.format == - AppSettings.pushNotificationsPusherFormat - .getItem(matrix!.store) && + AppSettings.pushNotificationsPusherFormat.value && mapEquals( currentPushers.single.data.additionalProperties, {"data_message": pusherDataMessageFormat}, @@ -258,8 +257,7 @@ class BackgroundPush { lang: 'en', data: PusherData( url: Uri.parse(gatewayUrl!), - format: AppSettings.pushNotificationsPusherFormat - .getItem(matrix!.store), + format: AppSettings.pushNotificationsPusherFormat.value, additionalProperties: {"data_message": pusherDataMessageFormat}, ), kind: 'http', @@ -325,7 +323,7 @@ class BackgroundPush { if (matrix == null) { return; } - if ((matrix?.store.getBool(SettingKeys.showNoGoogle) ?? false) == true) { + if (!AppSettings.showNoGoogle.value) { return; } await loadLocale(); @@ -356,8 +354,7 @@ class BackgroundPush { } } await setupPusher( - gatewayUrl: - AppSettings.pushNotificationsGatewayUrl.getItem(matrix!.store), + gatewayUrl: AppSettings.pushNotificationsGatewayUrl.value, token: _fcmToken, ); } @@ -414,18 +411,18 @@ class BackgroundPush { oldTokens: oldTokens, useDeviceSpecificAppId: true, ); - await matrix?.store.setString(SettingKeys.unifiedPushEndpoint, newEndpoint); - await matrix?.store.setBool(SettingKeys.unifiedPushRegistered, true); + await AppSettings.unifiedPushEndpoint.setItem(newEndpoint); + await AppSettings.unifiedPushRegistered.setItem(true); } Future _upUnregistered(String i) async { upAction = true; Logs().i('[Push] Removing UnifiedPush endpoint...'); - final oldEndpoint = - matrix?.store.getString(SettingKeys.unifiedPushEndpoint); - await matrix?.store.setBool(SettingKeys.unifiedPushRegistered, false); - await matrix?.store.remove(SettingKeys.unifiedPushEndpoint); - if (oldEndpoint?.isNotEmpty ?? false) { + final oldEndpoint = AppSettings.unifiedPushEndpoint.value; + await AppSettings.unifiedPushEndpoint + .setItem(AppSettings.unifiedPushEndpoint.defaultValue); + await AppSettings.unifiedPushRegistered.setItem(false); + if (oldEndpoint.isNotEmpty) { // remove the old pusher await setupPusher( oldTokens: {oldEndpoint}, diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index 5c5a07663..9fe1b2dfd 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -11,7 +11,6 @@ import 'package:matrix/matrix.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:universal_html/html.dart' as html; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/custom_http_client.dart'; @@ -101,8 +100,8 @@ abstract class ClientManager { String clientName, SharedPreferences store, ) async { - final shareKeysWith = AppSettings.shareKeysWith.getItem(store); - final enableSoftLogout = AppSettings.enableSoftLogout.getItem(store); + final shareKeysWith = AppSettings.shareKeysWith.value; + final enableSoftLogout = AppSettings.enableSoftLogout.value; return Client( clientName, @@ -147,7 +146,7 @@ abstract class ClientManager { await NotificationsClient().notify( title, body: body, - appName: AppConfig.applicationName, + appName: AppSettings.applicationName.value, hints: [ NotificationHint.soundName('message-new-instant'), ], diff --git a/lib/utils/init_with_restore.dart b/lib/utils/init_with_restore.dart index 523b22f9e..feb95e003 100644 --- a/lib/utils/init_with_restore.dart +++ b/lib/utils/init_with_restore.dart @@ -5,6 +5,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -57,13 +58,13 @@ extension InitWithRestoreExtension on Client { ? const FlutterSecureStorage() : null; await storage?.delete( - key: '${AppConfig.applicationName}_session_backup_$clientName', + key: '${AppSettings.applicationName.value}_session_backup_$clientName', ); } Future initWithRestore({void Function()? onMigration}) async { final storageKey = - '${AppConfig.applicationName}_session_backup_$clientName'; + '${AppSettings.applicationName.value}_session_backup_$clientName'; final storage = PlatformInfos.isMobile || PlatformInfos.isLinux ? const FlutterSecureStorage() : null; diff --git a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart index 055ce5011..a72666f4e 100644 --- a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart +++ b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart @@ -1,9 +1,12 @@ import 'package:matrix/matrix.dart'; -import '../../config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; extension VisibleInGuiExtension on List { - List filterByVisibleInGui({String? exceptionEventId}) => where( + List filterByVisibleInGui({ + String? exceptionEventId, + }) => + where( (event) => event.isVisibleInGui || event.eventId == exceptionEventId, ).toList(); } @@ -19,9 +22,9 @@ extension IsStateExtension on Event { // if a reaction has been redacted we also want it to be hidden in the timeline !{EventTypes.Reaction, EventTypes.Redaction}.contains(type) && // if we enabled to hide all redacted events, don't show those - (!AppConfig.hideRedactedEvents || !redacted) && + (!AppSettings.hideRedactedEvents.value || !redacted) && // if we enabled to hide all unknown events, don't show those - (!AppConfig.hideUnknownEvents || isEventTypeKnown); + (!AppSettings.hideUnknownEvents.value || isEventTypeKnown); bool get isState => !{ EventTypes.Message, diff --git a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart index 6169cf0f3..293053759 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/cipher.dart @@ -6,7 +6,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:matrix/matrix.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; @@ -52,8 +51,7 @@ Future getDatabaseCipher() async { } void _sendNoEncryptionWarning(Object exception) async { - final store = await SharedPreferences.getInstance(); - final isStored = AppSettings.noEncryptionWarningShown.getItem(store); + final isStored = AppSettings.noEncryptionWarningShown.value; if (isStored == true) return; @@ -63,5 +61,5 @@ void _sendNoEncryptionWarning(Object exception) async { exception.toString(), ); - await AppSettings.noEncryptionWarningShown.setItem(store, true); + await AppSettings.noEncryptionWarningShown.setItem(true); } diff --git a/lib/utils/notification_background_handler.dart b/lib/utils/notification_background_handler.dart index 531d7e19d..aa8ea17dd 100644 --- a/lib/utils/notification_background_handler.dart +++ b/lib/utils/notification_background_handler.dart @@ -6,7 +6,6 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vod; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/client_download_content_extension.dart'; @@ -59,7 +58,7 @@ void notificationTapBackground( await vod.init(); _vodInitialized = true; } - final store = await SharedPreferences.getInstance(); + final store = await AppSettings.init(); final client = (await ClientManager.getClients( initialize: false, store: store, @@ -71,10 +70,6 @@ void notificationTapBackground( waitUntilLoadCompletedLoaded: false, ); - AppConfig.sendPublicReadReceipts = - store.getBool(SettingKeys.sendPublicReadReceipts) ?? - AppConfig.sendPublicReadReceipts; - if (!client.isLogged()) { throw Exception('Notification tab in background but not logged in!'); } @@ -145,7 +140,7 @@ Future notificationTap( await room.setReadMarker( payload.eventId ?? room.lastEvent!.eventId, mRead: payload.eventId ?? room.lastEvent!.eventId, - public: AppConfig.sendPublicReadReceipts, + public: AppSettings.sendPublicReadReceipts.value, ); case FluffyChatNotificationActions.reply: final input = notificationResponse.input; diff --git a/lib/utils/platform_infos.dart b/lib/utils/platform_infos.dart index c180ff86c..bf6589df2 100644 --- a/lib/utils/platform_infos.dart +++ b/lib/utils/platform_infos.dart @@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; import '../config/app_config.dart'; @@ -36,7 +37,7 @@ abstract class PlatformInfos { static bool get platformCanRecord => (isMobile || isMacOS); static String get clientName => - '${AppConfig.applicationName} ${isWeb ? 'web' : Platform.operatingSystem}${kReleaseMode ? '' : 'Debug'}'; + '${AppSettings.applicationName.value} ${isWeb ? 'web' : Platform.operatingSystem}${kReleaseMode ? '' : 'Debug'}'; static Future getVersion() async { var version = kIsWeb ? 'Web' : 'Unknown'; @@ -88,7 +89,7 @@ abstract class PlatformInfos { height: 64, filterQuality: FilterQuality.medium, ), - applicationName: AppConfig.applicationName, + applicationName: AppSettings.applicationName.value, ); } } diff --git a/lib/utils/push_helper.dart b/lib/utils/push_helper.dart index 6a9dd6e4e..70f7a40f5 100644 --- a/lib/utils/push_helper.dart +++ b/lib/utils/push_helper.dart @@ -8,9 +8,9 @@ import 'package:collection/collection.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_shortcuts_new/flutter_shortcuts_new.dart'; import 'package:matrix/matrix.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/client_download_content_extension.dart'; import 'package:fluffychat/utils/client_manager.dart'; @@ -50,7 +50,7 @@ Future pushHelper( l10n.incomingMessages, number: notification.counts?.unread, ticker: l10n.unreadChatsInApp( - AppConfig.applicationName, + AppSettings.applicationName.value, (notification.counts?.unread ?? 0).toString(), ), importance: Importance.high, @@ -85,7 +85,7 @@ Future _tryPushHelper( client ??= (await ClientManager.getClients( initialize: false, - store: await SharedPreferences.getInstance(), + store: await AppSettings.init(), )) .first; final event = await client.getEventByPushNotification( diff --git a/lib/widgets/app_lock.dart b/lib/widgets/app_lock.dart index d337358d7..c4682d9f4 100644 --- a/lib/widgets/app_lock.dart +++ b/lib/widgets/app_lock.dart @@ -4,7 +4,6 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:matrix/matrix.dart'; import 'package:provider/provider.dart'; -import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/widgets/lock_screen.dart'; class AppLockWidget extends StatefulWidget { @@ -65,7 +64,7 @@ class AppLock extends State with WidgetsBindingObserver { Future changePincode(String? pincode) async { await const FlutterSecureStorage().write( - key: SettingKeys.appLockKey, + key: 'chat.fluffy.app_lock', value: pincode, ); _pincode = pincode; diff --git a/lib/widgets/config_viewer.dart b/lib/widgets/config_viewer.dart index adfb34407..b0537317c 100644 --- a/lib/widgets/config_viewer.dart +++ b/lib/widgets/config_viewer.dart @@ -21,7 +21,7 @@ class _ConfigViewerState extends State { String initialValue, ) async { if (appSetting is AppSettings) { - await appSetting.setItem(store, !(initialValue == 'true')); + await appSetting.setItem(!(initialValue == 'true')); setState(() {}); return; } @@ -35,13 +35,13 @@ class _ConfigViewerState extends State { if (value == null) return; if (appSetting is AppSettings) { - await appSetting.setItem(store, value); + await appSetting.setItem(value); } if (appSetting is AppSettings) { - await appSetting.setItem(store, int.parse(value)); + await appSetting.setItem(int.parse(value)); } if (appSetting is AppSettings) { - await appSetting.setItem(store, double.parse(value)); + await appSetting.setItem(double.parse(value)); } setState(() {}); @@ -78,16 +78,16 @@ class _ConfigViewerState extends State { final appSetting = AppSettings.values[i]; var value = ''; if (appSetting is AppSettings) { - value = appSetting.getItem(store); + value = appSetting.value; } if (appSetting is AppSettings) { - value = appSetting.getItem(store).toString(); + value = appSetting.value.toString(); } if (appSetting is AppSettings) { - value = appSetting.getItem(store).toString(); + value = appSetting.value.toString(); } if (appSetting is AppSettings) { - value = appSetting.getItem(store).toString(); + value = appSetting.value.toString(); } return ListTile( title: Text(appSetting.name), diff --git a/lib/widgets/fluffy_chat_app.dart b/lib/widgets/fluffy_chat_app.dart index cbe7f088a..8c37c814d 100644 --- a/lib/widgets/fluffy_chat_app.dart +++ b/lib/widgets/fluffy_chat_app.dart @@ -5,11 +5,11 @@ import 'package:matrix/matrix.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:fluffychat/config/routes.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/widgets/app_lock.dart'; import 'package:fluffychat/widgets/theme_builder.dart'; -import '../config/app_config.dart'; import '../utils/custom_scroll_behaviour.dart'; import 'matrix.dart'; @@ -43,7 +43,7 @@ class FluffyChatApp extends StatelessWidget { Widget build(BuildContext context) { return ThemeBuilder( builder: (context, themeMode, primaryColor) => MaterialApp.router( - title: AppConfig.applicationName, + title: AppSettings.applicationName.value, themeMode: themeMode, theme: FluffyThemes.buildTheme(context, Brightness.light, primaryColor), darkTheme: diff --git a/lib/widgets/layouts/login_scaffold.dart b/lib/widgets/layouts/login_scaffold.dart index 9b2bc944a..e0cc24577 100644 --- a/lib/widgets/layouts/login_scaffold.dart +++ b/lib/widgets/layouts/login_scaffold.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -107,7 +108,7 @@ class _PrivacyButtons extends StatelessWidget { ), ), TextButton( - onPressed: () => launchUrlString(AppConfig.privacyUrl), + onPressed: () => launchUrlString(AppSettings.privacyUrl.value), child: Text( L10n.of(context).privacy, style: shadowTextStyle, diff --git a/lib/widgets/local_notifications_extension.dart b/lib/widgets/local_notifications_extension.dart index 0cdc291ef..0d0069fa4 100644 --- a/lib/widgets/local_notifications_extension.dart +++ b/lib/widgets/local_notifications_extension.dart @@ -9,7 +9,7 @@ import 'package:image/image.dart'; import 'package:matrix/matrix.dart'; import 'package:universal_html/html.dart' as html; -import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/client_download_content_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; @@ -114,7 +114,7 @@ extension LocalNotificationsExtension on MatrixState { title, body: body, replacesId: linuxNotificationIds[roomId] ?? 0, - appName: AppConfig.applicationName, + appName: AppSettings.applicationName.value, appIcon: 'fluffychat', actions: [ NotificationAction( @@ -139,7 +139,7 @@ extension LocalNotificationsExtension on MatrixState { event.room.setReadMarker( event.eventId, mRead: event.eventId, - public: AppConfig.sendPublicReadReceipts, + public: AppSettings.sendPublicReadReceipts.value, ); break; case DesktopNotificationActions.openChat: diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 3bf20b123..55fd267ad 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:desktop_notifications/desktop_notifications.dart'; -import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:just_audio/just_audio.dart'; @@ -27,7 +26,6 @@ import 'package:fluffychat/utils/voip_plugin.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/fluffy_chat_app.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; -import '../config/app_config.dart'; import '../config/setting_keys.dart'; import '../pages/key_verification/key_verification_dialog.dart'; import '../utils/account_bundles.dart'; @@ -155,7 +153,7 @@ class MatrixState extends State with WidgetsBindingObserver { } final candidate = _loginClientCandidate ??= await ClientManager.createClient( - '${AppConfig.applicationName}-${DateTime.now().millisecondsSinceEpoch}', + '${AppSettings.applicationName.value}-${DateTime.now().millisecondsSinceEpoch}', store, ) ..onLoginStateChanged @@ -221,24 +219,6 @@ class MatrixState extends State with WidgetsBindingObserver { super.initState(); WidgetsBinding.instance.addObserver(this); initMatrix(); - if (PlatformInfos.isWeb) { - initConfig().then((_) => initSettings()); - } else { - initSettings(); - } - } - - Future initConfig() async { - try { - final configJsonString = - utf8.decode((await http.get(Uri.parse('config.json'))).bodyBytes); - final configJson = json.decode(configJsonString); - AppConfig.loadFromJson(configJson); - } on FormatException catch (_) { - Logs().v('[ConfigLoader] config.json not found'); - } catch (e) { - Logs().v('[ConfigLoader] config.json not found', e); - } } void _registerSubs(String name) { @@ -358,7 +338,7 @@ class MatrixState extends State with WidgetsBindingObserver { ); } if (result == OkCancelResult.cancel) { - await store.setBool(SettingKeys.showNoGoogle, true); + await AppSettings.showNoGoogle.setItem(true); } }, ); @@ -368,7 +348,7 @@ class MatrixState extends State with WidgetsBindingObserver { } void createVoipPlugin() async { - if (store.getBool(SettingKeys.experimentalVoip) == false) { + if (AppSettings.experimentalVoip.value) { voipPlugin = null; return; } @@ -390,55 +370,6 @@ class MatrixState extends State with WidgetsBindingObserver { } } - void initSettings() { - AppConfig.fontSizeFactor = - double.tryParse(store.getString(SettingKeys.fontSizeFactor) ?? '') ?? - AppConfig.fontSizeFactor; - - AppConfig.renderHtml = - store.getBool(SettingKeys.renderHtml) ?? AppConfig.renderHtml; - - AppConfig.swipeRightToLeftToReply = - store.getBool(SettingKeys.swipeRightToLeftToReply) ?? - AppConfig.swipeRightToLeftToReply; - - AppConfig.hideRedactedEvents = - store.getBool(SettingKeys.hideRedactedEvents) ?? - AppConfig.hideRedactedEvents; - - AppConfig.hideUnknownEvents = - store.getBool(SettingKeys.hideUnknownEvents) ?? - AppConfig.hideUnknownEvents; - - AppConfig.separateChatTypes = - store.getBool(SettingKeys.separateChatTypes) ?? - AppConfig.separateChatTypes; - - AppConfig.autoplayImages = - store.getBool(SettingKeys.autoplayImages) ?? AppConfig.autoplayImages; - - AppConfig.sendTypingNotifications = - store.getBool(SettingKeys.sendTypingNotifications) ?? - AppConfig.sendTypingNotifications; - - AppConfig.sendPublicReadReceipts = - store.getBool(SettingKeys.sendPublicReadReceipts) ?? - AppConfig.sendPublicReadReceipts; - - AppConfig.sendOnEnter = - store.getBool(SettingKeys.sendOnEnter) ?? AppConfig.sendOnEnter; - - AppConfig.experimentalVoip = store.getBool(SettingKeys.experimentalVoip) ?? - AppConfig.experimentalVoip; - - AppConfig.showPresences = - store.getBool(SettingKeys.showPresences) ?? AppConfig.showPresences; - - AppConfig.displayNavigationRail = - store.getBool(SettingKeys.displayNavigationRail) ?? - AppConfig.displayNavigationRail; - } - @override void dispose() { WidgetsBinding.instance.removeObserver(this); diff --git a/lib/widgets/settings_switch_list_tile.dart b/lib/widgets/settings_switch_list_tile.dart index f49b97598..f625f9e64 100644 --- a/lib/widgets/settings_switch_list_tile.dart +++ b/lib/widgets/settings_switch_list_tile.dart @@ -1,18 +1,16 @@ import 'package:flutter/material.dart'; -import 'matrix.dart'; +import 'package:fluffychat/config/setting_keys.dart'; class SettingsSwitchListTile extends StatefulWidget { - final bool defaultValue; - final String storeKey; + final AppSettings setting; final String title; final String? subtitle; final Function(bool)? onChanged; const SettingsSwitchListTile.adaptive({ super.key, - this.defaultValue = false, - required this.storeKey, + required this.setting, required this.title, this.subtitle, this.onChanged, @@ -27,13 +25,12 @@ class SettingsSwitchListTileState extends State { Widget build(BuildContext context) { final subtitle = widget.subtitle; return SwitchListTile.adaptive( - value: Matrix.of(context).store.getBool(widget.storeKey) ?? - widget.defaultValue, + value: widget.setting.value, title: Text(widget.title), subtitle: subtitle == null ? null : Text(subtitle), onChanged: (bool newValue) async { widget.onChanged?.call(newValue); - await Matrix.of(context).store.setBool(widget.storeKey, newValue); + await widget.setting.setItem(newValue); setState(() {}); }, ); From c434b3dddce286fe6949fb552a385ebb327c687f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Mon, 20 Oct 2025 07:01:37 +0200 Subject: [PATCH 18/21] feat: Add donation buttons except for PlayStore&AppStore version --- lib/l10n/intl_en.arb | 3 ++- lib/pages/chat_list/client_chooser_button.dart | 15 +++++++++++++++ lib/utils/background_push.dart | 2 ++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 7a285f1ad..d7836c494 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -3410,5 +3410,6 @@ "type": "String" } } - } + }, + "donate": "Donate" } diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index a9b13e731..dd01bf053 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -67,6 +67,17 @@ class ClientChooserButton extends StatelessWidget { ], ), ), + if (Matrix.of(context).backgroundPush?.firebaseEnabled == false) + PopupMenuItem( + value: SettingsAction.support, + child: Row( + children: [ + const Icon(Icons.favorite, color: Colors.red), + const SizedBox(width: 18), + Text(L10n.of(context).donate), + ], + ), + ), PopupMenuItem( value: SettingsAction.settings, child: Row( @@ -207,6 +218,9 @@ class ClientChooserButton extends StatelessWidget { case SettingsAction.invite: FluffyShare.shareInviteLink(context); break; + case SettingsAction.support: + // TODO: Implement me + break; case SettingsAction.settings: context.go('/rooms/settings'); break; @@ -226,6 +240,7 @@ enum SettingsAction { newGroup, setStatus, invite, + support, settings, archive, } diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index 4fa5a52b4..31b1e82e2 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -66,6 +66,7 @@ class BackgroundPush { } final pendingTests = >{}; + bool firebaseEnabled = false; //final firebase = FcmSharedIsolate(); @@ -74,6 +75,7 @@ class BackgroundPush { bool upAction = false; void _init() async { + //firebaseEnabled = true; try { if (PlatformInfos.isAndroid) { final port = ReceivePort(); From b856e83e83e202554ab783805ea0d086eb32c931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Mon, 20 Oct 2025 07:08:52 +0200 Subject: [PATCH 19/21] chore: Fix load config.json --- lib/config/setting_keys.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/config/setting_keys.dart b/lib/config/setting_keys.dart index 084e24007..0f51ea79b 100644 --- a/lib/config/setting_keys.dart +++ b/lib/config/setting_keys.dart @@ -68,15 +68,17 @@ enum AppSettings { const AppSettings(this.key, this.defaultValue); - static late final SharedPreferences store; + static SharedPreferences get store => _store!; + static SharedPreferences? _store; static Future init({loadWebConfigFile = true}) async { - final store = AppSettings.store = await SharedPreferences.getInstance(); + if (AppSettings._store != null) return AppSettings.store; + + final store = AppSettings._store = await SharedPreferences.getInstance(); if (store.getBool(AppSettings.sendOnEnter.key) == null) { await store.setBool(AppSettings.sendOnEnter.key, !PlatformInfos.isMobile); } - if (kIsWeb && loadWebConfigFile) { try { final configJsonString = @@ -85,7 +87,7 @@ enum AppSettings { json.decode(configJsonString) as Map; for (final setting in AppSettings.values) { if (store.get(setting.key) != null) continue; - final configValue = configJson[setting.key]; + final configValue = configJson[setting.name]; if (configValue == null) continue; if (configValue is bool) { await store.setBool(setting.key, configValue); From 136ddd7681873631b1cf4b2e09d502aae64fdb95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Mon, 20 Oct 2025 07:10:53 +0200 Subject: [PATCH 20/21] chore: Make privacy url not configurable --- lib/config/app_config.dart | 6 ++++++ lib/config/setting_keys.dart | 4 ---- lib/pages/homeserver_picker/homeserver_picker.dart | 4 ++-- lib/pages/settings/settings_view.dart | 5 +++-- lib/widgets/layouts/login_scaffold.dart | 4 ++-- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index e6a4b6c0c..4eb8aea34 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -50,4 +50,10 @@ abstract class AppConfig { host: 'servers.joinmatrix.org', path: 'servers.json', ); + + static final Uri privacyUrl = Uri( + scheme: 'https', + host: 'github.com', + path: '/krille-chan/fluffychat/blob/main/PRIVACY.md', + ); } diff --git a/lib/config/setting_keys.dart b/lib/config/setting_keys.dart index 0f51ea79b..1056929bc 100644 --- a/lib/config/setting_keys.dart +++ b/lib/config/setting_keys.dart @@ -52,10 +52,6 @@ enum AppSettings { // AppConfig-mirrored settings applicationName('chat.fluffy.application_name', 'FluffyChat'), defaultHomeserver('chat.fluffy.default_homeserver', 'matrix.org'), - privacyUrl( - 'chat.fluffy.privacy_url', - 'https://github.com/krille-chan/fluffychat/blob/main/PRIVACY.md', - ), // colorSchemeSeed stored as ARGB int colorSchemeSeedInt( 'chat.fluffy.color_scheme_seed', diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index 4e96a7154..9a1768d20 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -8,7 +8,7 @@ import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:universal_html/html.dart' as html; -import 'package:url_launcher/url_launcher_string.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; @@ -212,7 +212,7 @@ class HomeserverPickerController extends State { case MoreLoginActions.importBackup: restoreBackup(); case MoreLoginActions.privacy: - launchUrlString(AppSettings.privacyUrl.value); + launchUrl(AppConfig.privacyUrl); case MoreLoginActions.about: PlatformInfos.showDialog(context); } diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index aa9c3d50b..a92ee6a84 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -2,9 +2,10 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import 'package:fluffychat/config/setting_keys.dart'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; @@ -244,7 +245,7 @@ class SettingsView extends StatelessWidget { ListTile( leading: const Icon(Icons.privacy_tip_outlined), title: Text(L10n.of(context).privacy), - onTap: () => launchUrlString(AppSettings.privacyUrl.value), + onTap: () => launchUrl(AppConfig.privacyUrl), ), ListTile( leading: const Icon(Icons.info_outline_rounded), diff --git a/lib/widgets/layouts/login_scaffold.dart b/lib/widgets/layouts/login_scaffold.dart index e0cc24577..6559fad8f 100644 --- a/lib/widgets/layouts/login_scaffold.dart +++ b/lib/widgets/layouts/login_scaffold.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -108,7 +108,7 @@ class _PrivacyButtons extends StatelessWidget { ), ), TextButton( - onPressed: () => launchUrlString(AppSettings.privacyUrl.value), + onPressed: () => launchUrl(AppConfig.privacyUrl), child: Text( L10n.of(context).privacy, style: shadowTextStyle, From dfc97aa3d71bf7fefc6a5facd205d398ad3f99b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Mon, 20 Oct 2025 07:15:14 +0200 Subject: [PATCH 21/21] chore: Follow up donation button --- lib/config/app_config.dart | 1 + lib/pages/chat_list/client_chooser_button.dart | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 4eb8aea34..1e0b31ad1 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -36,6 +36,7 @@ abstract class AppConfig { 'https://github.com/krille-chan/fluffychat/issues'; static const String changelogUrl = 'https://github.com/krille-chan/fluffychat/blob/main/CHANGELOG.md'; + static const String donationUrl = 'https://ko-fi.com/krille'; static const Set defaultReactions = {'👍', '❤️', '😂', '😮', '😢'}; diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index dd01bf053..9161226f9 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -2,7 +2,9 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; @@ -67,7 +69,7 @@ class ClientChooserButton extends StatelessWidget { ], ), ), - if (Matrix.of(context).backgroundPush?.firebaseEnabled == false) + if (Matrix.of(context).backgroundPush?.firebaseEnabled != true) PopupMenuItem( value: SettingsAction.support, child: Row( @@ -219,7 +221,7 @@ class ClientChooserButton extends StatelessWidget { FluffyShare.shareInviteLink(context); break; case SettingsAction.support: - // TODO: Implement me + launchUrlString(AppConfig.donationUrl); break; case SettingsAction.settings: context.go('/rooms/settings');