Merge branch 'main' into dependabot/pub/record-6.1.2

pull/2241/head
Krille-chan 2 days ago committed by GitHub
commit 6a44ddba90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -34,6 +34,28 @@ body:
placeholder: "e.g. 1.12.0"
validations:
required: true
- type: dropdown
id: platform
attributes:
label: "Platform"
description: "Select the platform where the bug occurs."
options:
- Android (PlayStore)
- Android (F-Droid)
- Android (Other)
- iOS (iPhone)
- iOS (iPad)
- Web (Chrome(ium))
- Web (Firefox)
- Web (Safari)
- Linux (Snap)
- Linux (Flatpak)
- Linux (Other)
- macOS (iOS/iPadOS version)
- macOS (Self-compiled)
- Windows (Self-compiled)
validations:
required: true
- type: input
id: platform-info
attributes:

@ -1,5 +1,5 @@
blank_issues_enabled: true
blank_issues_enabled: false
contact_links:
- name: 👬 FluffyChat Community
url: https://matrix.to/#/#fluffychat:matrix.org
url: https://matrix.to/#/#fluffy-space:matrix.org
about: Please ask and answer questions here.

@ -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

@ -17,7 +17,6 @@
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"
android:maxSdkVersion="29" />
@ -125,16 +124,6 @@
android:foregroundServiceType="camera|microphone|mediaProjection">
</service>
<service android:name="io.wazo.callkeep.VoiceConnectionService"
android:label="Wazo"
android:foregroundServiceType="camera|microphone|mediaProjection"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>
<!-- From flutter_local_notifications package for notification actions -->
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver" />

@ -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
}

@ -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,
});
},
);

@ -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';
// 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 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;
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 bool hideTypingUsernames = false;
static const Set<String> defaultReactions = {'👍', '❤️', '😂', '😮', '😢'};
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,32 @@ 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 String donationUrl = 'https://ko-fi.com/krille';
static const Set<String> 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<String, dynamic> 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'];
}
}
static final Uri privacyUrl = Uri(
scheme: 'https',
host: 'github.com',
path: '/krille-chan/fluffychat/blob/main/PRIVACY.md',
);
}

@ -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: [

@ -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<T> {
textMessageMaxLength<int>('textMessageMaxLength', 16384),
@ -44,6 +16,9 @@ enum AppSettings<T> {
audioRecordingNoiseSuppress<bool>('audioRecordingNoiseSuppress', true),
audioRecordingBitRate<int>('audioRecordingBitRate', 64000),
audioRecordingSamplingRate<int>('audioRecordingSamplingRate', 44100),
showNoGoogle<bool>('chat.fluffy.show_no_google', false),
unifiedPushRegistered<bool>('chat.fluffy.unifiedpush.registered', false),
unifiedPushEndpoint<String>('chat.fluffy.unifiedpush.endpoint', ''),
pushNotificationsGatewayUrl<String>(
'pushNotificationsGatewayUrl',
'https://push.fluffychat.im/_matrix/push/v1/notify',
@ -52,6 +27,19 @@ enum AppSettings<T> {
'pushNotificationsPusherFormat',
'event_id_only',
),
renderHtml<bool>('chat.fluffy.renderHtml', true),
fontSizeFactor<double>('chat.fluffy.font_size_factor', 1.0),
hideRedactedEvents<bool>('chat.fluffy.hideRedactedEvents', false),
hideUnknownEvents<bool>('chat.fluffy.hideUnknownEvents', true),
separateChatTypes<bool>('chat.fluffy.separateChatTypes', false),
autoplayImages<bool>('chat.fluffy.autoplay_images', true),
sendTypingNotifications<bool>('chat.fluffy.send_typing_notifications', true),
sendPublicReadReceipts<bool>('chat.fluffy.send_public_read_receipts', true),
swipeRightToLeftToReply<bool>('chat.fluffy.swipeRightToLeftToReply', true),
sendOnEnter<bool>('chat.fluffy.send_on_enter', false),
showPresences<bool>('chat.fluffy.show_presences', true),
displayNavigationRail<bool>('chat.fluffy.display_navigation_rail', false),
experimentalVoip<bool>('chat.fluffy.experimental_voip', false),
shareKeysWith<String>('chat.fluffy.share_keys_with_2', 'all'),
noEncryptionWarningShown<bool>(
'chat.fluffy.no_encryption_warning_shown',
@ -61,40 +49,86 @@ enum AppSettings<T> {
'chat.fluffy.display_chat_details_column',
false,
),
// AppConfig-mirrored settings
applicationName<String>('chat.fluffy.application_name', 'FluffyChat'),
defaultHomeserver<String>('chat.fluffy.default_homeserver', 'matrix.org'),
// colorSchemeSeed stored as ARGB int
colorSchemeSeedInt<int>(
'chat.fluffy.color_scheme_seed',
0xFF5625BA,
),
enableSoftLogout<bool>('chat.fluffy.enable_soft_logout', false);
final String key;
final T defaultValue;
const AppSettings(this.key, this.defaultValue);
static SharedPreferences get store => _store!;
static SharedPreferences? _store;
static Future<SharedPreferences> init({loadWebConfigFile = true}) async {
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 =
utf8.decode((await http.get(Uri.parse('config.json'))).bodyBytes);
final configJson =
json.decode(configJsonString) as Map<String, Object?>;
for (final setting in AppSettings.values) {
if (store.get(setting.key) != null) continue;
final configValue = configJson[setting.name];
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> {
bool getItem(SharedPreferences store) => store.getBool(key) ?? defaultValue;
bool get value => AppSettings.store.getBool(key) ?? defaultValue;
Future<void> setItem(SharedPreferences store, bool value) =>
store.setBool(key, value);
Future<void> setItem(bool value) => AppSettings.store.setBool(key, value);
}
extension AppSettingsStringExtension on AppSettings<String> {
String getItem(SharedPreferences store) =>
store.getString(key) ?? defaultValue;
String get value => AppSettings.store.getString(key) ?? defaultValue;
Future<void> setItem(SharedPreferences store, String value) =>
store.setString(key, value);
Future<void> setItem(String value) => AppSettings.store.setString(key, value);
}
extension AppSettingsIntExtension on AppSettings<int> {
int getItem(SharedPreferences store) => store.getInt(key) ?? defaultValue;
int get value => AppSettings.store.getInt(key) ?? defaultValue;
Future<void> setItem(SharedPreferences store, int value) =>
store.setInt(key, value);
Future<void> setItem(int value) => AppSettings.store.setInt(key, value);
}
extension AppSettingsDoubleExtension on AppSettings<double> {
double getItem(SharedPreferences store) =>
store.getDouble(key) ?? defaultValue;
double get value => AppSettings.store.getDouble(key) ?? defaultValue;
Future<void> setItem(SharedPreferences store, double value) =>
store.setDouble(key, value);
Future<void> setItem(double value) => AppSettings.store.setDouble(key, value);
}

@ -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(

File diff suppressed because it is too large Load Diff

@ -2838,7 +2838,7 @@
}
}
},
"verifyOtherDeviceDescription": "Wenn du ein anderes Gerät verifizierst, können diese Geräte Schlüssel austauschen, was die Sicherheit insgesamt erhöht. 💪\n\nSobald du eine Verifizierung startest, erscheint ein Pop-up in der App auf beiden Geräten. Dort siehst du dann eine Reihe von Emojis oder Zahlen, die du miteinander vergleichen musst.\n\nAm besten hältst du beide Geräte bereit, bevor du die Verifizierung startest. 🤳",
"verifyOtherDeviceDescription": "Wenn du ein anderes Gerät verifizierst, können diese Geräte Schlüssel austauschen, was die Sicherheit insgesamt erhöht. 💪Sobald du eine Verifizierung startest, erscheint ein Pop-up in der App auf beiden Geräten. Dort siehst du dann eine Reihe von Emojis oder Zahlen, die du miteinander vergleichen musst. Am besten hältst du beide Geräte bereit, bevor du die Verifizierung startest. 🤳",
"@verifyOtherDeviceDescription": {},
"presenceStyle": "Statusmeldungen:",
"@presenceStyle": {
@ -3392,5 +3392,50 @@
"moreEvents": "Weitere Ereignisse",
"@moreEvents": {},
"declineInvitation": "Einladung ablehnen",
"@declineInvitation": {}
"@declineInvitation": {},
"noMessagesYet": "Noch keine Nachrichten",
"@noMessagesYet": {},
"longPressToRecordVoiceMessage": "Lange drücken, um eine Sprachnachricht aufzunehmen.",
"@longPressToRecordVoiceMessage": {},
"pause": "Pause",
"@pause": {},
"newSubSpace": "Neuer sub space",
"@newSubSpace": {},
"moveToDifferentSpace": "In einen anderen space wechseln",
"@moveToDifferentSpace": {},
"moveUp": "Nach oben",
"@moveUp": {},
"moveDown": "Nach unten",
"@moveDown": {},
"removeFromSpaceDescription": "Der Chat wird aus dem Space entfernt, erscheint aber weiterhin in Ihrer Chatliste.",
"@removeFromSpaceDescription": {},
"countChats": "{chats} Chats",
"@countChats": {
"type": "String",
"placeholders": {
"chats": {
"type": "int"
}
}
},
"spaceMemberOf": "Space-Mitglied von {spaces}",
"@spaceMemberOf": {
"type": "String",
"placeholders": {
"spaces": {
"type": "String"
}
}
},
"spaceMemberOfCanKnock": "Space-Mitglied von {spaces} kann klopfen",
"@spaceMemberOfCanKnock": {
"type": "String",
"placeholders": {
"spaces": {
"type": "String"
}
}
},
"donate": "Spenden",
"@donate": {}
}

File diff suppressed because it is too large Load Diff

@ -3378,5 +3378,38 @@
"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.",
"countChats": "{chats} chats",
"@countChats": {
"type": "String",
"placeholders": {
"chats": {
"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"
}
}
},
"donate": "Donate"
}

@ -3385,5 +3385,13 @@
"customReaction": "Erreakzio pertsonalizatua",
"@customReaction": {},
"declineInvitation": "Uko egin gonbidapenari",
"@declineInvitation": {}
"@declineInvitation": {},
"noMessagesYet": "Mezurik ez oraingoz",
"@noMessagesYet": {},
"longPressToRecordVoiceMessage": "Sakatuta mantendu ahots-mezua grabatzeko.",
"@longPressToRecordVoiceMessage": {},
"pause": "Gelditu",
"@pause": {},
"resume": "Jarraitu",
"@resume": {}
}

@ -3392,5 +3392,11 @@
"declineInvitation": "Diúltaigh don chuireadh",
"@declineInvitation": {},
"noMessagesYet": "Gan aon teachtaireachtaí fós",
"@noMessagesYet": {}
"@noMessagesYet": {},
"longPressToRecordVoiceMessage": "Brúigh go fada chun teachtaireacht gutha a thaifeadadh.",
"@longPressToRecordVoiceMessage": {},
"pause": "Sos",
"@pause": {},
"resume": "Atosú",
"@resume": {}
}

File diff suppressed because it is too large Load Diff

@ -1001,7 +1001,7 @@
},
"storeSecurlyOnThisDevice": "Droši uzglabāt šajā ierīcē",
"@storeSecurlyOnThisDevice": {},
"yourChatBackupHasBeenSetUp": "Tērzēšanu rezerves kopēšana tika iestatīta.",
"yourChatBackupHasBeenSetUp": "Tērzēšanu rezerves kopēšana iestatīta.",
"@yourChatBackupHasBeenSetUp": {},
"chatBackup": "Tērzēšanu rezerves kopēšana",
"@chatBackup": {

@ -152,7 +152,7 @@
}
}
},
"changedTheChatDescriptionTo": "{username} endret chatbeskrivelse til: «{description}»",
"changedTheChatDescriptionTo": "{username} endret chatbeskrivelsen til: '{description}'",
"@changedTheChatDescriptionTo": {
"type": "String",
"placeholders": {
@ -164,7 +164,7 @@
}
}
},
"changedTheChatNameTo": "{username} endret chatnavnet til: «{chatname}»",
"changedTheChatNameTo": "{username} endret chatnavnet til: '{chatname}'",
"@changedTheChatNameTo": {
"type": "String",
"placeholders": {
@ -185,7 +185,7 @@
}
}
},
"changedTheDisplaynameTo": "{username} endret visningsnavn til: {displayname}",
"changedTheDisplaynameTo": "{username} endret visningsnavn til: '{displayname}'",
"@changedTheDisplaynameTo": {
"type": "String",
"placeholders": {
@ -1851,7 +1851,7 @@
"@noMoreChatsFound": {},
"confirmMatrixId": "Vennligst bekreft din Matrix-ID for å slette kontoen din.",
"@confirmMatrixId": {},
"unread": "",
"unread": "Ulest",
"@unread": {},
"aboutHomeserver": "Om {homeserver}",
"@aboutHomeserver": {
@ -2708,5 +2708,426 @@
"changeTheCanonicalRoomAlias": "Endre hovedadressen til den offentlige chatten",
"@changeTheCanonicalRoomAlias": {},
"sendRoomNotifications": "Send en @room varsling",
"@sendRoomNotifications": {}
"@sendRoomNotifications": {},
"formattedMessagesDescription": "Vis rikt meldingsinnhold som fet skrift ved hjelp av markdown.",
"@formattedMessagesDescription": {},
"verifyOtherUser": "🔐 Verifiser annen bruker",
"@verifyOtherUser": {},
"verifyOtherDevice": "🔐 Verifiser annen enhet",
"@verifyOtherDevice": {},
"sendTypingNotificationsDescription": "Andre deltakere i en chat kan se når du skriver en ny melding.",
"@sendTypingNotificationsDescription": {},
"sendReadReceiptsDescription": "Andre deltakere i en chat kan se når du har lest en melding.",
"@sendReadReceiptsDescription": {},
"commandHint_logout": "Logg av din nåværende enhet",
"@commandHint_logout": {},
"commandHint_logoutall": "Logg ut alle aktive enheter",
"@commandHint_logoutall": {},
"resume": "Gjenoppta",
"@resume": {},
"unknownPushRule": "Ukjent push-regel '{rule}'",
"@unknownPushRule": {
"type": "String",
"placeholders": {
"rule": {
"type": "String"
}
}
},
"sentVoiceMessage": "🎙️ {duration} Talemelding fra {sender}",
"@sentVoiceMessage": {
"type": "String",
"placeholders": {
"sender": {
"type": "String"
},
"duration": {
"type": "String"
}
}
},
"deletePushRuleCanNotBeUndone": "Hvis du sletter denne varslingsinnstillingen, kan du ikke angre dette.",
"@deletePushRuleCanNotBeUndone": {},
"shareKeysWith": "Del nøkler med...",
"@shareKeysWith": {},
"canceledKeyVerification": "{sender} avbrøt nøkkelverifisering",
"@canceledKeyVerification": {
"type": "String",
"placeholders": {
"sender": {
"type": "String"
}
}
},
"completedKeyVerification": "{sender} fullførte nøkkelverifisering",
"@completedKeyVerification": {
"type": "String",
"placeholders": {
"sender": {
"type": "String"
}
}
},
"isReadyForKeyVerification": "{sender} er klar for nøkkelverifisering",
"@isReadyForKeyVerification": {
"type": "String",
"placeholders": {
"sender": {
"type": "String"
}
}
},
"requestedKeyVerification": "{sender} har bedt om nøkkelverifisering",
"@requestedKeyVerification": {
"type": "String",
"placeholders": {
"sender": {
"type": "String"
}
}
},
"notificationRuleEncryptedRoomOneToOne": "Kryptert rom én-til-én",
"@notificationRuleEncryptedRoomOneToOne": {},
"notificationRuleEncryptedRoomOneToOneDescription": "Varsler brukeren om meldinger i krypterte en-til-en-rom.",
"@notificationRuleEncryptedRoomOneToOneDescription": {},
"notificationRuleRoomOneToOne": "Rom én-til-én",
"@notificationRuleRoomOneToOne": {},
"customEmojisAndStickersBody": "Legg til eller del tilpassede emojier eller klistremerker som kan brukes i hvilken som helst chat.",
"@customEmojisAndStickersBody": {},
"homeserver": "Hjemmeserver",
"@homeserver": {},
"errorObtainingLocation": "Feil ved henting av posisjon: {error}",
"@errorObtainingLocation": {
"type": "String",
"placeholders": {
"error": {
"type": "String"
}
}
},
"chatDescription": "Chat beskrivelse",
"@chatDescription": {},
"hideRedactedMessages": "Skjul redigerte meldinger",
"@hideRedactedMessages": {},
"hideRedactedMessagesBody": "Hvis noen redigerer en melding, vil ikke denne meldingen lenger være synlig i chatten.",
"@hideRedactedMessagesBody": {},
"blockListDescription": "Du kan blokkere brukere som forstyrrer deg. Du vil ikke kunne motta meldinger eller rominvitasjoner fra brukerne på din personlige blokkeringsliste.",
"@blockListDescription": {},
"blockUsername": "Ignorer brukernavn",
"@blockUsername": {},
"inviteContactToGroupQuestion": "Vil du invitere {contact} til chatten «{groupName}»?",
"@inviteContactToGroupQuestion": {},
"noChatDescriptionYet": "Ingen chatbeskrivelse er opprettet ennå.",
"@noChatDescriptionYet": {},
"redactMessageDescription": "Meldingen vil bli redigert for alle deltakerne i denne samtalen. Dette kan ikke angres.",
"@redactMessageDescription": {},
"optionalRedactReason": "(Valgfritt) Årsak til redigering av denne meldingen...",
"@optionalRedactReason": {},
"locationDisabledNotice": "Lokasjonstjenester er deaktivert. Vennligst aktiver dem for at du skal kunne dele posisjonen din.",
"@locationDisabledNotice": {
"type": "String",
"placeholders": {}
},
"locationPermissionDeniedNotice": "Lokasjonstillatelse nektet. Gi dem tillatelse til å dele din lokasjon.",
"@locationPermissionDeniedNotice": {
"type": "String",
"placeholders": {}
},
"dehydrate": "Eksporter økten og slett enheten",
"@dehydrate": {},
"dehydrateWarning": "Denne handlingen kan ikke angres. Sørg for at du lagrer sikkerhetskopifilen på en trygg måte.",
"@dehydrateWarning": {},
"dehydrateTor": "TOR-brukere: Eksporter økt",
"@dehydrateTor": {},
"dehydrateTorLong": "For TOR-brukere anbefales det å eksportere økten før vinduet lukkes.",
"@dehydrateTorLong": {},
"hydrateTor": "TOR-brukere: Importer eksportert økt",
"@hydrateTor": {},
"hydrateTorLong": "Eksporterte du økten din sist gang på TOR? Importer den raskt og fortsett å chatte.",
"@hydrateTorLong": {},
"noEncryptionForPublicRooms": "Du kan bare aktivere kryptering på rom som ikke er offentlig tilgjengelig.",
"@noEncryptionForPublicRooms": {
"type": "String",
"placeholders": {}
},
"noMatrixServer": "{server1} er ingen matrix-server, bruk {server2} i stedet?",
"@noMatrixServer": {
"type": "String",
"placeholders": {
"server1": {
"type": "String"
},
"server2": {
"type": "String"
}
}
},
"roomNotificationSettings": "Innstillinger for romvarsler",
"@roomNotificationSettings": {},
"userSpecificNotificationSettings": "Brukerspesifikke varslingsinnstillinger",
"@userSpecificNotificationSettings": {},
"otherNotificationSettings": "Andre varslingsinnstillinger",
"@otherNotificationSettings": {},
"contentNotificationSettings": "Innstillinger for innholdsvarslinger",
"@contentNotificationSettings": {},
"generalNotificationSettings": "Generelle varslingsinnstillinger",
"@generalNotificationSettings": {},
"appIntroduction": "Med FluffyChat kan du chatte med vennene dine på tvers av forskjellige meldingstjenester. Finn ut mer på https://matrix.org eller trykk bare på *Fortsett*.",
"@appIntroduction": {},
"notificationRuleContainsUserNameDescription": "Varsler bruker når en melding inneholder ens brukernavn.",
"@notificationRuleContainsUserNameDescription": {},
"hideMemberChangesInPublicChats": "Skjul medlemsendringer i offentlige chatter",
"@hideMemberChangesInPublicChats": {},
"removeFromSpace": "Fjern fra området",
"@removeFromSpace": {},
"addToSpaceDescription": "Velg områder hvor denne chatten legges til.",
"@addToSpaceDescription": {},
"pleaseEnterRecoveryKeyDescription": "For å låse opp gamle meldinger, vennligst skriv inn gjenopprettingsnøkkelen som ble generert i en tidligere økt. Gjenopprettingsnøkkelen er IKKE passordet ditt.",
"@pleaseEnterRecoveryKeyDescription": {},
"reactedWith": "{sender} reagerte med {reaction}",
"@reactedWith": {
"type": "String",
"placeholders": {
"sender": {
"type": "String"
},
"reaction": {
"type": "String"
}
}
},
"pinMessage": "Fest til rommet",
"@pinMessage": {},
"emojis": "Emojier",
"@emojis": {},
"placeCall": "Ringe opp",
"@placeCall": {},
"unsupportedAndroidVersionLong": "Denne funksjonen krever en nyere Android-versjon. Se etter oppdateringer eller støtte for Lineage OS.",
"@unsupportedAndroidVersionLong": {},
"videoCallsBetaWarning": "Vær oppmerksom på at videosamtaler for øyeblikket er i betaversjon. Det fungerer kanskje ikke som forventet eller i det hele tatt.",
"@videoCallsBetaWarning": {},
"experimentalVideoCalls": "Eksperimentelle videoanrop",
"@experimentalVideoCalls": {},
"notificationRuleRoomnotifDescription": "Varsler brukeren når en melding inneholder @room.",
"@notificationRuleRoomnotifDescription": {},
"notificationRuleTombstoneDescription": "Varsler brukeren om meldinger om deaktivering av rom.",
"@notificationRuleTombstoneDescription": {},
"notificationRuleReactionDescription": "Demper varsler for reaksjoner.",
"@notificationRuleReactionDescription": {},
"notificationRuleSuppressEdits": "Demp redigeringer",
"@notificationRuleSuppressEdits": {},
"chatHasBeenAddedToThisSpace": "Chatten er lagt til i dette området",
"@chatHasBeenAddedToThisSpace": {},
"clearArchive": "Tøm arkivet",
"@clearArchive": {},
"commandHint_markasgroup": "Merk som gruppe",
"@commandHint_markasgroup": {},
"commandHint_ban": "Utesteng den gitte brukeren fra dette rommet",
"@commandHint_ban": {
"type": "String",
"description": "Usage hint for the command /ban"
},
"commandHint_clearcache": "Tøm cache",
"@commandHint_clearcache": {
"type": "String",
"description": "Usage hint for the command /clearcache"
},
"homeserverDescription": "Alle dataene dine lagres på hjemmeserveren, akkurat som hos en e-postleverandør. Du kan velge hvilken hjemmeserver du vil bruke, samtidig som du fortsatt kan kommunisere med alle. Lær mer på https://matrix.org.",
"@homeserverDescription": {},
"doesNotSeemToBeAValidHomeserver": "Ser ikke ut til å være en kompatibel hjemmeserver. Feil URL?",
"@doesNotSeemToBeAValidHomeserver": {},
"prepareSendingAttachment": "Forbered sending av vedlegg...",
"@prepareSendingAttachment": {},
"generatingVideoThumbnail": "Genererer videominiatyrbilde ...",
"@generatingVideoThumbnail": {},
"compressVideo": "Komprimerer video...",
"@compressVideo": {},
"welcomeText": "Hei, hei! 👋 Dette er FluffyChat. Du kan logge på hvilken som helst hjemmeserver som er kompatibel med https://matrix.org. Og deretter chatte med hvem som helst. Det er et stort desentralisert meldingsnettverk!",
"@welcomeText": {},
"notificationRuleIsUserMentionDescription": "Varsler brukeren når de er direkte nevnt i en melding.",
"@notificationRuleIsUserMentionDescription": {},
"notificationRuleContainsDisplayName": "Inneholder visningsnavn",
"@notificationRuleContainsDisplayName": {},
"notificationRuleContainsDisplayNameDescription": "Varsler brukeren når en melding inneholder ens visningsnavnet.",
"@notificationRuleContainsDisplayNameDescription": {},
"notificationRuleIsUserMention": "Brukeromtale",
"@notificationRuleIsUserMention": {},
"notificationRuleIsRoomMention": "Romomtale",
"@notificationRuleIsRoomMention": {},
"whatIsAHomeserver": "Hva er en hjemmeserver?",
"@whatIsAHomeserver": {},
"commandHint_me": "Beskriv deg selv",
"@commandHint_me": {
"type": "String",
"description": "Usage hint for the command /me"
},
"commandHint_myroomavatar": "Angi bilde for dette rommet (med mxc-uri)",
"@commandHint_myroomavatar": {
"type": "String",
"description": "Usage hint for the command /myroomavatar"
},
"commandHint_myroomnick": "Angi visningsnavnet ditt for dette rommet",
"@commandHint_myroomnick": {
"type": "String",
"description": "Usage hint for the command /myroomnick"
},
"commandHint_plain": "Send uformatert tekst",
"@commandHint_plain": {
"type": "String",
"description": "Usage hint for the command /plain"
},
"commandHint_react": "Send svar som en reaksjon",
"@commandHint_react": {
"type": "String",
"description": "Usage hint for the command /react"
},
"commandHint_send": "Send tekst",
"@commandHint_send": {
"type": "String",
"description": "Usage hint for the command /send"
},
"commandHint_unban": "Opphev utestengelsen til den gitte brukeren fra dette rommet",
"@commandHint_unban": {
"type": "String",
"description": "Usage hint for the command /unban"
},
"commandInvalid": "Ugyldig kommando",
"@commandInvalid": {
"type": "String"
},
"commandMissing": "{command} er ikke en kommando.",
"@commandMissing": {
"type": "String",
"placeholders": {
"command": {
"type": "String"
}
},
"description": "State that {command} is not a valid /command."
},
"checkList": "Sjekkliste",
"@checkList": {},
"createNewSpace": "Nytt område",
"@createNewSpace": {
"type": "String",
"placeholders": {}
},
"emoteKeyboardNoRecents": "Nylig brukte emotes vil vises her ...",
"@emoteKeyboardNoRecents": {
"type": "String",
"placeholders": {}
},
"unableToJoinChat": "Kan ikke bli med i chatten. Kanskje den andre parten allerede har lukket samtalen.",
"@unableToJoinChat": {},
"otherPartyNotLoggedIn": "Den andre parten er ikke logget inn og kan derfor ikke motta meldinger!",
"@otherPartyNotLoggedIn": {},
"notificationRuleIsRoomMentionDescription": "Varsler brukeren når det er en romomtale.",
"@notificationRuleIsRoomMentionDescription": {},
"notificationRuleRoomnotif": "Romvarsel",
"@notificationRuleRoomnotif": {},
"notificationRuleCall": "Anrop",
"@notificationRuleCall": {},
"notificationRuleCallDescription": "Varsler brukeren om anrop.",
"@notificationRuleCallDescription": {},
"waitingPartnerAcceptRequest": "Venter på at partneren skal godta forespørselen…",
"@waitingPartnerAcceptRequest": {
"type": "String",
"placeholders": {}
},
"waitingPartnerEmoji": "Venter på at partneren skal godta emojien…",
"@waitingPartnerEmoji": {
"type": "String",
"placeholders": {}
},
"wipeChatBackup": "Vil du slette sikkerhetskopien av chatten din for å opprette en ny gjenopprettingsnøkkel?",
"@wipeChatBackup": {
"type": "String",
"placeholders": {}
},
"switchToAccount": "Bytt til konto {number}",
"@switchToAccount": {
"type": "number",
"placeholders": {
"number": {
"type": "String"
}
}
},
"widgetEtherpad": "Tekstnotat",
"@widgetEtherpad": {},
"noOneCanJoin": "Ingen kan bli med",
"@noOneCanJoin": {},
"userWouldLikeToChangeTheChat": "{user} vil gjerne bli med i chatten.",
"@userWouldLikeToChangeTheChat": {
"placeholders": {
"user": {
"type": "String"
}
}
},
"noPublicLinkHasBeenCreatedYet": "Ingen offentlig lenke er opprettet ennå",
"@noPublicLinkHasBeenCreatedYet": {},
"commandHint_html": "Send HTML-formatert tekst",
"@commandHint_html": {
"type": "String",
"description": "Usage hint for the command /html"
},
"commandHint_kick": "Fjern den gitte brukeren fra dette rommet",
"@commandHint_kick": {
"type": "String",
"description": "Usage hint for the command /kick"
},
"commandHint_leave": "Forlat dette rommet",
"@commandHint_leave": {
"type": "String",
"description": "Usage hint for the command /leave"
},
"commandHint_discardsession": "Forkast økten",
"@commandHint_discardsession": {
"type": "String",
"description": "Usage hint for the command /discardsession"
},
"commandHint_dm": "Start en direkte chat\nBruk --no-encryption for å deaktivere kryptering",
"@commandHint_dm": {
"type": "String",
"description": "Usage hint for the command /dm"
},
"commandHint_create": "Opprett en tom gruppechat\nBruk --no-encryption for å deaktivere kryptering",
"@commandHint_create": {
"type": "String",
"description": "Usage hint for the command /create"
},
"redactedBy": "Redigert av {username}",
"@redactedBy": {
"type": "String",
"placeholders": {
"username": {
"type": "String"
}
}
},
"directChat": "Direkte chat",
"@directChat": {},
"redactedByBecause": "Redigert av {username} fordi: «{reason}»",
"@redactedByBecause": {
"type": "String",
"placeholders": {
"username": {
"type": "String"
},
"reason": {
"type": "String"
}
}
},
"redactMessage": "Rediger melding",
"@redactMessage": {
"type": "String",
"placeholders": {}
},
"roomVersion": "Rom versjon",
"@roomVersion": {
"type": "String",
"placeholders": {}
}
}

@ -1087,7 +1087,7 @@
"type": "String",
"placeholders": {}
},
"loadCountMoreParticipants": "Загрузить еще {count} участника(ов)",
"loadCountMoreParticipants": "",
"@loadCountMoreParticipants": {
"type": "String",
"placeholders": {
@ -1879,7 +1879,7 @@
"type": "String",
"placeholders": {}
},
"unreadChats": "{unreadCount, plural, other{{unreadCount} непрочитанных чата(ов)}}",
"unreadChats": "{unreadCount, plural, one{# непрочитанный чат} few{# непрочитанных чата} other{# непрочитанных чатов}}",
"@unreadChats": {
"type": "String",
"placeholders": {
@ -1888,7 +1888,7 @@
}
}
},
"userAndOthersAreTyping": "{username} и {count} других участников печатают…",
"userAndOthersAreTyping": "",
"@userAndOthersAreTyping": {
"type": "String",
"placeholders": {
@ -2328,7 +2328,7 @@
"@storeInAndroidKeystore": {},
"storeInAppleKeyChain": "Сохранить в Apple KeyChain",
"@storeInAppleKeyChain": {},
"countFiles": "{count} файлов",
"countFiles": "",
"@countFiles": {
"placeholders": {
"count": {
@ -2405,7 +2405,7 @@
"@noKeyForThisMessage": {},
"screenSharingTitle": "общий доступ к экрану",
"@screenSharingTitle": {},
"numChats": "{number} чатов",
"numChats": "",
"@numChats": {
"type": "number",
"placeholders": {
@ -2437,7 +2437,7 @@
"@readUpToHere": {},
"commandHint_hug": "Отправить обнимашки",
"@commandHint_hug": {},
"cuddleContent": "{senderName} улыбнулся(-ась) Вам",
"cuddleContent": "{senderName} улыбнулся(ась) Вам",
"@cuddleContent": {
"type": "String",
"placeholders": {
@ -2598,7 +2598,7 @@
"@inviteGroupChat": {},
"invalidInput": "Недопустимый ввод!",
"@invalidInput": {},
"wrongPinEntered": "Введён неверный пин-код! Повторите попытку через {seconds} секунд...",
"wrongPinEntered": "",
"@wrongPinEntered": {
"type": "String",
"placeholders": {
@ -2934,7 +2934,7 @@
"@commandHint_ignore": {},
"commandHint_unignore": "Не игнорировать данный matrix ID",
"@commandHint_unignore": {},
"unreadChatsInApp": "{appname}: {unread} непрочитанные чаты",
"unreadChatsInApp": "",
"@unreadChatsInApp": {
"type": "String",
"placeholders": {
@ -2946,7 +2946,7 @@
}
}
},
"thereAreCountUsersBlocked": "Сейчас заблокировано {count} пользователей.",
"thereAreCountUsersBlocked": "",
"@thereAreCountUsersBlocked": {
"type": "String",
"count": {}
@ -3009,7 +3009,7 @@
"@inviteOtherUsers": {},
"changeTheVisibilityOfChatHistory": "Изменить видимость истории чата",
"@changeTheVisibilityOfChatHistory": {},
"countChatsAndCountParticipants": "{chats} чатов и {participants} участников",
"countChatsAndCountParticipants": "",
"@countChatsAndCountParticipants": {
"type": "String",
"placeholders": {
@ -3097,7 +3097,7 @@
"@joinedChats": {},
"serverInformation": "Информация о сервере:",
"@serverInformation": {},
"sendingAttachmentCountOfCount": "Отправляю... {index} {length}...",
"sendingAttachmentCountOfCount": "Отправляется вложение {index} из {length}...",
"@sendingAttachmentCountOfCount": {
"type": "integer",
"placeholders": {
@ -3136,7 +3136,7 @@
"@italicText": {},
"unableToJoinChat": "Невозможно присоединиться к чату. Возможно, другая сторона уже закончила разговор.",
"@unableToJoinChat": {},
"serverLimitReached": "Ограничения сервера. Ожидайте{seconds} секунд...",
"serverLimitReached": "",
"@serverLimitReached": {
"type": "integer",
"placeholders": {
@ -3167,7 +3167,7 @@
"@version": {},
"website": "Сайт",
"@website": {},
"sendImages": "Отправить {count} изображений",
"sendImages": "",
"@sendImages": {
"type": "String",
"placeholders": {
@ -3283,5 +3283,25 @@
"commandHint_roomupgrade": "Обновить комнату до указанной версии",
"@commandHint_roomupgrade": {},
"notificationRuleInviteForMeDescription": "Уведомляет пользователя, когда его приглашают в комнату.",
"@notificationRuleInviteForMeDescription": {}
"@notificationRuleInviteForMeDescription": {},
"countInvited": "",
"@countInvited": {
"type": "String",
"placeholders": {
"count": {
"type": "int"
}
}
},
"unknownPushRule": "Неизвестное правило оповещения '{rule}'",
"@unknownPushRule": {
"type": "String",
"placeholders": {
"rule": {
"type": "String"
}
}
},
"checkList": "Контрольный список",
"@checkList": {}
}

@ -3393,5 +3393,44 @@
"pause": "暂停",
"@pause": {},
"resume": "继续",
"@resume": {}
"@resume": {},
"newSubSpace": "新建子空间",
"@newSubSpace": {},
"moveToDifferentSpace": "移动到别的空间",
"@moveToDifferentSpace": {},
"moveUp": "上移",
"@moveUp": {},
"moveDown": "下移",
"@moveDown": {},
"removeFromSpaceDescription": "将从空间移除该聊天,但仍出现在聊天列表中。",
"@removeFromSpaceDescription": {},
"countChats": "{chats} 个聊天",
"@countChats": {
"type": "String",
"placeholders": {
"chats": {
"type": "int"
}
}
},
"spaceMemberOf": "{spaces} 的空间成员",
"@spaceMemberOf": {
"type": "String",
"placeholders": {
"spaces": {
"type": "String"
}
}
},
"spaceMemberOfCanKnock": "{spaces} 的空间成员可以敲门",
"@spaceMemberOfCanKnock": {
"type": "String",
"placeholders": {
"spaces": {
"type": "String"
}
}
},
"donate": "捐赠",
"@donate": {}
}

@ -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<void> startGui(List<Client> 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) {

@ -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<ChatPageWithRoom>
}
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<ChatPageWithRoom>
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<ChatPageWithRoom>
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<ChatPageWithRoom>
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<ChatPageWithRoom>
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<ChatPageWithRoom>
_setReadMarkerFuture = timeline
.setReadMarker(
eventId: eventId,
public: AppConfig.sendPublicReadReceipts,
public: AppSettings.sendPublicReadReceipts.value,
)
.then((_) {
_setReadMarkerFuture = null;
@ -542,7 +544,7 @@ class ChatController extends State<ChatPageWithRoom>
Future<void> 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<ChatPageWithRoom>
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<ChatPageWithRoom>
_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<ChatPageWithRoom>
}
}
}
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<ChatPageWithRoom>
void toggleDisplayChatDetailsColumn() async {
await AppSettings.displayChatDetailsColumn.setItem(
Matrix.of(context).store,
!_displayChatDetailsColumn.value,
);
_displayChatDetailsColumn.value = !_displayChatDetailsColumn.value;

@ -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,

@ -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(

@ -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<CuteContent> {
@override
void initState() {
if (AppConfig.autoplayImages && !_isOverlayShown) {
if (AppSettings.autoplayImages.value && !_isOverlayShown) {
addOverlay();
}
super.initState();

@ -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,
),

@ -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,
),
),
),

@ -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,
),

@ -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,
),

@ -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

@ -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,

@ -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,
),

@ -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

@ -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<RecordingViewModel> {
}
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<RecordingViewModel> {
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 ?? '',

@ -26,6 +26,16 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
bool historyVisibilityLoading = false;
bool guestAccessLoading = false;
Room get room => Matrix.of(context).client.getRoomById(widget.roomId)!;
Set<Room> 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<Room>(),
};
String get roomVersion =>
room
@ -46,9 +56,20 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
joinRules.remove(JoinRules.knock);
}
// Not yet supported in FluffyChat:
joinRules.remove(JoinRules.restricted);
joinRules.remove(JoinRules.knockRestricted);
// Restricted is only supported for rooms up from version 8:
if (roomVersionInt != null && roomVersionInt <= 7) {
joinRules.remove(JoinRules.restricted);
}
// Knock-Restricted is only supported for rooms up from version 10:
if (roomVersionInt != null && roomVersionInt <= 9) {
joinRules.remove(JoinRules.knockRestricted);
}
if (knownSpaceParents.isEmpty) {
joinRules.remove(JoinRules.restricted);
joinRules.remove(JoinRules.knockRestricted);
}
// If an unsupported join rule is the current join rule, display it:
final currentJoinRule = room.joinRules;
@ -64,7 +85,13 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
});
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) {

@ -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<Room> 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(', '),
);
}
}
}

@ -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';
@ -72,11 +71,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,
});
@ -92,9 +93,7 @@ class ChatListController extends State<ChatList>
StreamSubscription? _intentUriStreamSubscription;
ActiveFilter activeFilter = AppConfig.separateChatTypes
? ActiveFilter.messages
: ActiveFilter.allChats;
late ActiveFilter activeFilter;
String? _activeSpaceId;
String? get activeSpaceId => _activeSpaceId;
@ -399,7 +398,11 @@ class ChatListController extends State<ChatList>
@override
void initState() {
activeFilter = AppSettings.separateChatTypes.value
? ActiveFilter.messages
: ActiveFilter.allChats;
_initReceiveSharingIntent();
_activeSpaceId = widget.activeSpace;
scrollController.addListener(_onScroll);
_waitForFirstSync();
@ -710,8 +713,7 @@ class ChatListController extends State<ChatList>
context: context,
);
if (result == OkCancelResult.ok) {
await Matrix.of(context).store.setBool(SettingKeys.showPresences, false);
AppConfig.showPresences = false;
AppSettings.showPresences.setItem(false);
setState(() {});
}
}

@ -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';
@ -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);
@ -123,7 +120,8 @@ class ChatListViewBody extends StatelessWidget {
),
),
],
if (!controller.isSearchMode && AppConfig.showPresences)
if (!controller.isSearchMode &&
AppSettings.showPresences.value)
GestureDetector(
onLongPress: () => controller.dismissStatusList(),
child: StatusMessageList(
@ -158,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,
]

@ -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;
@ -246,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
@ -318,41 +312,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,

@ -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,

@ -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,6 +69,17 @@ class ClientChooserButton extends StatelessWidget {
],
),
),
if (Matrix.of(context).backgroundPush?.firebaseEnabled != true)
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 +220,9 @@ class ClientChooserButton extends StatelessWidget {
case SettingsAction.invite:
FluffyShare.shareInviteLink(context);
break;
case SettingsAction.support:
launchUrlString(AppConfig.donationUrl);
break;
case SettingsAction.settings:
context.go('/rooms/settings');
break;
@ -226,6 +242,7 @@ enum SettingsAction {
newGroup,
setStatus,
invite,
support,
settings,
archive,
}

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
@ -8,26 +10,35 @@ 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;
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({
@ -35,8 +46,6 @@ class SpaceView extends StatefulWidget {
required this.onBack,
required this.onChatTab,
required this.activeChat,
required this.toParentSpace,
required this.onChatContext,
super.key,
});
@ -58,9 +67,28 @@ class _SpaceViewState extends State<SpaceView> {
}
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 +102,25 @@ class _SpaceViewState extends State<SpaceView> {
);
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 +148,7 @@ class _SpaceViewState extends State<SpaceView> {
),
);
if (mounted && joined == true) {
setState(() {
_discoveredChildren.remove(item);
});
setState(() {});
}
}
@ -129,6 +164,10 @@ class _SpaceViewState extends State<SpaceView> {
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 +190,11 @@ class _SpaceViewState extends State<SpaceView> {
}
}
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 +219,169 @@ class _SpaceViewState extends State<SpaceView> {
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<SpaceChildAction>(
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 +391,11 @@ class _SpaceViewState extends State<SpaceView> {
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 +410,7 @@ class _SpaceViewState extends State<SpaceView> {
title: ListTile(
contentPadding: EdgeInsets.zero,
leading: Avatar(
size: avatarSize,
mxContent: room?.avatar,
name: displayname,
border: BorderSide(width: 1, color: theme.dividerColor),
@ -252,18 +421,38 @@ class _SpaceViewState extends State<SpaceView> {
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<AddRoomType>(
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<SpaceActions>(
useRootNavigator: true,
onSelected: _onSpaceAction,
@ -290,6 +479,21 @@ class _SpaceViewState extends State<SpaceView> {
],
),
),
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 +509,6 @@ class _SpaceViewState extends State<SpaceView> {
),
],
),
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(
@ -327,29 +521,11 @@ class _SpaceViewState extends State<SpaceView> {
.where((s) => s.hasRoomUpdate)
.rateLimit(const Duration(seconds: 1)),
builder: (context, snapshot) {
final childrenIds = room.spaceChildren
.map((c) => c.roomId)
.whereType<String>()
.toSet();
final joinedRooms = room.client.rooms
.where((room) => childrenIds.remove(room.id))
.toList();
final joinedParents = room.spaceParents
.map((parent) {
final roomId = parent.roomId;
if (roomId == null) return null;
return room.client.getRoomById(roomId);
})
.whereType<Room>()
.toList();
final filter = _filterController.text.trim().toLowerCase();
return CustomScrollView(
slivers: [
SliverAppBar(
floating: true,
toolbarHeight: 72,
scrolledUnderElevation: 0,
backgroundColor: Colors.transparent,
automaticallyImplyLeading: false,
@ -359,11 +535,6 @@ class _SpaceViewState extends State<SpaceView> {
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(
@ -382,83 +553,11 @@ class _SpaceViewState extends State<SpaceView> {
),
),
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: 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 +567,7 @@ class _SpaceViewState extends State<SpaceView> {
child: TextButton(
onPressed: _isLoading ? null : _loadHierarchy,
child: _isLoading
? LinearProgressIndicator(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
)
? const CircularProgressIndicator.adaptive()
: Text(L10n.of(context).loadMore),
),
);
@ -484,6 +579,10 @@ class _SpaceViewState extends State<SpaceView> {
if (!displayname.toLowerCase().contains(filter)) {
return const SizedBox.shrink();
}
var joinedRoom = room.client.getRoomById(item.roomId);
if (joinedRoom?.membership == Membership.leave) {
joinedRoom = null;
}
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
@ -493,51 +592,83 @@ class _SpaceViewState extends State<SpaceView> {
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,
),
),
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,
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,
),
),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
if (joinedRoom != null)
UnreadBubble(room: joinedRoom)
else
const Icon(Icons.chevron_right_outlined),
],
),
),
),
),
@ -552,9 +683,3 @@ class _SpaceViewState extends State<SpaceView> {
);
}
}
enum SpaceActions {
settings,
invite,
leave,
}

@ -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(),
);
}
}

@ -8,9 +8,10 @@ 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';
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<HomeserverPicker> {
bool isLoading = false;
final TextEditingController homeserverController = TextEditingController(
text: AppConfig.defaultHomeserver,
text: AppSettings.defaultHomeserver.value,
);
String? error;
@ -211,7 +212,7 @@ class HomeserverPickerController extends State<HomeserverPicker> {
case MoreLoginActions.importBackup:
restoreBackup();
case MoreLoginActions.privacy:
launchUrlString(AppConfig.privacyUrl);
launchUrl(AppConfig.privacyUrl);
case MoreLoginActions.about:
PlatformInfos.showDialog(context);
}

@ -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,
),

@ -2,6 +2,7 @@ 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/app_config.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(AppConfig.privacyUrl),
onTap: () => launchUrl(AppConfig.privacyUrl),
),
ListTile(
leading: const Icon(Icons.info_outline_rounded),

@ -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,
),
],
),

@ -112,7 +112,6 @@ class SettingsSecurityController extends State<SettingsSecurity> {
void changeShareKeysWith(ShareKeysWith? shareKeysWith) async {
if (shareKeysWith == null) return;
AppSettings.shareKeysWith.setItem(
Matrix.of(context).store,
shareKeysWith.name,
);
Matrix.of(context).client.shareKeysWith = shareKeysWith;

@ -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),

@ -18,7 +18,9 @@ class SettingsStyle extends StatefulWidget {
class SettingsStyleController extends State<SettingsStyle> {
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<SettingsStyle> {
}
void changeFontSizeFactor(double d) {
setState(() => AppConfig.fontSizeFactor = d);
Matrix.of(context).store.setString(
SettingKeys.fontSizeFactor,
AppConfig.fontSizeFactor.toString(),
);
AppSettings.fontSizeFactor.setItem(d);
}
@override

@ -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,
),
],
),

@ -66,6 +66,7 @@ class BackgroundPush {
}
final pendingTests = <String, Completer<void>>{};
bool firebaseEnabled = false;
//<GOOGLE_SERVICES>final firebase = FcmSharedIsolate();
@ -74,6 +75,7 @@ class BackgroundPush {
bool upAction = false;
void _init() async {
//<GOOGLE_SERVICES>firebaseEnabled = true;
try {
if (PlatformInfos.isAndroid) {
final port = ReceivePort();
@ -217,8 +219,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 +259,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 +325,7 @@ class BackgroundPush {
if (matrix == null) {
return;
}
if ((matrix?.store.getBool(SettingKeys.showNoGoogle) ?? false) == true) {
if (!AppSettings.showNoGoogle.value) {
return;
}
await loadLocale();
@ -356,15 +356,19 @@ class BackgroundPush {
}
}
await setupPusher(
gatewayUrl:
AppSettings.pushNotificationsGatewayUrl.getItem(matrix!.store),
gatewayUrl: AppSettings.pushNotificationsGatewayUrl.value,
token: _fcmToken,
);
}
Future<void> 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<void> _newUpEndpoint(PushEndpoint newPushEndpoint, String i) async {
@ -409,18 +413,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<void> _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},

@ -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'),
],

@ -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<void> 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;

@ -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<Event> {
List<Event> filterByVisibleInGui({String? exceptionEventId}) => where(
List<Event> 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,

@ -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<String?> 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);
}

@ -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<void> 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;

@ -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<String> 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,
);
}
}

@ -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<void> 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<void> _tryPushHelper(
client ??= (await ClientManager.getClients(
initialize: false,
store: await SharedPreferences.getInstance(),
store: await AppSettings.init(),
))
.first;
final event = await client.getEventByPushNotification(
@ -278,25 +278,27 @@ Future<void> _tryPushHelper(
importance: Importance.high,
priority: Priority.max,
groupKey: event.room.spaceParents.firstOrNull?.roomId ?? 'rooms',
actions: <AndroidNotificationAction>[
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>[
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(

@ -10,7 +10,7 @@ class DialogTextField extends StatelessWidget {
final String? prefixText;
final String? suffixText;
final String? errorText;
final bool obscureText = false;
final bool obscureText;
final bool isDestructive = false;
final int? minLines;
final int? maxLines;
@ -32,6 +32,7 @@ class DialogTextField extends StatelessWidget {
this.controller,
this.counterText,
this.errorText,
this.obscureText = false,
});
@override
@ -64,20 +65,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(

@ -30,7 +30,9 @@ class PublicRoomDialog extends StatelessWidget {
final result = await showFutureLoadingDialog<String>(
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
@ -64,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;
}

@ -34,76 +34,74 @@ Future<String?> showTextInputDialog({
useRootNavigator: useRootNavigator,
builder: (context) {
final error = ValueNotifier<String?>(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(),
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,
),
const SizedBox(height: 16),
ValueListenableBuilder<String?>(
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,
);
},
options: const LinkifyOptions(humanize: false),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
),
],
),
),
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<String>(input);
},
autofocus: true,
child: Text(
okLabel ?? L10n.of(context).ok,
style: isDestructive
? TextStyle(color: Theme.of(context).colorScheme.error)
: null,
const SizedBox(height: 16),
ValueListenableBuilder<String?>(
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,
obscureText: obscureText,
);
},
),
),
],
],
),
),
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<String>(input);
},
autofocus: true,
child: Text(
okLabel ?? L10n.of(context).ok,
style: isDestructive
? TextStyle(color: Theme.of(context).colorScheme.error)
: null,
),
),
],
);
},
);

@ -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<AppLockWidget> with WidgetsBindingObserver {
Future<void> changePincode(String? pincode) async {
await const FlutterSecureStorage().write(
key: SettingKeys.appLockKey,
key: 'chat.fluffy.app_lock',
value: pincode,
);
_pincode = pincode;

@ -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(),
),

@ -21,7 +21,7 @@ class _ConfigViewerState extends State<ConfigViewer> {
String initialValue,
) async {
if (appSetting is AppSettings<bool>) {
await appSetting.setItem(store, !(initialValue == 'true'));
await appSetting.setItem(!(initialValue == 'true'));
setState(() {});
return;
}
@ -35,13 +35,13 @@ class _ConfigViewerState extends State<ConfigViewer> {
if (value == null) return;
if (appSetting is AppSettings<String>) {
await appSetting.setItem(store, value);
await appSetting.setItem(value);
}
if (appSetting is AppSettings<int>) {
await appSetting.setItem(store, int.parse(value));
await appSetting.setItem(int.parse(value));
}
if (appSetting is AppSettings<double>) {
await appSetting.setItem(store, double.parse(value));
await appSetting.setItem(double.parse(value));
}
setState(() {});
@ -78,16 +78,16 @@ class _ConfigViewerState extends State<ConfigViewer> {
final appSetting = AppSettings.values[i];
var value = '';
if (appSetting is AppSettings<String>) {
value = appSetting.getItem(store);
value = appSetting.value;
}
if (appSetting is AppSettings<int>) {
value = appSetting.getItem(store).toString();
value = appSetting.value.toString();
}
if (appSetting is AppSettings<bool>) {
value = appSetting.getItem(store).toString();
value = appSetting.value.toString();
}
if (appSetting is AppSettings<double>) {
value = appSetting.getItem(store).toString();
value = appSetting.value.toString();
}
return ListTile(
title: Text(appSetting.name),

@ -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:

@ -1,5 +1,6 @@
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';
@ -107,7 +108,7 @@ class _PrivacyButtons extends StatelessWidget {
),
),
TextButton(
onPressed: () => launchUrlString(AppConfig.privacyUrl),
onPressed: () => launchUrl(AppConfig.privacyUrl),
child: Text(
L10n.of(context).privacy,
style: shadowTextStyle,

@ -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:

@ -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<Matrix> 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<Matrix> with WidgetsBindingObserver {
super.initState();
WidgetsBinding.instance.addObserver(this);
initMatrix();
if (PlatformInfos.isWeb) {
initConfig().then((_) => initSettings());
} else {
initSettings();
}
}
Future<void> 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<Matrix> 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<Matrix> 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<Matrix> 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);

@ -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,

@ -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<bool> 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<SettingsSwitchListTile> {
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(() {});
},
);

@ -15,8 +15,11 @@
#include <gtk/gtk_plugin.h>
#include <handy_window/handy_window_plugin.h>
#include <record_linux/record_linux_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <sqlcipher_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <webcrypto/webcrypto_plugin.h>
#include <window_manager/window_manager_plugin.h>
#include <window_to_front/window_to_front_plugin.h>
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);

@ -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
)

@ -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"))
}

@ -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

@ -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:
@ -317,10 +317,10 @@ packages:
dependency: "direct main"
description:
name: desktop_drop
sha256: "927511f590ce01ee90d0d80f79bc71b9c919d8522d01e495e89a00c6f4a4fb5b"
sha256: e70b46b2d61f1af7a81a40d1f79b43c28a879e30a4ef31e87e9c27bea4d784e8
url: "https://pub.dev"
source: hosted
version: "0.6.1"
version: "0.7.0"
desktop_notifications:
dependency: "direct main"
description:
@ -333,10 +333,10 @@ packages:
dependency: "direct main"
description:
name: device_info_plus
sha256: "49413c8ca514dea7633e8def233b25efdf83ec8522955cc2c0e3ad802927e7c6"
sha256: dd0e8e02186b2196c7848c9d394a5fd6e5b57a43a546082c5820b1ec72317e33
url: "https://pub.dev"
source: hosted
version: "12.1.0"
version: "12.2.0"
device_info_plus_platform_interface:
dependency: transitive
description:
@ -519,10 +519,10 @@ packages:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "7ed76be64e8a7d01dfdf250b8434618e2a028c9dfa2a3c41dc9b531d4b3fc8a5"
sha256: "19ffb0a8bb7407875555e5e98d7343a633bb73707bae6c6a5f37c90014077875"
url: "https://pub.dev"
source: hosted
version: "19.4.2"
version: "19.5.0"
flutter_local_notifications_linux:
dependency: transitive
description:
@ -564,10 +564,10 @@ packages:
dependency: "direct dev"
description:
name: flutter_native_splash
sha256: "8321a6d11a8d13977fa780c89de8d257cce3d841eecfb7a4cadffcc4f12d82dc"
sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002"
url: "https://pub.dev"
source: hosted
version: "2.4.6"
version: "2.4.7"
flutter_new_badger:
dependency: "direct main"
description:
@ -1322,10 +1322,10 @@ packages:
dependency: transitive
description:
name: petitparser
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
version: "7.0.1"
platform:
dependency: transitive
description:
@ -1542,6 +1542,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
screen_retriever:
dependency: transitive
description:
name: screen_retriever
sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_linux:
dependency: transitive
description:
name: screen_retriever_linux
sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_macos:
dependency: transitive
description:
name: screen_retriever_macos
sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_platform_interface:
dependency: transitive
description:
name: screen_retriever_platform_interface
sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_windows:
dependency: transitive
description:
name: screen_retriever_windows
sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
scroll_to_index:
dependency: "direct main"
description:
@ -1887,34 +1927,50 @@ packages:
dependency: "direct main"
description:
name: unifiedpush
sha256: "1418375efb580af9640de4eaf4209cb6481f9a48792648ced3051f30e67d9568"
sha256: "8ed9767f750a1dc6159a77e2171641d0cb825dc87682d1ce1b8618689b79f58e"
url: "https://pub.dev"
source: hosted
version: "6.0.2"
version: "6.2.0"
unifiedpush_android:
dependency: transitive
description:
name: unifiedpush_android
sha256: "2f25db8eb2fc3183bf2e43db89fff20b2587adc1c361e1d1e06b223a0d45b50a"
sha256: "556796c81e8151ee8e4275baea2f7191119e8b1412ec35523cc2ac1c44c348bf"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.4.0"
unifiedpush_linux:
dependency: transitive
description:
name: unifiedpush_linux
sha256: c062d5eedd1cec70bcd33270cc4e01ae0ff6501f33d471167c06b34a968adfeb
url: "https://pub.dev"
source: hosted
version: "1.0.0"
unifiedpush_platform_interface:
dependency: transitive
description:
name: unifiedpush_platform_interface
sha256: bb49d2748211520e35e0374ab816faa8a2c635267e71909d334ad868d532eba5
sha256: "83372bc8d794b8b12ef6993b518d7be907dcfc2191bdf6de0ece5c4445d89880"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "4.0.0"
unifiedpush_storage_interface:
dependency: transitive
description:
name: unifiedpush_storage_interface
sha256: b8d423a4695efc616aa21d8ab48fb5ef99d6288c68b56282b8faac1579ceabd9
url: "https://pub.dev"
source: hosted
version: "1.0.0"
unifiedpush_ui:
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:
@ -2139,6 +2195,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webcrypto:
dependency: transitive
description:
name: webcrypto
sha256: e393b3d0b01694a8f81efecf278ed7392877130e6e7b29f578863e4f2d0b2ebd
url: "https://pub.dev"
source: hosted
version: "0.5.8"
webdriver:
dependency: transitive
description:
@ -2155,6 +2219,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.1"
webpush_encryption:
dependency: transitive
description:
name: webpush_encryption
sha256: "63046b7d6909f4a72ce3c153fa574726e257aaf21b1995ba063dc241a1b1520b"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
webrtc_interface:
dependency: "direct main"
description:
@ -2179,6 +2251,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
window_manager:
dependency: transitive
description:
name: window_manager
sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
window_to_front:
dependency: transitive
description:
@ -2207,10 +2287,10 @@ packages:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev"
source: hosted
version: "6.5.0"
version: "6.6.1"
yaml:
dependency: transitive
description:

@ -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
@ -18,9 +18,9 @@ dependencies:
collection: ^1.18.0
cross_file: ^0.3.4+2
cupertino_icons: any
desktop_drop: ^0.6.1
desktop_drop: ^0.7.0
desktop_notifications: ^0.6.3
device_info_plus: ^12.1.0
device_info_plus: ^12.2.0
dynamic_color: ^1.8.1
emoji_picker_flutter: ^4.3.0
emojis: ^0.9.9
@ -31,7 +31,7 @@ dependencies:
flutter_foreground_task: ^9.1.0
flutter_highlighter: ^0.1.1
flutter_linkify: ^6.0.0
flutter_local_notifications: ^19.4.2
flutter_local_notifications: ^19.5.0
flutter_localizations:
sdk: flutter
flutter_map: ^8.2.2
@ -75,8 +75,8 @@ dependencies:
sqlcipher_flutter_libs: ^0.6.8
swipe_to_action: ^0.3.0
tor_detector_web: ^1.1.0
unifiedpush: ^6.0.2
unifiedpush_ui: ^0.1.0
unifiedpush: ^6.2.0
unifiedpush_ui: ^0.2.0
universal_html: ^2.2.4
url_launcher: ^6.3.2
video_compress: ^3.1.4
@ -86,7 +86,7 @@ dependencies:
dev_dependencies:
flutter_lints: ^3.0.0
flutter_native_splash: ^2.4.6
flutter_native_splash: ^2.4.7
flutter_test:
sdk: flutter
import_sorter: ^4.6.0

@ -16,9 +16,12 @@
#include <geolocator_windows/geolocator_windows.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <record_windows/record_windows_plugin_c_api.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <sqlcipher_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <webcrypto/webcrypto_plugin.h>
#include <window_manager/window_manager_plugin.h>
#include <window_to_front/window_to_front_plugin.h>
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"));
}

@ -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
)

Loading…
Cancel
Save