Compare commits

...

48 Commits

Author SHA1 Message Date
b0f3b6b5c3 Configure message style 2025-09-23 19:39:27 +08:00
cb2af379fa Provide three styles of message 2025-09-23 19:05:44 +08:00
38f8103265 Search and jump to message 2025-09-23 16:56:02 +08:00
06bb18bdaa 💄 Flashing message background when jumped 2025-09-23 16:19:54 +08:00
84c38500d0 Edited message diff 2025-09-23 15:20:45 +08:00
9529bbf08b 💄 Save historic sync changes message 2025-09-23 14:58:07 +08:00
8baf77bcf7 ♻️ Refactor room message sorting 2025-09-23 14:21:45 +08:00
b2ac5fbef2 ♻️ Optimize message data structure 2025-09-23 14:00:43 +08:00
c79b1d7aab 🐛 Fix file upload 2025-09-21 23:20:36 +08:00
LittleSheep
4f55a8209c 🔀 Merge pull request #179 from Texas0295/v3
[Feature] add pool-aware file upload with default pool selection
2025-09-21 23:19:30 +08:00
Texas0295
ace302111a [REF] unify file upload logic and pool utils
- merge putMediaToCloud and putFileToPool into putFileToCloud
  with FileUploadMode for media-safe vs generic uploads

Signed-off-by: Texas0295 <kimura@texas0295.top>
2025-09-21 22:38:49 +08:00
Texas0295
1391fa0dde [REF] unify pool handling with extension methods
- Move pool filtering and parsing logic into SnFilePool extension
- Replace PoolService and pool_utils with unified extension
- Update settings screen to use pools.filterValid() + resolveDefaultPoolId
- Cleanup references in compose_shared.dart
- Remove obsolete files: pool_service.dart, pool_utils.dart

Signed-off-by: Texas0295 <kimura@texas0295.top>
2025-09-21 22:10:25 +08:00
Texas0295
cbdc7acdcd [REF] refactor file pool model and imports
- Refactor SnFilePool using freezed + sealed for consistency with other Sn models
- Add extension method listFromResponse for PoolService compatibility
- Update PoolService and utils to use SnFilePool
- Replace relative imports (../) with package imports for clarity and maintainability

Signed-off-by: Texas0295 <kimura@texas0295.top>
2025-09-21 21:37:07 +08:00
Texas0295
b80d91825a migrate file upload from tus to FileUploader API
The tus-based upload flow (/drive/tus) has been removed upstream in favor
of a new multipart upload protocol. This commit replaces all TusClient
usage with the new FileUploader service that follows the official
/drive/files/upload/{create,chunk,complete} endpoints.

Changes include:
- remove tus_client_dart dependency and related code
- add putFileToPool() backed by FileUploader.uploadFile()
- update uploadAttachment() to call the new putFileToPool
- preserve poolId support, filename, and mimetype handling
- ensure progress callbacks fire at start and completion

This aligns the client with the new upload protocol while keeping the
same Compose UI and settings logic introduced in earlier patches.

Signed-off-by: Texas0295 <kimura@texas0295.top>
2025-09-21 20:33:17 +08:00
Texas0295
1a703b7eba add default pool selection with validation and fallback
- extend AppSettings with defaultPoolId
- add pool filtering utility to exclude media-only pools
- add resolveDefaultPoolId with fallback to safe pool
- update SettingsScreen with default pool dropdown
- integrate uploadAttachment with default pool resolution

Signed-off-by: Texas0295 <kimura@texas0295.top>
2025-09-21 20:33:12 +08:00
Texas0295
3621ea7744 support default file pool selection
- add defaultPoolId to AppSettings + persistence
- extend SettingsScreen with pool dropdown
- update uploadAttachment to use defaultPoolId with fallback

Signed-off-by: Texas0295 <kimura@texas0295.top>
2025-09-21 20:33:12 +08:00
Texas0295
b638343f02 add pool fetching service and provider
- define FilePool model
- implement PoolService with /drive/pools endpoint
- add Riverpod providers (poolServiceProvider, poolsProvider)

Signed-off-by: Texas0295 <kimura@texas0295.top>
2025-09-21 20:33:12 +08:00
Texas0295
269a64cabb add general file upload support with pool-aware tus client
- add "uploadFile" i18n key (en, zh-CN, zh-TW)
- introduce putFileToPool for tus upload with X-FilePool header
- add ComposeLogic.pickGeneralFile for arbitrary files
- extend uploadAttachment to support poolId override
- add toolbar button for general file upload

Signed-off-by: Texas0295 <kimura@texas0295.top>
2025-09-21 20:33:11 +08:00
406e5187a8 🐛 Fix captcha 2025-09-21 19:42:13 +08:00
9bdd08d8dd New protocol to upload file 2025-09-21 18:46:48 +08:00
d737232dcf 💄 Optimize auth devices 2025-09-21 15:33:09 +08:00
c9d751479e 🐛 Fix iOS build 2025-09-21 15:32:52 +08:00
a2c2bfe585 ♻️ Replace the pattle_generator 2025-09-21 14:50:30 +08:00
c7f9da0dee ⬆️ Upgrade deps and flutter 2025-09-21 02:06:34 +08:00
LittleSheep
a243cda1df 🔀 Merge pull request #178 from Texas0295/v3
[FIX] chat: fix message pagination logic
2025-09-17 22:17:47 +08:00
Texas0295
7b238f32fd [FIX] chat: fix message pagination logic
`loadInitial()` fetched 100 messages while `_page_size` was 20,
so `_has_more` turned false too early and history stopped around
10h back. `loadMore()` also used `_currentPage * _page_size`
for offset, causing duplicates.

Use `_page_size` for initial load and rely on current message
count as offset. This removes `_currentPage` entirely and lets
older messages load correctly.

Signed-off-by: Texas0295 <kimura@texas0295.top>
2025-09-17 19:36:51 +08:00
313af28d7f 👽 Update service usage 2025-09-17 00:48:01 +08:00
c64e1e208c Desktop device name, close #7 2025-09-14 19:38:01 +08:00
c9b07a9a2a 🐛 Fix errors 2025-09-14 17:38:11 +08:00
55c0e355f1 ♻️ Refactor ICP server to make it available across platform 2025-09-13 20:32:39 +08:00
be414891ec 🐛 Fix pipe path 2025-09-13 17:59:33 +08:00
787876ab6a 🗑️ Removed unused method 2025-09-13 15:57:45 +08:00
8578cde620 🐛 Fix expired at must non null 2025-09-13 15:46:07 +08:00
14d55d45a8 🐛 Fix ipc server on web 2025-09-13 15:31:05 +08:00
724391584e 🐛 Fix and optimize alert 2025-09-13 15:31:05 +08:00
3a5e45808a 🐛 Fix firebase analytics not available on windows 2025-09-13 15:29:38 +08:00
488055955c ♻️ Splitting up ipc servers in unix and windows 2025-09-13 12:59:54 +08:00
LittleSheep
313ebc64cc 🔀 Merge pull request #177 from Texas0295/v3
[FIX] tray: ensure Show Window works reliably on Linux/Wayland
2025-09-12 21:07:56 +08:00
Texas0295
1ed8b1d0c1 [FIX] tray: ensure Show Window works reliably on Linux/Wayland
Avoid relying on appWindow.isVisible, which is not trustworthy under

bitsdojo on Linux/Wayland. Instead, always run show → restore → show

sequence to guarantee window is re-mapped and raised.


Signed-off-by: Texas0295<kimura@texas0295.top>
2025-09-12 13:26:15 +08:00
4af816d931 🐛 Fix windows rpc 2025-09-11 01:15:52 +08:00
1c058a4323 ♻️ Better windows support 2025-09-11 01:06:58 +08:00
461ed1fcda ♻️ FFI windows rpc ipc implmentation 2025-09-11 00:56:26 +08:00
5363afa558 🐛 Trying to fix windows rpc ipc 2025-09-11 00:33:44 +08:00
f0d2737da8 Windows RPC IPC 2025-09-11 00:23:14 +08:00
1f2f80aa3e 🐛 Trying to fix windows notification issue 2025-09-10 23:40:19 +08:00
240a872e65 Rollback windows gha changes 2025-09-10 23:12:57 +08:00
c1ec6f0849 Merge branch 'v3' of https://github.com/Solsynth/Solian into v3 2025-09-10 22:48:54 +08:00
ab42686d4d 🔨 Trying to fix windows build issue 2025-09-10 22:48:50 +08:00
93 changed files with 6266 additions and 3055 deletions

View File

@@ -30,6 +30,8 @@
"fieldEmailAddressMustBeValid": "The email address must be valid.", "fieldEmailAddressMustBeValid": "The email address must be valid.",
"logout": "Logout", "logout": "Logout",
"updateYourProfile": "Profile Settings", "updateYourProfile": "Profile Settings",
"settingsDefaultPool": "Default file pool",
"settingsDefaultPoolHelper": "Select the default storage pool for file uploads",
"accountBasicInfo": "Basic Info", "accountBasicInfo": "Basic Info",
"accountProfile": "Your Profile", "accountProfile": "Your Profile",
"saveChanges": "Save Changes", "saveChanges": "Save Changes",
@@ -168,6 +170,7 @@
"addPhoto": "Add photo", "addPhoto": "Add photo",
"addAudio": "Add audio", "addAudio": "Add audio",
"addFile": "Add file", "addFile": "Add file",
"uploadFile": "Upload File",
"recordAudio": "Record Audio", "recordAudio": "Record Audio",
"linkAttachment": "Link Attachment", "linkAttachment": "Link Attachment",
"fileIdCannotBeEmpty": "File ID cannot be empty", "fileIdCannotBeEmpty": "File ID cannot be empty",
@@ -447,6 +450,8 @@
"lastActiveAt": "Last active at {}", "lastActiveAt": "Last active at {}",
"authDeviceLogout": "Logout", "authDeviceLogout": "Logout",
"authDeviceLogoutHint": "Are you sure you want to logout this device? This will also disable the push notification to this device.", "authDeviceLogoutHint": "Are you sure you want to logout this device? This will also disable the push notification to this device.",
"authDeviceChallenges": "Device Usage",
"authDeviceHint": "Swipe left to edit label, swipe right to logout device.",
"typingHint": { "typingHint": {
"one": "{} is typing...", "one": "{} is typing...",
"other": "{} are typing..." "other": "{} are typing..."
@@ -468,6 +473,7 @@
"settingsKeyboardShortcutSettings": "Settings", "settingsKeyboardShortcutSettings": "Settings",
"settingsKeyboardShortcutNewMessage": "New Message", "settingsKeyboardShortcutNewMessage": "New Message",
"settingsKeyboardShortcutCloseDialog": "Close Dialog", "settingsKeyboardShortcutCloseDialog": "Close Dialog",
"settingsMessageDisplayStyle": "Message Display Style",
"close": "Close", "close": "Close",
"drafts": "Drafts", "drafts": "Drafts",
"noDrafts": "No drafts yet", "noDrafts": "No drafts yet",
@@ -1016,5 +1022,6 @@
"currentEmbed": "Current Embed", "currentEmbed": "Current Embed",
"noEmbed": "No embed yet", "noEmbed": "No embed yet",
"save": "Save", "save": "Save",
"webView": "Web View" "webView": "Web View",
"messageActions": "Message Actions"
} }

View File

@@ -122,6 +122,9 @@
"addVideo": "添加视频", "addVideo": "添加视频",
"addPhoto": "添加照片", "addPhoto": "添加照片",
"addFile": "添加文件", "addFile": "添加文件",
"uploadFile": "上传文件",
"settingsDefaultPool": "选择文件池",
"settingsDefaultPoolHelper": "为文件上传选择一个默认池",
"createDirectMessage": "创建新私人消息", "createDirectMessage": "创建新私人消息",
"gotoDirectMessage": "前往私信", "gotoDirectMessage": "前往私信",
"react": "反应", "react": "反应",

View File

@@ -122,6 +122,10 @@
"addVideo": "添加視頻", "addVideo": "添加視頻",
"addPhoto": "添加照片", "addPhoto": "添加照片",
"addFile": "添加文件", "addFile": "添加文件",
"uploadFile": "上傳文件",
"settingsDefaultPool": "選擇文件池",
"settingsDefaultPoolHelper": "爲文件上傳選擇一個默認池",
"createDirectMessage": "創建新私人消息", "createDirectMessage": "創建新私人消息",
"gotoDirectMessage": "前往私信", "gotoDirectMessage": "前往私信",
"react": "反應", "react": "反應",

View File

@@ -5,3 +5,7 @@ targets:
options: options:
explicit_to_json: true explicit_to_json: true
field_rename: snake field_rename: snake
drift_dev:
options:
databases:
app_database: lib/database/drift_db.dart

File diff suppressed because one or more lines are too long

View File

@@ -149,9 +149,9 @@ PODS:
- flutter_udid (0.0.1): - flutter_udid (0.0.1):
- Flutter - Flutter
- SAMKeychain - SAMKeychain
- flutter_webrtc (1.1.0): - flutter_webrtc (1.2.0):
- Flutter - Flutter
- WebRTC-SDK (= 137.7151.03) - WebRTC-SDK (= 137.7151.04)
- gal (1.0.0): - gal (1.0.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@@ -219,7 +219,7 @@ PODS:
- livekit_client (2.5.0): - livekit_client (2.5.0):
- Flutter - Flutter
- flutter_webrtc - flutter_webrtc
- WebRTC-SDK (= 137.7151.03) - WebRTC-SDK (= 137.7151.04)
- local_auth_darwin (0.0.1): - local_auth_darwin (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@@ -299,7 +299,7 @@ PODS:
- Flutter - Flutter
- wakelock_plus (0.0.1): - wakelock_plus (0.0.1):
- Flutter - Flutter
- WebRTC-SDK (137.7151.03) - WebRTC-SDK (137.7151.04)
DEPENDENCIES: DEPENDENCIES:
- Alamofire - Alamofire
@@ -499,7 +499,7 @@ SPEC CHECKSUMS:
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544 flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
flutter_webrtc: b0b2e04411747142962164a1cfa43a1af9a0afac flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
gal: baecd024ebfd13c441269ca7404792a7152fde89 gal: baecd024ebfd13c441269ca7404792a7152fde89
GoogleAdsOnDeviceConversion: 9090c435cde08903e8dd1ba2c77fbec9e46d9afe GoogleAdsOnDeviceConversion: 9090c435cde08903e8dd1ba2c77fbec9e46d9afe
GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af
@@ -508,8 +508,8 @@ SPEC CHECKSUMS:
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
Kingfisher: ff0d31a1f07bdff6a1ebb3ba08b8e6e567b6500c Kingfisher: ff0d31a1f07bdff6a1ebb3ba08b8e6e567b6500c
livekit_client: f810c81bbbc229a84f60b09e66603ac4e93f7599 livekit_client: a6f5fa86ac28ccd7ded53626a5379961db311ab4
local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19 local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
@@ -536,7 +536,7 @@ SPEC CHECKSUMS:
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12 volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
WebRTC-SDK: 69d4e56b0b4b27d788e87bab9b9a1326ed05b1e3 WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f

View File

@@ -566,7 +566,7 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n";
}; };
4815E0A19398E51078F4160D /* [CP] Check Pods Manifest.lock */ = { 4815E0A19398E51078F4160D /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
@@ -883,6 +883,7 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian; PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
@@ -1096,6 +1097,7 @@
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
@@ -1137,6 +1139,7 @@
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };
@@ -1177,6 +1180,7 @@
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };
@@ -1434,6 +1438,7 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian; PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -1462,6 +1467,7 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian; PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";

View File

@@ -12,7 +12,7 @@ class AppDatabase extends _$AppDatabase {
AppDatabase(super.e); AppDatabase(super.e);
@override @override
int get schemaVersion => 6; int get schemaVersion => 7;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
@@ -21,8 +21,8 @@ class AppDatabase extends _$AppDatabase {
}, },
onUpgrade: (Migrator m, int from, int to) async { onUpgrade: (Migrator m, int from, int to) async {
if (from < 2) { if (from < 2) {
// Add isRead column with default value false // Add isDeleted column with default value false
await m.addColumn(chatMessages, chatMessages.isRead); await m.addColumn(chatMessages, chatMessages.isDeleted);
} }
if (from < 4) { if (from < 4) {
// Drop old draft tables if they exist // Drop old draft tables if they exist
@@ -32,6 +32,19 @@ class AppDatabase extends _$AppDatabase {
// Migrate from old schema to new schema with separate searchable fields // Migrate from old schema to new schema with separate searchable fields
await _migrateToVersion6(m); await _migrateToVersion6(m);
} }
if (from < 7) {
// Add new columns from SnChatMessage
await m.addColumn(chatMessages, chatMessages.updatedAt);
await m.addColumn(chatMessages, chatMessages.deletedAt);
await m.addColumn(chatMessages, chatMessages.type);
await m.addColumn(chatMessages, chatMessages.meta);
await m.addColumn(chatMessages, chatMessages.membersMentioned);
await m.addColumn(chatMessages, chatMessages.editedAt);
await m.addColumn(chatMessages, chatMessages.attachments);
await m.addColumn(chatMessages, chatMessages.reactions);
await m.addColumn(chatMessages, chatMessages.repliedMessageId);
await m.addColumn(chatMessages, chatMessages.forwardedMessageId);
}
}, },
); );
@@ -116,12 +129,6 @@ class AppDatabase extends _$AppDatabase {
)).write(ChatMessagesCompanion(status: Value(status))); )).write(ChatMessagesCompanion(status: Value(status)));
} }
Future<int> markMessageAsRead(String id) {
return (update(chatMessages)..where(
(m) => m.id.equals(id),
)).write(ChatMessagesCompanion(isRead: const Value(true)));
}
Future<int> deleteMessage(String id) { Future<int> deleteMessage(String id) {
return (delete(chatMessages)..where((m) => m.id.equals(id))).go(); return (delete(chatMessages)..where((m) => m.id.equals(id))).go();
} }
@@ -134,15 +141,27 @@ class AppDatabase extends _$AppDatabase {
Future<List<LocalChatMessage>> searchMessages( Future<List<LocalChatMessage>> searchMessages(
String roomId, String roomId,
String query, String query, {
) async { bool? withAttachments,
}) async {
var selectStatement = select(chatMessages) var selectStatement = select(chatMessages)
..where((m) => m.roomId.equals(roomId)); ..where((m) => m.roomId.equals(roomId));
if (query.isNotEmpty) { if (query.isNotEmpty) {
final searchTerm = '%${query}%';
selectStatement = selectStatement =
selectStatement selectStatement..where(
..where((m) => m.content.like('%${query.toLowerCase()}%')); (m) =>
m.content.like(searchTerm) |
m.meta.like(searchTerm) |
m.attachments.like(searchTerm) |
m.type.like(searchTerm),
);
}
if (withAttachments == true) {
selectStatement =
selectStatement..where((m) => m.attachments.equals('[]').not());
} }
final messages = final messages =
@@ -154,16 +173,26 @@ class AppDatabase extends _$AppDatabase {
// Convert between Drift and model objects // Convert between Drift and model objects
ChatMessagesCompanion messageToCompanion(LocalChatMessage message) { ChatMessagesCompanion messageToCompanion(LocalChatMessage message) {
final remote = message.toRemoteMessage();
return ChatMessagesCompanion( return ChatMessagesCompanion(
id: Value(message.id), id: Value(message.id),
roomId: Value(message.roomId), roomId: Value(message.roomId),
senderId: Value(message.senderId), senderId: Value(message.senderId),
content: Value(message.toRemoteMessage().content), content: Value(remote.content),
nonce: Value(message.nonce), nonce: Value(message.nonce),
data: Value(jsonEncode(message.data)), data: Value(jsonEncode(message.data)),
createdAt: Value(message.createdAt), createdAt: Value(message.createdAt),
status: Value(message.status), status: Value(message.status),
isRead: Value(message.isRead), updatedAt: Value(remote.updatedAt),
deletedAt: Value(remote.deletedAt),
type: Value(remote.type),
meta: Value(remote.meta),
membersMentioned: Value(remote.membersMentioned),
editedAt: Value(remote.editedAt),
attachments: Value(remote.attachments.map((e) => e.toJson()).toList()),
reactions: Value(remote.reactions.map((e) => e.toJson()).toList()),
repliedMessageId: Value(remote.repliedMessageId),
forwardedMessageId: Value(remote.forwardedMessageId),
); );
} }
@@ -177,7 +206,18 @@ class AppDatabase extends _$AppDatabase {
createdAt: dbMessage.createdAt, createdAt: dbMessage.createdAt,
status: dbMessage.status, status: dbMessage.status,
nonce: dbMessage.nonce, nonce: dbMessage.nonce,
isRead: dbMessage.isRead, content: dbMessage.content,
isDeleted: dbMessage.isDeleted,
updatedAt: dbMessage.updatedAt,
deletedAt: dbMessage.deletedAt,
type: dbMessage.type,
meta: dbMessage.meta,
membersMentioned: dbMessage.membersMentioned,
editedAt: dbMessage.editedAt,
attachments: dbMessage.attachments,
reactions: dbMessage.reactions,
repliedMessageId: dbMessage.repliedMessageId,
forwardedMessageId: dbMessage.forwardedMessageId,
); );
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,41 @@
import 'dart:convert';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:island/models/chat.dart'; import 'package:island/models/chat.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
class MapConverter extends TypeConverter<Map<String, dynamic>, String> {
const MapConverter();
@override
Map<String, dynamic> fromSql(String fromDb) => json.decode(fromDb);
@override
String toSql(Map<String, dynamic> value) => json.encode(value);
}
class ListStringConverter extends TypeConverter<List<String>, String> {
const ListStringConverter();
@override
List<String> fromSql(String fromDb) => List<String>.from(json.decode(fromDb));
@override
String toSql(List<String> value) => json.encode(value);
}
class ListMapConverter
extends TypeConverter<List<Map<String, dynamic>>, String> {
const ListMapConverter();
@override
List<Map<String, dynamic>> fromSql(String fromDb) =>
List<Map<String, dynamic>>.from(json.decode(fromDb));
@override
String toSql(List<Map<String, dynamic>> value) => json.encode(value);
}
class ChatMessages extends Table { class ChatMessages extends Table {
TextColumn get id => text()(); TextColumn get id => text()();
TextColumn get roomId => text()(); TextColumn get roomId => text()();
@@ -11,7 +45,24 @@ class ChatMessages extends Table {
TextColumn get data => text()(); TextColumn get data => text()();
DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get createdAt => dateTime()();
IntColumn get status => intEnum<MessageStatus>()(); IntColumn get status => intEnum<MessageStatus>()();
BoolColumn get isRead => boolean().withDefault(const Constant(false))(); BoolColumn get isDeleted =>
boolean().nullable().withDefault(const Constant(false))();
DateTimeColumn get updatedAt => dateTime().nullable()();
DateTimeColumn get deletedAt => dateTime().nullable()();
TextColumn get type => text().withDefault(const Constant('text'))();
TextColumn get meta =>
text().map(const MapConverter()).withDefault(const Constant('{}'))();
TextColumn get membersMentioned =>
text()
.map(const ListStringConverter())
.withDefault(const Constant('[]'))();
DateTimeColumn get editedAt => dateTime().nullable()();
TextColumn get attachments =>
text().map(const ListMapConverter()).withDefault(const Constant('[]'))();
TextColumn get reactions =>
text().map(const ListMapConverter()).withDefault(const Constant('[]'))();
TextColumn get repliedMessageId => text().nullable()();
TextColumn get forwardedMessageId => text().nullable()();
@override @override
Set<Column> get primaryKey => {id}; Set<Column> get primaryKey => {id};
@@ -25,8 +76,19 @@ class LocalChatMessage {
final DateTime createdAt; final DateTime createdAt;
MessageStatus status; MessageStatus status;
final String? nonce; final String? nonce;
final String? content;
final bool? isDeleted;
final DateTime? updatedAt;
final DateTime? deletedAt;
final String type;
final Map<String, dynamic> meta;
final List<String> membersMentioned;
final DateTime? editedAt;
final List<Map<String, dynamic>> attachments;
final List<Map<String, dynamic>> reactions;
final String? repliedMessageId;
final String? forwardedMessageId;
List<UniversalFile>? localAttachments; List<UniversalFile>? localAttachments;
bool isRead;
LocalChatMessage({ LocalChatMessage({
required this.id, required this.id,
@@ -36,8 +98,19 @@ class LocalChatMessage {
required this.createdAt, required this.createdAt,
required this.nonce, required this.nonce,
required this.status, required this.status,
this.content,
this.isDeleted,
this.updatedAt,
this.deletedAt,
required this.type,
required this.meta,
required this.membersMentioned,
this.editedAt,
required this.attachments,
required this.reactions,
this.repliedMessageId,
this.forwardedMessageId,
this.localAttachments, this.localAttachments,
this.isRead = false,
}); });
SnChatMessage toRemoteMessage() { SnChatMessage toRemoteMessage() {
@@ -48,7 +121,6 @@ class LocalChatMessage {
SnChatMessage message, SnChatMessage message,
MessageStatus status, { MessageStatus status, {
String? nonce, String? nonce,
bool isRead = false,
}) { }) {
return LocalChatMessage( return LocalChatMessage(
id: message.id, id: message.id,
@@ -58,7 +130,18 @@ class LocalChatMessage {
createdAt: message.createdAt, createdAt: message.createdAt,
status: status, status: status,
nonce: nonce ?? message.nonce, nonce: nonce ?? message.nonce,
isRead: isRead, content: message.content,
isDeleted: false,
updatedAt: message.updatedAt,
deletedAt: null,
type: message.type,
meta: message.meta,
membersMentioned: message.membersMentioned,
editedAt: message.editedAt,
attachments: message.attachments.map((e) => e.toJson()).toList(),
reactions: message.reactions.map((e) => e.toJson()).toList(),
repliedMessageId: message.repliedMessageId,
forwardedMessageId: message.forwardedMessageId,
); );
} }
} }

View File

@@ -181,7 +181,7 @@ class IslandApp extends HookConsumerWidget {
} }
useEffect(() { useEffect(() {
if (!kIsWeb && Platform.isLinux) { if (!kIsWeb && (Platform.isLinux || Platform.isWindows)) {
return null; return null;
} }

View File

@@ -14,11 +14,11 @@ sealed class AppToken with _$AppToken {
@freezed @freezed
sealed class GeoIpLocation with _$GeoIpLocation { sealed class GeoIpLocation with _$GeoIpLocation {
const factory GeoIpLocation({ const factory GeoIpLocation({
required double latitude, required double? latitude,
required double longitude, required double? longitude,
required String countryCode, required String? countryCode,
required String country, required String? country,
required String city, required String? city,
}) = _GeoIpLocation; }) = _GeoIpLocation;
factory GeoIpLocation.fromJson(Map<String, dynamic> json) => factory GeoIpLocation.fromJson(Map<String, dynamic> json) =>
@@ -29,7 +29,7 @@ sealed class GeoIpLocation with _$GeoIpLocation {
sealed class SnAuthChallenge with _$SnAuthChallenge { sealed class SnAuthChallenge with _$SnAuthChallenge {
const factory SnAuthChallenge({ const factory SnAuthChallenge({
required String id, required String id,
required DateTime expiredAt, required DateTime? expiredAt,
required int stepRemain, required int stepRemain,
required int stepTotal, required int stepTotal,
required int failedAttempts, required int failedAttempts,
@@ -57,7 +57,7 @@ sealed class SnAuthSession with _$SnAuthSession {
required String id, required String id,
required String? label, required String? label,
required DateTime lastGrantedAt, required DateTime lastGrantedAt,
required DateTime expiredAt, required DateTime? expiredAt,
required String accountId, required String accountId,
required String challengeId, required String challengeId,
required SnAuthChallenge challenge, required SnAuthChallenge challenge,

View File

@@ -272,7 +272,7 @@ as String,
/// @nodoc /// @nodoc
mixin _$GeoIpLocation { mixin _$GeoIpLocation {
double get latitude; double get longitude; String get countryCode; String get country; String get city; double? get latitude; double? get longitude; String? get countryCode; String? get country; String? get city;
/// Create a copy of GeoIpLocation /// Create a copy of GeoIpLocation
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -305,7 +305,7 @@ abstract mixin class $GeoIpLocationCopyWith<$Res> {
factory $GeoIpLocationCopyWith(GeoIpLocation value, $Res Function(GeoIpLocation) _then) = _$GeoIpLocationCopyWithImpl; factory $GeoIpLocationCopyWith(GeoIpLocation value, $Res Function(GeoIpLocation) _then) = _$GeoIpLocationCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
double latitude, double longitude, String countryCode, String country, String city double? latitude, double? longitude, String? countryCode, String? country, String? city
}); });
@@ -322,14 +322,14 @@ class _$GeoIpLocationCopyWithImpl<$Res>
/// Create a copy of GeoIpLocation /// Create a copy of GeoIpLocation
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? latitude = null,Object? longitude = null,Object? countryCode = null,Object? country = null,Object? city = null,}) { @pragma('vm:prefer-inline') @override $Res call({Object? latitude = freezed,Object? longitude = freezed,Object? countryCode = freezed,Object? country = freezed,Object? city = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable latitude: freezed == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable
as double,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable as double?,longitude: freezed == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable
as double,countryCode: null == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable as double?,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
as String,country: null == country ? _self.country : country // ignore: cast_nullable_to_non_nullable as String?,country: freezed == country ? _self.country : country // ignore: cast_nullable_to_non_nullable
as String,city: null == city ? _self.city : city // ignore: cast_nullable_to_non_nullable as String?,city: freezed == city ? _self.city : city // ignore: cast_nullable_to_non_nullable
as String, as String?,
)); ));
} }
@@ -411,7 +411,7 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( double latitude, double longitude, String countryCode, String country, String city)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( double? latitude, double? longitude, String? countryCode, String? country, String? city)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _GeoIpLocation() when $default != null: case _GeoIpLocation() when $default != null:
return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _: return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _:
@@ -432,7 +432,7 @@ return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( double latitude, double longitude, String countryCode, String country, String city) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( double? latitude, double? longitude, String? countryCode, String? country, String? city) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _GeoIpLocation(): case _GeoIpLocation():
return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);} return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);}
@@ -449,7 +449,7 @@ return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( double latitude, double longitude, String countryCode, String country, String city)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( double? latitude, double? longitude, String? countryCode, String? country, String? city)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _GeoIpLocation() when $default != null: case _GeoIpLocation() when $default != null:
return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _: return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _:
@@ -467,11 +467,11 @@ class _GeoIpLocation implements GeoIpLocation {
const _GeoIpLocation({required this.latitude, required this.longitude, required this.countryCode, required this.country, required this.city}); const _GeoIpLocation({required this.latitude, required this.longitude, required this.countryCode, required this.country, required this.city});
factory _GeoIpLocation.fromJson(Map<String, dynamic> json) => _$GeoIpLocationFromJson(json); factory _GeoIpLocation.fromJson(Map<String, dynamic> json) => _$GeoIpLocationFromJson(json);
@override final double latitude; @override final double? latitude;
@override final double longitude; @override final double? longitude;
@override final String countryCode; @override final String? countryCode;
@override final String country; @override final String? country;
@override final String city; @override final String? city;
/// Create a copy of GeoIpLocation /// Create a copy of GeoIpLocation
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -506,7 +506,7 @@ abstract mixin class _$GeoIpLocationCopyWith<$Res> implements $GeoIpLocationCopy
factory _$GeoIpLocationCopyWith(_GeoIpLocation value, $Res Function(_GeoIpLocation) _then) = __$GeoIpLocationCopyWithImpl; factory _$GeoIpLocationCopyWith(_GeoIpLocation value, $Res Function(_GeoIpLocation) _then) = __$GeoIpLocationCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
double latitude, double longitude, String countryCode, String country, String city double? latitude, double? longitude, String? countryCode, String? country, String? city
}); });
@@ -523,14 +523,14 @@ class __$GeoIpLocationCopyWithImpl<$Res>
/// Create a copy of GeoIpLocation /// Create a copy of GeoIpLocation
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? latitude = null,Object? longitude = null,Object? countryCode = null,Object? country = null,Object? city = null,}) { @override @pragma('vm:prefer-inline') $Res call({Object? latitude = freezed,Object? longitude = freezed,Object? countryCode = freezed,Object? country = freezed,Object? city = freezed,}) {
return _then(_GeoIpLocation( return _then(_GeoIpLocation(
latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable latitude: freezed == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable
as double,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable as double?,longitude: freezed == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable
as double,countryCode: null == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable as double?,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
as String,country: null == country ? _self.country : country // ignore: cast_nullable_to_non_nullable as String?,country: freezed == country ? _self.country : country // ignore: cast_nullable_to_non_nullable
as String,city: null == city ? _self.city : city // ignore: cast_nullable_to_non_nullable as String?,city: freezed == city ? _self.city : city // ignore: cast_nullable_to_non_nullable
as String, as String?,
)); ));
} }
@@ -541,7 +541,7 @@ as String,
/// @nodoc /// @nodoc
mixin _$SnAuthChallenge { mixin _$SnAuthChallenge {
String get id; DateTime get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String? get nonce; GeoIpLocation? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; DateTime? get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String? get nonce; GeoIpLocation? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAuthChallenge /// Create a copy of SnAuthChallenge
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -574,7 +574,7 @@ abstract mixin class $SnAuthChallengeCopyWith<$Res> {
factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl; factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
}); });
@@ -591,11 +591,11 @@ class _$SnAuthChallengeCopyWithImpl<$Res>
/// Create a copy of SnAuthChallenge /// Create a copy of SnAuthChallenge
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? expiredAt = freezed,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable as DateTime?,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable
as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable
as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable
as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
@@ -704,7 +704,7 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _SnAuthChallenge() when $default != null: case _SnAuthChallenge() when $default != null:
return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
@@ -725,7 +725,7 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _SnAuthChallenge(): case _SnAuthChallenge():
return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);} return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
@@ -742,7 +742,7 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _SnAuthChallenge() when $default != null: case _SnAuthChallenge() when $default != null:
return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
@@ -761,7 +761,7 @@ class _SnAuthChallenge implements SnAuthChallenge {
factory _SnAuthChallenge.fromJson(Map<String, dynamic> json) => _$SnAuthChallengeFromJson(json); factory _SnAuthChallenge.fromJson(Map<String, dynamic> json) => _$SnAuthChallengeFromJson(json);
@override final String id; @override final String id;
@override final DateTime expiredAt; @override final DateTime? expiredAt;
@override final int stepRemain; @override final int stepRemain;
@override final int stepTotal; @override final int stepTotal;
@override final int failedAttempts; @override final int failedAttempts;
@@ -829,7 +829,7 @@ abstract mixin class _$SnAuthChallengeCopyWith<$Res> implements $SnAuthChallenge
factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl; factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
}); });
@@ -846,11 +846,11 @@ class __$SnAuthChallengeCopyWithImpl<$Res>
/// Create a copy of SnAuthChallenge /// Create a copy of SnAuthChallenge
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? expiredAt = freezed,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAuthChallenge( return _then(_SnAuthChallenge(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable as DateTime?,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable
as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable
as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable
as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
@@ -888,7 +888,7 @@ $GeoIpLocationCopyWith<$Res>? get location {
/// @nodoc /// @nodoc
mixin _$SnAuthSession { mixin _$SnAuthSession {
String get id; String? get label; DateTime get lastGrantedAt; DateTime get expiredAt; String get accountId; String get challengeId; SnAuthChallenge get challenge; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String? get label; DateTime get lastGrantedAt; DateTime? get expiredAt; String get accountId; String get challengeId; SnAuthChallenge get challenge; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAuthSession /// Create a copy of SnAuthSession
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -921,7 +921,7 @@ abstract mixin class $SnAuthSessionCopyWith<$Res> {
factory $SnAuthSessionCopyWith(SnAuthSession value, $Res Function(SnAuthSession) _then) = _$SnAuthSessionCopyWithImpl; factory $SnAuthSessionCopyWith(SnAuthSession value, $Res Function(SnAuthSession) _then) = _$SnAuthSessionCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
}); });
@@ -938,13 +938,13 @@ class _$SnAuthSessionCopyWithImpl<$Res>
/// Create a copy of SnAuthSession /// Create a copy of SnAuthSession
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = null,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = freezed,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable as String,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as String?,lastGrantedAt: null == lastGrantedAt ? _self.lastGrantedAt : lastGrantedAt // ignore: cast_nullable_to_non_nullable as String?,lastGrantedAt: null == lastGrantedAt ? _self.lastGrantedAt : lastGrantedAt // ignore: cast_nullable_to_non_nullable
as DateTime,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable as DateTime,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,challengeId: null == challengeId ? _self.challengeId : challengeId // ignore: cast_nullable_to_non_nullable as String,challengeId: null == challengeId ? _self.challengeId : challengeId // ignore: cast_nullable_to_non_nullable
as String,challenge: null == challenge ? _self.challenge : challenge // ignore: cast_nullable_to_non_nullable as String,challenge: null == challenge ? _self.challenge : challenge // ignore: cast_nullable_to_non_nullable
as SnAuthChallenge,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as SnAuthChallenge,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
@@ -1041,7 +1041,7 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _SnAuthSession() when $default != null: case _SnAuthSession() when $default != null:
return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
@@ -1062,7 +1062,7 @@ return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.a
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _SnAuthSession(): case _SnAuthSession():
return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);} return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);}
@@ -1079,7 +1079,7 @@ return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.a
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _SnAuthSession() when $default != null: case _SnAuthSession() when $default != null:
return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
@@ -1100,7 +1100,7 @@ class _SnAuthSession implements SnAuthSession {
@override final String id; @override final String id;
@override final String? label; @override final String? label;
@override final DateTime lastGrantedAt; @override final DateTime lastGrantedAt;
@override final DateTime expiredAt; @override final DateTime? expiredAt;
@override final String accountId; @override final String accountId;
@override final String challengeId; @override final String challengeId;
@override final SnAuthChallenge challenge; @override final SnAuthChallenge challenge;
@@ -1141,7 +1141,7 @@ abstract mixin class _$SnAuthSessionCopyWith<$Res> implements $SnAuthSessionCopy
factory _$SnAuthSessionCopyWith(_SnAuthSession value, $Res Function(_SnAuthSession) _then) = __$SnAuthSessionCopyWithImpl; factory _$SnAuthSessionCopyWith(_SnAuthSession value, $Res Function(_SnAuthSession) _then) = __$SnAuthSessionCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
}); });
@@ -1158,13 +1158,13 @@ class __$SnAuthSessionCopyWithImpl<$Res>
/// Create a copy of SnAuthSession /// Create a copy of SnAuthSession
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = null,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = freezed,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAuthSession( return _then(_SnAuthSession(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable as String,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as String?,lastGrantedAt: null == lastGrantedAt ? _self.lastGrantedAt : lastGrantedAt // ignore: cast_nullable_to_non_nullable as String?,lastGrantedAt: null == lastGrantedAt ? _self.lastGrantedAt : lastGrantedAt // ignore: cast_nullable_to_non_nullable
as DateTime,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable as DateTime,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,challengeId: null == challengeId ? _self.challengeId : challengeId // ignore: cast_nullable_to_non_nullable as String,challengeId: null == challengeId ? _self.challengeId : challengeId // ignore: cast_nullable_to_non_nullable
as String,challenge: null == challenge ? _self.challenge : challenge // ignore: cast_nullable_to_non_nullable as String,challenge: null == challenge ? _self.challenge : challenge // ignore: cast_nullable_to_non_nullable
as SnAuthChallenge,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as SnAuthChallenge,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable

View File

@@ -15,11 +15,11 @@ Map<String, dynamic> _$AppTokenToJson(_AppToken instance) => <String, dynamic>{
_GeoIpLocation _$GeoIpLocationFromJson(Map<String, dynamic> json) => _GeoIpLocation _$GeoIpLocationFromJson(Map<String, dynamic> json) =>
_GeoIpLocation( _GeoIpLocation(
latitude: (json['latitude'] as num).toDouble(), latitude: (json['latitude'] as num?)?.toDouble(),
longitude: (json['longitude'] as num).toDouble(), longitude: (json['longitude'] as num?)?.toDouble(),
countryCode: json['country_code'] as String, countryCode: json['country_code'] as String?,
country: json['country'] as String, country: json['country'] as String?,
city: json['city'] as String, city: json['city'] as String?,
); );
Map<String, dynamic> _$GeoIpLocationToJson(_GeoIpLocation instance) => Map<String, dynamic> _$GeoIpLocationToJson(_GeoIpLocation instance) =>
@@ -34,7 +34,10 @@ Map<String, dynamic> _$GeoIpLocationToJson(_GeoIpLocation instance) =>
_SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) => _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
_SnAuthChallenge( _SnAuthChallenge(
id: json['id'] as String, id: json['id'] as String,
expiredAt: DateTime.parse(json['expired_at'] as String), expiredAt:
json['expired_at'] == null
? null
: DateTime.parse(json['expired_at'] as String),
stepRemain: (json['step_remain'] as num).toInt(), stepRemain: (json['step_remain'] as num).toInt(),
stepTotal: (json['step_total'] as num).toInt(), stepTotal: (json['step_total'] as num).toInt(),
failedAttempts: (json['failed_attempts'] as num).toInt(), failedAttempts: (json['failed_attempts'] as num).toInt(),
@@ -66,7 +69,7 @@ _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$SnAuthChallengeToJson(_SnAuthChallenge instance) => Map<String, dynamic> _$SnAuthChallengeToJson(_SnAuthChallenge instance) =>
<String, dynamic>{ <String, dynamic>{
'id': instance.id, 'id': instance.id,
'expired_at': instance.expiredAt.toIso8601String(), 'expired_at': instance.expiredAt?.toIso8601String(),
'step_remain': instance.stepRemain, 'step_remain': instance.stepRemain,
'step_total': instance.stepTotal, 'step_total': instance.stepTotal,
'failed_attempts': instance.failedAttempts, 'failed_attempts': instance.failedAttempts,
@@ -89,7 +92,10 @@ _SnAuthSession _$SnAuthSessionFromJson(Map<String, dynamic> json) =>
id: json['id'] as String, id: json['id'] as String,
label: json['label'] as String?, label: json['label'] as String?,
lastGrantedAt: DateTime.parse(json['last_granted_at'] as String), lastGrantedAt: DateTime.parse(json['last_granted_at'] as String),
expiredAt: DateTime.parse(json['expired_at'] as String), expiredAt:
json['expired_at'] == null
? null
: DateTime.parse(json['expired_at'] as String),
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
challengeId: json['challenge_id'] as String, challengeId: json['challenge_id'] as String,
challenge: SnAuthChallenge.fromJson( challenge: SnAuthChallenge.fromJson(
@@ -108,7 +114,7 @@ Map<String, dynamic> _$SnAuthSessionToJson(_SnAuthSession instance) =>
'id': instance.id, 'id': instance.id,
'label': instance.label, 'label': instance.label,
'last_granted_at': instance.lastGrantedAt.toIso8601String(), 'last_granted_at': instance.lastGrantedAt.toIso8601String(),
'expired_at': instance.expiredAt.toIso8601String(), 'expired_at': instance.expiredAt?.toIso8601String(),
'account_id': instance.accountId, 'account_id': instance.accountId,
'challenge_id': instance.challengeId, 'challenge_id': instance.challengeId,
'challenge': instance.challenge.toJson(), 'challenge': instance.challenge.toJson(),

View File

@@ -40,7 +40,7 @@ sealed class SnChatMessage with _$SnChatMessage {
String? content, String? content,
String? nonce, String? nonce,
@Default({}) Map<String, dynamic> meta, @Default({}) Map<String, dynamic> meta,
@Default([]) List<String> membersMetioned, @Default([]) List<String> membersMentioned,
DateTime? editedAt, DateTime? editedAt,
@Default([]) List<SnCloudFile> attachments, @Default([]) List<SnCloudFile> attachments,
@Default([]) List<SnChatReaction> reactions, @Default([]) List<SnChatReaction> reactions,
@@ -117,23 +117,10 @@ class MessageChangeAction {
static const String delete = "delete"; static const String delete = "delete";
} }
@freezed
sealed class MessageChange with _$MessageChange {
const factory MessageChange({
required String messageId,
required String action,
SnChatMessage? message,
required DateTime timestamp,
}) = _MessageChange;
factory MessageChange.fromJson(Map<String, dynamic> json) =>
_$MessageChangeFromJson(json);
}
@freezed @freezed
sealed class MessageSyncResponse with _$MessageSyncResponse { sealed class MessageSyncResponse with _$MessageSyncResponse {
const factory MessageSyncResponse({ const factory MessageSyncResponse({
@Default([]) List<MessageChange> changes, @Default([]) List<SnChatMessage> messages,
required DateTime currentTimestamp, required DateTime currentTimestamp,
}) = _MessageSyncResponse; }) = _MessageSyncResponse;

View File

@@ -391,7 +391,7 @@ $SnRealmCopyWith<$Res>? get realm {
/// @nodoc /// @nodoc
mixin _$SnChatMessage { mixin _$SnChatMessage {
DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String get type; String? get content; String? get nonce; Map<String, dynamic> get meta; List<String> get membersMetioned; DateTime? get editedAt; List<SnCloudFile> get attachments; List<SnChatReaction> get reactions; String? get repliedMessageId; String? get forwardedMessageId; String get senderId; SnChatMember get sender; String get chatRoomId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String get type; String? get content; String? get nonce; Map<String, dynamic> get meta; List<String> get membersMentioned; DateTime? get editedAt; List<SnCloudFile> get attachments; List<SnChatReaction> get reactions; String? get repliedMessageId; String? get forwardedMessageId; String get senderId; SnChatMember get sender; String get chatRoomId;
/// Create a copy of SnChatMessage /// Create a copy of SnChatMessage
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -404,16 +404,16 @@ $SnChatMessageCopyWith<SnChatMessage> get copyWith => _$SnChatMessageCopyWithImp
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other.meta, meta)&&const DeepCollectionEquality().equals(other.membersMetioned, membersMetioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)); return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other.meta, meta)&&const DeepCollectionEquality().equals(other.membersMentioned, membersMentioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,type,content,nonce,const DeepCollectionEquality().hash(meta),const DeepCollectionEquality().hash(membersMetioned),editedAt,const DeepCollectionEquality().hash(attachments),const DeepCollectionEquality().hash(reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId); int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,type,content,nonce,const DeepCollectionEquality().hash(meta),const DeepCollectionEquality().hash(membersMentioned),editedAt,const DeepCollectionEquality().hash(attachments),const DeepCollectionEquality().hash(reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId);
@override @override
String toString() { String toString() {
return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, type: $type, content: $content, nonce: $nonce, meta: $meta, membersMetioned: $membersMetioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)'; return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, type: $type, content: $content, nonce: $nonce, meta: $meta, membersMentioned: $membersMentioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)';
} }
@@ -424,7 +424,7 @@ abstract mixin class $SnChatMessageCopyWith<$Res> {
factory $SnChatMessageCopyWith(SnChatMessage value, $Res Function(SnChatMessage) _then) = _$SnChatMessageCopyWithImpl; factory $SnChatMessageCopyWith(SnChatMessage value, $Res Function(SnChatMessage) _then) = _$SnChatMessageCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMentioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId
}); });
@@ -441,7 +441,7 @@ class _$SnChatMessageCopyWithImpl<$Res>
/// Create a copy of SnChatMessage /// Create a copy of SnChatMessage
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? type = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMetioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) { @pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? type = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMentioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
@@ -451,7 +451,7 @@ as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non
as String,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable as String,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
as String?,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable as String?,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable
as String?,meta: null == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable as String?,meta: null == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,membersMetioned: null == membersMetioned ? _self.membersMetioned : membersMetioned // ignore: cast_nullable_to_non_nullable as Map<String, dynamic>,membersMentioned: null == membersMentioned ? _self.membersMentioned : membersMentioned // ignore: cast_nullable_to_non_nullable
as List<String>,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: cast_nullable_to_non_nullable as List<String>,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,attachments: null == attachments ? _self.attachments : attachments // ignore: cast_nullable_to_non_nullable as DateTime?,attachments: null == attachments ? _self.attachments : attachments // ignore: cast_nullable_to_non_nullable
as List<SnCloudFile>,reactions: null == reactions ? _self.reactions : reactions // ignore: cast_nullable_to_non_nullable as List<SnCloudFile>,reactions: null == reactions ? _self.reactions : reactions // ignore: cast_nullable_to_non_nullable
@@ -551,10 +551,10 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMentioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _SnChatMessage() when $default != null: case _SnChatMessage() when $default != null:
return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMetioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);case _: return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMentioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);case _:
return orElse(); return orElse();
} }
@@ -572,10 +572,10 @@ return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.t
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMentioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _SnChatMessage(): case _SnChatMessage():
return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMetioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);} return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMentioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);}
} }
/// A variant of `when` that fallback to returning `null` /// A variant of `when` that fallback to returning `null`
/// ///
@@ -589,10 +589,10 @@ return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.t
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMentioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _SnChatMessage() when $default != null: case _SnChatMessage() when $default != null:
return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMetioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);case _: return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMentioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);case _:
return null; return null;
} }
@@ -604,7 +604,7 @@ return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.t
@JsonSerializable() @JsonSerializable()
class _SnChatMessage implements SnChatMessage { class _SnChatMessage implements SnChatMessage {
const _SnChatMessage({required this.createdAt, required this.updatedAt, this.deletedAt, required this.id, this.type = 'text', this.content, this.nonce, final Map<String, dynamic> meta = const {}, final List<String> membersMetioned = const [], this.editedAt, final List<SnCloudFile> attachments = const [], final List<SnChatReaction> reactions = const [], this.repliedMessageId, this.forwardedMessageId, required this.senderId, required this.sender, required this.chatRoomId}): _meta = meta,_membersMetioned = membersMetioned,_attachments = attachments,_reactions = reactions; const _SnChatMessage({required this.createdAt, required this.updatedAt, this.deletedAt, required this.id, this.type = 'text', this.content, this.nonce, final Map<String, dynamic> meta = const {}, final List<String> membersMentioned = const [], this.editedAt, final List<SnCloudFile> attachments = const [], final List<SnChatReaction> reactions = const [], this.repliedMessageId, this.forwardedMessageId, required this.senderId, required this.sender, required this.chatRoomId}): _meta = meta,_membersMentioned = membersMentioned,_attachments = attachments,_reactions = reactions;
factory _SnChatMessage.fromJson(Map<String, dynamic> json) => _$SnChatMessageFromJson(json); factory _SnChatMessage.fromJson(Map<String, dynamic> json) => _$SnChatMessageFromJson(json);
@override final DateTime createdAt; @override final DateTime createdAt;
@@ -621,11 +621,11 @@ class _SnChatMessage implements SnChatMessage {
return EqualUnmodifiableMapView(_meta); return EqualUnmodifiableMapView(_meta);
} }
final List<String> _membersMetioned; final List<String> _membersMentioned;
@override@JsonKey() List<String> get membersMetioned { @override@JsonKey() List<String> get membersMentioned {
if (_membersMetioned is EqualUnmodifiableListView) return _membersMetioned; if (_membersMentioned is EqualUnmodifiableListView) return _membersMentioned;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_membersMetioned); return EqualUnmodifiableListView(_membersMentioned);
} }
@override final DateTime? editedAt; @override final DateTime? editedAt;
@@ -662,16 +662,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other._meta, _meta)&&const DeepCollectionEquality().equals(other._membersMetioned, _membersMetioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other._meta, _meta)&&const DeepCollectionEquality().equals(other._membersMentioned, _membersMentioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,type,content,nonce,const DeepCollectionEquality().hash(_meta),const DeepCollectionEquality().hash(_membersMetioned),editedAt,const DeepCollectionEquality().hash(_attachments),const DeepCollectionEquality().hash(_reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId); int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,type,content,nonce,const DeepCollectionEquality().hash(_meta),const DeepCollectionEquality().hash(_membersMentioned),editedAt,const DeepCollectionEquality().hash(_attachments),const DeepCollectionEquality().hash(_reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId);
@override @override
String toString() { String toString() {
return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, type: $type, content: $content, nonce: $nonce, meta: $meta, membersMetioned: $membersMetioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)'; return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, type: $type, content: $content, nonce: $nonce, meta: $meta, membersMentioned: $membersMentioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)';
} }
@@ -682,7 +682,7 @@ abstract mixin class _$SnChatMessageCopyWith<$Res> implements $SnChatMessageCopy
factory _$SnChatMessageCopyWith(_SnChatMessage value, $Res Function(_SnChatMessage) _then) = __$SnChatMessageCopyWithImpl; factory _$SnChatMessageCopyWith(_SnChatMessage value, $Res Function(_SnChatMessage) _then) = __$SnChatMessageCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMentioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId
}); });
@@ -699,7 +699,7 @@ class __$SnChatMessageCopyWithImpl<$Res>
/// Create a copy of SnChatMessage /// Create a copy of SnChatMessage
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? type = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMetioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) { @override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? type = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMentioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) {
return _then(_SnChatMessage( return _then(_SnChatMessage(
createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
@@ -709,7 +709,7 @@ as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non
as String,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable as String,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
as String?,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable as String?,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable
as String?,meta: null == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable as String?,meta: null == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,membersMetioned: null == membersMetioned ? _self._membersMetioned : membersMetioned // ignore: cast_nullable_to_non_nullable as Map<String, dynamic>,membersMentioned: null == membersMentioned ? _self._membersMentioned : membersMentioned // ignore: cast_nullable_to_non_nullable
as List<String>,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: cast_nullable_to_non_nullable as List<String>,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,attachments: null == attachments ? _self._attachments : attachments // ignore: cast_nullable_to_non_nullable as DateTime?,attachments: null == attachments ? _self._attachments : attachments // ignore: cast_nullable_to_non_nullable
as List<SnCloudFile>,reactions: null == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable as List<SnCloudFile>,reactions: null == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable
@@ -1691,300 +1691,10 @@ $SnChatMessageCopyWith<$Res>? get lastMessage {
} }
/// @nodoc
mixin _$MessageChange {
String get messageId; String get action; SnChatMessage? get message; DateTime get timestamp;
/// Create a copy of MessageChange
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$MessageChangeCopyWith<MessageChange> get copyWith => _$MessageChangeCopyWithImpl<MessageChange>(this as MessageChange, _$identity);
/// Serializes this MessageChange to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is MessageChange&&(identical(other.messageId, messageId) || other.messageId == messageId)&&(identical(other.action, action) || other.action == action)&&(identical(other.message, message) || other.message == message)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,messageId,action,message,timestamp);
@override
String toString() {
return 'MessageChange(messageId: $messageId, action: $action, message: $message, timestamp: $timestamp)';
}
}
/// @nodoc
abstract mixin class $MessageChangeCopyWith<$Res> {
factory $MessageChangeCopyWith(MessageChange value, $Res Function(MessageChange) _then) = _$MessageChangeCopyWithImpl;
@useResult
$Res call({
String messageId, String action, SnChatMessage? message, DateTime timestamp
});
$SnChatMessageCopyWith<$Res>? get message;
}
/// @nodoc
class _$MessageChangeCopyWithImpl<$Res>
implements $MessageChangeCopyWith<$Res> {
_$MessageChangeCopyWithImpl(this._self, this._then);
final MessageChange _self;
final $Res Function(MessageChange) _then;
/// Create a copy of MessageChange
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? messageId = null,Object? action = null,Object? message = freezed,Object? timestamp = null,}) {
return _then(_self.copyWith(
messageId: null == messageId ? _self.messageId : messageId // ignore: cast_nullable_to_non_nullable
as String,action: null == action ? _self.action : action // ignore: cast_nullable_to_non_nullable
as String,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as SnChatMessage?,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
as DateTime,
));
}
/// Create a copy of MessageChange
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnChatMessageCopyWith<$Res>? get message {
if (_self.message == null) {
return null;
}
return $SnChatMessageCopyWith<$Res>(_self.message!, (value) {
return _then(_self.copyWith(message: value));
});
}
}
/// Adds pattern-matching-related methods to [MessageChange].
extension MessageChangePatterns on MessageChange {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _MessageChange value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _MessageChange() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _MessageChange value) $default,){
final _that = this;
switch (_that) {
case _MessageChange():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _MessageChange value)? $default,){
final _that = this;
switch (_that) {
case _MessageChange() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String messageId, String action, SnChatMessage? message, DateTime timestamp)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _MessageChange() when $default != null:
return $default(_that.messageId,_that.action,_that.message,_that.timestamp);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String messageId, String action, SnChatMessage? message, DateTime timestamp) $default,) {final _that = this;
switch (_that) {
case _MessageChange():
return $default(_that.messageId,_that.action,_that.message,_that.timestamp);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String messageId, String action, SnChatMessage? message, DateTime timestamp)? $default,) {final _that = this;
switch (_that) {
case _MessageChange() when $default != null:
return $default(_that.messageId,_that.action,_that.message,_that.timestamp);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _MessageChange implements MessageChange {
const _MessageChange({required this.messageId, required this.action, this.message, required this.timestamp});
factory _MessageChange.fromJson(Map<String, dynamic> json) => _$MessageChangeFromJson(json);
@override final String messageId;
@override final String action;
@override final SnChatMessage? message;
@override final DateTime timestamp;
/// Create a copy of MessageChange
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$MessageChangeCopyWith<_MessageChange> get copyWith => __$MessageChangeCopyWithImpl<_MessageChange>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$MessageChangeToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MessageChange&&(identical(other.messageId, messageId) || other.messageId == messageId)&&(identical(other.action, action) || other.action == action)&&(identical(other.message, message) || other.message == message)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,messageId,action,message,timestamp);
@override
String toString() {
return 'MessageChange(messageId: $messageId, action: $action, message: $message, timestamp: $timestamp)';
}
}
/// @nodoc
abstract mixin class _$MessageChangeCopyWith<$Res> implements $MessageChangeCopyWith<$Res> {
factory _$MessageChangeCopyWith(_MessageChange value, $Res Function(_MessageChange) _then) = __$MessageChangeCopyWithImpl;
@override @useResult
$Res call({
String messageId, String action, SnChatMessage? message, DateTime timestamp
});
@override $SnChatMessageCopyWith<$Res>? get message;
}
/// @nodoc
class __$MessageChangeCopyWithImpl<$Res>
implements _$MessageChangeCopyWith<$Res> {
__$MessageChangeCopyWithImpl(this._self, this._then);
final _MessageChange _self;
final $Res Function(_MessageChange) _then;
/// Create a copy of MessageChange
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? messageId = null,Object? action = null,Object? message = freezed,Object? timestamp = null,}) {
return _then(_MessageChange(
messageId: null == messageId ? _self.messageId : messageId // ignore: cast_nullable_to_non_nullable
as String,action: null == action ? _self.action : action // ignore: cast_nullable_to_non_nullable
as String,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as SnChatMessage?,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
as DateTime,
));
}
/// Create a copy of MessageChange
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnChatMessageCopyWith<$Res>? get message {
if (_self.message == null) {
return null;
}
return $SnChatMessageCopyWith<$Res>(_self.message!, (value) {
return _then(_self.copyWith(message: value));
});
}
}
/// @nodoc /// @nodoc
mixin _$MessageSyncResponse { mixin _$MessageSyncResponse {
List<MessageChange> get changes; DateTime get currentTimestamp; List<SnChatMessage> get messages; DateTime get currentTimestamp;
/// Create a copy of MessageSyncResponse /// Create a copy of MessageSyncResponse
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -1997,16 +1707,16 @@ $MessageSyncResponseCopyWith<MessageSyncResponse> get copyWith => _$MessageSyncR
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is MessageSyncResponse&&const DeepCollectionEquality().equals(other.changes, changes)&&(identical(other.currentTimestamp, currentTimestamp) || other.currentTimestamp == currentTimestamp)); return identical(this, other) || (other.runtimeType == runtimeType&&other is MessageSyncResponse&&const DeepCollectionEquality().equals(other.messages, messages)&&(identical(other.currentTimestamp, currentTimestamp) || other.currentTimestamp == currentTimestamp));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(changes),currentTimestamp); int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(messages),currentTimestamp);
@override @override
String toString() { String toString() {
return 'MessageSyncResponse(changes: $changes, currentTimestamp: $currentTimestamp)'; return 'MessageSyncResponse(messages: $messages, currentTimestamp: $currentTimestamp)';
} }
@@ -2017,7 +1727,7 @@ abstract mixin class $MessageSyncResponseCopyWith<$Res> {
factory $MessageSyncResponseCopyWith(MessageSyncResponse value, $Res Function(MessageSyncResponse) _then) = _$MessageSyncResponseCopyWithImpl; factory $MessageSyncResponseCopyWith(MessageSyncResponse value, $Res Function(MessageSyncResponse) _then) = _$MessageSyncResponseCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
List<MessageChange> changes, DateTime currentTimestamp List<SnChatMessage> messages, DateTime currentTimestamp
}); });
@@ -2034,10 +1744,10 @@ class _$MessageSyncResponseCopyWithImpl<$Res>
/// Create a copy of MessageSyncResponse /// Create a copy of MessageSyncResponse
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? changes = null,Object? currentTimestamp = null,}) { @pragma('vm:prefer-inline') @override $Res call({Object? messages = null,Object? currentTimestamp = null,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
changes: null == changes ? _self.changes : changes // ignore: cast_nullable_to_non_nullable messages: null == messages ? _self.messages : messages // ignore: cast_nullable_to_non_nullable
as List<MessageChange>,currentTimestamp: null == currentTimestamp ? _self.currentTimestamp : currentTimestamp // ignore: cast_nullable_to_non_nullable as List<SnChatMessage>,currentTimestamp: null == currentTimestamp ? _self.currentTimestamp : currentTimestamp // ignore: cast_nullable_to_non_nullable
as DateTime, as DateTime,
)); ));
} }
@@ -2120,10 +1830,10 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<MessageChange> changes, DateTime currentTimestamp)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<SnChatMessage> messages, DateTime currentTimestamp)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _MessageSyncResponse() when $default != null: case _MessageSyncResponse() when $default != null:
return $default(_that.changes,_that.currentTimestamp);case _: return $default(_that.messages,_that.currentTimestamp);case _:
return orElse(); return orElse();
} }
@@ -2141,10 +1851,10 @@ return $default(_that.changes,_that.currentTimestamp);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<MessageChange> changes, DateTime currentTimestamp) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<SnChatMessage> messages, DateTime currentTimestamp) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _MessageSyncResponse(): case _MessageSyncResponse():
return $default(_that.changes,_that.currentTimestamp);} return $default(_that.messages,_that.currentTimestamp);}
} }
/// A variant of `when` that fallback to returning `null` /// A variant of `when` that fallback to returning `null`
/// ///
@@ -2158,10 +1868,10 @@ return $default(_that.changes,_that.currentTimestamp);}
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<MessageChange> changes, DateTime currentTimestamp)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<SnChatMessage> messages, DateTime currentTimestamp)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _MessageSyncResponse() when $default != null: case _MessageSyncResponse() when $default != null:
return $default(_that.changes,_that.currentTimestamp);case _: return $default(_that.messages,_that.currentTimestamp);case _:
return null; return null;
} }
@@ -2173,14 +1883,14 @@ return $default(_that.changes,_that.currentTimestamp);case _:
@JsonSerializable() @JsonSerializable()
class _MessageSyncResponse implements MessageSyncResponse { class _MessageSyncResponse implements MessageSyncResponse {
const _MessageSyncResponse({final List<MessageChange> changes = const [], required this.currentTimestamp}): _changes = changes; const _MessageSyncResponse({final List<SnChatMessage> messages = const [], required this.currentTimestamp}): _messages = messages;
factory _MessageSyncResponse.fromJson(Map<String, dynamic> json) => _$MessageSyncResponseFromJson(json); factory _MessageSyncResponse.fromJson(Map<String, dynamic> json) => _$MessageSyncResponseFromJson(json);
final List<MessageChange> _changes; final List<SnChatMessage> _messages;
@override@JsonKey() List<MessageChange> get changes { @override@JsonKey() List<SnChatMessage> get messages {
if (_changes is EqualUnmodifiableListView) return _changes; if (_messages is EqualUnmodifiableListView) return _messages;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_changes); return EqualUnmodifiableListView(_messages);
} }
@override final DateTime currentTimestamp; @override final DateTime currentTimestamp;
@@ -2198,16 +1908,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MessageSyncResponse&&const DeepCollectionEquality().equals(other._changes, _changes)&&(identical(other.currentTimestamp, currentTimestamp) || other.currentTimestamp == currentTimestamp)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _MessageSyncResponse&&const DeepCollectionEquality().equals(other._messages, _messages)&&(identical(other.currentTimestamp, currentTimestamp) || other.currentTimestamp == currentTimestamp));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_changes),currentTimestamp); int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_messages),currentTimestamp);
@override @override
String toString() { String toString() {
return 'MessageSyncResponse(changes: $changes, currentTimestamp: $currentTimestamp)'; return 'MessageSyncResponse(messages: $messages, currentTimestamp: $currentTimestamp)';
} }
@@ -2218,7 +1928,7 @@ abstract mixin class _$MessageSyncResponseCopyWith<$Res> implements $MessageSync
factory _$MessageSyncResponseCopyWith(_MessageSyncResponse value, $Res Function(_MessageSyncResponse) _then) = __$MessageSyncResponseCopyWithImpl; factory _$MessageSyncResponseCopyWith(_MessageSyncResponse value, $Res Function(_MessageSyncResponse) _then) = __$MessageSyncResponseCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
List<MessageChange> changes, DateTime currentTimestamp List<SnChatMessage> messages, DateTime currentTimestamp
}); });
@@ -2235,10 +1945,10 @@ class __$MessageSyncResponseCopyWithImpl<$Res>
/// Create a copy of MessageSyncResponse /// Create a copy of MessageSyncResponse
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? changes = null,Object? currentTimestamp = null,}) { @override @pragma('vm:prefer-inline') $Res call({Object? messages = null,Object? currentTimestamp = null,}) {
return _then(_MessageSyncResponse( return _then(_MessageSyncResponse(
changes: null == changes ? _self._changes : changes // ignore: cast_nullable_to_non_nullable messages: null == messages ? _self._messages : messages // ignore: cast_nullable_to_non_nullable
as List<MessageChange>,currentTimestamp: null == currentTimestamp ? _self.currentTimestamp : currentTimestamp // ignore: cast_nullable_to_non_nullable as List<SnChatMessage>,currentTimestamp: null == currentTimestamp ? _self.currentTimestamp : currentTimestamp // ignore: cast_nullable_to_non_nullable
as DateTime, as DateTime,
)); ));
} }

View File

@@ -69,8 +69,8 @@ _SnChatMessage _$SnChatMessageFromJson(Map<String, dynamic> json) =>
content: json['content'] as String?, content: json['content'] as String?,
nonce: json['nonce'] as String?, nonce: json['nonce'] as String?,
meta: json['meta'] as Map<String, dynamic>? ?? const {}, meta: json['meta'] as Map<String, dynamic>? ?? const {},
membersMetioned: membersMentioned:
(json['members_metioned'] as List<dynamic>?) (json['members_mentioned'] as List<dynamic>?)
?.map((e) => e as String) ?.map((e) => e as String)
.toList() ?? .toList() ??
const [], const [],
@@ -105,7 +105,7 @@ Map<String, dynamic> _$SnChatMessageToJson(_SnChatMessage instance) =>
'content': instance.content, 'content': instance.content,
'nonce': instance.nonce, 'nonce': instance.nonce,
'meta': instance.meta, 'meta': instance.meta,
'members_metioned': instance.membersMetioned, 'members_mentioned': instance.membersMentioned,
'edited_at': instance.editedAt?.toIso8601String(), 'edited_at': instance.editedAt?.toIso8601String(),
'attachments': instance.attachments.map((e) => e.toJson()).toList(), 'attachments': instance.attachments.map((e) => e.toJson()).toList(),
'reactions': instance.reactions.map((e) => e.toJson()).toList(), 'reactions': instance.reactions.map((e) => e.toJson()).toList(),
@@ -227,30 +227,11 @@ Map<String, dynamic> _$SnChatSummaryToJson(_SnChatSummary instance) =>
'last_message': instance.lastMessage?.toJson(), 'last_message': instance.lastMessage?.toJson(),
}; };
_MessageChange _$MessageChangeFromJson(Map<String, dynamic> json) =>
_MessageChange(
messageId: json['message_id'] as String,
action: json['action'] as String,
message:
json['message'] == null
? null
: SnChatMessage.fromJson(json['message'] as Map<String, dynamic>),
timestamp: DateTime.parse(json['timestamp'] as String),
);
Map<String, dynamic> _$MessageChangeToJson(_MessageChange instance) =>
<String, dynamic>{
'message_id': instance.messageId,
'action': instance.action,
'message': instance.message?.toJson(),
'timestamp': instance.timestamp.toIso8601String(),
};
_MessageSyncResponse _$MessageSyncResponseFromJson(Map<String, dynamic> json) => _MessageSyncResponse _$MessageSyncResponseFromJson(Map<String, dynamic> json) =>
_MessageSyncResponse( _MessageSyncResponse(
changes: messages:
(json['changes'] as List<dynamic>?) (json['messages'] as List<dynamic>?)
?.map((e) => MessageChange.fromJson(e as Map<String, dynamic>)) ?.map((e) => SnChatMessage.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
const [], const [],
currentTimestamp: DateTime.parse(json['current_timestamp'] as String), currentTimestamp: DateTime.parse(json['current_timestamp'] as String),
@@ -259,7 +240,7 @@ _MessageSyncResponse _$MessageSyncResponseFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$MessageSyncResponseToJson( Map<String, dynamic> _$MessageSyncResponseToJson(
_MessageSyncResponse instance, _MessageSyncResponse instance,
) => <String, dynamic>{ ) => <String, dynamic>{
'changes': instance.changes.map((e) => e.toJson()).toList(), 'messages': instance.messages.map((e) => e.toJson()).toList(),
'current_timestamp': instance.currentTimestamp.toIso8601String(), 'current_timestamp': instance.currentTimestamp.toIso8601String(),
}; };

53
lib/models/file_pool.dart Normal file
View File

@@ -0,0 +1,53 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'file_pool.freezed.dart';
part 'file_pool.g.dart';
@freezed
sealed class SnFilePool with _$SnFilePool {
const factory SnFilePool({
required String id,
required String name,
String? description,
Map<String, dynamic>? storageConfig,
Map<String, dynamic>? billingConfig,
Map<String, dynamic>? policyConfig,
bool? isHidden,
String? accountId,
String? resourceIdentifier,
DateTime? createdAt,
DateTime? updatedAt,
DateTime? deletedAt,
}) = _SnFilePool;
factory SnFilePool.fromJson(Map<String, dynamic> json) =>
_$SnFilePoolFromJson(json);
}
extension SnFilePoolList on List<SnFilePool> {
static List<SnFilePool> listFromResponse(dynamic data) {
if (data is List) {
return data
.whereType<Map<String, dynamic>>()
.map(SnFilePool.fromJson)
.toList();
}
throw ArgumentError('Unexpected response format: $data');
}
List<SnFilePool> filterValid() {
return where((p) {
final accept = p.policyConfig?['accept_types'];
if (accept is List) {
final acceptsOnlyMedia = accept.every((t) =>
t is String &&
(t.startsWith('image/') ||
t.startsWith('video/') ||
t.startsWith('audio/')));
if (acceptsOnlyMedia) return false;
}
return true;
}).toList();
}
}

View File

@@ -0,0 +1,328 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'file_pool.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnFilePool {
String get id; String get name; String? get description; Map<String, dynamic>? get storageConfig; Map<String, dynamic>? get billingConfig; Map<String, dynamic>? get policyConfig; bool? get isHidden; String? get accountId; String? get resourceIdentifier; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnFilePool
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnFilePoolCopyWith<SnFilePool> get copyWith => _$SnFilePoolCopyWithImpl<SnFilePool>(this as SnFilePool, _$identity);
/// Serializes this SnFilePool to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnFilePool&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.storageConfig, storageConfig)&&const DeepCollectionEquality().equals(other.billingConfig, billingConfig)&&const DeepCollectionEquality().equals(other.policyConfig, policyConfig)&&(identical(other.isHidden, isHidden) || other.isHidden == isHidden)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(storageConfig),const DeepCollectionEquality().hash(billingConfig),const DeepCollectionEquality().hash(policyConfig),isHidden,accountId,resourceIdentifier,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnFilePool(id: $id, name: $name, description: $description, storageConfig: $storageConfig, billingConfig: $billingConfig, policyConfig: $policyConfig, isHidden: $isHidden, accountId: $accountId, resourceIdentifier: $resourceIdentifier, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnFilePoolCopyWith<$Res> {
factory $SnFilePoolCopyWith(SnFilePool value, $Res Function(SnFilePool) _then) = _$SnFilePoolCopyWithImpl;
@useResult
$Res call({
String id, String name, String? description, Map<String, dynamic>? storageConfig, Map<String, dynamic>? billingConfig, Map<String, dynamic>? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class _$SnFilePoolCopyWithImpl<$Res>
implements $SnFilePoolCopyWith<$Res> {
_$SnFilePoolCopyWithImpl(this._self, this._then);
final SnFilePool _self;
final $Res Function(SnFilePool) _then;
/// Create a copy of SnFilePool
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? storageConfig = freezed,Object? billingConfig = freezed,Object? policyConfig = freezed,Object? isHidden = freezed,Object? accountId = freezed,Object? resourceIdentifier = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,storageConfig: freezed == storageConfig ? _self.storageConfig : storageConfig // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,billingConfig: freezed == billingConfig ? _self.billingConfig : billingConfig // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,policyConfig: freezed == policyConfig ? _self.policyConfig : policyConfig // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,isHidden: freezed == isHidden ? _self.isHidden : isHidden // ignore: cast_nullable_to_non_nullable
as bool?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String?,resourceIdentifier: freezed == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable
as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// Adds pattern-matching-related methods to [SnFilePool].
extension SnFilePoolPatterns on SnFilePool {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnFilePool value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnFilePool() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnFilePool value) $default,){
final _that = this;
switch (_that) {
case _SnFilePool():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnFilePool value)? $default,){
final _that = this;
switch (_that) {
case _SnFilePool() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String? description, Map<String, dynamic>? storageConfig, Map<String, dynamic>? billingConfig, Map<String, dynamic>? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnFilePool() when $default != null:
return $default(_that.id,_that.name,_that.description,_that.storageConfig,_that.billingConfig,_that.policyConfig,_that.isHidden,_that.accountId,_that.resourceIdentifier,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String? description, Map<String, dynamic>? storageConfig, Map<String, dynamic>? billingConfig, Map<String, dynamic>? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnFilePool():
return $default(_that.id,_that.name,_that.description,_that.storageConfig,_that.billingConfig,_that.policyConfig,_that.isHidden,_that.accountId,_that.resourceIdentifier,_that.createdAt,_that.updatedAt,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String? description, Map<String, dynamic>? storageConfig, Map<String, dynamic>? billingConfig, Map<String, dynamic>? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnFilePool() when $default != null:
return $default(_that.id,_that.name,_that.description,_that.storageConfig,_that.billingConfig,_that.policyConfig,_that.isHidden,_that.accountId,_that.resourceIdentifier,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnFilePool implements SnFilePool {
const _SnFilePool({required this.id, required this.name, this.description, final Map<String, dynamic>? storageConfig, final Map<String, dynamic>? billingConfig, final Map<String, dynamic>? policyConfig, this.isHidden, this.accountId, this.resourceIdentifier, this.createdAt, this.updatedAt, this.deletedAt}): _storageConfig = storageConfig,_billingConfig = billingConfig,_policyConfig = policyConfig;
factory _SnFilePool.fromJson(Map<String, dynamic> json) => _$SnFilePoolFromJson(json);
@override final String id;
@override final String name;
@override final String? description;
final Map<String, dynamic>? _storageConfig;
@override Map<String, dynamic>? get storageConfig {
final value = _storageConfig;
if (value == null) return null;
if (_storageConfig is EqualUnmodifiableMapView) return _storageConfig;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
final Map<String, dynamic>? _billingConfig;
@override Map<String, dynamic>? get billingConfig {
final value = _billingConfig;
if (value == null) return null;
if (_billingConfig is EqualUnmodifiableMapView) return _billingConfig;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
final Map<String, dynamic>? _policyConfig;
@override Map<String, dynamic>? get policyConfig {
final value = _policyConfig;
if (value == null) return null;
if (_policyConfig is EqualUnmodifiableMapView) return _policyConfig;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
@override final bool? isHidden;
@override final String? accountId;
@override final String? resourceIdentifier;
@override final DateTime? createdAt;
@override final DateTime? updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnFilePool
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnFilePoolCopyWith<_SnFilePool> get copyWith => __$SnFilePoolCopyWithImpl<_SnFilePool>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnFilePoolToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnFilePool&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._storageConfig, _storageConfig)&&const DeepCollectionEquality().equals(other._billingConfig, _billingConfig)&&const DeepCollectionEquality().equals(other._policyConfig, _policyConfig)&&(identical(other.isHidden, isHidden) || other.isHidden == isHidden)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(_storageConfig),const DeepCollectionEquality().hash(_billingConfig),const DeepCollectionEquality().hash(_policyConfig),isHidden,accountId,resourceIdentifier,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnFilePool(id: $id, name: $name, description: $description, storageConfig: $storageConfig, billingConfig: $billingConfig, policyConfig: $policyConfig, isHidden: $isHidden, accountId: $accountId, resourceIdentifier: $resourceIdentifier, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnFilePoolCopyWith<$Res> implements $SnFilePoolCopyWith<$Res> {
factory _$SnFilePoolCopyWith(_SnFilePool value, $Res Function(_SnFilePool) _then) = __$SnFilePoolCopyWithImpl;
@override @useResult
$Res call({
String id, String name, String? description, Map<String, dynamic>? storageConfig, Map<String, dynamic>? billingConfig, Map<String, dynamic>? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class __$SnFilePoolCopyWithImpl<$Res>
implements _$SnFilePoolCopyWith<$Res> {
__$SnFilePoolCopyWithImpl(this._self, this._then);
final _SnFilePool _self;
final $Res Function(_SnFilePool) _then;
/// Create a copy of SnFilePool
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? storageConfig = freezed,Object? billingConfig = freezed,Object? policyConfig = freezed,Object? isHidden = freezed,Object? accountId = freezed,Object? resourceIdentifier = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,}) {
return _then(_SnFilePool(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,storageConfig: freezed == storageConfig ? _self._storageConfig : storageConfig // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,billingConfig: freezed == billingConfig ? _self._billingConfig : billingConfig // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,policyConfig: freezed == policyConfig ? _self._policyConfig : policyConfig // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,isHidden: freezed == isHidden ? _self.isHidden : isHidden // ignore: cast_nullable_to_non_nullable
as bool?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String?,resourceIdentifier: freezed == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable
as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
// dart format on

View File

@@ -0,0 +1,47 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'file_pool.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_SnFilePool _$SnFilePoolFromJson(Map<String, dynamic> json) => _SnFilePool(
id: json['id'] as String,
name: json['name'] as String,
description: json['description'] as String?,
storageConfig: json['storage_config'] as Map<String, dynamic>?,
billingConfig: json['billing_config'] as Map<String, dynamic>?,
policyConfig: json['policy_config'] as Map<String, dynamic>?,
isHidden: json['is_hidden'] as bool?,
accountId: json['account_id'] as String?,
resourceIdentifier: json['resource_identifier'] as String?,
createdAt:
json['created_at'] == null
? null
: DateTime.parse(json['created_at'] as String),
updatedAt:
json['updated_at'] == null
? null
: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
Map<String, dynamic> _$SnFilePoolToJson(_SnFilePool instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'description': instance.description,
'storage_config': instance.storageConfig,
'billing_config': instance.billingConfig,
'policy_config': instance.policyConfig,
'is_hidden': instance.isHidden,
'account_id': instance.accountId,
'resource_identifier': instance.resourceIdentifier,
'created_at': instance.createdAt?.toIso8601String(),
'updated_at': instance.updatedAt?.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@@ -9,7 +9,9 @@ import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io; import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_web_socket/shelf_web_socket.dart'; import 'package:shelf_web_socket/shelf_web_socket.dart';
import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:path/path.dart' as path;
// Conditional imports for IPC server - use web stubs on web platform
import 'ipc_server.dart' if (dart.library.html) 'ipc_server.web.dart';
const String kRpcLogPrefix = 'arRPC.websocket'; const String kRpcLogPrefix = 'arRPC.websocket';
const String kRpcIpcLogPrefix = 'arRPC.ipc'; const String kRpcIpcLogPrefix = 'arRPC.ipc';
@@ -43,14 +45,14 @@ class IpcErrorCodes {
static const int invalidEncoding = 4005; static const int invalidEncoding = 4005;
} }
// Reference https://github.com/OpenAsar/arrpc/blob/main/src/transports/ipc.js
class ActivityRpcServer { class ActivityRpcServer {
static const List<int> portRange = [6463, 6472]; // Ports 64636472 static const List<int> portRange = [6463, 6472]; // Ports 64636472
Map<String, Function> Map<String, Function>
handlers; // {connection: (socket), message: (socket, data), close: (socket)} handlers; // {connection: (socket), message: (socket, data), close: (socket)}
HttpServer? _httpServer; HttpServer? _httpServer;
ServerSocket? _ipcServer; IpcServer? _ipcServer;
final List<WebSocketChannel> _wsSockets = []; final List<WebSocketChannel> _wsSockets = [];
final List<_IpcSocketWrapper> _ipcSockets = [];
ActivityRpcServer(this.handlers); ActivityRpcServer(this.handlers);
@@ -58,109 +60,20 @@ class ActivityRpcServer {
handlers = newHandlers; handlers = newHandlers;
} }
// Encode IPC packet // Start the server
static Uint8List encodeIpcPacket(int type, Map<String, dynamic> data) {
final jsonData = jsonEncode(data);
final dataBytes = utf8.encode(jsonData);
final dataSize = dataBytes.length;
final buffer = ByteData(8 + dataSize);
buffer.setInt32(0, type, Endian.little);
buffer.setInt32(4, dataSize, Endian.little);
buffer.buffer.asUint8List().setRange(8, 8 + dataSize, dataBytes);
return buffer.buffer.asUint8List();
}
Future<String> _getMacOsSystemTmpDir() async {
final result = await Process.run('getconf', ['DARWIN_USER_TEMP_DIR']);
return (result.stdout as String).trim();
}
// Find available IPC socket path
Future<String> _findAvailableIpcPath() async {
// Build list of directories to try, with macOS-specific handling
final baseDirs = <String>[];
if (Platform.isMacOS) {
try {
final macTempDir = await _getMacOsSystemTmpDir();
if (macTempDir.isNotEmpty) {
baseDirs.add(macTempDir);
}
} catch (e) {
developer.log(
'Failed to get macOS system temp dir: $e',
name: kRpcIpcLogPrefix,
);
}
}
// Add other standard directories
final otherDirs = [
Platform.environment['XDG_RUNTIME_DIR'], // User runtime directory
Platform.environment['TMPDIR'], // App container temp (fallback)
Platform.environment['TMP'],
Platform.environment['TEMP'],
'/tmp', // System temp directory - most compatible
];
baseDirs.addAll(
otherDirs.where((dir) => dir != null && dir.isNotEmpty).cast<String>(),
);
for (final baseDir in baseDirs) {
for (int i = 0; i < 10; i++) {
final socketPath = path.join(baseDir, '$kIpcBasePath-$i');
try {
final socket = await ServerSocket.bind(
InternetAddress(socketPath, type: InternetAddressType.unix),
0,
);
socket.close();
// Clean up the test socket
try {
await File(socketPath).delete();
} catch (_) {}
developer.log(
'IPC socket will be created at: $socketPath',
name: kRpcIpcLogPrefix,
);
return socketPath;
} catch (e) {
// Path not available, try next
if (i == 0) {
// Log only for the first attempt per directory
developer.log(
'IPC path $socketPath not available: $e',
name: kRpcIpcLogPrefix,
);
}
continue;
}
}
}
throw Exception(
'No available IPC socket paths found in any temp directory',
);
}
// Start the WebSocket server
Future<void> start() async { Future<void> start() async {
int port = portRange[0]; int port = portRange[0];
bool wsSuccess = false; bool wsSuccess = false;
// Start WebSocket server // Start WebSocket server
while (port <= portRange[1]) { while (port <= portRange[1]) {
developer.log('trying port $port', name: kRpcLogPrefix); developer.log('Trying port $port', name: kRpcLogPrefix);
try { try {
// Start HTTP server
_httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port); _httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port);
developer.log('listening on $port', name: kRpcLogPrefix); developer.log('Listening on $port', name: kRpcLogPrefix);
// Handle WebSocket upgrades
shelf_io.serveRequests(_httpServer!, (Request request) async { shelf_io.serveRequests(_httpServer!, (Request request) async {
developer.log('new request', name: kRpcLogPrefix); developer.log('New request', name: kRpcLogPrefix);
if (request.headers['upgrade']?.toLowerCase() == 'websocket') { if (request.headers['upgrade']?.toLowerCase() == 'websocket') {
final handler = webSocketHandler((WebSocketChannel channel, _) { final handler = webSocketHandler((WebSocketChannel channel, _) {
_wsSockets.add(channel); _wsSockets.add(channel);
@@ -169,7 +82,7 @@ class ActivityRpcServer {
return handler(request); return handler(request);
} }
developer.log( developer.log(
'new request disposed due to not websocket', 'New request disposed due to not websocket',
name: kRpcLogPrefix, name: kRpcLogPrefix,
); );
return Response.notFound('Not a WebSocket request'); return Response.notFound('Not a WebSocket request');
@@ -178,12 +91,12 @@ class ActivityRpcServer {
break; break;
} catch (e) { } catch (e) {
if (e is SocketException && e.osError?.errorCode == 98) { if (e is SocketException && e.osError?.errorCode == 98) {
// EADDRINUSE
developer.log('$port in use!', name: kRpcLogPrefix); developer.log('$port in use!', name: kRpcLogPrefix);
} else { } else {
developer.log('http error: $e', name: kRpcLogPrefix); developer.log('HTTP error: $e', name: kRpcLogPrefix);
} }
port++; port++;
await Future.delayed(Duration(milliseconds: 100)); // Add delay
} }
} }
@@ -193,27 +106,24 @@ class ActivityRpcServer {
); );
} }
// Start IPC server (skip on macOS due to sandboxing) // Start IPC server
final shouldStartIpc = !Platform.isMacOS; final shouldStartIpc = !Platform.isMacOS && !kIsWeb;
if (shouldStartIpc) { if (shouldStartIpc) {
try { try {
final ipcPath = await _findAvailableIpcPath(); _ipcServer = MultiPlatformIpcServer();
_ipcServer = await ServerSocket.bind(
InternetAddress(ipcPath, type: InternetAddressType.unix),
0,
);
developer.log('IPC listening at $ipcPath', name: kRpcIpcLogPrefix);
_ipcServer!.listen((Socket socket) { // Set up IPC handlers
_onIpcConnection(socket); _ipcServer!.handlePacket = (socket, packet, _) {
}); _handleIpcPacket(socket, packet);
};
await _ipcServer!.start();
} catch (e) { } catch (e) {
developer.log('IPC server error: $e', name: kRpcIpcLogPrefix); developer.log('IPC server error: $e', name: kRpcIpcLogPrefix);
// Continue without IPC if it fails
} }
} else { } else {
developer.log( developer.log(
'IPC server disabled on macOS in production mode due to sandboxing', 'IPC server disabled on macOS or web in production mode',
name: kRpcIpcLogPrefix, name: kRpcIpcLogPrefix,
); );
} }
@@ -223,24 +133,23 @@ class ActivityRpcServer {
Future<void> stop() async { Future<void> stop() async {
// Stop WebSocket server // Stop WebSocket server
for (var socket in _wsSockets) { for (var socket in _wsSockets) {
await socket.sink.close(); try {
await socket.sink.close();
} catch (e) {
developer.log('Error closing WebSocket: $e', name: kRpcLogPrefix);
}
} }
_wsSockets.clear(); _wsSockets.clear();
await _httpServer?.close(); await _httpServer?.close(force: true);
// Stop IPC server // Stop IPC server
for (var socket in _ipcSockets) { await _ipcServer?.stop();
socket.close();
}
_ipcSockets.clear();
await _ipcServer?.close();
developer.log('servers stopped', name: kRpcLogPrefix); developer.log('Servers stopped', name: kRpcLogPrefix);
} }
// Handle new WebSocket connection // Handle new WebSocket connection
void _onWsConnection(WebSocketChannel socket, Request request) { void _onWsConnection(WebSocketChannel socket, Request request) {
// Parse query parameters
final uri = request.url; final uri = request.url;
final params = uri.queryParameters; final params = uri.queryParameters;
final ver = int.tryParse(params['v'] ?? '1') ?? 1; final ver = int.tryParse(params['v'] ?? '1') ?? 1;
@@ -249,43 +158,38 @@ class ActivityRpcServer {
final origin = request.headers['origin'] ?? ''; final origin = request.headers['origin'] ?? '';
developer.log( developer.log(
'new WS connection! origin: $origin, params: $params', 'New WS connection! origin: $origin, params: $params',
name: kRpcLogPrefix, name: kRpcLogPrefix,
); );
// Validate origin
if (origin.isNotEmpty && if (origin.isNotEmpty &&
![ ![
'https://discord.com', 'https://discord.com',
'https://ptb.discord.com', 'https://ptb.discord.com',
'https://canary.discord.com', 'https://canary.discord.com',
].contains(origin)) { ].contains(origin)) {
developer.log('disallowed origin: $origin', name: kRpcLogPrefix); developer.log('Disallowed origin: $origin', name: kRpcLogPrefix);
socket.sink.close(); socket.sink.close();
return; return;
} }
// Validate encoding
if (encoding != 'json') { if (encoding != 'json') {
developer.log( developer.log(
'unsupported encoding requested: $encoding', 'Unsupported encoding requested: $encoding',
name: kRpcLogPrefix, name: kRpcLogPrefix,
); );
socket.sink.close(); socket.sink.close();
return; return;
} }
// Validate version
if (ver != 1) { if (ver != 1) {
developer.log('unsupported version requested: $ver', name: kRpcLogPrefix); developer.log('Unsupported version requested: $ver', name: kRpcLogPrefix);
socket.sink.close(); socket.sink.close();
return; return;
} }
// Store client info on socket
final socketWithMeta = _WsSocketWrapper(socket, clientId, encoding); final socketWithMeta = _WsSocketWrapper(socket, clientId, encoding);
// Set up event listeners
socket.stream.listen( socket.stream.listen(
(data) => _onWsMessage(socketWithMeta, data), (data) => _onWsMessage(socketWithMeta, data),
onError: (e) { onError: (e) {
@@ -298,36 +202,27 @@ class ActivityRpcServer {
}, },
); );
// Notify handler of new connection
handlers['connection']?.call(socketWithMeta); handlers['connection']?.call(socketWithMeta);
} }
// Handle new IPC connection
void _onIpcConnection(Socket socket) {
developer.log('new IPC connection!', name: kRpcIpcLogPrefix);
final socketWrapper = _IpcSocketWrapper(socket);
_ipcSockets.add(socketWrapper);
// Set up event listeners
socket.listen(
(data) => _onIpcData(socketWrapper, data),
onError: (e) {
developer.log('IPC socket error: $e', name: kRpcIpcLogPrefix);
socket.close();
},
onDone: () {
developer.log('IPC socket closed', name: kRpcIpcLogPrefix);
handlers['close']?.call(socketWrapper);
_ipcSockets.remove(socketWrapper);
},
);
}
// Handle incoming WebSocket message // Handle incoming WebSocket message
void _onWsMessage(_WsSocketWrapper socket, dynamic data) { Future<void> _onWsMessage(_WsSocketWrapper socket, dynamic data) async {
if (data is! String) {
developer.log(
'Invalid WebSocket message: not a string',
name: kRpcLogPrefix,
);
return;
}
try { try {
final jsonData = jsonDecode(data as String); final jsonData = await compute(jsonDecode, data);
if (jsonData is! Map<String, dynamic>) {
developer.log(
'Invalid WebSocket message: not a JSON object',
name: kRpcLogPrefix,
);
return;
}
developer.log('WS message: $jsonData', name: kRpcLogPrefix); developer.log('WS message: $jsonData', name: kRpcLogPrefix);
handlers['message']?.call(socket, jsonData); handlers['message']?.call(socket, jsonData);
} catch (e) { } catch (e) {
@@ -335,22 +230,8 @@ class ActivityRpcServer {
} }
} }
// Handle incoming IPC data
void _onIpcData(_IpcSocketWrapper socket, List<int> data) {
try {
socket.addData(data);
final packets = socket.readPackets();
for (final packet in packets) {
_handleIpcPacket(socket, packet);
}
} catch (e) {
developer.log('IPC data error: $e', name: kRpcIpcLogPrefix);
socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString());
}
}
// Handle IPC packet // Handle IPC packet
void _handleIpcPacket(_IpcSocketWrapper socket, _IpcPacket packet) { void _handleIpcPacket(IpcSocketWrapper socket, IpcPacket packet) {
switch (packet.type) { switch (packet.type) {
case IpcTypes.ping: case IpcTypes.ping:
developer.log('IPC ping received', name: kRpcIpcLogPrefix); developer.log('IPC ping received', name: kRpcIpcLogPrefix);
@@ -359,7 +240,6 @@ class ActivityRpcServer {
case IpcTypes.pong: case IpcTypes.pong:
developer.log('IPC pong received', name: kRpcIpcLogPrefix); developer.log('IPC pong received', name: kRpcIpcLogPrefix);
// Handle pong if needed
break; break;
case IpcTypes.handshake: case IpcTypes.handshake:
@@ -388,13 +268,12 @@ class ActivityRpcServer {
} }
// Handle IPC handshake // Handle IPC handshake
void _onIpcHandshake(_IpcSocketWrapper socket, Map<String, dynamic> params) { void _onIpcHandshake(IpcSocketWrapper socket, Map<String, dynamic> params) {
developer.log('IPC handshake: $params', name: kRpcIpcLogPrefix); developer.log('IPC handshake: $params', name: kRpcIpcLogPrefix);
final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1; final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1;
final clientId = params['client_id']?.toString() ?? ''; final clientId = params['client_id']?.toString() ?? '';
// Validate version
if (ver != 1) { if (ver != 1) {
developer.log( developer.log(
'IPC unsupported version requested: $ver', 'IPC unsupported version requested: $ver',
@@ -404,7 +283,6 @@ class ActivityRpcServer {
return; return;
} }
// Validate client ID
if (clientId.isEmpty) { if (clientId.isEmpty) {
developer.log('IPC client ID required', name: kRpcIpcLogPrefix); developer.log('IPC client ID required', name: kRpcIpcLogPrefix);
socket.closeWithCode(IpcErrorCodes.invalidClientId); socket.closeWithCode(IpcErrorCodes.invalidClientId);
@@ -413,7 +291,6 @@ class ActivityRpcServer {
socket.clientId = clientId; socket.clientId = clientId;
// Notify handler of new connection
handlers['connection']?.call(socket); handlers['connection']?.call(socket);
} }
} }
@@ -432,74 +309,6 @@ class _WsSocketWrapper {
} }
} }
// IPC wrapper
class _IpcSocketWrapper {
final Socket socket;
String clientId = '';
bool handshook = false;
final List<int> _buffer = [];
_IpcSocketWrapper(this.socket);
void addData(List<int> data) {
_buffer.addAll(data);
}
void send(Map<String, dynamic> msg) {
developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix);
final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.frame, msg);
socket.add(packet);
}
void sendPong(dynamic data) {
final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {});
socket.add(packet);
}
void close() {
socket.close();
}
void closeWithCode(int code, [String message = '']) {
final closeData = {'code': code, 'message': message};
final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.close, closeData);
socket.add(packet);
socket.close();
}
List<_IpcPacket> readPackets() {
final packets = <_IpcPacket>[];
while (_buffer.length >= 8) {
final buffer = Uint8List.fromList(_buffer);
final byteData = ByteData.view(buffer.buffer);
final type = byteData.getInt32(0, Endian.little);
final dataSize = byteData.getInt32(4, Endian.little);
if (_buffer.length < 8 + dataSize) break;
final dataBytes = _buffer.sublist(8, 8 + dataSize);
final jsonStr = utf8.decode(dataBytes);
final jsonData = jsonDecode(jsonStr);
packets.add(_IpcPacket(type, jsonData));
_buffer.removeRange(0, 8 + dataSize);
}
return packets;
}
}
// IPC Packet structure
class _IpcPacket {
final int type;
final Map<String, dynamic> data;
_IpcPacket(this.type, this.data);
}
// State management for server status and activities // State management for server status and activities
class ServerState { class ServerState {
final String status; final String status;
@@ -522,7 +331,6 @@ class ServerStateNotifier extends StateNotifier<ServerState> {
: super(ServerState(status: 'Server not started')); : super(ServerState(status: 'Server not started'));
Future<void> start() async { Future<void> start() async {
// Only start server on desktop platforms
if (!Platform.isAndroid && !Platform.isIOS && !kIsWeb) { if (!Platform.isAndroid && !Platform.isIOS && !kIsWeb) {
try { try {
await server.start(); await server.start();
@@ -531,7 +339,9 @@ class ServerStateNotifier extends StateNotifier<ServerState> {
state = state.copyWith(status: 'Server failed: $e'); state = state.copyWith(status: 'Server failed: $e');
} }
} else { } else {
state = state.copyWith(status: 'Server disabled on mobile/web'); Future(() {
state = state.copyWith(status: 'Server disabled on mobile/web');
});
} }
} }
@@ -554,9 +364,8 @@ final rpcServerStateProvider =
final clientId = final clientId =
socket is _WsSocketWrapper socket is _WsSocketWrapper
? socket.clientId ? socket.clientId
: (socket as _IpcSocketWrapper).clientId; : (socket as IpcSocketWrapper).clientId;
notifier.updateStatus('Client connected (ID: $clientId)'); notifier.updateStatus('Client connected (ID: $clientId)');
// Send READY event
socket.send({ socket.send({
'cmd': 'DISPATCH', 'cmd': 'DISPATCH',
'data': { 'data': {
@@ -575,7 +384,7 @@ final rpcServerStateProvider =
}, },
}, },
'evt': 'READY', 'evt': 'READY',
'nonce': '12345', // Should be dynamic 'nonce': '12345',
}); });
}, },
'message': (socket, dynamic data) async { 'message': (socket, dynamic data) async {
@@ -583,7 +392,6 @@ final rpcServerStateProvider =
notifier.addActivity( notifier.addActivity(
'Activity: ${data['args']['activity']['details'] ?? 'Unknown'}', 'Activity: ${data['args']['activity']['details'] ?? 'Unknown'}',
); );
// Call setRemoteActivityStatus
final label = data['args']['activity']['details'] ?? 'Unknown'; final label = data['args']['activity']['details'] ?? 'Unknown';
final appId = socket.clientId; final appId = socket.clientId;
try { try {
@@ -594,7 +402,6 @@ final rpcServerStateProvider =
name: kRpcLogPrefix, name: kRpcLogPrefix,
); );
} }
// Echo back success
socket.send({ socket.send({
'cmd': 'SET_ACTIVITY', 'cmd': 'SET_ACTIVITY',
'data': data['args']['activity'], 'data': data['args']['activity'],

View File

@@ -0,0 +1,297 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer' as developer;
import 'dart:io';
import 'dart:typed_data';
import 'package:dart_ipc/dart_ipc.dart';
import 'package:path/path.dart' as path;
const String kRpcIpcLogPrefix = 'arRPC.ipc';
// IPC Packet Types
class IpcTypes {
static const int handshake = 0;
static const int frame = 1;
static const int close = 2;
static const int ping = 3;
static const int pong = 4;
}
// IPC Close Codes
class IpcCloseCodes {
static const int closeNormal = 1000;
static const int closeUnsupported = 1003;
static const int closeAbnormal = 1006;
}
// IPC Error Codes
class IpcErrorCodes {
static const int invalidClientId = 4000;
static const int invalidOrigin = 4001;
static const int rateLimited = 4002;
static const int tokenRevoked = 4003;
static const int invalidVersion = 4004;
static const int invalidEncoding = 4005;
}
// IPC Packet structure
class IpcPacket {
final int type;
final Map<String, dynamic> data;
IpcPacket(this.type, this.data);
}
// Abstract base class for IPC server
abstract class IpcServer {
final List<IpcSocketWrapper> _sockets = [];
// Encode IPC packet
static Uint8List encodeIpcPacket(int type, Map<String, dynamic> data) {
final jsonData = jsonEncode(data);
final dataBytes = utf8.encode(jsonData);
final dataSize = dataBytes.length;
final buffer = ByteData(8 + dataSize);
buffer.setInt32(0, type, Endian.little);
buffer.setInt32(4, dataSize, Endian.little);
buffer.buffer.asUint8List().setRange(8, 8 + dataSize, dataBytes);
return buffer.buffer.asUint8List();
}
Future<void> start();
Future<void> stop();
void addSocket(IpcSocketWrapper socket) {
_sockets.add(socket);
}
void removeSocket(IpcSocketWrapper socket) {
_sockets.remove(socket);
}
List<IpcSocketWrapper> get sockets => _sockets;
void Function(
IpcSocketWrapper socket,
IpcPacket packet,
Map<String, Function> handlers,
)?
handlePacket;
}
// Abstract base class for IPC socket wrapper
abstract class IpcSocketWrapper {
String clientId = '';
bool handshook = false;
final List<int> _buffer = [];
void addData(List<int> data) {
_buffer.addAll(data);
}
void send(Map<String, dynamic> msg);
void sendPong(dynamic data);
void close();
void closeWithCode(int code, [String message = '']);
List<IpcPacket> readPackets() {
final packets = <IpcPacket>[];
while (_buffer.length >= 8) {
final buffer = Uint8List.fromList(_buffer);
final byteData = ByteData.view(buffer.buffer);
final type = byteData.getInt32(0, Endian.little);
final dataSize = byteData.getInt32(4, Endian.little);
if (_buffer.length < 8 + dataSize) break;
final dataBytes = _buffer.sublist(8, 8 + dataSize);
final jsonStr = utf8.decode(dataBytes);
final jsonData = jsonDecode(jsonStr);
packets.add(IpcPacket(type, jsonData));
_buffer.removeRange(0, 8 + dataSize);
}
return packets;
}
}
// Multiplatform IPC Server implementation using dart_ipc
class MultiPlatformIpcServer extends IpcServer {
StreamSubscription? _serverSubscription;
@override
Future<void> start() async {
try {
final ipcPath = Platform.isWindows
? r'\\.\pipe\discord-ipc-0'
: await _findAvailableUnixIpcPath();
final serverSocket = await bind(ipcPath);
developer.log(
'IPC listening at $ipcPath',
name: kRpcIpcLogPrefix,
);
_serverSubscription = serverSocket.listen((socket) {
final socketWrapper = MultiPlatformIpcSocketWrapper(socket);
addSocket(socketWrapper);
developer.log(
'New IPC connection!',
name: kRpcIpcLogPrefix,
);
_handleIpcData(socketWrapper);
});
} catch (e) {
throw Exception('Failed to start IPC server: $e');
}
}
@override
Future<void> stop() async {
for (var socket in sockets) {
try {
socket.close();
} catch (e) {
developer.log('Error closing IPC socket: $e', name: kRpcIpcLogPrefix);
}
}
sockets.clear();
_serverSubscription?.cancel();
}
// Handle incoming IPC data
void _handleIpcData(MultiPlatformIpcSocketWrapper socket) {
final startTime = DateTime.now();
socket.socket.listen((data) {
final readStart = DateTime.now();
socket.addData(data);
final readDuration = DateTime.now().difference(readStart).inMicroseconds;
developer.log(
'Read data took $readDuration microseconds',
name: kRpcIpcLogPrefix,
);
final packets = socket.readPackets();
for (final packet in packets) {
handlePacket?.call(socket, packet, {});
}
}, onDone: () {
developer.log('IPC connection closed', name: kRpcIpcLogPrefix);
socket.close();
}, onError: (e) {
developer.log('IPC data error: $e', name: kRpcIpcLogPrefix);
socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString());
});
final totalDuration = DateTime.now().difference(startTime).inMicroseconds;
developer.log(
'_handleIpcData took $totalDuration microseconds',
name: kRpcIpcLogPrefix,
);
}
Future<String> _getMacOsSystemTmpDir() async {
final result = await Process.run('getconf', ['DARWIN_USER_TEMP_DIR']);
return (result.stdout as String).trim();
}
// Find available IPC socket path for Unix-like systems
Future<String> _findAvailableUnixIpcPath() async {
// Build list of directories to try, with macOS-specific handling
final baseDirs = <String>[];
if (Platform.isMacOS) {
try {
final macTempDir = await _getMacOsSystemTmpDir();
if (macTempDir.isNotEmpty) {
baseDirs.add(macTempDir);
}
} catch (e) {
developer.log(
'Failed to get macOS system temp dir: $e',
name: kRpcIpcLogPrefix,
);
}
}
// Add other standard directories
final otherDirs = [
Platform.environment['XDG_RUNTIME_DIR'],
Platform.environment['TMPDIR'],
Platform.environment['TMP'],
Platform.environment['TEMP'],
'/tmp',
];
baseDirs.addAll(
otherDirs.where((dir) => dir != null && dir.isNotEmpty).cast<String>(),
);
for (final baseDir in baseDirs) {
for (int i = 0; i < 10; i++) {
final socketPath = path.join(baseDir, 'discord-ipc-$i');
try {
final socket = await bind(socketPath);
socket.close();
try {
await File(socketPath).delete();
} catch (_) {}
developer.log(
'IPC socket will be created at: $socketPath',
name: kRpcIpcLogPrefix,
);
return socketPath;
} catch (e) {
if (i == 0) {
developer.log(
'IPC path $socketPath not available: $e',
name: kRpcIpcLogPrefix,
);
}
continue;
}
}
}
throw Exception(
'No available IPC socket paths found in any temp directory',
);
}
}
// Multiplatform IPC Socket Wrapper
class MultiPlatformIpcSocketWrapper extends IpcSocketWrapper {
final dynamic socket;
MultiPlatformIpcSocketWrapper(this.socket);
@override
void send(Map<String, dynamic> msg) {
developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix);
final packet = IpcServer.encodeIpcPacket(IpcTypes.frame, msg);
socket.add(packet);
}
@override
void sendPong(dynamic data) {
final packet = IpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {});
socket.add(packet);
}
@override
void close() {
socket.close();
}
@override
void closeWithCode(int code, [String message = '']) {
final closeData = {'code': code, 'message': message};
final packet = IpcServer.encodeIpcPacket(IpcTypes.close, closeData);
socket.add(packet);
socket.close();
}
}

View File

@@ -0,0 +1,61 @@
// Stub implementation for web platform
// This file provides empty implementations to avoid import errors on web
// IPC Packet Types
class IpcTypes {
static const int handshake = 0;
static const int frame = 1;
static const int close = 2;
static const int ping = 3;
static const int pong = 4;
}
// IPC Close Codes
class IpcCloseCodes {
static const int closeNormal = 1000;
static const int closeUnsupported = 1003;
static const int closeAbnormal = 1006;
}
// IPC Error Codes
class IpcErrorCodes {
static const int invalidClientId = 4000;
static const int invalidOrigin = 4001;
static const int rateLimited = 4002;
static const int tokenRevoked = 4003;
static const int invalidVersion = 4004;
static const int invalidEncoding = 4005;
}
// IPC Packet structure
class IpcPacket {
final int type;
final Map<String, dynamic> data;
IpcPacket(this.type, this.data);
}
class IpcServer {
Future<void> start() async {}
Future<void> stop() async {}
void Function(dynamic, dynamic, dynamic)? handlePacket;
void addSocket(dynamic socket) {}
void removeSocket(dynamic socket) {}
List<dynamic> get sockets => [];
}
class IpcSocketWrapper {
String clientId = '';
bool handshook = false;
void addData(List<int> data) {}
void send(Map<String, dynamic> msg) {}
void sendPong(dynamic data) {}
void close() {}
void closeWithCode(int code, [String message = '']) {}
List<dynamic> readPackets() => [];
}
class MultiPlatformIpcServer extends IpcServer {}
class MultiPlatformIpcSocketWrapper extends IpcSocketWrapper {}

View File

@@ -25,6 +25,8 @@ const kAppSoundEffects = 'app_sound_effects';
const kAppAprilFoolFeatures = 'app_april_fool_features'; const kAppAprilFoolFeatures = 'app_april_fool_features';
const kAppWindowSize = 'app_window_size'; const kAppWindowSize = 'app_window_size';
const kAppEnterToSend = 'app_enter_to_send'; const kAppEnterToSend = 'app_enter_to_send';
const kAppDefaultPoolId = 'app_default_pool_id';
const kAppMessageDisplayStyle = 'app_message_display_style';
const kFeaturedPostsCollapsedId = const kFeaturedPostsCollapsedId =
'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post 'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post
@@ -65,6 +67,8 @@ sealed class AppSettings with _$AppSettings {
required String? customFonts, required String? customFonts,
required int? appColorScheme, // The color stored via the int type required int? appColorScheme, // The color stored via the int type
required Size? windowSize, // The window size for desktop platforms required Size? windowSize, // The window size for desktop platforms
required String? defaultPoolId,
required String messageDisplayStyle,
}) = _AppSettings; }) = _AppSettings;
} }
@@ -84,6 +88,8 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
customFonts: prefs.getString(kAppCustomFonts), customFonts: prefs.getString(kAppCustomFonts),
appColorScheme: prefs.getInt(kAppColorSchemeStoreKey), appColorScheme: prefs.getInt(kAppColorSchemeStoreKey),
windowSize: _getWindowSizeFromPrefs(prefs), windowSize: _getWindowSizeFromPrefs(prefs),
defaultPoolId: prefs.getString(kAppDefaultPoolId),
messageDisplayStyle: prefs.getString(kAppMessageDisplayStyle) ?? 'bubble',
); );
} }
@@ -104,13 +110,23 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
return null; return null;
} }
void setDefaultPoolId(String? value) {
final prefs = ref.read(sharedPreferencesProvider);
if (value != null) {
prefs.setString(kAppDefaultPoolId, value);
} else {
prefs.remove(kAppDefaultPoolId);
}
state = state.copyWith(defaultPoolId: value);
}
void setAutoTranslate(bool value) { void setAutoTranslate(bool value) {
final prefs = ref.read(sharedPreferencesProvider); final prefs = ref.read(sharedPreferencesProvider);
prefs.setBool(kAppAutoTranslate, value); prefs.setBool(kAppAutoTranslate, value);
state = state.copyWith(autoTranslate: value); state = state.copyWith(autoTranslate: value);
} }
void setDataSavingMode(bool value){ void setDataSavingMode(bool value) {
final prefs = ref.read(sharedPreferencesProvider); final prefs = ref.read(sharedPreferencesProvider);
prefs.setBool(kAppDataSavingMode, value); prefs.setBool(kAppDataSavingMode, value);
state = state.copyWith(dataSavingMode: value); state = state.copyWith(dataSavingMode: value);
@@ -174,6 +190,12 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
Size? getWindowSize() { Size? getWindowSize() {
return state.windowSize; return state.windowSize;
} }
void setMessageDisplayStyle(String value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setString(kAppMessageDisplayStyle, value);
state = state.copyWith(messageDisplayStyle: value);
}
} }
final updateInfoProvider = final updateInfoProvider =

View File

@@ -15,7 +15,8 @@ T _$identity<T>(T value) => value;
mixin _$AppSettings { mixin _$AppSettings {
bool get autoTranslate; bool get dataSavingMode; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend; bool get appBarTransparent; bool get showBackgroundImage; String? get customFonts; int? get appColorScheme;// The color stored via the int type bool get autoTranslate; bool get dataSavingMode; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend; bool get appBarTransparent; bool get showBackgroundImage; String? get customFonts; int? get appColorScheme;// The color stored via the int type
Size? get windowSize; Size? get windowSize;// The window size for desktop platforms
String? get defaultPoolId; String get messageDisplayStyle;
/// Create a copy of AppSettings /// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -26,16 +27,16 @@ $AppSettingsCopyWith<AppSettings> get copyWith => _$AppSettingsCopyWithImpl<AppS
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)); return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle));
} }
@override @override
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize); int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,defaultPoolId,messageDisplayStyle);
@override @override
String toString() { String toString() {
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize)'; return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle)';
} }
@@ -46,7 +47,7 @@ abstract mixin class $AppSettingsCopyWith<$Res> {
factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl; factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle
}); });
@@ -63,7 +64,7 @@ class _$AppSettingsCopyWithImpl<$Res>
/// Create a copy of AppSettings /// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable
@@ -75,7 +76,9 @@ as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundI
as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable
as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable
as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable
as Size?, as Size?,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
as String,
)); ));
} }
@@ -157,10 +160,10 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _AppSettings() when $default != null: case _AppSettings() when $default != null:
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize);case _: return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId,_that.messageDisplayStyle);case _:
return orElse(); return orElse();
} }
@@ -178,10 +181,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _AppSettings(): case _AppSettings():
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize);} return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId,_that.messageDisplayStyle);}
} }
/// A variant of `when` that fallback to returning `null` /// A variant of `when` that fallback to returning `null`
/// ///
@@ -195,10 +198,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _AppSettings() when $default != null: case _AppSettings() when $default != null:
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize);case _: return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId,_that.messageDisplayStyle);case _:
return null; return null;
} }
@@ -210,7 +213,7 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
class _AppSettings implements AppSettings { class _AppSettings implements AppSettings {
const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.windowSize}); const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.windowSize, required this.defaultPoolId, required this.messageDisplayStyle});
@override final bool autoTranslate; @override final bool autoTranslate;
@@ -224,6 +227,9 @@ class _AppSettings implements AppSettings {
@override final int? appColorScheme; @override final int? appColorScheme;
// The color stored via the int type // The color stored via the int type
@override final Size? windowSize; @override final Size? windowSize;
// The window size for desktop platforms
@override final String? defaultPoolId;
@override final String messageDisplayStyle;
/// Create a copy of AppSettings /// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -235,16 +241,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle));
} }
@override @override
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize); int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,defaultPoolId,messageDisplayStyle);
@override @override
String toString() { String toString() {
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize)'; return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle)';
} }
@@ -255,7 +261,7 @@ abstract mixin class _$AppSettingsCopyWith<$Res> implements $AppSettingsCopyWith
factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl; factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle
}); });
@@ -272,7 +278,7 @@ class __$AppSettingsCopyWithImpl<$Res>
/// Create a copy of AppSettings /// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,}) {
return _then(_AppSettings( return _then(_AppSettings(
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable
@@ -284,7 +290,9 @@ as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundI
as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable
as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable
as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable
as Size?, as Size?,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
as String,
)); ));
} }

View File

@@ -7,7 +7,7 @@ part of 'config.dart';
// ************************************************************************** // **************************************************************************
String _$appSettingsNotifierHash() => String _$appSettingsNotifierHash() =>
r'cd18bff2614a94e3523634e6c577cefad0367eba'; r'9f0979f18b107e61185391e7c39bd81ac4b8ca50';
/// See also [AppSettingsNotifier]. /// See also [AppSettingsNotifier].
@ProviderFor(AppSettingsNotifier) @ProviderFor(AppSettingsNotifier)

View File

@@ -0,0 +1,852 @@
import "dart:async";
import "dart:developer" as developer;
import "package:dio/dio.dart";
import "package:drift/drift.dart" show Variable;
import "package:easy_localization/easy_localization.dart";
import "package:flutter/material.dart";
import "package:island/database/drift_db.dart";
import "package:island/database/message.dart";
import "package:island/models/chat.dart";
import "package:island/models/file.dart";
import "package:island/pods/config.dart";
import "package:island/pods/database.dart";
import "package:island/pods/network.dart";
import "package:island/services/file.dart";
import "package:island/widgets/alert.dart";
import "package:riverpod_annotation/riverpod_annotation.dart";
import "package:uuid/uuid.dart";
import "package:island/screens/chat/chat.dart";
import "package:island/pods/room_providers.dart";
part 'messages_notifier.g.dart';
@riverpod
class MessagesNotifier extends _$MessagesNotifier {
late final Dio _apiClient;
late final AppDatabase _database;
late final SnChatRoom _room;
late final SnChatMember _identity;
final Map<String, LocalChatMessage> _pendingMessages = {};
final Map<String, Map<int, double>> _fileUploadProgress = {};
int? _totalCount;
String? _searchQuery;
bool? _withLinks;
bool? _withAttachments;
late final String _roomId;
static const int _pageSize = 20;
bool _hasMore = true;
bool _isSyncing = false;
bool _isJumping = false;
@override
FutureOr<List<LocalChatMessage>> build(String roomId) async {
_roomId = roomId;
_apiClient = ref.watch(apiClientProvider);
_database = ref.watch(databaseProvider);
final room = await ref.watch(chatroomProvider(roomId).future);
final identity = await ref.watch(chatroomIdentityProvider(roomId).future);
if (room == null) {
throw Exception('Room not found');
}
_room = room;
// Allow building even if identity is null for public rooms
if (identity != null) {
_identity = identity;
}
developer.log(
'MessagesNotifier built for room $roomId',
name: 'MessagesNotifier',
);
// Only setup sync and lifecycle listeners if user is a member
if (identity != null) {
ref.listen(appLifecycleStateProvider, (_, next) {
if (next.hasValue && next.value == AppLifecycleState.resumed) {
developer.log(
'App resumed, syncing messages',
name: 'MessagesNotifier',
);
syncMessages();
}
});
}
loadInitial();
return [];
}
List<LocalChatMessage> _sortMessages(List<LocalChatMessage> messages) {
messages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
return messages;
}
Future<List<LocalChatMessage>> _getCachedMessages({
int offset = 0,
int take = 20,
}) async {
developer.log(
'Getting cached messages from offset $offset, take $take',
name: 'MessagesNotifier',
);
final List<LocalChatMessage> dbMessages;
if (_searchQuery != null && _searchQuery!.isNotEmpty) {
dbMessages = await _database.searchMessages(
_roomId,
_searchQuery ?? '',
withAttachments: _withAttachments,
);
} else {
final chatMessagesFromDb = await _database.getMessagesForRoom(
_roomId,
offset: offset,
limit: take,
);
dbMessages =
chatMessagesFromDb.map(_database.companionToMessage).toList();
}
List<LocalChatMessage> filteredMessages = dbMessages;
if (_withLinks == true) {
filteredMessages =
filteredMessages.where((msg) => _hasLink(msg)).toList();
}
final dbLocalMessages = filteredMessages;
// Always ensure unique messages to prevent duplicate keys
final uniqueMessages = <LocalChatMessage>[];
final seenIds = <String>{};
for (final message in dbLocalMessages) {
if (seenIds.add(message.id)) {
uniqueMessages.add(message);
}
}
if (offset == 0) {
final pendingForRoom =
_pendingMessages.values
.where((msg) => msg.roomId == _roomId)
.toList();
final allMessages = [...pendingForRoom, ...uniqueMessages];
_sortMessages(allMessages); // Use the helper function
final finalUniqueMessages = <LocalChatMessage>[];
final finalSeenIds = <String>{};
for (final message in allMessages) {
if (finalSeenIds.add(message.id)) {
finalUniqueMessages.add(message);
}
}
return finalUniqueMessages;
}
return uniqueMessages;
}
Future<List<LocalChatMessage>> _fetchAndCacheMessages({
int offset = 0,
int take = 20,
}) async {
developer.log(
'Fetching messages from API, offset $offset, take $take',
name: 'MessagesNotifier',
);
if (_totalCount == null) {
final response = await _apiClient.get(
'/sphere/chat/$_roomId/messages',
queryParameters: {'offset': 0, 'take': 1},
);
_totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0');
}
if (offset >= _totalCount!) {
return [];
}
final response = await _apiClient.get(
'/sphere/chat/$_roomId/messages',
queryParameters: {'offset': offset, 'take': take},
);
final List<dynamic> data = response.data;
_totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0');
final messages =
data.map((json) {
final remoteMessage = SnChatMessage.fromJson(json);
return LocalChatMessage.fromRemoteMessage(
remoteMessage,
MessageStatus.sent,
);
}).toList();
for (final message in messages) {
await _database.saveMessage(_database.messageToCompanion(message));
if (message.nonce != null) {
_pendingMessages.removeWhere(
(_, pendingMsg) => pendingMsg.nonce == message.nonce,
);
}
}
return messages;
}
Future<void> syncMessages() async {
if (_isSyncing) {
developer.log(
'Sync already in progress, skipping.',
name: 'MessagesNotifier',
);
return;
}
_isSyncing = true;
developer.log('Starting message sync', name: 'MessagesNotifier');
Future.microtask(() => ref.read(isSyncingProvider.notifier).state = true);
try {
final dbMessages = await _database.getMessagesForRoom(
_room.id,
offset: 0,
limit: 1,
);
final lastMessage =
dbMessages.isEmpty
? null
: _database.companionToMessage(dbMessages.first);
if (lastMessage == null) {
developer.log(
'No local messages, fetching from network',
name: 'MessagesNotifier',
);
final newMessages = await _fetchAndCacheMessages(
offset: 0,
take: _pageSize,
);
state = AsyncValue.data(newMessages);
return;
}
final resp = await _apiClient.post(
'/sphere/chat/${_room.id}/sync',
data: {
'last_sync_timestamp':
lastMessage.toRemoteMessage().updatedAt.millisecondsSinceEpoch,
},
);
final response = MessageSyncResponse.fromJson(resp.data);
developer.log(
'Sync response: ${response.messages.length} changes',
name: 'MessagesNotifier',
);
for (final message in response.messages) {
switch (message.type) {
case "messages.update":
case "messages.update.links":
await receiveMessageUpdate(message);
break;
case "messages.delete":
await receiveMessageDeletion(message.id.toString());
break;
}
// Still need receive the message to show the history actions
await receiveMessage(message);
}
} catch (err, stackTrace) {
developer.log(
'Error syncing messages',
name: 'MessagesNotifier',
error: err,
stackTrace: stackTrace,
);
showErrorAlert(err);
} finally {
developer.log('Finished message sync', name: 'MessagesNotifier');
Future.microtask(
() => ref.read(isSyncingProvider.notifier).state = false,
);
_isSyncing = false;
}
}
Future<List<LocalChatMessage>> listMessages({
int offset = 0,
int take = 20,
bool synced = false,
}) async {
try {
if (offset == 0 &&
!synced &&
(_searchQuery == null || _searchQuery!.isEmpty)) {
_fetchAndCacheMessages(offset: 0, take: take).catchError((_) {
return <LocalChatMessage>[];
});
}
final localMessages = await _getCachedMessages(
offset: offset,
take: take,
);
if (localMessages.isNotEmpty) {
return localMessages;
}
if (_searchQuery == null || _searchQuery!.isEmpty) {
return await _fetchAndCacheMessages(offset: offset, take: take);
} else {
return []; // If searching, and no local messages, don't fetch from network
}
} catch (e) {
final localMessages = await _getCachedMessages(
offset: offset,
take: take,
);
if (localMessages.isNotEmpty) {
return localMessages;
}
rethrow;
}
}
Future<void> loadInitial() async {
developer.log('Loading initial messages', name: 'MessagesNotifier');
if (_searchQuery == null || _searchQuery!.isEmpty) {
syncMessages();
}
final messages = await _getCachedMessages(offset: 0, take: _pageSize);
_hasMore = messages.length == _pageSize;
state = AsyncValue.data(messages);
}
Future<void> loadMore() async {
if (!_hasMore || state is AsyncLoading) return;
developer.log('Loading more messages', name: 'MessagesNotifier');
try {
final currentMessages = state.value ?? [];
final offset = currentMessages.length;
final newMessages = await listMessages(offset: offset, take: _pageSize);
if (newMessages.isEmpty || newMessages.length < _pageSize) {
_hasMore = false;
}
state = AsyncValue.data(
_sortMessages([...currentMessages, ...newMessages]),
);
} catch (err, stackTrace) {
developer.log(
'Error loading more messages',
name: 'MessagesNotifier',
error: err,
stackTrace: stackTrace,
);
showErrorAlert(err);
}
}
Future<void> sendMessage(
String content,
List<UniversalFile> attachments, {
SnChatMessage? editingTo,
SnChatMessage? forwardingTo,
SnChatMessage? replyingTo,
Function(String, Map<int, double>)? onProgress,
}) async {
final nonce = const Uuid().v4();
developer.log(
'Sending message with nonce $nonce',
name: 'MessagesNotifier',
);
final baseUrl = ref.read(serverUrlProvider);
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Access token is null');
final mockMessage = SnChatMessage(
id: 'pending_$nonce',
chatRoomId: _roomId,
senderId: _identity.id,
content: content,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
nonce: nonce,
sender: _identity,
);
final localMessage = LocalChatMessage.fromRemoteMessage(
mockMessage,
MessageStatus.pending,
);
_pendingMessages[localMessage.id] = localMessage;
_fileUploadProgress[localMessage.id] = {};
await _database.saveMessage(_database.messageToCompanion(localMessage));
final currentMessages = state.value ?? [];
state = AsyncValue.data([localMessage, ...currentMessages]);
try {
var cloudAttachments = List.empty(growable: true);
for (var idx = 0; idx < attachments.length; idx++) {
final cloudFile =
await putFileToCloud(
fileData: attachments[idx],
atk: token,
baseUrl: baseUrl,
filename: attachments[idx].data.name ?? 'Post media',
mimetype:
attachments[idx].data.mimeType ??
switch (attachments[idx].type) {
UniversalFileType.image => 'image/unknown',
UniversalFileType.video => 'video/unknown',
UniversalFileType.audio => 'audio/unknown',
UniversalFileType.file => 'application/octet-stream',
},
onProgress: (progress, _) {
_fileUploadProgress[localMessage.id]?[idx] = progress;
onProgress?.call(
localMessage.id,
_fileUploadProgress[localMessage.id] ?? {},
);
},
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
cloudAttachments.add(cloudFile);
}
final response = await _apiClient.request(
editingTo == null
? '/sphere/chat/$_roomId/messages'
: '/sphere/chat/$_roomId/messages/${editingTo.id}',
data: {
'content': content,
'attachments_id': cloudAttachments.map((e) => e.id).toList(),
'replied_message_id': replyingTo?.id,
'forwarded_message_id': forwardingTo?.id,
'meta': {},
'nonce': nonce,
},
options: Options(method: editingTo == null ? 'POST' : 'PATCH'),
);
final remoteMessage = SnChatMessage.fromJson(response.data);
final updatedMessage = LocalChatMessage.fromRemoteMessage(
remoteMessage,
MessageStatus.sent,
);
_pendingMessages.remove(localMessage.id);
await _database.deleteMessage(localMessage.id);
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
final currentMessages = state.value ?? [];
if (editingTo != null) {
final newMessages =
currentMessages
.where((m) => m.id != localMessage.id) // remove pending message
.map(
(m) => m.id == editingTo.id ? updatedMessage : m,
) // update original message
.toList();
state = AsyncValue.data(newMessages);
} else {
final newMessages =
currentMessages.map((m) {
if (m.id == localMessage.id) {
return updatedMessage;
}
return m;
}).toList();
state = AsyncValue.data(newMessages);
}
developer.log(
'Message with nonce $nonce sent successfully',
name: 'MessagesNotifier',
);
} catch (e, stackTrace) {
developer.log(
'Failed to send message with nonce $nonce',
name: 'MessagesNotifier',
error: e,
stackTrace: stackTrace,
);
localMessage.status = MessageStatus.failed;
_pendingMessages[localMessage.id] = localMessage;
await _database.updateMessageStatus(
localMessage.id,
MessageStatus.failed,
);
final newMessages =
(state.value ?? []).map((m) {
if (m.id == localMessage.id) {
return m..status = MessageStatus.failed;
}
return m;
}).toList();
state = AsyncValue.data(newMessages);
showErrorAlert(e);
}
}
Future<void> retryMessage(String pendingMessageId) async {
developer.log(
'Retrying message $pendingMessageId',
name: 'MessagesNotifier',
);
final message = await fetchMessageById(pendingMessageId);
if (message == null) {
throw Exception('Message not found');
}
message.status = MessageStatus.pending;
_pendingMessages[pendingMessageId] = message;
await _database.updateMessageStatus(
pendingMessageId,
MessageStatus.pending,
);
try {
var remoteMessage = message.toRemoteMessage();
final response = await _apiClient.post(
'/sphere/chat/${message.roomId}/messages',
data: {
'content': remoteMessage.content,
'attachments_id': remoteMessage.attachments,
'meta': remoteMessage.meta,
'nonce': message.nonce,
},
);
remoteMessage = SnChatMessage.fromJson(response.data);
final updatedMessage = LocalChatMessage.fromRemoteMessage(
remoteMessage,
MessageStatus.sent,
);
_pendingMessages.remove(pendingMessageId);
await _database.deleteMessage(pendingMessageId);
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
final newMessages =
(state.value ?? []).map((m) {
if (m.id == pendingMessageId) {
return updatedMessage;
}
return m;
}).toList();
state = AsyncValue.data(newMessages);
} catch (e, stackTrace) {
developer.log(
'Failed to retry message $pendingMessageId',
name: 'MessagesNotifier',
error: e,
stackTrace: stackTrace,
);
message.status = MessageStatus.failed;
_pendingMessages[pendingMessageId] = message;
await _database.updateMessageStatus(
pendingMessageId,
MessageStatus.failed,
);
final newMessages =
(state.value ?? []).map((m) {
if (m.id == pendingMessageId) {
return m..status = MessageStatus.failed;
}
return m;
}).toList();
state = AsyncValue.data(_sortMessages(newMessages));
showErrorAlert(e);
}
}
Future<void> receiveMessage(SnChatMessage remoteMessage) async {
if (remoteMessage.chatRoomId != _roomId) return;
developer.log(
'Received new message ${remoteMessage.id}',
name: 'MessagesNotifier',
);
final localMessage = LocalChatMessage.fromRemoteMessage(
remoteMessage,
MessageStatus.sent,
);
if (remoteMessage.nonce != null) {
_pendingMessages.removeWhere(
(_, pendingMsg) => pendingMsg.nonce == remoteMessage.nonce,
);
}
await _database.saveMessage(_database.messageToCompanion(localMessage));
final currentMessages = state.value ?? [];
final existingIndex = currentMessages.indexWhere(
(m) =>
m.id == localMessage.id ||
(localMessage.nonce != null && m.nonce == localMessage.nonce),
);
if (existingIndex >= 0) {
final newList = [...currentMessages];
newList[existingIndex] = localMessage;
state = AsyncValue.data(_sortMessages(newList));
} else {
state = AsyncValue.data(
_sortMessages([localMessage, ...currentMessages]),
);
}
}
Future<void> receiveMessageUpdate(SnChatMessage remoteMessage) async {
if (remoteMessage.chatRoomId != _roomId) return;
developer.log(
'Received message update ${remoteMessage.id}',
name: 'MessagesNotifier',
);
final updatedMessage = LocalChatMessage.fromRemoteMessage(
remoteMessage,
MessageStatus.sent,
);
await _database.updateMessage(_database.messageToCompanion(updatedMessage));
final currentMessages = state.value ?? [];
final index = currentMessages.indexWhere((m) => m.id == updatedMessage.id);
if (index >= 0) {
final newList = [...currentMessages];
newList[index] = updatedMessage;
state = AsyncValue.data(_sortMessages(newList));
}
}
Future<void> receiveMessageDeletion(String messageId) async {
developer.log(
'Received message deletion $messageId',
name: 'MessagesNotifier',
);
_pendingMessages.remove(messageId);
final currentMessages = state.value ?? [];
final messageIndex = currentMessages.indexWhere((m) => m.id == messageId);
LocalChatMessage? messageToUpdate;
if (messageIndex != -1) {
messageToUpdate = currentMessages[messageIndex];
} else {
messageToUpdate = await fetchMessageById(messageId);
}
if (messageToUpdate == null) return;
final remote = messageToUpdate.toRemoteMessage();
final updatedRemote = remote.copyWith(
content: 'This message was deleted',
deletedAt: DateTime.now(),
attachments: [],
);
final deletedMessage = LocalChatMessage.fromRemoteMessage(
updatedRemote,
messageToUpdate.status,
);
await _database.saveMessage(_database.messageToCompanion(deletedMessage));
if (messageIndex != -1) {
final newList = [...currentMessages];
newList[messageIndex] = deletedMessage;
state = AsyncValue.data(newList);
}
}
Future<void> deleteMessage(String messageId) async {
developer.log('Deleting message $messageId', name: 'MessagesNotifier');
try {
await _apiClient.delete('/sphere/chat/$_roomId/messages/$messageId');
await receiveMessageDeletion(messageId);
} catch (err, stackTrace) {
developer.log(
'Error deleting message $messageId',
name: 'MessagesNotifier',
error: err,
stackTrace: stackTrace,
);
showErrorAlert(err);
}
}
void searchMessages(String query, {bool? withLinks, bool? withAttachments}) {
_searchQuery = query.trim();
_withLinks = withLinks;
_withAttachments = withAttachments;
loadInitial();
}
void clearSearch() {
_searchQuery = null;
_withLinks = null;
_withAttachments = null;
loadInitial();
}
Future<LocalChatMessage?> fetchMessageById(String messageId) async {
developer.log(
'Fetching message by id $messageId',
name: 'MessagesNotifier',
);
try {
final localMessage =
await (_database.select(_database.chatMessages)
..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
if (localMessage != null) {
return _database.companionToMessage(localMessage);
}
final response = await _apiClient.get(
'/sphere/chat/$_roomId/messages/$messageId',
);
final remoteMessage = SnChatMessage.fromJson(response.data);
final message = LocalChatMessage.fromRemoteMessage(
remoteMessage,
MessageStatus.sent,
);
await _database.saveMessage(_database.messageToCompanion(message));
return message;
} catch (e) {
if (e is DioException) return null;
rethrow;
}
}
Future<int> jumpToMessage(String messageId) async {
developer.log(
'Starting jump to message $messageId',
name: 'MessagesNotifier',
);
if (_isJumping) {
developer.log(
'Jump already in progress, skipping',
name: 'MessagesNotifier',
);
return -1;
}
_isJumping = true;
try {
developer.log('Fetching message $messageId', name: 'MessagesNotifier');
final message = await fetchMessageById(messageId);
if (message == null) {
developer.log('Message $messageId not found', name: 'MessagesNotifier');
showSnackBar('messageNotFound'.tr());
return -1;
}
// Check if message is already in current state to avoid duplicate loading
final currentMessages = state.value ?? [];
final existingIndex = currentMessages.indexWhere(
(m) => m.id == messageId,
);
if (existingIndex >= 0) {
developer.log(
'Message $messageId already in current state at index $existingIndex, jumping directly',
name: 'MessagesNotifier',
);
return existingIndex;
}
developer.log(
'Message $messageId not in current state, loading messages around it',
name: 'MessagesNotifier',
);
// Count messages newer than this one
final query = _database.customSelect(
'SELECT COUNT(*) as count FROM chat_messages WHERE room_id = ? AND created_at > ?',
variables: [
Variable.withString(_roomId),
Variable.withDateTime(message.createdAt),
],
readsFrom: {_database.chatMessages},
);
final result = await query.getSingle();
final newerCount = result.read<int>('count');
// Load messages around this position
final offset =
(newerCount - _pageSize ~/ 2).clamp(0, double.infinity).toInt();
developer.log(
'Loading messages with offset $offset, take $_pageSize',
name: 'MessagesNotifier',
);
final loadedMessages = await _getCachedMessages(
offset: offset,
take: _pageSize,
);
// Check if loaded messages are already in current state
final currentIds = currentMessages.map((m) => m.id).toSet();
final newMessages =
loadedMessages.where((m) => !currentIds.contains(m.id)).toList();
developer.log(
'Loaded ${loadedMessages.length} messages, ${newMessages.length} are new',
name: 'MessagesNotifier',
);
if (newMessages.isNotEmpty) {
// Merge with current messages
final allMessages = [...currentMessages, ...newMessages];
final uniqueMessages = <LocalChatMessage>[];
final seenIds = <String>{};
for (final message in allMessages) {
if (seenIds.add(message.id)) {
uniqueMessages.add(message);
}
}
_sortMessages(uniqueMessages);
state = AsyncValue.data(uniqueMessages);
developer.log(
'Updated state with ${uniqueMessages.length} total messages',
name: 'MessagesNotifier',
);
}
final finalIndex = (state.value ?? []).indexWhere(
(m) => m.id == messageId,
);
developer.log(
'Final index for message $messageId is $finalIndex',
name: 'MessagesNotifier',
);
return finalIndex;
} finally {
_isJumping = false;
}
}
bool _hasLink(LocalChatMessage message) {
final content = message.toRemoteMessage().content;
if (content == null) return false;
final urlRegex = RegExp(r'https?://[^\s/$.?#].[^\s]*');
return urlRegex.hasMatch(content);
}
}

View File

@@ -1,12 +1,12 @@
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
part of 'room.dart'; part of 'messages_notifier.dart';
// ************************************************************************** // **************************************************************************
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$messagesNotifierHash() => r'fc3b66dfb8dd3fc55d142dae5c5e7bdc67eca5d4'; String _$messagesNotifierHash() => r'37bab723531a5c5248471ef810b74cf57b3dc237';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {

View File

@@ -0,0 +1,28 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file_pool.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
final poolsProvider = FutureProvider<List<SnFilePool>>((ref) async {
final dio = ref.watch(apiClientProvider);
final response = await dio.get('/drive/pools');
final pools = SnFilePoolList.listFromResponse(response.data);
return pools.filterValid();
});
String resolveDefaultPoolId(WidgetRef ref, List<SnFilePool> pools) {
final settings = ref.watch(appSettingsNotifierProvider);
final validPools = pools.filterValid();
final configuredId = settings.defaultPoolId;
if (configuredId != null && validPools.any((p) => p.id == configuredId)) {
return configuredId;
}
if (validPools.isNotEmpty) {
return validPools.first.id;
}
// DEFAULT: Solar Network Driver
return '500e5ed8-bd44-4359-bc0a-ec85e2adf447'; }

View File

@@ -0,0 +1,34 @@
import "dart:async";
import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
final isSyncingProvider = StateProvider.autoDispose<bool>((ref) => false);
final flashingMessagesProvider = StateProvider<Set<String>>((ref) => {});
final appLifecycleStateProvider = StreamProvider<AppLifecycleState>((ref) {
final controller = StreamController<AppLifecycleState>();
final observer = _AppLifecycleObserver((state) {
if (controller.isClosed) return;
controller.add(state);
});
WidgetsBinding.instance.addObserver(observer);
ref.onDispose(() {
WidgetsBinding.instance.removeObserver(observer);
controller.close();
});
return controller.stream;
});
class _AppLifecycleObserver extends WidgetsBindingObserver {
final ValueChanged<AppLifecycleState> onChange;
_AppLifecycleObserver(this.onChange);
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
onChange(state);
}
}

View File

@@ -30,7 +30,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
final user = SnAccount.fromJson(response.data); final user = SnAccount.fromJson(response.data);
state = AsyncValue.data(user); state = AsyncValue.data(user);
if (kIsWeb || !Platform.isLinux) { if (kIsWeb || !(Platform.isLinux || Platform.isWindows)) {
FirebaseAnalytics.instance.setUserId(id: user.id); FirebaseAnalytics.instance.setUserId(id: user.id);
} }
} catch (error, stackTrace) { } catch (error, stackTrace) {
@@ -44,9 +44,12 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
: 'failedToLoadUserInfoNetwork') : 'failedToLoadUserInfoNetwork')
.tr() .tr()
.trim(), .trim(),
'${error.response!.statusCode}\n${error.response?.headers}', '',
jsonEncode(error.response?.data), '${error.response?.statusCode ?? 'Network Error'}',
].join('\n\n'), if (error.response?.headers != null) error.response?.headers,
if (error.response?.data != null)
jsonEncode(error.response?.data),
].join('\n'),
iconStyle: IconStyle.error, iconStyle: IconStyle.error,
neutralButtonTitle: 'retry'.tr(), neutralButtonTitle: 'retry'.tr(),
negativeButtonTitle: 'okay'.tr(), negativeButtonTitle: 'okay'.tr(),
@@ -87,7 +90,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
final prefs = _ref.read(sharedPreferencesProvider); final prefs = _ref.read(sharedPreferencesProvider);
await prefs.remove(kTokenPairStoreKey); await prefs.remove(kTokenPairStoreKey);
_ref.invalidate(tokenProvider); _ref.invalidate(tokenProvider);
if (kIsWeb || !Platform.isLinux) { if (kIsWeb || !(Platform.isLinux || Platform.isWindows)) {
FirebaseAnalytics.instance.setUserId(id: null); FirebaseAnalytics.instance.setUserId(id: null);
} }
} }

View File

@@ -38,7 +38,7 @@ import 'package:island/screens/chat/chat.dart';
import 'package:island/screens/chat/room.dart'; import 'package:island/screens/chat/room.dart';
import 'package:island/screens/chat/room_detail.dart'; import 'package:island/screens/chat/room_detail.dart';
import 'package:island/screens/chat/call.dart'; import 'package:island/screens/chat/call.dart';
import 'package:island/screens/chat/search_messages_screen.dart'; import 'package:island/screens/chat/search_messages.dart';
import 'package:island/screens/creators/hub.dart'; import 'package:island/screens/creators/hub.dart';
import 'package:island/screens/creators/posts/post_manage_list.dart'; import 'package:island/screens/creators/posts/post_manage_list.dart';
import 'package:island/screens/creators/stickers/stickers.dart'; import 'package:island/screens/creators/stickers/stickers.dart';
@@ -86,11 +86,7 @@ Widget _tabPagesTransitionBuilder(
} }
bool get _supportsAnalytics => bool get _supportsAnalytics =>
kIsWeb || kIsWeb || Platform.isAndroid || Platform.isIOS || Platform.isMacOS;
Platform.isAndroid ||
Platform.isIOS ||
Platform.isMacOS ||
Platform.isWindows;
// Provider for the router // Provider for the router
final routerProvider = Provider<GoRouter>((ref) { final routerProvider = Provider<GoRouter>((ref) {

View File

@@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/services/udid.native.dart'; import 'package:island/services/udid.dart' as udid;
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@@ -68,7 +68,7 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
try { try {
final deviceInfoPlugin = DeviceInfoPlugin(); final deviceInfoPlugin = DeviceInfoPlugin();
_deviceInfo = await deviceInfoPlugin.deviceInfo; _deviceInfo = await deviceInfoPlugin.deviceInfo;
_deviceUdid = await getUdid(); _deviceUdid = await udid.getUdid();
if (mounted) { if (mounted) {
setState(() {}); setState(() {});
} }
@@ -174,12 +174,20 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
context, context,
title: 'Device Information', title: 'Device Information',
children: [ children: [
_buildInfoItem( FutureBuilder<String>(
context, future: udid.getDeviceName(),
icon: Symbols.label, builder: (context, snapshot) {
label: 'aboutDeviceName'.tr(), final value =
value: snapshot.hasData
_deviceInfo?.data['name'] ?? 'unknown'.tr(), ? snapshot.data!
: 'unknown'.tr();
return _buildInfoItem(
context,
icon: Symbols.label,
label: 'aboutDeviceName'.tr(),
value: value,
);
},
), ),
_buildInfoItem( _buildInfoItem(
context, context,

View File

@@ -66,7 +66,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
final token = await getToken(ref.watch(tokenProvider)); final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null'); if (token == null) throw ArgumentError('Token is null');
final cloudFile = final cloudFile =
await putMediaToCloud( await putFileToCloud(
fileData: UniversalFile( fileData: UniversalFile(
data: result, data: result,
type: UniversalFileType.image, type: UniversalFileType.image,

View File

@@ -32,7 +32,7 @@ import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/markdown.dart'; import 'package:island/widgets/content/markdown.dart';
import 'package:island/widgets/safety/abuse_report_helper.dart'; import 'package:island/widgets/safety/abuse_report_helper.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:palette_generator/palette_generator.dart'; import 'package:island/services/color_extraction.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@@ -581,14 +581,14 @@ Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async {
try { try {
final account = await ref.watch(accountProvider(uname).future); final account = await ref.watch(accountProvider(uname).future);
if (account.profile.background == null) return null; if (account.profile.background == null) return null;
final palette = await PaletteGenerator.fromImageProvider( final colors = await ColorExtractionService.getColorsFromImage(
CloudImageWidget.provider( CloudImageWidget.provider(
fileId: account.profile.background!.id, fileId: account.profile.background!.id,
serverUrl: ref.watch(serverUrlProvider), serverUrl: ref.watch(serverUrlProvider),
), ),
); );
final dominantColor = palette.dominantColor?.color; if (colors.isEmpty) return null;
if (dominantColor == null) return null; final dominantColor = colors.first;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
} catch (_) { } catch (_) {
return null; return null;

View File

@@ -268,7 +268,7 @@ class _AccountBadgesProviderElement
} }
String _$accountAppbarForcegroundColorHash() => String _$accountAppbarForcegroundColorHash() =>
r'8ee0cae10817b77fb09548a482f5247662b4374c'; r'127fcc7fd6ec6a41ac4a6975276b5271aa4fa7d0';
/// See also [accountAppbarForcegroundColor]. /// See also [accountAppbarForcegroundColor].
@ProviderFor(accountAppbarForcegroundColor) @ProviderFor(accountAppbarForcegroundColor)

View File

@@ -1,17 +1,10 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/network.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'captcha.config.g.dart'; part 'captcha.config.g.dart';
@riverpod @riverpod
Future<String> captchaUrl(Ref ref) async { Future<String> captchaUrl(Ref ref) async {
final apiClient = ref.watch(apiClientProvider); const baseUrl = "https://solian.app";
final resp = await apiClient.get('/.well-known/services'); return '$baseUrl/auth/captcha';
final serviceMapping = await resp.data;
var baseUrl = serviceMapping['DysonNetwork.Pass'] as String;
// The backend using self-signed certicates on development
// Which mobile simulator might not accept, use this to avoid errors
if (baseUrl.contains('https://localhost')) baseUrl = 'http://localhost:5216';
return '$baseUrl/captcha';
} }

View File

@@ -6,7 +6,7 @@ part of 'captcha.config.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$captchaUrlHash() => r'bbed0d18272dd205069642b3c6583ea2eef735d1'; String _$captchaUrlHash() => r'd46bc43032cef504547cd528a40c23cf76f27cc8';
/// See also [captchaUrl]. /// See also [captchaUrl].
@ProviderFor(captchaUrl) @ProviderFor(captchaUrl)

View File

@@ -1,9 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@@ -42,22 +39,6 @@ final Map<int, (String, String, IconData)> kFactorTypes = {
4: ('authFactorPin', 'authFactorPinDescription', Symbols.nest_secure_alarm), 4: ('authFactorPin', 'authFactorPinDescription', Symbols.nest_secure_alarm),
}; };
Future<String?> getDeviceName() async {
if (kIsWeb) return null;
String? name;
if (Platform.isIOS) {
final deviceInfo = await DeviceInfoPlugin().iosInfo;
name = deviceInfo.name;
} else if (Platform.isAndroid) {
final deviceInfo = await DeviceInfoPlugin().androidInfo;
name = deviceInfo.name;
} else if (Platform.isWindows) {
final deviceInfo = await DeviceInfoPlugin().windowsInfo;
name = deviceInfo.computerName;
}
return name;
}
class LoginScreen extends HookConsumerWidget { class LoginScreen extends HookConsumerWidget {
const LoginScreen({super.key}); const LoginScreen({super.key});

View File

@@ -543,7 +543,7 @@ class EditChatScreen extends HookConsumerWidget {
final token = await getToken(ref.watch(tokenProvider)); final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null'); if (token == null) throw ArgumentError('Token is null');
final cloudFile = final cloudFile =
await putMediaToCloud( await putFileToCloud(
fileData: UniversalFile( fileData: UniversalFile(
data: result, data: result,
type: UniversalFileType.image, type: UniversalFileType.image,

View File

@@ -0,0 +1,221 @@
import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:gap/gap.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:island/database/message.dart";
import "package:island/screens/chat/chat.dart";
import "package:island/widgets/content/cloud_files.dart";
import "package:super_sliver_list/super_sliver_list.dart";
import "package:easy_localization/easy_localization.dart";
import "package:go_router/go_router.dart";
import "package:material_symbols_icons/symbols.dart";
import "package:styled_widget/styled_widget.dart";
import "package:island/models/chat.dart";
import "package:island/widgets/alert.dart";
import "package:island/widgets/app_scaffold.dart";
import "package:island/widgets/chat/message_item.dart";
import "package:island/widgets/response.dart";
import "package:island/pods/network.dart";
import "package:island/services/responsive.dart";
import "package:island/pods/messages_notifier.dart";
class PublicRoomPreview extends HookConsumerWidget {
final String id;
final SnChatRoom room;
const PublicRoomPreview({super.key, required this.id, required this.room});
@override
Widget build(BuildContext context, WidgetRef ref) {
final messages = ref.watch(messagesNotifierProvider(id));
final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier);
final scrollController = useScrollController();
final listController = useMemoized(() => ListController(), []);
var isLoading = false;
// Add scroll listener for pagination
useEffect(() {
void onScroll() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (isLoading) return;
isLoading = true;
messagesNotifier.loadMore().then((_) => isLoading = false);
}
}
scrollController.addListener(onScroll);
return () => scrollController.removeListener(onScroll);
}, [scrollController]);
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
SuperListView.builder(
listController: listController,
padding: EdgeInsets.symmetric(vertical: 16),
controller: scrollController,
reverse: true, // Show newest messages at the bottom
itemCount: messageList.length,
findChildIndexCallback: (key) {
final valueKey = key as ValueKey;
final messageId = valueKey.value as String;
return messageList.indexWhere((m) => m.id == messageId);
},
extentEstimation: (_, _) => 40,
itemBuilder: (context, index) {
final message = messageList[index];
final nextMessage =
index < messageList.length - 1 ? messageList[index + 1] : null;
final isLastInGroup =
nextMessage == null ||
nextMessage.senderId != message.senderId ||
nextMessage.createdAt
.difference(message.createdAt)
.inMinutes
.abs() >
3;
return MessageItem(
message: message,
isCurrentUser: false, // User is not a member, so not current user
onAction: null, // No actions allowed in preview mode
onJump: (_) {}, // No jump functionality in preview
progress: null,
showAvatar: isLastInGroup,
);
},
);
final compactHeader = isWideScreen(context);
Widget comfortHeaderWidget() => Column(
spacing: 4,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 26,
width: 26,
child:
(room.type == 1 && room.picture?.id == null)
? SplitAvatarWidget(
filesId:
room.members!
.map((e) => e.account.profile.picture?.id)
.toList(),
)
: room.picture?.id != null
? ProfilePictureWidget(
fileId: room.picture?.id,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(
child: Text(
room.name![0].toUpperCase(),
style: const TextStyle(fontSize: 12),
),
),
),
Text(
(room.type == 1 && room.name == null)
? room.members!.map((e) => e.account.nick).join(', ')
: room.name!,
).fontSize(15),
],
);
Widget compactHeaderWidget() => Row(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 26,
width: 26,
child:
(room.type == 1 && room.picture?.id == null)
? SplitAvatarWidget(
filesId:
room.members!
.map((e) => e.account.profile.picture?.id)
.toList(),
)
: room.picture?.id != null
? ProfilePictureWidget(
fileId: room.picture?.id,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(
child: Text(
room.name![0].toUpperCase(),
style: const TextStyle(fontSize: 12),
),
),
),
Text(
(room.type == 1 && room.name == null)
? room.members!.map((e) => e.account.nick).join(', ')
: room.name!,
).fontSize(19),
],
);
return AppScaffold(
appBar: AppBar(
leading: !compactHeader ? const Center(child: PageBackButton()) : null,
automaticallyImplyLeading: false,
toolbarHeight: compactHeader ? null : 64,
title: compactHeader ? compactHeaderWidget() : comfortHeaderWidget(),
actions: [
IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {
context.pushNamed('chatDetail', pathParameters: {'id': id});
},
),
const Gap(8),
],
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: messages.when(
data:
(messageList) =>
messageList.isEmpty
? Center(child: Text('No messages yet'.tr()))
: chatMessageListWidget(messageList),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => messagesNotifier.loadInitial(),
),
),
),
// Join button at the bottom for public rooms
Container(
padding: const EdgeInsets.all(16),
child: FilledButton.tonalIcon(
onPressed: () async {
try {
showLoadingModal(context);
final apiClient = ref.read(apiClientProvider);
await apiClient.post('/sphere/chat/${room.id}/members/me');
ref.invalidate(chatroomIdentityProvider(id));
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
},
label: Text('chatJoin').tr(),
icon: const Icon(Icons.add),
),
),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:island/pods/database.dart'; import 'package:island/pods/database.dart';
import 'package:island/screens/chat/search_messages.dart';
part 'room_detail.freezed.dart'; part 'room_detail.freezed.dart';
part 'room_detail.g.dart'; part 'room_detail.g.dart';
@@ -401,11 +402,17 @@ class ChatDetailScreen extends HookConsumerWidget {
), ),
), ),
), ),
onTap: () { onTap: () async {
context.pushNamed( final result = await context.pushNamed(
'searchMessages', 'searchMessages',
pathParameters: {'id': id}, pathParameters: {'id': id},
); );
if (result is SearchMessagesResult) {
// Navigate back to room screen with message to jump to
if (context.mounted) {
context.pop(result.messageId);
}
}
}, },
), ),
], ],

View File

@@ -1,13 +1,20 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/screens/chat/room.dart'; import 'package:island/pods/messages_notifier.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/chat/message_item.dart'; import 'package:island/widgets/chat/message_list_tile.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:super_sliver_list/super_sliver_list.dart'; import 'package:super_sliver_list/super_sliver_list.dart';
// Class to represent the result when popping from search messages
class SearchMessagesResult {
final String messageId;
const SearchMessagesResult(this.messageId);
}
class SearchMessagesScreen extends HookConsumerWidget { class SearchMessagesScreen extends HookConsumerWidget {
final String roomId; final String roomId;
@@ -112,24 +119,24 @@ class SearchMessagesScreen extends HookConsumerWidget {
? Center(child: Text('noMessagesFound'.tr())) ? Center(child: Text('noMessagesFound'.tr()))
: SuperListView.builder( : SuperListView.builder(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
reverse: true, // Show newest messages at the bottom reverse: false, // Show newest messages at the top
itemCount: messageList.length, itemCount: messageList.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final message = messageList[index]; final message = messageList[index];
// Simplified MessageItem for search results, no grouping logic return MessageListTile(
return MessageItem(
message: message, message: message,
isCurrentUser: onJump: (messageId) {
false, // Or determine based on actual user // Return the search result and pop back to room detail
onAction: null, context.pop(SearchMessagesResult(messageId));
onJump: (_) {}, },
progress: null,
showAvatar: true,
); );
}, },
), ),
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('errorGeneric'.tr(args: [error.toString()]))), error:
(error, _) => Center(
child: Text('errorGeneric'.tr(args: [error.toString()])),
),
), ),
), ),
], ],

View File

@@ -78,6 +78,7 @@ class EditPublisherScreen extends HookConsumerWidget {
result = await cropImage( result = await cropImage(
context, context,
image: result, image: result,
replacePath: true,
allowedAspectRatios: [ allowedAspectRatios: [
if (position == 'background') if (position == 'background')
CropAspectRatio(height: 7, width: 16) CropAspectRatio(height: 7, width: 16)
@@ -98,7 +99,7 @@ class EditPublisherScreen extends HookConsumerWidget {
final token = await getToken(ref.watch(tokenProvider)); final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null'); if (token == null) throw ArgumentError('Token is null');
final cloudFile = final cloudFile =
await putMediaToCloud( await putFileToCloud(
fileData: UniversalFile( fileData: UniversalFile(
data: result, data: result,
type: UniversalFileType.image, type: UniversalFileType.image,

View File

@@ -141,7 +141,7 @@ class EditAppScreen extends HookConsumerWidget {
final token = await getToken(ref.watch(tokenProvider)); final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null'); if (token == null) throw ArgumentError('Token is null');
final cloudFile = final cloudFile =
await putMediaToCloud( await putFileToCloud(
fileData: UniversalFile( fileData: UniversalFile(
data: result, data: result,
type: UniversalFileType.image, type: UniversalFileType.image,

View File

@@ -127,7 +127,7 @@ class EditBotScreen extends HookConsumerWidget {
final token = await getToken(ref.watch(tokenProvider)); final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null'); if (token == null) throw ArgumentError('Token is null');
final cloudFile = final cloudFile =
await putMediaToCloud( await putFileToCloud(
fileData: UniversalFile( fileData: UniversalFile(
data: result, data: result,
type: UniversalFileType.image, type: UniversalFileType.image,

View File

@@ -39,7 +39,7 @@ class NotificationUnreadCountNotifier
try { try {
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
final response = await client.get('/pusher/notifications/count'); final response = await client.get('/ring/notifications/count');
return (response.data as num).toInt(); return (response.data as num).toInt();
} catch (_) { } catch (_) {
return 0; return 0;
@@ -89,7 +89,7 @@ class NotificationListNotifier extends _$NotificationListNotifier
final queryParams = {'offset': offset, 'take': _pageSize}; final queryParams = {'offset': offset, 'take': _pageSize};
final response = await client.get( final response = await client.get(
'/pusher/notifications', '/ring/notifications',
queryParameters: queryParams, queryParameters: queryParams,
); );
final total = int.parse(response.headers.value('X-Total') ?? '0'); final total = int.parse(response.headers.value('X-Total') ?? '0');
@@ -121,7 +121,7 @@ class NotificationScreen extends HookConsumerWidget {
Future<void> markAllRead() async { Future<void> markAllRead() async {
showLoadingModal(context); showLoadingModal(context);
final apiClient = ref.watch(apiClientProvider); final apiClient = ref.watch(apiClientProvider);
await apiClient.post('/pusher/notifications/all/read'); await apiClient.post('/ring/notifications/all/read');
if (!context.mounted) return; if (!context.mounted) return;
hideLoadingModal(context); hideLoadingModal(context);
ref.invalidate(notificationListNotifierProvider); ref.invalidate(notificationListNotifierProvider);

View File

@@ -7,7 +7,7 @@ part of 'notification.dart';
// ************************************************************************** // **************************************************************************
String _$notificationUnreadCountNotifierHash() => String _$notificationUnreadCountNotifierHash() =>
r'0763b66bd64e5a9b7c317887e109ab367515dfa4'; r'08c773809958d96a7ce82acf04af1f9e0b23e119';
/// See also [NotificationUnreadCountNotifier]. /// See also [NotificationUnreadCountNotifier].
@ProviderFor(NotificationUnreadCountNotifier) @ProviderFor(NotificationUnreadCountNotifier)
@@ -28,7 +28,7 @@ final notificationUnreadCountNotifierProvider =
typedef _$NotificationUnreadCountNotifier = AutoDisposeAsyncNotifier<int>; typedef _$NotificationUnreadCountNotifier = AutoDisposeAsyncNotifier<int>;
String _$notificationListNotifierHash() => String _$notificationListNotifierHash() =>
r'5099466db475bbcf1ab6b514eb072f1dc4c6f930'; r'260046e11f45b0d67ab25bcbdc8604890d71ccc7';
/// See also [NotificationListNotifier]. /// See also [NotificationListNotifier].
@ProviderFor(NotificationListNotifier) @ProviderFor(NotificationListNotifier)

View File

@@ -2,7 +2,6 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';

View File

@@ -21,7 +21,7 @@ import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/markdown.dart'; import 'package:island/widgets/content/markdown.dart';
import 'package:island/widgets/post/post_list.dart'; import 'package:island/widgets/post/post_list.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:palette_generator/palette_generator.dart'; import 'package:island/services/color_extraction.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@@ -278,14 +278,14 @@ Future<Color?> publisherAppbarForcegroundColor(Ref ref, String pubName) async {
try { try {
final publisher = await ref.watch(publisherProvider(pubName).future); final publisher = await ref.watch(publisherProvider(pubName).future);
if (publisher.background == null) return null; if (publisher.background == null) return null;
final palette = await PaletteGenerator.fromImageProvider( final colors = await ColorExtractionService.getColorsFromImage(
CloudImageWidget.provider( CloudImageWidget.provider(
fileId: publisher.background!.id, fileId: publisher.background!.id,
serverUrl: ref.watch(serverUrlProvider), serverUrl: ref.watch(serverUrlProvider),
), ),
); );
final dominantColor = palette.dominantColor?.color; if (colors.isEmpty) return null;
if (dominantColor == null) return null; final dominantColor = colors.first;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
} catch (_) { } catch (_) {
return null; return null;

View File

@@ -400,7 +400,7 @@ class _PublisherSubscriptionStatusProviderElement
} }
String _$publisherAppbarForcegroundColorHash() => String _$publisherAppbarForcegroundColorHash() =>
r'd781a806a242aea5c1609ec98c97c52fdd9f7db1'; r'cd9a9816177a6eecc2bc354acebbbd48892ffdd7';
/// See also [publisherAppbarForcegroundColor]. /// See also [publisherAppbarForcegroundColor].
@ProviderFor(publisherAppbarForcegroundColor) @ProviderFor(publisherAppbarForcegroundColor)

View File

@@ -8,7 +8,7 @@ import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_pfc.dart'; import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/account/status.dart'; import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/post/post_list.dart'; import 'package:island/widgets/post/post_list.dart';
import 'package:palette_generator/palette_generator.dart'; import 'package:island/services/color_extraction.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -32,14 +32,14 @@ part 'realm_detail.g.dart';
Future<Color?> realmAppbarForegroundColor(Ref ref, String realmSlug) async { Future<Color?> realmAppbarForegroundColor(Ref ref, String realmSlug) async {
final realm = await ref.watch(realmProvider(realmSlug).future); final realm = await ref.watch(realmProvider(realmSlug).future);
if (realm?.background == null) return null; if (realm?.background == null) return null;
final palette = await PaletteGenerator.fromImageProvider( final colors = await ColorExtractionService.getColorsFromImage(
CloudImageWidget.provider( CloudImageWidget.provider(
fileId: realm!.background!.id, fileId: realm!.background!.id,
serverUrl: ref.watch(serverUrlProvider), serverUrl: ref.watch(serverUrlProvider),
), ),
); );
final dominantColor = palette.dominantColor?.color; if (colors.isEmpty) return null;
if (dominantColor == null) return null; final dominantColor = colors.first;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
} }

View File

@@ -7,7 +7,7 @@ part of 'realm_detail.dart';
// ************************************************************************** // **************************************************************************
String _$realmAppbarForegroundColorHash() => String _$realmAppbarForegroundColorHash() =>
r'14b5563d861996ea182d0d2db7aa5c2bb3bbaf48'; r'8131c047a984318a4cc3fbb5daa5ef0ce44dfae5';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {

View File

@@ -211,7 +211,7 @@ class EditRealmScreen extends HookConsumerWidget {
final token = await getToken(ref.watch(tokenProvider)); final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Access token is null'); if (token == null) throw ArgumentError('Access token is null');
final cloudFile = final cloudFile =
await putMediaToCloud( await putFileToCloud(
fileData: UniversalFile( fileData: UniversalFile(
data: result, data: result,
type: UniversalFileType.image, type: UniversalFileType.image,

View File

@@ -12,14 +12,17 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/color_extraction.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/pool_provider.dart';
import 'package:island/models/file_pool.dart';
class SettingsScreen extends HookConsumerWidget { class SettingsScreen extends HookConsumerWidget {
const SettingsScreen({super.key}); const SettingsScreen({super.key});
@@ -33,7 +36,8 @@ class SettingsScreen extends HookConsumerWidget {
final isDesktop = final isDesktop =
!kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux); !kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux);
final isWide = isWideScreen(context); final isWide = isWideScreen(context);
final pools = ref.watch(poolsProvider);
final user = ref.watch(userInfoProvider);
final docBasepath = useState<String?>(null); final docBasepath = useState<String?>(null);
useEffect(() { useEffect(() {
@@ -127,6 +131,48 @@ class SettingsScreen extends HookConsumerWidget {
), ),
), ),
// Message display style settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsMessageDisplayStyle').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.chat),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
isExpanded: true,
items: [
DropdownMenuItem<String>(
value: 'bubble',
child: Text('Bubble').fontSize(14),
),
DropdownMenuItem<String>(
value: 'discord',
child: Text('Discord').fontSize(14),
),
DropdownMenuItem<String>(
value: 'irc',
child: Text('IRC').fontSize(14),
),
],
value: settings.messageDisplayStyle,
onChanged: (String? value) {
if (value != null) {
ref
.read(appSettingsNotifierProvider.notifier)
.setMessageDisplayStyle(value);
showSnackBar('settingsApplied'.tr());
}
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5),
height: 40,
width: 140,
),
menuItemStyleData: const MenuItemStyleData(height: 40),
),
),
),
// Color scheme settings // Color scheme settings
ListTile( ListTile(
minLeadingWidth: 48, minLeadingWidth: 48,
@@ -293,24 +339,26 @@ class SettingsScreen extends HookConsumerWidget {
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () async { onTap: () async {
showLoadingModal(context); showLoadingModal(context);
final palette = await PaletteGenerator.fromImageProvider( final colors = await ColorExtractionService.getColorsFromImage(
FileImage( FileImage(
File('${docBasepath.value}/$kAppBackgroundImagePath'), File('${docBasepath.value}/$kAppBackgroundImagePath'),
), ),
); );
if (palette.darkVibrantColor == null || if (colors.isEmpty) {
palette.lightVibrantColor == null) {
if (context.mounted) hideLoadingModal(context); if (context.mounted) hideLoadingModal(context);
showErrorAlert( showErrorAlert(
'Unable to calculate the domiant color of the background image.', 'Unable to calculate the dominant color of the background image.',
); );
return; return;
} }
if (!context.mounted) return; if (!context.mounted) return;
final colorScheme = ColorScheme.fromSeed(
seedColor: colors.first,
);
final color = final color =
MediaQuery.of(context).platformBrightness == Brightness.dark MediaQuery.of(context).platformBrightness == Brightness.dark
? palette.darkVibrantColor!.color ? colorScheme.primary
: palette.lightVibrantColor!.color; : colorScheme.primary;
ref ref
.read(appSettingsNotifierProvider.notifier) .read(appSettingsNotifierProvider.notifier)
.setAppColorScheme(color.value); .setAppColorScheme(color.value);
@@ -365,6 +413,68 @@ class SettingsScreen extends HookConsumerWidget {
), ),
), ),
), ),
if (user.value != null)
pools.when(
data: (data) {
final validPools = data.filterValid();
final currentPoolId = resolveDefaultPoolId(ref, data);
return ListTile(
isThreeLine: true,
minLeadingWidth: 48,
title: Text('settingsDefaultPool').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.cloud),
subtitle: Text(
'settingsDefaultPoolHelper'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
isExpanded: true,
items:
validPools.map((p) {
return DropdownMenuItem<String>(
value: p.id,
child: Text(
p.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
).fontSize(14),
);
}).toList(),
value: currentPoolId,
onChanged: (value) {
ref
.read(appSettingsNotifierProvider.notifier)
.setDefaultPoolId(value);
showSnackBar('settingsApplied'.tr());
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5),
height: 40,
width: 120,
),
menuItemStyleData: const MenuItemStyleData(height: 40),
),
),
);
},
loading:
() => const ListTile(
minLeadingWidth: 48,
title: Text('Loading pools...'),
leading: CircularProgressIndicator(),
),
error:
(err, st) => ListTile(
minLeadingWidth: 48,
title: Text('settingsDefaultPool').tr(),
subtitle: Text('Error: $err'),
leading: const Icon(Icons.error, color: Colors.red),
),
),
]; ];
final behaviorSettings = [ final behaviorSettings = [

View File

@@ -48,11 +48,12 @@ class TrayService {
void handleAction(MenuItem item) { void handleAction(MenuItem item) {
switch (item.key) { switch (item.key) {
case 'show_window': case 'show_window':
if (appWindow.isVisible) { () async {
appWindow.restore(); appWindow.show();
} else { appWindow.restore();
appWindow.show(); await Future.delayed(const Duration(milliseconds: 32));
} appWindow.show();
}();
break; break;
case 'exit_app': case 'exit_app':
appWindow.close(); appWindow.close();

View File

@@ -0,0 +1,49 @@
import 'package:flutter/widgets.dart';
import 'package:image/image.dart' as img;
import 'package:material_color_utilities/material_color_utilities.dart' as mcu;
class ColorExtractionService {
/// Extracts dominant colors from an image provider.
/// Returns a list of colors suitable for UI theming.
static Future<List<Color>> getColorsFromImage(ImageProvider provider) async {
try {
if (provider is FileImage) {
final bytes = await provider.file.readAsBytes();
final image = img.decodeImage(bytes);
if (image == null) return [];
final Map<int, int> colorToCount = {};
for (int y = 0; y < image.height; y++) {
for (int x = 0; x < image.width; x++) {
final pixel = image.getPixel(x, y) as int;
final r = (pixel >> 24) & 0xff;
final g = (pixel >> 16) & 0xff;
final b = (pixel >> 8) & 0xff;
final a = pixel & 0xff;
if (a == 0) continue;
final argb = (a << 24) | (r << 16) | (g << 8) | b;
colorToCount[argb] = (colorToCount[argb] ?? 0) + 1;
}
}
final List<int> filteredResults = mcu.Score.score(
colorToCount,
desired: 1,
filter: true,
);
final List<int> scoredResults = mcu.Score.score(
colorToCount,
desired: 4,
filter: false,
);
return <dynamic>{
...filteredResults,
...scoredResults,
}.toList().map((argb) => Color(argb)).toList();
} else {
return [];
}
} catch (e) {
debugPrint('Error getting colors from image: $e');
return [];
}
}
}

View File

@@ -1,21 +1,23 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:croppy/croppy.dart'; import 'package:croppy/croppy.dart';
import 'package:cross_file/cross_file.dart'; import 'package:cross_file/cross_file.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/services/file_uploader.dart';
import 'package:native_exif/native_exif.dart'; import 'package:native_exif/native_exif.dart';
import 'package:tus_client_dart/tus_client_dart.dart'; import 'package:path_provider/path_provider.dart';
enum FileUploadMode { generic, mediaSafe }
Future<XFile?> cropImage( Future<XFile?> cropImage(
BuildContext context, { BuildContext context, {
required XFile image, required XFile image,
List<CropAspectRatio?>? allowedAspectRatios, List<CropAspectRatio?>? allowedAspectRatios,
bool replacePath = false, bool replacePath = true,
}) async { }) async {
final result = await showMaterialImageCropper( final result = await showMaterialImageCropper(
context, context,
@@ -40,64 +42,63 @@ Future<XFile?> cropImage(
); );
} }
Completer<SnCloudFile?> putMediaToCloud({ Completer<SnCloudFile?> putFileToCloud({
required UniversalFile fileData, required UniversalFile fileData,
required String atk, required String atk,
required String baseUrl, required String baseUrl,
String? poolId,
String? filename, String? filename,
String? mimetype, String? mimetype,
FileUploadMode? mode,
Function(double progress, Duration estimate)? onProgress, Function(double progress, Duration estimate)? onProgress,
}) { }) {
final completer = Completer<SnCloudFile?>(); final completer = Completer<SnCloudFile?>();
// Process the image to remove GPS EXIF data if needed final effectiveMode =
if (fileData.isOnDevice && fileData.type == UniversalFileType.image) { mode ??
(fileData.type == UniversalFileType.file
? FileUploadMode.generic
: FileUploadMode.mediaSafe);
if (effectiveMode == FileUploadMode.mediaSafe &&
fileData.isOnDevice &&
fileData.type == UniversalFileType.image) {
final data = fileData.data; final data = fileData.data;
if (data is XFile && !kIsWeb && (Platform.isIOS || Platform.isAndroid)) { if (data is XFile && !kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
// Use native_exif to selectively remove GPS data
Exif.fromPath(data.path) Exif.fromPath(data.path)
.then((exif) { .then((exif) async {
// Remove GPS-related attributes final gpsAttributes = {
final gpsAttributes = [ 'GPSLatitude': '',
'GPSLatitude', 'GPSLatitudeRef': '',
'GPSLatitudeRef', 'GPSLongitude': '',
'GPSLongitude', 'GPSLongitudeRef': '',
'GPSLongitudeRef', 'GPSAltitude': '',
'GPSAltitude', 'GPSAltitudeRef': '',
'GPSAltitudeRef', 'GPSTimeStamp': '',
'GPSTimeStamp', 'GPSProcessingMethod': '',
'GPSProcessingMethod', 'GPSDateStamp': '',
'GPSDateStamp', };
]; await exif.writeAttributes(gpsAttributes);
// Create a map of attributes to clear
final clearAttributes = <String, String>{};
for (final attr in gpsAttributes) {
clearAttributes[attr] = '';
}
// Write empty values to remove GPS data
return exif.writeAttributes(clearAttributes);
}) })
.then((_) { .then(
// Continue with upload after GPS data is removed (_) => _processUpload(
_processUpload(
fileData, fileData,
atk, atk,
baseUrl, baseUrl,
poolId,
filename, filename,
mimetype, mimetype,
onProgress, onProgress,
completer, completer,
); ),
}) )
.catchError((e) { .catchError((e) {
// If there's an error, continue with the original file
debugPrint('Error removing GPS EXIF data: $e'); debugPrint('Error removing GPS EXIF data: $e');
_processUpload( return _processUpload(
fileData, fileData,
atk, atk,
baseUrl, baseUrl,
poolId,
filename, filename,
mimetype, mimetype,
onProgress, onProgress,
@@ -109,11 +110,11 @@ Completer<SnCloudFile?> putMediaToCloud({
} }
} }
// If not an image or on web, continue with normal upload
_processUpload( _processUpload(
fileData, fileData,
atk, atk,
baseUrl, baseUrl,
poolId,
filename, filename,
mimetype, mimetype,
onProgress, onProgress,
@@ -127,6 +128,7 @@ Completer<SnCloudFile?> _processUpload(
UniversalFile fileData, UniversalFile fileData,
String atk, String atk,
String baseUrl, String baseUrl,
String? poolId,
String? filename, String? filename,
String? mimetype, String? mimetype,
Function(double progress, Duration estimate)? onProgress, Function(double progress, Duration estimate)? onProgress,
@@ -168,26 +170,81 @@ Completer<SnCloudFile?> _processUpload(
return completer; return completer;
} }
final Map<String, String> metadata = { // Create Dio instance
'filename': actualFilename, final dio = Dio(
'content-type': actualMimetype, BaseOptions(
}; baseUrl: baseUrl,
headers: {
'Authorization': 'AtField $atk',
'Accept': 'application/json',
'Content-Type': 'application/json',
},
),
);
final client = TusClient(file); final uploader = FileUploader(dio);
client
.upload( // Get File object
uri: Uri.parse('$baseUrl/drive/tus'), File fileObj;
headers: {'Authorization': 'AtField $atk'}, if (file.path.isNotEmpty) {
metadata: metadata, fileObj = File(file.path);
onComplete: (lastResponse) { // Call progress start
final resp = jsonDecode(lastResponse!.headers['x-fileinfo']!); onProgress?.call(0.0, Duration.zero);
completer.complete(SnCloudFile.fromJson(resp)); uploader
}, .uploadFile(
onProgress: (double progress, Duration estimate) { file: fileObj,
onProgress?.call(progress, estimate); fileName: actualFilename,
}, contentType: actualMimetype,
) poolId: poolId,
.catchError(completer.completeError); )
.then((result) {
// Call progress end
onProgress?.call(1.0, Duration.zero);
completer.complete(result);
})
.catchError((e) {
completer.completeError(e);
throw e;
});
} else {
// Write to temp file
getTemporaryDirectory()
.then((tempDir) {
final tempFile = File('${tempDir.path}/temp_upload_$actualFilename');
file
.readAsBytes()
.then((bytes) => tempFile.writeAsBytes(bytes))
.then((_) {
fileObj = tempFile;
// Call progress start
onProgress?.call(0.0, Duration.zero);
uploader
.uploadFile(
file: fileObj,
fileName: actualFilename,
contentType: actualMimetype,
poolId: poolId,
)
.then((result) {
// Call progress end
onProgress?.call(1.0, Duration.zero);
completer.complete(result);
})
.catchError((e) {
completer.completeError(e);
throw e;
});
})
.catchError((e) {
completer.completeError(e);
throw e;
});
})
.catchError((e) {
completer.completeError(e);
throw e;
});
}
return completer; return completer;
} }

View File

@@ -0,0 +1,155 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/network.dart';
class FileUploader {
final Dio _dio;
FileUploader(this._dio);
/// Calculates the MD5 hash of a file.
Future<String> _calculateFileHash(File file) async {
final bytes = await file.readAsBytes();
final digest = md5.convert(bytes);
return digest.toString();
}
/// Creates an upload task for the given file.
Future<Map<String, dynamic>> createUploadTask({
required File file,
required String fileName,
required String contentType,
String? poolId,
String? bundleId,
String? encryptPassword,
String? expiredAt,
int? chunkSize,
}) async {
final hash = await _calculateFileHash(file);
final fileSize = await file.length();
final response = await _dio.post(
'/drive/files/upload/create',
data: {
'hash': hash,
'file_name': fileName,
'file_size': fileSize,
'content_type': contentType,
'pool_id': poolId,
'bundle_id': bundleId,
'encrypt_password': encryptPassword,
'expired_at': expiredAt,
'chunk_size': chunkSize,
},
);
return response.data;
}
/// Uploads a single chunk of the file.
Future<void> uploadChunk({
required String taskId,
required int chunkIndex,
required Uint8List chunkData,
}) async {
final formData = FormData.fromMap({
'chunk': MultipartFile.fromBytes(
chunkData,
filename: 'chunk_$chunkIndex',
),
});
await _dio.post(
'/drive/files/upload/chunk/$taskId/$chunkIndex',
data: formData,
);
}
/// Completes the upload and returns the CloudFile object.
Future<SnCloudFile> completeUpload(String taskId) async {
final response = await _dio.post('/drive/files/upload/complete/$taskId');
return SnCloudFile.fromJson(response.data);
}
/// Uploads a file in chunks using the multi-part API.
Future<SnCloudFile> uploadFile({
required File file,
required String fileName,
required String contentType,
String? poolId,
String? bundleId,
String? encryptPassword,
String? expiredAt,
int? customChunkSize,
}) async {
// Step 1: Create upload task
final createResponse = await createUploadTask(
file: file,
fileName: fileName,
contentType: contentType,
poolId: poolId,
bundleId: bundleId,
encryptPassword: encryptPassword,
expiredAt: expiredAt,
chunkSize: customChunkSize,
);
if (createResponse['file_exists'] == true) {
// File already exists, return the existing file
return SnCloudFile.fromJson(createResponse['file']);
}
final taskId = createResponse['task_id'] as String;
final chunkSize = createResponse['chunk_size'] as int;
final chunksCount = createResponse['chunks_count'] as int;
// Step 2: Upload chunks
final stream = file.openRead();
final chunks = <Uint8List>[];
int bytesRead = 0;
final buffer = BytesBuilder();
await for (final chunk in stream) {
buffer.add(chunk);
bytesRead += chunk.length;
if (bytesRead >= chunkSize) {
chunks.add(buffer.takeBytes());
bytesRead = 0;
}
}
// Add remaining bytes as last chunk
if (buffer.length > 0) {
chunks.add(buffer.takeBytes());
}
// Ensure we have the correct number of chunks
if (chunks.length != chunksCount) {
throw Exception(
'Chunk count mismatch: expected $chunksCount, got ${chunks.length}',
);
}
// Upload each chunk
for (int i = 0; i < chunks.length; i++) {
await uploadChunk(taskId: taskId, chunkIndex: i, chunkData: chunks[i]);
}
// Step 3: Complete upload
return await completeUpload(taskId);
}
}
// Riverpod provider for the FileUploader service
final fileUploaderProvider = Provider<FileUploader>((ref) {
final dio = ref.watch(apiClientProvider);
return FileUploader(dio);
});

View File

@@ -1,230 +1,47 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:island/main.dart';
import 'package:island/route.dart';
import 'package:island/models/account.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/widgets/app_notification.dart';
import 'package:top_snackbar_flutter/top_snack_bar.dart';
import 'package:url_launcher/url_launcher_string.dart';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = // Conditional imports based on platform
FlutterLocalNotificationsPlugin(); import 'notify.windows.dart' as windows_notify;
import 'notify.universal.dart' as universal_notify;
AppLifecycleState _appLifecycleState = AppLifecycleState.resumed;
void _onAppLifecycleChanged(AppLifecycleState state) {
_appLifecycleState = state;
}
// Platform-specific delegation
Future<void> initializeLocalNotifications() async { Future<void> initializeLocalNotifications() async {
const AndroidInitializationSettings initializationSettingsAndroid = if (Platform.isWindows) {
AndroidInitializationSettings('@mipmap/ic_launcher'); return windows_notify.initializeLocalNotifications();
} else {
const DarwinInitializationSettings initializationSettingsIOS = return universal_notify.initializeLocalNotifications();
DarwinInitializationSettings();
const DarwinInitializationSettings initializationSettingsMacOS =
DarwinInitializationSettings();
const LinuxInitializationSettings initializationSettingsLinux =
LinuxInitializationSettings(defaultActionName: 'Open notification');
const WindowsInitializationSettings initializationSettingsWindows =
WindowsInitializationSettings(
appName: 'Island',
appUserModelId: 'dev.solsynth.solian',
guid: 'dev.solsynth.solian',
);
const InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
macOS: initializationSettingsMacOS,
linux: initializationSettingsLinux,
windows: initializationSettingsWindows,
);
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (NotificationResponse response) async {
final payload = response.payload;
if (payload != null) {
if (payload.startsWith('/')) {
// In-app routes
rootNavigatorKey.currentContext?.push(payload);
} else {
// External URLs
launchUrlString(payload);
}
}
},
);
WidgetsBinding.instance.addObserver(
LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged),
);
}
class LifecycleEventHandler extends WidgetsBindingObserver {
final void Function(AppLifecycleState) onAppLifecycleChanged;
LifecycleEventHandler({required this.onAppLifecycleChanged});
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
onAppLifecycleChanged(state);
} }
} }
StreamSubscription<WebSocketPacket> setupNotificationListener( StreamSubscription setupNotificationListener(
BuildContext context, BuildContext context,
WidgetRef ref, WidgetRef ref,
) { ) {
final ws = ref.watch(websocketProvider); if (Platform.isWindows) {
return ws.dataStream.listen((pkt) async { return windows_notify.setupNotificationListener(context, ref);
if (pkt.type == "notifications.new") { } else {
final notification = SnNotification.fromJson(pkt.data!); return universal_notify.setupNotificationListener(context, ref);
if (_appLifecycleState == AppLifecycleState.resumed) { }
// App is focused, show in-app notification
log(
'[Notification] Showing in-app notification: ${notification.title}',
);
showTopSnackBar(
globalOverlay.currentState!,
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: NotificationCard(notification: notification),
),
),
onTap: () {
if (notification.meta['action_uri'] != null) {
var uri = notification.meta['action_uri'] as String;
if (uri.startsWith('/')) {
// In-app routes
rootNavigatorKey.currentContext?.push(
notification.meta['action_uri'],
);
} else {
// External URLs
launchUrlString(uri);
}
}
},
onDismissed: () {},
dismissType: DismissType.onSwipe,
displayDuration: const Duration(seconds: 5),
snackBarPosition: SnackBarPosition.top,
padding: EdgeInsets.only(
left: 16,
right: 16,
top:
(!kIsWeb &&
(Platform.isMacOS ||
Platform.isWindows ||
Platform.isLinux))
? 28
// ignore: use_build_context_synchronously
: MediaQuery.of(context).padding.top + 16,
bottom: 16,
),
);
} else {
// App is in background, show system notification (only on supported platforms)
if (!kIsWeb && !Platform.isIOS) {
log(
'[Notification] Showing system notification: ${notification.title}',
);
const AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
'channel_id',
'channel_name',
channelDescription: 'channel_description',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
);
const NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails,
);
await flutterLocalNotificationsPlugin.show(
0,
notification.title,
notification.content,
notificationDetails,
payload: notification.meta['action_uri'] as String?,
);
} else {
log(
'[Notification] Skipping system notification for unsupported platform: ${notification.title}',
);
}
}
}
});
} }
Future<void> subscribePushNotification( Future<void> subscribePushNotification(
Dio apiClient, { Dio apiClient, {
bool detailedErrors = false, bool detailedErrors = false,
}) async { }) async {
if (!kIsWeb && Platform.isLinux) { if (Platform.isWindows) {
return; return windows_notify.subscribePushNotification(
}
await FirebaseMessaging.instance.requestPermission(
alert: true,
badge: true,
sound: true,
);
String? deviceToken;
if (kIsWeb) {
deviceToken = await FirebaseMessaging.instance.getToken(
vapidKey:
"BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU",
);
} else if (Platform.isAndroid) {
deviceToken = await FirebaseMessaging.instance.getToken();
} else if (Platform.isIOS) {
deviceToken = await FirebaseMessaging.instance.getAPNSToken();
}
FirebaseMessaging.instance.onTokenRefresh
.listen((fcmToken) {
_putTokenToRemote(apiClient, fcmToken, 1);
})
.onError((err) {
log("Failed to get firebase cloud messaging push token: $err");
});
if (deviceToken != null) {
_putTokenToRemote(
apiClient, apiClient,
deviceToken, detailedErrors: detailedErrors,
!kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1, );
} else {
return universal_notify.subscribePushNotification(
apiClient,
detailedErrors: detailedErrors,
); );
} else if (detailedErrors) {
throw Exception("Failed to get device token for push notifications.");
} }
} }
Future<void> _putTokenToRemote(
Dio apiClient,
String token,
int provider,
) async {
await apiClient.put(
"/pusher/notifications/subscription",
data: {"provider": provider, "device_token": token},
);
}

View File

@@ -0,0 +1,232 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:island/main.dart';
import 'package:island/route.dart';
import 'package:island/models/account.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/widgets/app_notification.dart';
import 'package:top_snackbar_flutter/top_snack_bar.dart';
import 'package:url_launcher/url_launcher_string.dart';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
AppLifecycleState _appLifecycleState = AppLifecycleState.resumed;
void _onAppLifecycleChanged(AppLifecycleState state) {
_appLifecycleState = state;
}
Future<void> initializeLocalNotifications() async {
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
const DarwinInitializationSettings initializationSettingsIOS =
DarwinInitializationSettings();
const DarwinInitializationSettings initializationSettingsMacOS =
DarwinInitializationSettings();
const LinuxInitializationSettings initializationSettingsLinux =
LinuxInitializationSettings(defaultActionName: 'Open notification');
const WindowsInitializationSettings initializationSettingsWindows =
WindowsInitializationSettings(
appName: 'Island',
appUserModelId: 'dev.solsynth.solian',
guid: 'dev.solsynth.solian',
);
const InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
macOS: initializationSettingsMacOS,
linux: initializationSettingsLinux,
windows: initializationSettingsWindows,
);
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (NotificationResponse response) async {
final payload = response.payload;
if (payload != null) {
if (payload.startsWith('/')) {
// In-app routes
rootNavigatorKey.currentContext?.push(payload);
} else {
// External URLs
launchUrlString(payload);
}
}
},
);
WidgetsBinding.instance.addObserver(
LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged),
);
}
class LifecycleEventHandler extends WidgetsBindingObserver {
final void Function(AppLifecycleState) onAppLifecycleChanged;
LifecycleEventHandler({required this.onAppLifecycleChanged});
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
onAppLifecycleChanged(state);
}
}
StreamSubscription<WebSocketPacket> setupNotificationListener(
BuildContext context,
WidgetRef ref,
) {
final ws = ref.watch(websocketProvider);
return ws.dataStream.listen((pkt) async {
if (pkt.type == "notifications.new") {
final notification = SnNotification.fromJson(pkt.data!);
if (_appLifecycleState == AppLifecycleState.resumed) {
// App is focused, show in-app notification
log(
'[Notification] Showing in-app notification: ${notification.title}',
);
showTopSnackBar(
globalOverlay.currentState!,
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: NotificationCard(notification: notification),
),
),
onTap: () {
if (notification.meta['action_uri'] != null) {
var uri = notification.meta['action_uri'] as String;
if (uri.startsWith('/')) {
// In-app routes
rootNavigatorKey.currentContext?.push(
notification.meta['action_uri'],
);
} else {
// External URLs
launchUrlString(uri);
}
}
},
onDismissed: () {},
dismissType: DismissType.onSwipe,
displayDuration: const Duration(seconds: 5),
snackBarPosition: SnackBarPosition.top,
padding: EdgeInsets.only(
left: 16,
right: 16,
top:
(!kIsWeb &&
(Platform.isMacOS ||
Platform.isWindows ||
Platform.isLinux))
? 28
// ignore: use_build_context_synchronously
: MediaQuery.of(context).padding.top + 16,
bottom: 16,
),
);
} else {
// App is in background, show system notification (only on supported platforms)
if (!kIsWeb && !Platform.isIOS) {
log(
'[Notification] Showing system notification: ${notification.title}',
);
// Use flutter_local_notifications for universal platforms
const AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
'channel_id',
'channel_name',
channelDescription: 'channel_description',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
);
const NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails,
);
await flutterLocalNotificationsPlugin.show(
0,
notification.title,
notification.content,
notificationDetails,
payload: notification.meta['action_uri'] as String?,
);
} else {
log(
'[Notification] Skipping system notification for unsupported platform: ${notification.title}',
);
}
}
}
});
}
Future<void> subscribePushNotification(
Dio apiClient, {
bool detailedErrors = false,
}) async {
if (!kIsWeb && Platform.isLinux) {
return;
}
await FirebaseMessaging.instance.requestPermission(
alert: true,
badge: true,
sound: true,
);
String? deviceToken;
if (kIsWeb) {
deviceToken = await FirebaseMessaging.instance.getToken(
vapidKey:
"BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU",
);
} else if (Platform.isAndroid) {
deviceToken = await FirebaseMessaging.instance.getToken();
} else if (Platform.isIOS) {
deviceToken = await FirebaseMessaging.instance.getAPNSToken();
}
FirebaseMessaging.instance.onTokenRefresh
.listen((fcmToken) {
_putTokenToRemote(apiClient, fcmToken, 1);
})
.onError((err) {
log("Failed to get firebase cloud messaging push token: $err");
});
if (deviceToken != null) {
_putTokenToRemote(
apiClient,
deviceToken,
!kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1,
);
} else if (detailedErrors) {
throw Exception("Failed to get device token for push notifications.");
}
}
Future<void> _putTokenToRemote(
Dio apiClient,
String token,
int provider,
) async {
await apiClient.put(
"/ring/notifications/subscription",
data: {"provider": provider, "device_token": token},
);
}

View File

@@ -0,0 +1,176 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:island/main.dart';
import 'package:island/route.dart';
import 'package:island/models/account.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/widgets/app_notification.dart';
import 'package:top_snackbar_flutter/top_snack_bar.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:windows_notification/windows_notification.dart'
as windows_notification;
import 'package:windows_notification/notification_message.dart';
import 'package:dio/dio.dart';
// Windows notification instance
windows_notification.WindowsNotification? windowsNotification;
AppLifecycleState _appLifecycleState = AppLifecycleState.resumed;
void _onAppLifecycleChanged(AppLifecycleState state) {
_appLifecycleState = state;
}
Future<void> initializeLocalNotifications() async {
// Initialize Windows notification for Windows platform
windowsNotification = windows_notification.WindowsNotification(
applicationId: 'dev.solsynth.solian',
);
WidgetsBinding.instance.addObserver(
LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged),
);
}
class LifecycleEventHandler extends WidgetsBindingObserver {
final void Function(AppLifecycleState) onAppLifecycleChanged;
LifecycleEventHandler({required this.onAppLifecycleChanged});
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
onAppLifecycleChanged(state);
}
}
StreamSubscription<WebSocketPacket> setupNotificationListener(
BuildContext context,
WidgetRef ref,
) {
final ws = ref.watch(websocketProvider);
return ws.dataStream.listen((pkt) async {
if (pkt.type == "notifications.new") {
final notification = SnNotification.fromJson(pkt.data!);
if (_appLifecycleState == AppLifecycleState.resumed) {
// App is focused, show in-app notification
log(
'[Notification] Showing in-app notification: ${notification.title}',
);
showTopSnackBar(
globalOverlay.currentState!,
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: NotificationCard(notification: notification),
),
),
onTap: () {
if (notification.meta['action_uri'] != null) {
var uri = notification.meta['action_uri'] as String;
if (uri.startsWith('/')) {
// In-app routes
rootNavigatorKey.currentContext?.push(
notification.meta['action_uri'],
);
} else {
// External URLs
launchUrlString(uri);
}
}
},
onDismissed: () {},
dismissType: DismissType.onSwipe,
displayDuration: const Duration(seconds: 5),
snackBarPosition: SnackBarPosition.top,
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 28, // Windows specific padding
bottom: 16,
),
);
} else {
// App is in background, show Windows system notification
log(
'[Notification] Showing Windows system notification: ${notification.title}',
);
if (windowsNotification != null) {
// Use Windows notification for Windows platform
final notificationMessage = NotificationMessage.fromPluginTemplate(
DateTime.now().millisecondsSinceEpoch.toString(), // unique id
notification.title,
notification.content,
launch: notification.meta['action_uri'] as String?,
);
await windowsNotification!.showNotificationPluginTemplate(
notificationMessage,
);
}
}
}
});
}
Future<void> subscribePushNotification(
Dio apiClient, {
bool detailedErrors = false,
}) async {
if (!kIsWeb && Platform.isLinux) {
return;
}
await FirebaseMessaging.instance.requestPermission(
alert: true,
badge: true,
sound: true,
);
String? deviceToken;
if (kIsWeb) {
deviceToken = await FirebaseMessaging.instance.getToken(
vapidKey:
"BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU",
);
} else if (Platform.isAndroid) {
deviceToken = await FirebaseMessaging.instance.getToken();
} else if (Platform.isIOS) {
deviceToken = await FirebaseMessaging.instance.getAPNSToken();
}
FirebaseMessaging.instance.onTokenRefresh
.listen((fcmToken) {
_putTokenToRemote(apiClient, fcmToken, 1);
})
.onError((err) {
log("Failed to get firebase cloud messaging push token: $err");
});
if (deviceToken != null) {
_putTokenToRemote(
apiClient,
deviceToken,
!kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1,
);
} else if (detailedErrors) {
throw Exception("Failed to get device token for push notifications.");
}
}
Future<void> _putTokenToRemote(
Dio apiClient,
String token,
int provider,
) async {
await apiClient.put(
"/ring/notifications/subscription",
data: {"provider": provider, "device_token": token},
);
}

View File

@@ -14,7 +14,7 @@ Future<void> initializeTzdb() async {
} }
Future<String> getMachineTz() async { Future<String> getMachineTz() async {
return await FlutterTimezone.getLocalTimezone(); return (await FlutterTimezone.getLocalTimezone()).identifier;
} }
List<String> getAvailableTz() { List<String> getAvailableTz() {

View File

@@ -13,7 +13,7 @@ Future<void> initializeTzdb() async {
} }
Future<String> getMachineTz() async { Future<String> getMachineTz() async {
return await FlutterTimezone.getLocalTimezone(); return (await FlutterTimezone.getLocalTimezone()).identifier;
} }
List<String> getAvailableTz() { List<String> getAvailableTz() {

View File

@@ -1 +1,3 @@
export 'udid.native.dart' if (dart.library.html) 'udid.web.dart'; export 'udid.native.dart'
if (dart.library.html) 'udid.web.dart'
if (dart.library.io) 'udid.native.dart';

View File

@@ -1,3 +1,6 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_udid/flutter_udid.dart'; import 'package:flutter_udid/flutter_udid.dart';
String? _cachedUdid; String? _cachedUdid;
@@ -9,3 +12,18 @@ Future<String> getUdid() async {
_cachedUdid = await FlutterUdid.consistentUdid; _cachedUdid = await FlutterUdid.consistentUdid;
return _cachedUdid!; return _cachedUdid!;
} }
Future<String> getDeviceName() async {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) {
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
return androidInfo.device;
} else if (Platform.isIOS) {
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
return iosInfo.name;
} else if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) {
return Platform.localHostname;
} else {
return 'unknown'.tr();
}
}

View File

@@ -9,3 +9,18 @@ Future<String> getUdid() async {
final hash = sha256.convert(bytes); final hash = sha256.convert(bytes);
return hash.toString(); return hash.toString();
} }
Future<String> getDeviceName() async {
final userAgent = window.navigator.userAgent;
if (userAgent.contains('Chrome') && !userAgent.contains('Edg')) {
return 'Chrome';
} else if (userAgent.contains('Firefox')) {
return 'Firefox';
} else if (userAgent.contains('Safari') && !userAgent.contains('Chrome')) {
return 'Safari';
} else if (userAgent.contains('Edg')) {
return 'Edge';
} else {
return 'Browser';
}
}

View File

@@ -6,10 +6,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/account.dart'; import 'package:island/models/account.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/services/time.dart';
import 'package:island/services/udid.dart'; import 'package:island/services/udid.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:island/widgets/extended_refresh_indicator.dart'; import 'package:island/widgets/extended_refresh_indicator.dart';
@@ -43,32 +45,11 @@ class _DeviceListTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ExpansionTile(
isThreeLine: true, title: Row(
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), spacing: 8,
leading: Icon(switch (device.platform) {
0 => Icons.device_unknown, // Unidentified
1 => Icons.web, // Web
2 => Icons.phone_iphone, // iOS
3 => Icons.phone_android, // Android
4 => Icons.laptop_mac, // macOS
5 => Icons.window, // Windows
6 => Icons.computer, // Linux
_ => Icons.device_unknown, // fallback
}).padding(top: 4),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Text( Flexible(child: Text(device.deviceLabel ?? device.deviceName)),
'lastActiveAt'.tr(
args: [
DateFormat().format(
device.challenges.first.createdAt.toLocal(),
),
],
),
),
Text(device.challenges.first.ipAddress),
if (device.isCurrent) if (device.isCurrent)
Row( Row(
children: [ children: [
@@ -82,10 +63,29 @@ class _DeviceListTile extends StatelessWidget {
), ),
), ),
], ],
).padding(top: 4), ),
], ],
), ),
title: Text(device.deviceLabel ?? device.deviceName), subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'lastActiveAt'.tr(
args: [device.challenges.first.createdAt.formatSystem()],
),
),
],
),
leading: Icon(switch (device.platform) {
0 => Icons.device_unknown, // Unidentified
1 => Icons.web, // Web
2 => Icons.phone_iphone, // iOS
3 => Icons.phone_android, // Android
4 => Icons.laptop_mac, // macOS
5 => Icons.window, // Windows
6 => Icons.computer, // Linux
_ => Icons.device_unknown, // fallback
}).padding(top: 4),
trailing: trailing:
isWideScreen(context) isWideScreen(context)
? Row( ? Row(
@@ -105,6 +105,36 @@ class _DeviceListTile extends StatelessWidget {
], ],
) )
: null, : null,
expandedCrossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text('authDeviceChallenges'.tr()),
),
for (final challenge in device.challenges)
ListTile(
minTileHeight: 48,
title: Text(DateFormat().format(challenge.createdAt.toLocal())),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(challenge.ipAddress),
if (challenge.location != null)
Row(
spacing: 4,
children:
[challenge.location?.city, challenge.location?.country]
.where((e) => e?.isNotEmpty ?? false)
.map((e) => Text(e!))
.toList(),
),
],
),
),
],
); );
} }
} }
@@ -176,72 +206,116 @@ class AccountSessionSheet extends HookConsumerWidget {
return SheetScaffold( return SheetScaffold(
titleText: 'authSessions'.tr(), titleText: 'authSessions'.tr(),
child: authDevices.when( child: Column(
data: children: [
(data) => ExtendedRefreshIndicator( if (!wideScreen)
onRefresh: Container(
() => Future.sync(() => ref.invalidate(authDevicesProvider)), width: double.infinity,
child: ListView.builder( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: EdgeInsets.zero, color: Theme.of(context).colorScheme.surfaceContainerHigh,
itemCount: data.length, child: Row(
itemBuilder: (context, index) { mainAxisAlignment: MainAxisAlignment.center,
final device = data[index]; crossAxisAlignment: CrossAxisAlignment.start,
if (wideScreen) { spacing: 8,
return _DeviceListTile( children: [
device: device, const Icon(Symbols.info, size: 16).padding(top: 2),
updateDeviceLabel: updateDeviceLabel, Flexible(
logoutDevice: logoutDevice, child: Text(
); 'authDeviceHint'.tr(),
} else { style: TextStyle(
return Dismissible( color: Theme.of(context).colorScheme.onSurfaceVariant,
key: Key('device-${device.id}'),
direction:
device.isCurrent
? DismissDirection.startToEnd
: DismissDirection.horizontal,
background: Container(
color: Colors.blue,
alignment: Alignment.centerLeft,
padding: EdgeInsets.symmetric(horizontal: 20),
child: Icon(Icons.edit, color: Colors.white),
), ),
secondaryBackground: Container( ),
color: Colors.red, ),
alignment: Alignment.centerRight, ],
padding: EdgeInsets.symmetric(horizontal: 20),
child: Icon(Icons.logout, color: Colors.white),
),
confirmDismiss: (direction) async {
if (direction == DismissDirection.startToEnd) {
updateDeviceLabel(device.deviceId);
return false;
} else {
final confirm = await showConfirmAlert(
'authDeviceLogoutHint'.tr(),
'authDeviceLogout'.tr(),
);
if (confirm && context.mounted) {
logoutDevice(device.deviceId);
}
return false; // Don't dismiss
}
},
child: _DeviceListTile(
device: device,
updateDeviceLabel: updateDeviceLabel,
logoutDevice: logoutDevice,
),
);
}
},
), ),
), ),
error: Expanded(
(err, _) => ResponseErrorWidget( child: authDevices.when(
error: err, data:
onRetry: () => ref.invalidate(authDevicesProvider), (data) => ExtendedRefreshIndicator(
onRefresh:
() => Future.sync(
() => ref.invalidate(authDevicesProvider),
),
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: data.length,
itemBuilder: (context, index) {
final device = data[index];
if (wideScreen) {
return _DeviceListTile(
device: device,
updateDeviceLabel: updateDeviceLabel,
logoutDevice: logoutDevice,
);
} else {
return Dismissible(
key: Key('device-${device.id}'),
direction:
device.isCurrent
? DismissDirection.startToEnd
: DismissDirection.horizontal,
background: Container(
color: Colors.blue,
alignment: Alignment.centerLeft,
padding: EdgeInsets.symmetric(horizontal: 20),
child: Icon(Icons.edit, color: Colors.white),
),
secondaryBackground: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.symmetric(horizontal: 20),
child: Icon(Icons.logout, color: Colors.white),
),
confirmDismiss: (direction) async {
if (direction == DismissDirection.startToEnd) {
updateDeviceLabel(device.deviceId);
return false;
} else {
final confirm = await showConfirmAlert(
'authDeviceLogoutHint'.tr(),
'authDeviceLogout'.tr(),
);
if (confirm && context.mounted) {
try {
showLoadingModal(context);
final apiClient = ref.watch(
apiClientProvider,
);
await apiClient.delete(
'/id/accounts/me/devices/${device.deviceId}',
);
ref.invalidate(authDevicesProvider);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted)
hideLoadingModal(context);
}
}
return confirm;
}
},
child: _DeviceListTile(
device: device,
updateDeviceLabel: updateDeviceLabel,
logoutDevice: logoutDevice,
),
);
}
},
),
),
error:
(err, _) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(authDevicesProvider),
),
loading: () => ResponseLoadingWidget(),
), ),
loading: () => ResponseLoadingWidget(), ),
],
), ),
); );
} }

View File

@@ -3,7 +3,7 @@ import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/activity_rpc.dart'; import 'package:island/pods/activity/activity_rpc.dart';
import 'package:island/pods/websocket.dart'; import 'package:island/pods/websocket.dart';
import 'package:island/screens/tray_manager.dart'; import 'package:island/screens/tray_manager.dart';
import 'package:island/services/notify.dart'; import 'package:island/services/notify.dart';

View File

@@ -0,0 +1,327 @@
import "dart:async";
import "dart:io";
import "package:easy_localization/easy_localization.dart";
import "package:flutter/foundation.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:gap/gap.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:image_picker/image_picker.dart";
import "package:island/models/chat.dart";
import "package:island/models/file.dart";
import "package:island/pods/config.dart";
import "package:island/widgets/content/attachment_preview.dart";
import "package:material_symbols_icons/material_symbols_icons.dart";
import "package:pasteboard/pasteboard.dart";
import "package:styled_widget/styled_widget.dart";
import "package:material_symbols_icons/symbols.dart";
import "package:island/widgets/stickers/picker.dart";
class ChatInput extends HookConsumerWidget {
final TextEditingController messageController;
final SnChatRoom chatRoom;
final VoidCallback onSend;
final VoidCallback onClear;
final Function(bool isPhoto) onPickFile;
final SnChatMessage? messageReplyingTo;
final SnChatMessage? messageForwardingTo;
final SnChatMessage? messageEditingTo;
final List<UniversalFile> attachments;
final Function(int) onUploadAttachment;
final Function(int) onDeleteAttachment;
final Function(int, int) onMoveAttachment;
final Function(List<UniversalFile>) onAttachmentsChanged;
const ChatInput({
super.key,
required this.messageController,
required this.chatRoom,
required this.onSend,
required this.onClear,
required this.onPickFile,
required this.messageReplyingTo,
required this.messageForwardingTo,
required this.messageEditingTo,
required this.attachments,
required this.onUploadAttachment,
required this.onDeleteAttachment,
required this.onMoveAttachment,
required this.onAttachmentsChanged,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final inputFocusNode = useFocusNode();
final enterToSend = ref.watch(appSettingsNotifierProvider).enterToSend;
final isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS);
void send() {
onSend.call();
WidgetsBinding.instance.addPostFrameCallback((_) {
inputFocusNode.requestFocus();
});
}
Future<void> handlePaste() async {
final clipboard = await Pasteboard.image;
if (clipboard == null) return;
onAttachmentsChanged([
...attachments,
UniversalFile(
data: XFile.fromData(clipboard, mimeType: "image/jpeg"),
type: UniversalFileType.image,
),
]);
}
void handleKeyPress(
BuildContext context,
WidgetRef ref,
RawKeyEvent event,
) {
if (event is! RawKeyDownEvent) return;
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
if (isPaste && isModifierPressed) {
handlePaste();
return;
}
final enterToSend = ref.read(appSettingsNotifierProvider).enterToSend;
final isEnter = event.logicalKey == LogicalKeyboardKey.enter;
if (isEnter) {
if (enterToSend && !isModifierPressed) {
send();
} else if (!enterToSend && isModifierPressed) {
send();
}
}
}
return Material(
elevation: 8,
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
if (attachments.isNotEmpty)
SizedBox(
height: 280,
child: ListView.separated(
padding: EdgeInsets.symmetric(horizontal: 12),
scrollDirection: Axis.horizontal,
itemCount: attachments.length,
itemBuilder: (context, idx) {
return SizedBox(
height: 280,
width: 280,
child: AttachmentPreview(
item: attachments[idx],
onRequestUpload: () => onUploadAttachment(idx),
onDelete: () => onDeleteAttachment(idx),
onUpdate: (value) {
attachments[idx] = value;
onAttachmentsChanged(attachments);
},
onMove: (delta) => onMoveAttachment(idx, delta),
),
);
},
separatorBuilder: (_, _) => const Gap(8),
),
).padding(top: 12),
if (messageReplyingTo != null ||
messageForwardingTo != null ||
messageEditingTo != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(8),
),
margin: const EdgeInsets.only(left: 8, right: 8, top: 8),
child: Row(
children: [
Icon(
messageReplyingTo != null
? Symbols.reply
: messageForwardingTo != null
? Symbols.forward
: Symbols.edit,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
const Gap(8),
Expanded(
child: Text(
messageReplyingTo != null
? 'Replying to ${messageReplyingTo?.sender.account.nick}'
: messageForwardingTo != null
? 'Forwarding message'
: 'Editing message',
style: Theme.of(context).textTheme.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
IconButton(
icon: const Icon(Icons.close, size: 20),
onPressed: onClear,
padding: EdgeInsets.zero,
style: ButtonStyle(
minimumSize: WidgetStatePropertyAll(Size(28, 28)),
),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
child: Row(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip: 'stickers'.tr(),
icon: const Icon(Symbols.add_reaction),
onPressed: () {
final size = MediaQuery.of(context).size;
showStickerPickerPopover(
context,
Offset(
20,
size.height -
480 -
MediaQuery.of(context).padding.bottom,
),
onPick: (placeholder) {
// Insert placeholder at current cursor position
final text = messageController.text;
final selection = messageController.selection;
final start =
selection.start >= 0
? selection.start
: text.length;
final end =
selection.end >= 0
? selection.end
: text.length;
final newText = text.replaceRange(
start,
end,
placeholder,
);
messageController.value = TextEditingValue(
text: newText,
selection: TextSelection.collapsed(
offset: start + placeholder.length,
),
);
},
);
},
),
PopupMenuButton(
icon: const Icon(Symbols.photo_library),
itemBuilder:
(context) => [
PopupMenuItem(
onTap: () => onPickFile(true),
child: Row(
spacing: 12,
children: [
const Icon(Symbols.photo),
Text('addPhoto').tr(),
],
),
),
PopupMenuItem(
onTap: () => onPickFile(false),
child: Row(
spacing: 12,
children: [
const Icon(Symbols.video_call),
Text('addVideo').tr(),
],
),
),
],
),
],
),
Expanded(
child: RawKeyboardListener(
focusNode: FocusNode(),
onKey: (event) => handleKeyPress(context, ref, event),
child: TextField(
focusNode: inputFocusNode,
controller: messageController,
onSubmitted:
(enterToSend && isMobile)
? (_) {
send();
}
: null,
keyboardType:
(enterToSend && isMobile)
? TextInputType.text
: TextInputType.multiline,
textInputAction: TextInputAction.send,
inputFormatters: [
if (enterToSend && !isMobile)
TextInputFormatter.withFunction((oldValue, newValue) {
if (newValue.text.endsWith('\n')) {
return oldValue;
}
return newValue;
}),
],
decoration: InputDecoration(
hintText:
(chatRoom.type == 1 && chatRoom.name == null)
? 'chatDirectMessageHint'.tr(
args: [
chatRoom.members!
.map((e) => e.account.nick)
.join(', '),
],
)
: 'chatMessageHint'.tr(args: [chatRoom.name!]),
border: InputBorder.none,
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
counterText:
messageController.text.length > 1024
? '${messageController.text.length}/4096'
: null,
),
maxLines: 3,
minLines: 1,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
),
IconButton(
icon: const Icon(Icons.send),
color: Theme.of(context).colorScheme.primary,
onPressed: send,
),
],
).padding(bottom: MediaQuery.of(context).padding.bottom),
),
],
),
);
}
}

View File

@@ -0,0 +1,171 @@
import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:island/models/chat.dart';
import 'package:island/pods/call.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:pretty_diff_text/pretty_diff_text.dart';
import 'package:styled_widget/styled_widget.dart';
class MessageContent extends StatelessWidget {
final SnChatMessage item;
final String? translatedText;
const MessageContent({super.key, required this.item, this.translatedText});
@override
Widget build(BuildContext context) {
if (item.type == 'messages.delete' || item.deletedAt != null) {
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Symbols.delete,
size: 14,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.6),
),
const Gap(4),
Text(
item.content ?? 'Deleted a message',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.6),
fontStyle: FontStyle.italic,
),
),
],
);
}
switch (item.type) {
case 'call.start':
case 'call.ended':
return _MessageContentCall(
isEnded: item.type == 'call.ended',
duration: item.meta['duration']?.toDouble(),
);
case 'messages.update':
case 'messages.update.links':
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Symbols.edit,
size: 14,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.6),
),
const Gap(4),
if (item.meta['previous_content'] is String)
PrettyDiffText(
oldText: item.meta['previous_content'],
newText: item.content ?? 'Edited a message',
defaultTextStyle: Theme.of(
context,
).textTheme.bodySmall!.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
addedTextStyle: TextStyle(
backgroundColor: Theme.of(
context,
).colorScheme.primaryFixedDim.withOpacity(0.4),
),
deletedTextStyle: TextStyle(
decoration: TextDecoration.lineThrough,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.7),
),
)
else
Text(
item.content ?? 'Edited a message',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.6),
),
),
],
);
case 'text':
default:
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownTextContent(
content: item.content ?? '*${item.type} has no content*',
isSelectable: true,
linesMargin: EdgeInsets.zero,
),
if (translatedText?.isNotEmpty ?? false)
...([
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: math.min(
280,
MediaQuery.of(context).size.width * 0.4,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('translated').tr().fontSize(11).opacity(0.75),
const Gap(8),
Flexible(child: Divider()),
],
).padding(vertical: 4),
),
MarkdownTextContent(
content: translatedText!,
isSelectable: true,
linesMargin: EdgeInsets.zero,
),
]),
],
);
}
}
static bool hasContent(SnChatMessage item) {
return item.type != 'text' || (item.content?.isNotEmpty ?? false);
}
}
class _MessageContentCall extends StatelessWidget {
final bool isEnded;
final double? duration;
const _MessageContentCall({required this.isEnded, this.duration});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isEnded ? Symbols.call_end : Symbols.phone_in_talk,
size: 16,
color: Theme.of(context).colorScheme.primary,
),
Gap(4),
Text(
isEnded
? 'Call ended after ${formatDuration(Duration(seconds: duration!.toInt()))}'
: 'Call started',
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
],
);
}
}

View File

@@ -0,0 +1,69 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/database/message.dart';
import 'package:styled_widget/styled_widget.dart';
class MessageIndicators extends StatelessWidget {
final DateTime? editedAt;
final MessageStatus? status;
final bool isCurrentUser;
final Color textColor;
const MessageIndicators({
super.key,
this.editedAt,
this.status,
required this.isCurrentUser,
required this.textColor,
});
@override
Widget build(BuildContext context) {
return Row(
spacing: 4,
mainAxisSize: MainAxisSize.min,
children: [
if (editedAt != null)
Text(
'edited'.tr().toLowerCase(),
style: TextStyle(fontSize: 11, color: textColor.withOpacity(0.7)),
),
if (isCurrentUser && status != null)
_buildStatusIcon(
context,
status!,
textColor.withOpacity(0.7),
).padding(bottom: 3),
],
);
}
Widget _buildStatusIcon(
BuildContext context,
MessageStatus status,
Color textColor,
) {
switch (status) {
case MessageStatus.pending:
return Icon(Icons.access_time, size: 12, color: textColor);
case MessageStatus.sent:
return Icon(Icons.check, size: 12, color: textColor);
case MessageStatus.failed:
return Consumer(
builder:
(context, ref, _) => GestureDetector(
onTap: () {
// This would need to be passed in or accessed differently
// For now, just show the error icon
},
child: const Icon(
Icons.error_outline,
size: 12,
color: Colors.red,
),
),
);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,87 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:island/database/message.dart';
import 'package:island/models/embed.dart';
import 'package:island/utils/mapping.dart';
import 'package:island/widgets/chat/message_content.dart';
import 'package:island/widgets/chat/message_sender_info.dart';
import 'package:island/widgets/content/cloud_file_collection.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/embed/link.dart';
class MessageListTile extends StatelessWidget {
final LocalChatMessage message;
final Function(String messageId) onJump;
const MessageListTile({
super.key,
required this.message,
required this.onJump,
});
@override
Widget build(BuildContext context) {
final remoteMessage = message.toRemoteMessage();
final sender = remoteMessage.sender;
return ListTile(
leading: CircleAvatar(
radius: 20,
backgroundColor: Colors.transparent,
child: ProfilePictureWidget(
fileId: sender.account.profile.picture?.id,
radius: 20,
),
),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MessageSenderInfo(
sender: sender,
createdAt: message.createdAt,
textColor: Theme.of(context).colorScheme.onSurfaceVariant,
showAvatar: false,
),
const SizedBox(height: 4),
MessageContent(item: remoteMessage),
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (remoteMessage.attachments.isNotEmpty)
LayoutBuilder(
builder: (context, constraints) {
return CloudFileList(
files: remoteMessage.attachments,
maxWidth: constraints.maxWidth,
padding: EdgeInsets.symmetric(vertical: 4),
);
},
),
if (remoteMessage.meta['embeds'] != null)
...((remoteMessage.meta['embeds'] as List<dynamic>)
.map((embed) => convertMapKeysToSnakeCase(embed))
.where((embed) => embed['type'] == 'link')
.map((embed) => SnScrappedLink.fromJson(embed))
.map(
(link) => LayoutBuilder(
builder: (context, constraints) {
return EmbedLinkWidget(
link: link,
maxWidth: math.min(constraints.maxWidth, 480),
margin: const EdgeInsets.symmetric(vertical: 4),
);
},
),
)
.toList()),
],
),
onTap: () => onJump(message.id),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
dense: true,
);
}
}

View File

@@ -0,0 +1,158 @@
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:gap/gap.dart';
import 'package:island/models/chat.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/content/cloud_files.dart';
class MessageSenderInfo extends StatelessWidget {
final SnChatMember sender;
final DateTime createdAt;
final Color textColor;
final bool showAvatar;
final bool isCompact;
const MessageSenderInfo({
super.key,
required this.sender,
required this.createdAt,
required this.textColor,
this.showAvatar = true,
this.isCompact = false,
});
@override
Widget build(BuildContext context) {
final timestamp =
DateTime.now().difference(createdAt).inDays > 365
? DateFormat('yyyy/MM/dd HH:mm').format(createdAt.toLocal())
: DateTime.now().difference(createdAt).inDays > 0
? DateFormat('MM/dd HH:mm').format(createdAt.toLocal())
: DateFormat('HH:mm').format(createdAt.toLocal());
if (isCompact) {
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
if (showAvatar)
AccountPfcGestureDetector(
uname: sender.account.name,
child: ProfilePictureWidget(
fileId: sender.account.profile.picture?.id,
radius: 14,
),
),
if (showAvatar) const Gap(4),
AccountName(
account: sender.account,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: textColor,
fontWeight: FontWeight.w500,
),
),
const Gap(6),
Text(
timestamp,
style: TextStyle(fontSize: 10, color: textColor.withOpacity(0.7)),
),
],
);
}
if (showAvatar) {
return Row(
spacing: 8,
children: [
AccountPfcGestureDetector(
uname: sender.account.name,
child: ProfilePictureWidget(
fileId: sender.account.profile.picture?.id,
radius: 14,
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
AccountName(
account: sender.account,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: textColor,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 4),
Badge(
label:
Text(
sender.role >= 100
? 'permissionOwner'
: sender.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
),
],
),
Text(
timestamp,
style: TextStyle(
fontSize: 10,
color: textColor.withOpacity(0.7),
),
),
],
),
),
],
);
}
return Row(
spacing: 8,
mainAxisSize: MainAxisSize.min,
children: [
if (showAvatar)
AccountPfcGestureDetector(
uname: sender.account.name,
child: ProfilePictureWidget(
fileId: sender.account.profile.picture?.id,
radius: 16,
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 2,
children: [
Text(timestamp, style: TextStyle(fontSize: 10, color: textColor)),
Row(
mainAxisSize: MainAxisSize.min,
spacing: 5,
children: [
AccountName(
account: sender.account,
style: Theme.of(context).textTheme.bodySmall,
),
Badge(
label:
Text(
sender.role >= 100
? 'permissionOwner'
: sender.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
),
],
),
],
),
],
);
}
}

View File

@@ -0,0 +1,223 @@
import "package:easy_localization/easy_localization.dart";
import "package:flutter/material.dart";
import "package:go_router/go_router.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:gap/gap.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:island/database/message.dart";
import "package:island/models/chat.dart";
import "package:island/pods/messages_notifier.dart";
import "package:island/pods/network.dart";
import "package:island/services/responsive.dart";
import "package:island/widgets/alert.dart";
import "package:island/widgets/app_scaffold.dart";
import "package:island/widgets/chat/message_item.dart";
import "package:island/widgets/content/cloud_files.dart";
import "package:island/widgets/response.dart";
import "package:material_symbols_icons/material_symbols_icons.dart";
import "package:styled_widget/styled_widget.dart";
import "package:super_sliver_list/super_sliver_list.dart";
import "package:material_symbols_icons/symbols.dart";
import "package:riverpod_annotation/riverpod_annotation.dart";
import "package:island/screens/chat/chat.dart";
class PublicRoomPreview extends HookConsumerWidget {
final String id;
final SnChatRoom room;
const PublicRoomPreview({super.key, required this.id, required this.room});
@override
Widget build(BuildContext context, WidgetRef ref) {
final messages = ref.watch(messagesNotifierProvider(id));
final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier);
final scrollController = useScrollController();
final listController = useMemoized(() => ListController(), []);
var isLoading = false;
// Add scroll listener for pagination
useEffect(() {
void onScroll() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (isLoading) return;
isLoading = true;
messagesNotifier.loadMore().then((_) => isLoading = false);
}
}
scrollController.addListener(onScroll);
return () => scrollController.removeListener(onScroll);
}, [scrollController]);
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
SuperListView.builder(
listController: listController,
padding: EdgeInsets.symmetric(vertical: 16),
controller: scrollController,
reverse: true, // Show newest messages at the bottom
itemCount: messageList.length,
findChildIndexCallback: (key) {
final valueKey = key as ValueKey;
final messageId = valueKey.value as String;
return messageList.indexWhere((m) => m.id == messageId);
},
extentEstimation: (_, _) => 40,
itemBuilder: (context, index) {
final message = messageList[index];
final nextMessage =
index < messageList.length - 1 ? messageList[index + 1] : null;
final isLastInGroup =
nextMessage == null ||
nextMessage.senderId != message.senderId ||
nextMessage.createdAt
.difference(message.createdAt)
.inMinutes
.abs() >
3;
return MessageItem(
message: message,
isCurrentUser: false, // User is not a member, so not current user
onAction: null, // No actions allowed in preview mode
onJump: (_) {}, // No jump functionality in preview
progress: null,
showAvatar: isLastInGroup,
);
},
);
final compactHeader = isWideScreen(context);
Widget comfortHeaderWidget() => Column(
spacing: 4,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 26,
width: 26,
child:
(room.type == 1 && room.picture?.id == null)
? SplitAvatarWidget(
filesId:
room.members!
.map((e) => e.account.profile.picture?.id)
.toList(),
)
: room.picture?.id != null
? ProfilePictureWidget(
fileId: room.picture?.id,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(
child: Text(
room.name![0].toUpperCase(),
style: const TextStyle(fontSize: 12),
),
),
),
Text(
(room.type == 1 && room.name == null)
? room.members!.map((e) => e.account.nick).join(', ')
: room.name!,
).fontSize(15),
],
);
Widget compactHeaderWidget() => Row(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 26,
width: 26,
child:
(room.type == 1 && room.picture?.id == null)
? SplitAvatarWidget(
filesId:
room.members!
.map((e) => e.account.profile.picture?.id)
.toList(),
)
: room.picture?.id != null
? ProfilePictureWidget(
fileId: room.picture?.id,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(
child: Text(
room.name![0].toUpperCase(),
style: const TextStyle(fontSize: 12),
),
),
),
Text(
(room.type == 1 && room.name == null)
? room.members!.map((e) => e.account.nick).join(', ')
: room.name!,
).fontSize(19),
],
);
return AppScaffold(
appBar: AppBar(
leading: !compactHeader ? const Center(child: PageBackButton()) : null,
automaticallyImplyLeading: false,
toolbarHeight: compactHeader ? null : 64,
title: compactHeader ? compactHeaderWidget() : comfortHeaderWidget(),
actions: [
IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {
context.pushNamed('chatDetail', pathParameters: {'id': id});
},
),
const Gap(8),
],
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: messages.when(
data:
(messageList) =>
messageList.isEmpty
? Center(child: Text('No messages yet'.tr()))
: chatMessageListWidget(messageList),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => messagesNotifier.loadInitial(),
),
),
),
// Join button at the bottom for public rooms
Container(
padding: const EdgeInsets.all(16),
child: FilledButton.tonalIcon(
onPressed: () async {
try {
showLoadingModal(context);
final apiClient = ref.read(apiClientProvider);
await apiClient.post('/sphere/chat/${room.id}/members/me');
ref.invalidate(chatroomIdentityProvider(id));
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
},
label: Text('chatJoin').tr(),
icon: const Icon(Icons.add),
),
),
],
),
);
}
}

View File

@@ -228,7 +228,7 @@ class CheckInWidget extends HookConsumerWidget {
), ),
Column( Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 3, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
AnimatedSwitcher( AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
@@ -244,7 +244,7 @@ class CheckInWidget extends HookConsumerWidget {
loading: () => Text('checkInNone').tr().fontSize(15).bold(), loading: () => Text('checkInNone').tr().fontSize(15).bold(),
error: (err, stack) => Text('error').tr().fontSize(15).bold(), error: (err, stack) => Text('error').tr().fontSize(15).bold(),
), ),
), ).padding(right: 4),
IconButton.outlined( IconButton.outlined(
iconSize: 16, iconSize: 16,
visualDensity: const VisualDensity( visualDensity: const VisualDensity(

View File

@@ -9,6 +9,11 @@ String _parseRemoteError(DioException err) {
String? message; String? message;
if (err.response?.data is String) { if (err.response?.data is String) {
message = err.response?.data; message = err.response?.data;
} else if (err.response?.data?['message'] != null) {
message = <String?>[
err.response?.data?['message']?.toString(),
err.response?.data?['detail']?.toString(),
].where((e) => e != null).cast<String>().map((e) => e.trim()).join('\n');
} else if (err.response?.data?['errors'] != null) { } else if (err.response?.data?['errors'] != null) {
final errors = err.response?.data['errors'] as Map<String, dynamic>; final errors = err.response?.data['errors'] as Map<String, dynamic>;
message = errors.values message = errors.values

View File

@@ -8,17 +8,26 @@ import 'package:easy_localization/easy_localization.dart';
String _parseRemoteError(DioException err) { String _parseRemoteError(DioException err) {
log('${err.requestOptions.method} ${err.requestOptions.uri} ${err.message}'); log('${err.requestOptions.method} ${err.requestOptions.uri} ${err.message}');
if (err.response?.data is String) return err.response?.data; String? message;
if (err.response?.data?['errors'] != null) { if (err.response?.data is String) {
message = err.response?.data;
} else if (err.response?.data?['message'] != null) {
message = <String?>[
err.response?.data?['message']?.toString(),
err.response?.data?['detail']?.toString(),
].where((e) => e != null).cast<String>().map((e) => e.trim()).join('\n');
} else if (err.response?.data?['errors'] != null) {
final errors = err.response?.data['errors'] as Map<String, dynamic>; final errors = err.response?.data['errors'] as Map<String, dynamic>;
return errors.values message = errors.values
.map( .map(
(ele) => (ele) =>
(ele as List<dynamic>).map((ele) => ele.toString()).join('\n'), (ele as List<dynamic>).map((ele) => ele.toString()).join('\n'),
) )
.join('\n'); .join('\n');
} }
return err.message ?? err.toString(); if (message == null || message.isEmpty) message = err.response?.statusMessage;
message ??= err.message;
return message ?? err.toString();
} }
void showErrorAlert(dynamic err) async { void showErrorAlert(dynamic err) async {

View File

@@ -55,7 +55,7 @@ class CloudFilePicker extends HookConsumerWidget {
uploadPosition.value = idx; uploadPosition.value = idx;
final file = files.value[idx]; final file = files.value[idx];
final cloudFile = final cloudFile =
await putMediaToCloud( await putFileToCloud(
fileData: file, fileData: file,
atk: token, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,

View File

@@ -1,4 +1,5 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:mime/mime.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
@@ -19,6 +20,7 @@ import 'package:island/widgets/alert.dart';
import 'package:island/widgets/post/compose_link_attachments.dart'; import 'package:island/widgets/post/compose_link_attachments.dart';
import 'package:island/widgets/post/compose_poll.dart'; import 'package:island/widgets/post/compose_poll.dart';
import 'package:island/widgets/post/compose_recorder.dart'; import 'package:island/widgets/post/compose_recorder.dart';
import 'package:island/pods/pool_provider.dart';
import 'package:pasteboard/pasteboard.dart'; import 'package:pasteboard/pasteboard.dart';
import 'package:textfield_tags/textfield_tags.dart'; import 'package:textfield_tags/textfield_tags.dart';
import 'dart:async'; import 'dart:async';
@@ -183,7 +185,7 @@ class ComposeLogic {
if (attachment.data is! SnCloudFile) { if (attachment.data is! SnCloudFile) {
try { try {
final cloudFile = final cloudFile =
await putMediaToCloud( await putFileToCloud(
fileData: attachment, fileData: attachment,
atk: token, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
@@ -386,6 +388,30 @@ class ComposeLogic {
}; };
} }
static Future<void> pickGeneralFile(WidgetRef ref, ComposeState state) async {
final result = await FilePicker.platform.pickFiles(
type: FileType.any,
allowMultiple: true,
);
if (result == null || result.count == 0) return;
final newFiles = <UniversalFile>[];
for (final f in result.files) {
if (f.path == null) continue;
final mimeType =
lookupMimeType(f.path!, headerBytes: f.bytes) ??
'application/octet-stream';
final xfile = XFile(f.path!, name: f.name, mimeType: mimeType);
final uf = UniversalFile(data: xfile, type: UniversalFileType.file);
newFiles.add(uf);
}
state.attachments.value = [...state.attachments.value, ...newFiles];
}
static Future<void> pickPhotoMedia(WidgetRef ref, ComposeState state) async { static Future<void> pickPhotoMedia(WidgetRef ref, ComposeState state) async {
final result = await FilePicker.platform.pickFiles( final result = await FilePicker.platform.pickFiles(
type: FileType.image, type: FileType.image,
@@ -479,8 +505,9 @@ class ComposeLogic {
static Future<void> uploadAttachment( static Future<void> uploadAttachment(
WidgetRef ref, WidgetRef ref,
ComposeState state, ComposeState state,
int index, int index, {
) async { String? poolId, // For Unit Test
}) async {
final attachment = state.attachments.value[index]; final attachment = state.attachments.value[index];
if (attachment.isOnCloud) return; if (attachment.isOnCloud) return;
@@ -489,22 +516,34 @@ class ComposeLogic {
if (token == null) throw ArgumentError('Token is null'); if (token == null) throw ArgumentError('Token is null');
try { try {
// Update progress state
state.attachmentProgress.value = { state.attachmentProgress.value = {
...state.attachmentProgress.value, ...state.attachmentProgress.value,
index: 0, index: 0,
}; };
// Upload file to cloud SnCloudFile? cloudFile;
final cloudFile =
await putMediaToCloud( final pools = await ref.read(poolsProvider.future);
final selectedPoolId = resolveDefaultPoolId(ref, pools);
cloudFile =
await putFileToCloud(
fileData: attachment, fileData: attachment,
atk: token, atk: token,
baseUrl: baseUrl, baseUrl: baseUrl,
filename: attachment.data.name ?? 'Post media', poolId: selectedPoolId,
filename:
attachment.data.name ??
(attachment.type == UniversalFileType.file
? 'General file'
: 'Post media'),
mimetype: mimetype:
attachment.data.mimeType ?? attachment.data.mimeType ??
getMimeTypeFromFileType(attachment.type), getMimeTypeFromFileType(attachment.type),
mode:
attachment.type == UniversalFileType.file
? FileUploadMode.generic
: FileUploadMode.mediaSafe,
onProgress: (progress, _) { onProgress: (progress, _) {
state.attachmentProgress.value = { state.attachmentProgress.value = {
...state.attachmentProgress.value, ...state.attachmentProgress.value,
@@ -517,14 +556,12 @@ class ComposeLogic {
throw ArgumentError('Failed to upload the file...'); throw ArgumentError('Failed to upload the file...');
} }
// Update attachments list with cloud file
final clone = List.of(state.attachments.value); final clone = List.of(state.attachments.value);
clone[index] = UniversalFile(data: cloudFile, type: attachment.type); clone[index] = UniversalFile(data: cloudFile, type: attachment.type);
state.attachments.value = clone; state.attachments.value = clone;
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err.toString());
} finally { } finally {
// Clean up progress state
state.attachmentProgress.value = {...state.attachmentProgress.value} state.attachmentProgress.value = {...state.attachmentProgress.value}
..remove(index); ..remove(index);
} }
@@ -643,7 +680,6 @@ class ComposeLogic {
.where((entry) => entry.value.isOnDevice) .where((entry) => entry.value.isOnDevice)
.map((entry) => uploadAttachment(ref, state, entry.key)), .map((entry) => uploadAttachment(ref, state, entry.key)),
); );
// Prepare API request // Prepare API request
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
final isNewPost = originalPost == null; final isNewPost = originalPost == null;

View File

@@ -25,6 +25,10 @@ class ComposeToolbar extends HookConsumerWidget {
ComposeLogic.pickVideoMedia(ref, state); ComposeLogic.pickVideoMedia(ref, state);
} }
void pickGeneralFile() {
ComposeLogic.pickGeneralFile(ref, state);
}
void addAudio() { void addAudio() {
ComposeLogic.recordAudioMedia(ref, state, context); ComposeLogic.recordAudioMedia(ref, state, context);
} }
@@ -96,6 +100,12 @@ class ComposeToolbar extends HookConsumerWidget {
icon: const Icon(Symbols.mic), icon: const Icon(Symbols.mic),
color: colorScheme.primary, color: colorScheme.primary,
), ),
IconButton(
onPressed: pickGeneralFile,
tooltip: 'uploadFile'.tr(),
icon: const Icon(Symbols.file_upload),
color: colorScheme.primary,
),
IconButton( IconButton(
onPressed: linkAttachment, onPressed: linkAttachment,
icon: const Icon(Symbols.attach_file), icon: const Icon(Symbols.attach_file),

View File

@@ -7,7 +7,7 @@ part of 'post_award_history_sheet.dart';
// ************************************************************************** // **************************************************************************
String _$postAwardListNotifierHash() => String _$postAwardListNotifierHash() =>
r'492ae59a5dbbfb5c98f863f036023193b6e08668'; r'834d08f90ef352a2dfb0192455c75b1620e859c2';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {

View File

@@ -247,7 +247,7 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
for (var idx = 0; idx < universalFiles.length; idx++) { for (var idx = 0; idx < universalFiles.length; idx++) {
final file = universalFiles[idx]; final file = universalFiles[idx];
final cloudFile = final cloudFile =
await putMediaToCloud( await putFileToCloud(
fileData: file, fileData: file,
atk: token, atk: token,
baseUrl: serverUrl, baseUrl: serverUrl,

View File

@@ -111,9 +111,9 @@ PODS:
- flutter_udid (0.0.1): - flutter_udid (0.0.1):
- FlutterMacOS - FlutterMacOS
- SAMKeychain - SAMKeychain
- flutter_webrtc (1.1.0): - flutter_webrtc (1.2.0):
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (= 137.7151.03) - WebRTC-SDK (= 137.7151.04)
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- gal (1.0.0): - gal (1.0.0):
- Flutter - Flutter
@@ -175,7 +175,7 @@ PODS:
- livekit_client (2.5.0): - livekit_client (2.5.0):
- flutter_webrtc - flutter_webrtc
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (= 137.7151.03) - WebRTC-SDK (= 137.7151.04)
- local_auth_darwin (0.0.1): - local_auth_darwin (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@@ -247,7 +247,7 @@ PODS:
- FlutterMacOS - FlutterMacOS
- wakelock_plus (0.0.1): - wakelock_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (137.7151.03) - WebRTC-SDK (137.7151.04)
DEPENDENCIES: DEPENDENCIES:
- bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`) - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
@@ -419,17 +419,17 @@ SPEC CHECKSUMS:
flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0 flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0
flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284 flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54 flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
flutter_timezone: d59eea86178cbd7943cd2431cc2eaa9850f935d8 flutter_timezone: d272288c69082ad571630e0d17140b3d6b93dc0c
flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495 flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
flutter_webrtc: 1ce7fe9a42f085286378355a575e682edd7f114d flutter_webrtc: 718eae22a371cd94e5d56aa4f301443ebc5bb737
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
gal: baecd024ebfd13c441269ca7404792a7152fde89 gal: baecd024ebfd13c441269ca7404792a7152fde89
GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
livekit_client: 5a5c0f1081978542bbf9a986c7ac9bffcdb73906 livekit_client: 95b4a47f51f98a8be3a181c3fa251be7823dddd4
local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19 local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
@@ -451,8 +451,8 @@ SPEC CHECKSUMS:
tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166 tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497 wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b
WebRTC-SDK: 69d4e56b0b4b27d788e87bab9b9a1326ed05b1e3 WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
PODFILE CHECKSUM: 346bfb2deb41d4a6ebd6f6799f92188bde2d246f PODFILE CHECKSUM: 346bfb2deb41d4a6ebd6f6799f92188bde2d246f

View File

@@ -6,8 +6,6 @@
<array> <array>
<string>Default</string> <string>Default</string>
</array> </array>
<key>com.apple.developer.device-information.user-assigned-device-name</key>
<true/>
<key>com.apple.security.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true/>
<key>com.apple.security.cs.allow-jit</key> <key>com.apple.security.cs.allow-jit</key>

View File

@@ -6,8 +6,6 @@
<array> <array>
<string>Default</string> <string>Default</string>
</array> </array>
<key>com.apple.developer.device-information.user-assigned-device-name</key>
<true/>
<key>com.apple.security.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true/>
<key>com.apple.security.device.audio-input</key> <key>com.apple.security.device.audio-input</key>

View File

@@ -285,10 +285,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: code_builder name: code_builder
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.10.1" version: "4.11.0"
collection: collection:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -333,10 +333,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: croppy name: croppy
sha256: "2a69059d9ec007b79d6a494854094b2e3c0a4f7ed609cf55a4805c9de9ec171d" sha256: ca70a77cd5a981172d69382a7b43629d15d6c868475b2bbb45efce32cfc58b86
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.6" version: "1.4.0"
cross_file: cross_file:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -401,6 +401,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0+7.7.0" version: "1.0.0+7.7.0"
dart_ipc:
dependency: "direct main"
description:
name: dart_ipc
sha256: "6cad558cda5304017c1f581df4c96fd4f8e4ee212aae7bfa4357716236faa9ba"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:
@@ -413,10 +421,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dart_webrtc name: dart_webrtc
sha256: "3bfa069a8b14a53ba506f6dd529e9b88c878ba0cc238f311051a39bf1e53d075" sha256: "51bcda4ba5d7dd9e65a309244ce3ac0b58025e6e1f6d7442cee4cd02134ef65f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.3+hotfix.5" version: "1.6.0"
dbus: dbus:
dependency: transitive dependency: transitive
description: description:
@@ -441,6 +449,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.3" version: "7.0.3"
diff_match_patch:
dependency: transitive
description:
name: diff_match_patch
sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4"
url: "https://pub.dev"
source: hosted
version: "0.4.1"
dio: dio:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -546,7 +562,7 @@ packages:
source: hosted source: hosted
version: "1.3.3" version: "1.3.3"
ffi: ffi:
dependency: transitive dependency: "direct main"
description: description:
name: ffi name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
@@ -565,10 +581,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: e7e16c9d15c36330b94ca0e2ad8cb61f93cd5282d0158c09805aed13b5452f22 sha256: f2d9f173c2c14635cc0e9b14c143c49ef30b4934e8d1d274d6206fcb0086a06f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.3.2" version: "10.3.3"
file_saver: file_saver:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -709,10 +725,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: fl_chart name: fl_chart
sha256: d3f82f4a38e33ba23d05a08ff304d7d8b22d2a59a5503f20bd802966e915db89 sha256: "7ca9a40f4eb85949190e54087be8b4d6ac09dc4c54238d782a34cf1f7c011de9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -906,10 +922,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_local_notifications name: flutter_local_notifications
sha256: a9966c850de5e445331b854fa42df96a8020066d67f125a5964cbc6556643f68 sha256: "7ed76be64e8a7d01dfdf250b8434618e2a028c9dfa2a3c41dc9b531d4b3fc8a5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "19.4.1" version: "19.4.2"
flutter_local_notifications_linux: flutter_local_notifications_linux:
dependency: transitive dependency: transitive
description: description:
@@ -930,10 +946,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_local_notifications_windows name: flutter_local_notifications_windows
sha256: ed46d7ae4ec9d19e4c8fa2badac5fe27ba87a3fe387343ce726f927af074ec98 sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.0.3"
flutter_localizations: flutter_localizations:
dependency: transitive dependency: transitive
description: flutter description: flutter
@@ -1076,10 +1092,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_timezone name: flutter_timezone
sha256: "13b2109ad75651faced4831bf262e32559e44aa549426eab8a597610d385d934" sha256: ccad42fbb5d01d51d3eb281cc4428fca556cc4063c52bd9fa40f80cd93b8e649
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.1" version: "5.0.0"
flutter_typeahead: flutter_typeahead:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1105,10 +1121,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_webrtc name: flutter_webrtc
sha256: "945d0a38b90fbca8257eadb167d8fb9fa7075d9a1939fd2953c10054454d1de2" sha256: "16ca9e30d428bae3dd32933e875c9f67c5843d1fa726c37cf1fc479eb9294549"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.2.0"
font_awesome_flutter: font_awesome_flutter:
dependency: transitive dependency: transitive
description: description:
@@ -1177,10 +1193,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: eb059dfe59f08546e9787f895bd01652076f996bcbf485a8609ef990419ad227 sha256: b1488741c9ce37b72e026377c69a59c47378493156fc38efb5a54f6def3f92a3
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "16.2.1" version: "16.2.2"
google_fonts: google_fonts:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1262,7 +1278,7 @@ packages:
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
image: image:
dependency: transitive dependency: "direct main"
description: description:
name: image name: image
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
@@ -1281,10 +1297,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: image_picker_android name: image_picker_android
sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e" sha256: "8dfe08ea7fcf7467dbaf6889e72eebd5e0d6711caae201fdac780eb45232cd02"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.13+1" version: "0.8.13+3"
image_picker_for_web: image_picker_for_web:
dependency: transitive dependency: transitive
description: description:
@@ -1393,10 +1409,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "11.0.1" version: "11.0.2"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
@@ -1433,10 +1449,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: livekit_client name: livekit_client
sha256: "011affc0fca22b2f9b0e8827219dad9948f84f2bf057980693de13039de904c7" sha256: "4c1663c1e6ac20a743d9a46c7bc71f17e1949db99d245750c68661d554e30cd2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.0+hotfix.3" version: "2.5.1"
local_auth: local_auth:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1449,18 +1465,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: local_auth_android name: local_auth_android
sha256: "48924f4a8b3cc45994ad5993e2e232d3b00788a305c1bf1c7db32cef281ce9a3" sha256: "1ee0e63fb8b5c6fa286796b5fb1570d256857c2f4a262127e728b36b80a570cf"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.52" version: "1.0.53"
local_auth_darwin: local_auth_darwin:
dependency: transitive dependency: transitive
description: description:
name: local_auth_darwin name: local_auth_darwin
sha256: "0e9706a8543a4a2eee60346294d6a633dd7c3ee60fae6b752570457c4ff32055" sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.6.0" version: "1.6.1"
local_auth_platform_interface: local_auth_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -1537,10 +1553,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: material_symbols_icons name: material_symbols_icons
sha256: "2cfd19bf1c3016b0de7298eb3d3444fcb6ef093d934deb870ceb946af89cfa58" sha256: "9e2042673fda5dda0b77e262220b3c34cac5806a3833da85522e41bb27fbf6c0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2872.0" version: "4.2873.0"
media_kit: media_kit:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1697,10 +1713,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: package_info_plus name: package_info_plus
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.3.1" version: "9.0.0"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -1709,14 +1725,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.1" version: "3.2.1"
palette_generator:
dependency: "direct main"
description:
name: palette_generator
sha256: "4420f7ccc3f0a4a906144e73f8b6267cd940b64f57a7262e95cb8cec3a8ae0ed"
url: "https://pub.dev"
source: hosted
version: "0.3.3+7"
pasteboard: pasteboard:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1873,10 +1881,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: pool name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.1" version: "1.5.2"
posix: posix:
dependency: transitive dependency: transitive
description: description:
@@ -1885,6 +1893,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.3" version: "6.0.3"
pretty_diff_text:
dependency: "direct main"
description:
name: pretty_diff_text
sha256: "2d4decf2bb6dac14c4e7a2cbd2da9ae7e004ccadc1155c723a0ca20a5daa7c57"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
process_run: process_run:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1969,10 +1985,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: record_android name: record_android
sha256: "8361a791c9a3fa5c065f0b8b5adb10f12531f8538c86b19474cf7b56ea80d426" sha256: "854627cd78d8d66190377f98477eee06ca96ab7c9f2e662700daf33dbf7e6673"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.2"
record_ios: record_ios:
dependency: transitive dependency: transitive
description: description:
@@ -2137,10 +2153,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: share_plus name: share_plus
sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1 sha256: "3424e9d5c22fd7f7590254ba09465febd6f8827c8b19a44350de4ac31d92d3a6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "11.1.0" version: "12.0.0"
share_plus_platform_interface: share_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -2576,10 +2592,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7" sha256: "199bc33e746088546a39cc5f36bac5a278c5e53b40cb3196f99e7345fdcfae6b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.18" version: "6.3.22"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
@@ -2704,18 +2720,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: wakelock_plus name: wakelock_plus
sha256: a474e314c3e8fb5adef1f9ae2d247e57467ad557fa7483a2b895bc1b421c5678 sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.4.0"
wakelock_plus_platform_interface: wakelock_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: wakelock_plus_platform_interface name: wakelock_plus_platform_interface
sha256: e10444072e50dbc4999d7316fd303f7ea53d31c824aa5eb05d7ccbdd98985207 sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.3" version: "1.3.0"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
@@ -2765,7 +2781,7 @@ packages:
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
win32: win32:
dependency: transitive dependency: "direct main"
description: description:
name: win32 name: win32
sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
@@ -2780,6 +2796,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
windows_notification:
dependency: "direct main"
description:
name: windows_notification
sha256: be3e650874615f315402c9b9f3656e29af156709c4b5cc272cb4ca0ab7ba94a8
url: "https://pub.dev"
source: hosted
version: "1.3.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@@ -2806,4 +2830,4 @@ packages:
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.9.0 <4.0.0" dart: ">=3.9.0 <4.0.0"
flutter: ">=3.32.0" flutter: ">=3.35.0"

View File

@@ -39,7 +39,7 @@ dependencies:
flutter_hooks: ^0.21.3+1 flutter_hooks: ^0.21.3+1
hooks_riverpod: ^2.6.1 hooks_riverpod: ^2.6.1
bitsdojo_window: ^0.1.6 bitsdojo_window: ^0.1.6
go_router: ^16.2.1 go_router: ^16.2.2
styled_widget: ^0.4.1 styled_widget: ^0.4.1
shared_preferences: ^2.5.3 shared_preferences: ^2.5.3
flutter_riverpod: ^2.6.1 flutter_riverpod: ^2.6.1
@@ -67,29 +67,29 @@ dependencies:
easy_localization: ^3.0.8 easy_localization: ^3.0.8
flutter_inappwebview: ^6.1.5 flutter_inappwebview: ^6.1.5
animations: ^2.0.11 animations: ^2.0.11
package_info_plus: ^8.3.1 package_info_plus: ^9.0.0
device_info_plus: ^11.5.0 device_info_plus: ^11.5.0
tus_client_dart: tus_client_dart:
git: https://github.com/LittleSheep2Code/tus_client.git git: https://github.com/LittleSheep2Code/tus_client.git
cross_file: ^0.3.4+2 cross_file: ^0.3.4+2
image_picker: ^1.2.0 image_picker: ^1.2.0
file_picker: ^10.3.2 file_picker: ^10.3.3
riverpod_annotation: ^2.6.1 riverpod_annotation: ^2.6.1
image_picker_platform_interface: ^2.11.0 image_picker_platform_interface: ^2.11.0
image_picker_android: ^0.8.13+1 image_picker_android: ^0.8.13+3
super_context_menu: ^0.9.1 super_context_menu: ^0.9.1
modal_bottom_sheet: ^3.0.0 modal_bottom_sheet: ^3.0.0
firebase_messaging: ^16.0.1 firebase_messaging: ^16.0.1
flutter_udid: ^4.0.0 flutter_udid: ^4.0.0
firebase_core: ^4.1.0 firebase_core: ^4.1.0
web_socket_channel: ^3.0.3 web_socket_channel: ^3.0.3
material_symbols_icons: ^4.2872.0 material_symbols_icons: ^4.2873.0
drift: ^2.28.1 drift: ^2.28.1
drift_flutter: ^0.2.6 drift_flutter: ^0.2.6
path: ^1.9.1 path: ^1.9.1
collection: ^1.19.1 collection: ^1.19.1
markdown_editor_plus: ^0.2.15 markdown_editor_plus: ^0.2.15
croppy: ^1.3.6 croppy: ^1.4.0
table_calendar: ^3.2.0 table_calendar: ^3.2.0
relative_time: ^5.0.0 relative_time: ^5.0.0
dropdown_button2: ^2.3.9 dropdown_button2: ^2.3.9
@@ -103,24 +103,25 @@ dependencies:
gal: ^2.3.2 gal: ^2.3.2
dismissible_page: ^1.0.2 dismissible_page: ^1.0.2
super_sliver_list: ^0.4.1 super_sliver_list: ^0.4.1
livekit_client: ^2.5.0+hotfix.3 livekit_client: ^2.5.1
pasteboard: ^0.4.0 pasteboard: ^0.4.0
flutter_colorpicker: ^1.1.0 flutter_colorpicker: ^1.1.0
image: ^4.5.4
record: ^6.1.1 record: ^6.1.1
qr_flutter: ^4.1.0 qr_flutter: ^4.1.0
flutter_otp_text_field: ^1.5.1+1 flutter_otp_text_field: ^1.5.1+1
palette_generator: ^0.3.3+7
flutter_popup_card: ^0.0.6 flutter_popup_card: ^0.0.6
timezone: ^0.10.1 timezone: ^0.10.1
flutter_timezone: ^4.1.1 flutter_timezone: ^5.0.0
fl_chart: ^1.1.0 fl_chart: ^1.1.1
sign_in_with_apple: ^7.0.1 sign_in_with_apple: ^7.0.1
flutter_svg: ^2.2.1 flutter_svg: ^2.2.1
native_exif: ^0.6.2 native_exif: ^0.6.2
local_auth: ^2.3.0 local_auth: ^2.3.0
flutter_secure_storage: ^9.2.4 flutter_secure_storage: ^9.2.4
flutter_math_fork: ^0.7.4 flutter_math_fork: ^0.7.4
share_plus: ^11.1.0 share_plus: ^12.0.0
receive_sharing_intent: ^1.8.1 receive_sharing_intent: ^1.8.1
top_snackbar_flutter: ^3.3.0 top_snackbar_flutter: ^3.3.0
textfield_tags: textfield_tags:
@@ -141,12 +142,17 @@ dependencies:
flutter_card_swiper: ^7.0.2 flutter_card_swiper: ^7.0.2
file_saver: ^0.3.1 file_saver: ^0.3.1
tray_manager: ^0.5.1 tray_manager: ^0.5.1
flutter_webrtc: ^1.1.0 flutter_webrtc: ^1.2.0
flutter_local_notifications: ^19.4.1 flutter_local_notifications: ^19.4.2
wakelock_plus: ^1.3.2 wakelock_plus: ^1.4.0
slide_countdown: ^2.0.2 slide_countdown: ^2.0.2
shelf: ^1.4.2 shelf: ^1.4.2
shelf_web_socket: ^3.0.0 shelf_web_socket: ^3.0.0
windows_notification: ^1.3.0
win32: ^5.14.0
ffi: ^2.1.4
dart_ipc: ^1.0.1
pretty_diff_text: ^2.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@@ -1,6 +1,6 @@
; ================================================== ; ==================================================
#define AppVersion "3.2.0" #define AppVersion "3.2.0"
#define BuildNumber "124" #define BuildNumber "132"
; ================================================== ; ==================================================
#define FullVersion AppVersion + "." + BuildNumber #define FullVersion AppVersion + "." + BuildNumber

View File

@@ -8,6 +8,7 @@
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h> #include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
#include <connectivity_plus/connectivity_plus_windows_plugin.h> #include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <dart_ipc/dart_ipc_plugin_c_api.h>
#include <file_saver/file_saver_plugin.h> #include <file_saver/file_saver_plugin.h>
#include <file_selector_windows/file_selector_windows.h> #include <file_selector_windows/file_selector_windows.h>
#include <firebase_core/firebase_core_plugin_c_api.h> #include <firebase_core/firebase_core_plugin_c_api.h>
@@ -31,12 +32,15 @@
#include <tray_manager/tray_manager_plugin.h> #include <tray_manager/tray_manager_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
#include <volume_controller/volume_controller_plugin_c_api.h> #include <volume_controller/volume_controller_plugin_c_api.h>
#include <windows_notification/windows_notification_plugin_c_api.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
BitsdojoWindowPluginRegisterWithRegistrar( BitsdojoWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
ConnectivityPlusWindowsPluginRegisterWithRegistrar( ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
DartIpcPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DartIpcPluginCApi"));
FileSaverPluginRegisterWithRegistrar( FileSaverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSaverPlugin")); registry->GetRegistrarForPlugin("FileSaverPlugin"));
FileSelectorWindowsRegisterWithRegistrar( FileSelectorWindowsRegisterWithRegistrar(
@@ -83,4 +87,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
VolumeControllerPluginCApiRegisterWithRegistrar( VolumeControllerPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("VolumeControllerPluginCApi")); registry->GetRegistrarForPlugin("VolumeControllerPluginCApi"));
WindowsNotificationPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowsNotificationPluginCApi"));
} }

View File

@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
bitsdojo_window_windows bitsdojo_window_windows
connectivity_plus connectivity_plus
dart_ipc
file_saver file_saver
file_selector_windows file_selector_windows
firebase_core firebase_core
@@ -28,6 +29,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
tray_manager tray_manager
url_launcher_windows url_launcher_windows
volume_controller volume_controller
windows_notification
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST