Compare commits
	
		
			58 Commits
		
	
	
		
			8578cde620
			...
			3.2.0+134
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 28b6eade48 | |||
| 1de7ef8c96 | |||
| 67eac5dcf5 | |||
| 7a44bfa075 | |||
| 1c2f25a152 | |||
| be26ea280e | |||
| b4996d069f | |||
| bf4892b34d | |||
| 5f84751fd5 | |||
| 457d1bac60 | |||
| 02ec11845b | |||
| 612f1bf004 | |||
| fd80b713ad | |||
| 508805368c | |||
| 98eb28a4ec | |||
| d1a2f59dd1 | |||
| bb9adb963a | |||
| 83e40cd860 | |||
| c06fb12f6a | |||
| 6600cf4df8 | |||
| 4293daaa2f | |||
| 866674ddde | |||
| 27d478ba4f | |||
| cccade763f | |||
| f760b85186 | |||
| e68c5f4f92 | |||
| b0f3b6b5c3 | |||
| cb2af379fa | |||
| 38f8103265 | |||
| 06bb18bdaa | |||
| 84c38500d0 | |||
| 9529bbf08b | |||
| 8baf77bcf7 | |||
| b2ac5fbef2 | |||
| c79b1d7aab | |||
|  | 4f55a8209c | ||
|  | ace302111a | ||
|  | 1391fa0dde | ||
|  | cbdc7acdcd | ||
|  | b80d91825a | ||
|  | 1a703b7eba | ||
|  | 3621ea7744 | ||
|  | b638343f02 | ||
|  | 269a64cabb | ||
| 406e5187a8 | |||
| 9bdd08d8dd | |||
| d737232dcf | |||
| c9d751479e | |||
| a2c2bfe585 | |||
| c7f9da0dee | |||
|  | a243cda1df | ||
|  | 7b238f32fd | ||
| 313af28d7f | |||
| c64e1e208c | |||
| c9b07a9a2a | |||
| 55c0e355f1 | |||
| be414891ec | |||
| 787876ab6a | 
| @@ -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", | ||||||
| @@ -261,14 +264,14 @@ | |||||||
|   "createStickerPack": "Create a Sticker Pack", |   "createStickerPack": "Create a Sticker Pack", | ||||||
|   "editStickerPack": "Edit Sticker Pack", |   "editStickerPack": "Edit Sticker Pack", | ||||||
|   "deleteStickerPack": "Delete Sticker Pack", |   "deleteStickerPack": "Delete Sticker Pack", | ||||||
|   "deleteStickerPackHint": "Are you sure to delete this sticker pack? This action cannot be undone.", |   "deleteStickerPackHint": "Are you sure you want to delete this sticker pack? This action cannot be undone.", | ||||||
|   "stickerPackPrefix": "Prefix", |   "stickerPackPrefix": "Prefix", | ||||||
|   "stickerPackPrefixHint": "The prefix will be added before each stickers' slug in this pack.", |   "stickerPackPrefixHint": "The prefix will be added before each stickers' slug in this pack.", | ||||||
|   "stickers": "Stickers", |   "stickers": "Stickers", | ||||||
|   "createSticker": "Create a Sticker", |   "createSticker": "Create a Sticker", | ||||||
|   "editSticker": "Edit Sticker", |   "editSticker": "Edit Sticker", | ||||||
|   "deleteSticker": "Delete Sticker", |   "deleteSticker": "Delete Sticker", | ||||||
|   "deleteStickerHint": "Are you sure to delete this sticker? This action cannot be undone.", |   "deleteStickerHint": "Are you sure you want to delete this sticker? This action cannot be undone.", | ||||||
|   "stickerImage": "Image", |   "stickerImage": "Image", | ||||||
|   "stickerSlug": "Slug", |   "stickerSlug": "Slug", | ||||||
|   "stickerSlugHint": "The slug will be combined with the prefix to form the sticker's unique identifier.", |   "stickerSlugHint": "The slug will be combined with the prefix to form the sticker's unique identifier.", | ||||||
| @@ -333,13 +336,25 @@ | |||||||
|   "levelingProgress": "Leveling Progress", |   "levelingProgress": "Leveling Progress", | ||||||
|   "levelingProgressExperience": "{} EXP", |   "levelingProgressExperience": "{} EXP", | ||||||
|   "levelingProgressLevel": "Level {}", |   "levelingProgressLevel": "Level {}", | ||||||
|  |   "levelingStage1": "Novice", | ||||||
|  |   "levelingStage2": "Apprentice", | ||||||
|  |   "levelingStage3": "Journeyman", | ||||||
|  |   "levelingStage4": "Adept", | ||||||
|  |   "levelingStage5": "Expert", | ||||||
|  |   "levelingStage6": "Master", | ||||||
|  |   "levelingStage7": "Grandmaster", | ||||||
|  |   "levelingStage8": "Legend", | ||||||
|  |   "levelingStage9": "Myth", | ||||||
|  |   "levelingStage10": "Immortal", | ||||||
|  |   "levelingStage11": "Divine", | ||||||
|  |   "levelingStage12": "Transcendent", | ||||||
|   "fileUploadingProgress": "Uploading file #{}: {}%", |   "fileUploadingProgress": "Uploading file #{}: {}%", | ||||||
|   "removeChatMember": "Remove Chat Room Member", |   "removeChatMember": "Remove Chat Room Member", | ||||||
|   "removeChatMemberHint": "Are you sure to remove this member from the room?", |   "removeChatMemberHint": "Are you sure you want to remove this member from the room?", | ||||||
|   "removeRealmMember": "Remove Realm Member", |   "removeRealmMember": "Remove Realm Member", | ||||||
|   "removeRealmMemberHint": "Are you sure to remove this member from the realm?", |   "removeRealmMemberHint": "Are you sure you want to remove this member from the realm?", | ||||||
|   "removePublisherMember": "Remove Publisher Member", |   "removePublisherMember": "Remove Publisher Member", | ||||||
|   "removePublisherMemberHint": "Are you sure to remove this member from the publisher?", |   "removePublisherMemberHint": "Are you sure you want to remove this member from the publisher?", | ||||||
|   "memberRole": "Member Role", |   "memberRole": "Member Role", | ||||||
|   "memberRoleHint": "Greater number has higher permission.", |   "memberRoleHint": "Greater number has higher permission.", | ||||||
|   "memberRoleEdit": "Edit role for @{}", |   "memberRoleEdit": "Edit role for @{}", | ||||||
| @@ -348,9 +363,9 @@ | |||||||
|   "brokenLink": "Unable open link {}... It might be broken or missing uri parts...", |   "brokenLink": "Unable open link {}... It might be broken or missing uri parts...", | ||||||
|   "copyToClipboard": "Copy to clipboard", |   "copyToClipboard": "Copy to clipboard", | ||||||
|   "leaveChatRoom": "Leave Chat Room", |   "leaveChatRoom": "Leave Chat Room", | ||||||
|   "leaveChatRoomHint": "Are you sure to leave this chat room?", |   "leaveChatRoomHint": "Are you sure you want to leave this chat room?", | ||||||
|   "leaveRealm": "Leave Realm", |   "leaveRealm": "Leave Realm", | ||||||
|   "leaveRealmHint": "Are you sure to leave this realm?", |   "leaveRealmHint": "Are you sure you want to leave this realm?", | ||||||
|   "walletNotFound": "Wallet not found", |   "walletNotFound": "Wallet not found", | ||||||
|   "walletCreateHint": "You don't have a wallet yet. Create one to start using the Solar Network eWallet.", |   "walletCreateHint": "You don't have a wallet yet. Create one to start using the Solar Network eWallet.", | ||||||
|   "walletCreate": "Create a Wallet", |   "walletCreate": "Create a Wallet", | ||||||
| @@ -447,11 +462,13 @@ | |||||||
|   "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..." | ||||||
|   }, |   }, | ||||||
|   "authDeviceEditLabel": "Edit Label", |   "authDeviceEditLabel": "Edit Device Label", | ||||||
|   "authDeviceLabelTitle": "Edit Device Label", |   "authDeviceLabelTitle": "Edit Device Label", | ||||||
|   "authDeviceLabelHint": "Enter a name for this device", |   "authDeviceLabelHint": "Enter a name for this device", | ||||||
|   "authDeviceSwipeEditHint": "Swipe left to edit label", |   "authDeviceSwipeEditHint": "Swipe left to edit label", | ||||||
| @@ -468,6 +485,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", | ||||||
| @@ -518,7 +536,7 @@ | |||||||
|   "contactMethodPrimary": "Primary", |   "contactMethodPrimary": "Primary", | ||||||
|   "contactMethodSetPrimary": "Set as Primary", |   "contactMethodSetPrimary": "Set as Primary", | ||||||
|   "contactMethodSetPrimaryHint": "Set this contact method as your primary contact method for account recovery and notifications", |   "contactMethodSetPrimaryHint": "Set this contact method as your primary contact method for account recovery and notifications", | ||||||
|   "contactMethodDeleteHint": "Are you sure to delete this contact method? This action cannot be undone.", |   "contactMethodDeleteHint": "Are you sure you want to delete this contact method? This action cannot be undone.", | ||||||
|   "contactMethodMakePublic": "Make Public", |   "contactMethodMakePublic": "Make Public", | ||||||
|   "contactMethodMakePrivate": "Make Private", |   "contactMethodMakePrivate": "Make Private", | ||||||
|   "contactMethodPublic": "Public", |   "contactMethodPublic": "Public", | ||||||
| @@ -552,6 +570,7 @@ | |||||||
|   "checkInResultT2": "Mid", |   "checkInResultT2": "Mid", | ||||||
|   "checkInResultT3": "Good", |   "checkInResultT3": "Good", | ||||||
|   "checkInResultT4": "Best", |   "checkInResultT4": "Best", | ||||||
|  |   "checkInResultT5": "Birthday", | ||||||
|   "accountProfileView": "View Profile", |   "accountProfileView": "View Profile", | ||||||
|   "unspecified": "Unspecified", |   "unspecified": "Unspecified", | ||||||
|   "added": "Added", |   "added": "Added", | ||||||
| @@ -644,8 +663,6 @@ | |||||||
|   "abuseReportSuccess": "Report submitted successfully. Thank you for helping keep our community safe.", |   "abuseReportSuccess": "Report submitted successfully. Thank you for helping keep our community safe.", | ||||||
|   "abuseReportError": "Failed to submit report. Please try again.", |   "abuseReportError": "Failed to submit report. Please try again.", | ||||||
|   "abuseReportReasonRequired": "Please provide details about the issue", |   "abuseReportReasonRequired": "Please provide details about the issue", | ||||||
|   "abuseReportSuccessTitle": "Report Submitted", |  | ||||||
|   "abuseReportErrorTitle": "Error", |  | ||||||
|   "abuseReportTypeSpam": "Spam or Misleading", |   "abuseReportTypeSpam": "Spam or Misleading", | ||||||
|   "abuseReportTypeHarassment": "Harassment or Abuse", |   "abuseReportTypeHarassment": "Harassment or Abuse", | ||||||
|   "abuseReportTypeInappropriate": "Inappropriate Content", |   "abuseReportTypeInappropriate": "Inappropriate Content", | ||||||
| @@ -827,11 +844,6 @@ | |||||||
|   "postCategorySports": "Sports", |   "postCategorySports": "Sports", | ||||||
|   "postCategoryFinance": "Finance", |   "postCategoryFinance": "Finance", | ||||||
|   "postCategoryLife": "Life", |   "postCategoryLife": "Life", | ||||||
|   "postCategoryArt": "Art", |  | ||||||
|   "postCategoryStudy": "Study", |  | ||||||
|   "postCategoryGaming": "Gaming", |  | ||||||
|   "postCategoryProgramming": "Programming", |  | ||||||
|   "postCategoryMusic": "Music", |  | ||||||
|   "links": "Links", |   "links": "Links", | ||||||
|   "addLink": "Add link", |   "addLink": "Add link", | ||||||
|   "linkKey": "Link Name", |   "linkKey": "Link Name", | ||||||
| @@ -890,6 +902,15 @@ | |||||||
|   "attachmentOnDevice": "On-device", |   "attachmentOnDevice": "On-device", | ||||||
|   "attachmentOnCloud": "On-cloud", |   "attachmentOnCloud": "On-cloud", | ||||||
|   "attachments": "Attachments", |   "attachments": "Attachments", | ||||||
|  |   "uploadAttachment": "Upload Attachment", | ||||||
|  |   "attachmentPreview": "Attachment Preview", | ||||||
|  |   "selectPool": "Select Pool", | ||||||
|  |   "choosePool": "Choose a pool", | ||||||
|  |   "errorLoadingPools": "Error loading pools", | ||||||
|  |   "quotaCostInfo": "This upload will cost {} quota points", | ||||||
|  |   "uploadConstraints": "Upload Constraints", | ||||||
|  |   "fileSizeExceeded": "File size exceeds the maximum limit of {}", | ||||||
|  |   "fileTypeNotAccepted": "File type is not accepted by this pool", | ||||||
|   "publisherCollabInvitation": "Collabration invitations", |   "publisherCollabInvitation": "Collabration invitations", | ||||||
|   "publisherCollabInvitationCount": { |   "publisherCollabInvitationCount": { | ||||||
|     "zero": "No invitation", |     "zero": "No invitation", | ||||||
| @@ -1006,6 +1027,11 @@ | |||||||
|   "expandPoll": "Expand Poll", |   "expandPoll": "Expand Poll", | ||||||
|   "collapsePoll": "Collapse Poll", |   "collapsePoll": "Collapse Poll", | ||||||
|   "embedView": "Embed View", |   "embedView": "Embed View", | ||||||
|  |   "auto": "Auto", | ||||||
|  |   "manual": "Manual", | ||||||
|  |   "iframeCode": "Iframe Code", | ||||||
|  |   "iframeCodeHint": "<iframe src=\"...\" width=\"...\" height=\"...\">", | ||||||
|  |   "parseIframe": "Parse Iframe", | ||||||
|   "embedUri": "Embed URI", |   "embedUri": "Embed URI", | ||||||
|   "aspectRatio": "Aspect Ratio", |   "aspectRatio": "Aspect Ratio", | ||||||
|   "renderer": "Renderer", |   "renderer": "Renderer", | ||||||
| @@ -1016,5 +1042,26 @@ | |||||||
|   "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", | ||||||
|  |   "viewEmbedLoadHint": "Tap to load", | ||||||
|  |   "files": "Files", | ||||||
|  |   "confirmDeleteFile": "Are you sure you want to delete this file?", | ||||||
|  |   "deleteFile": "Delete File", | ||||||
|  |   "failedToDeleteFile": "Failed to delete file", | ||||||
|  |   "drive": "Drive", | ||||||
|  |   "allPools": "All Pools", | ||||||
|  |   "includeRecycled": "Include Recycled", | ||||||
|  |   "confirmDeleteRecycledFiles": "Are you sure you want to delete all recycled files?", | ||||||
|  |   "deleteRecycledFiles": "Delete Recycled Files", | ||||||
|  |   "recycledFilesDeleted": "Recycled files deleted successfully", | ||||||
|  |   "failedToDeleteRecycledFiles": "Failed to delete recycled files", | ||||||
|  |   "upload": "Upload", | ||||||
|  |   "fileInfoTitle": "File Information", | ||||||
|  |   "fileHashCopied": "File hash copied to clipboard", | ||||||
|  |   "fileIdCopied": "File ID copied to clipboard", | ||||||
|  |   "fileNameCopied": "File name copied to clipboard", | ||||||
|  |   "fileMetadata": "File Metadata", | ||||||
|  |   "userMetadata": "User Metadata", | ||||||
|  |   "valueCopied": "Value copied to clipboard" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -122,6 +122,9 @@ | |||||||
|   "addVideo": "添加视频", |   "addVideo": "添加视频", | ||||||
|   "addPhoto": "添加照片", |   "addPhoto": "添加照片", | ||||||
|   "addFile": "添加文件", |   "addFile": "添加文件", | ||||||
|  |   "uploadFile": "上传文件", | ||||||
|  |   "settingsDefaultPool": "选择文件池", | ||||||
|  |   "settingsDefaultPoolHelper": "为文件上传选择一个默认池", | ||||||
|   "createDirectMessage": "创建新私人消息", |   "createDirectMessage": "创建新私人消息", | ||||||
|   "gotoDirectMessage": "前往私信", |   "gotoDirectMessage": "前往私信", | ||||||
|   "react": "反应", |   "react": "反应", | ||||||
| @@ -280,6 +283,18 @@ | |||||||
|   "levelingProgress": "等级进度", |   "levelingProgress": "等级进度", | ||||||
|   "levelingProgressExperience": "{} 经验值", |   "levelingProgressExperience": "{} 经验值", | ||||||
|   "levelingProgressLevel": "等级 {}", |   "levelingProgressLevel": "等级 {}", | ||||||
|  |   "levelingStage1": "新手", | ||||||
|  |   "levelingStage2": "学徒", | ||||||
|  |   "levelingStage3": "熟练工", | ||||||
|  |   "levelingStage4": "行家", | ||||||
|  |   "levelingStage5": "专家", | ||||||
|  |   "levelingStage6": "大师", | ||||||
|  |   "levelingStage7": "宗师", | ||||||
|  |   "levelingStage8": "传奇", | ||||||
|  |   "levelingStage9": "神话", | ||||||
|  |   "levelingStage10": "不朽", | ||||||
|  |   "levelingStage11": "神圣", | ||||||
|  |   "levelingStage12": "超凡", | ||||||
|   "fileUploadingProgress": "正在上传文件 #{}: {}%", |   "fileUploadingProgress": "正在上传文件 #{}: {}%", | ||||||
|   "removeChatMember": "移除聊天室成员", |   "removeChatMember": "移除聊天室成员", | ||||||
|   "removeChatMemberHint": "确定要将此成员从聊天室中移除吗?", |   "removeChatMemberHint": "确定要将此成员从聊天室中移除吗?", | ||||||
|   | |||||||
| @@ -122,6 +122,10 @@ | |||||||
|     "addVideo": "添加視頻", |     "addVideo": "添加視頻", | ||||||
|     "addPhoto": "添加照片", |     "addPhoto": "添加照片", | ||||||
|     "addFile": "添加文件", |     "addFile": "添加文件", | ||||||
|  |     "uploadFile": "上傳文件", | ||||||
|  |     "settingsDefaultPool": "選擇文件池", | ||||||
|  |     "settingsDefaultPoolHelper": "爲文件上傳選擇一個默認池", | ||||||
|  |   | ||||||
|     "createDirectMessage": "創建新私人消息", |     "createDirectMessage": "創建新私人消息", | ||||||
|     "gotoDirectMessage": "前往私信", |     "gotoDirectMessage": "前往私信", | ||||||
|     "react": "反應", |     "react": "反應", | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								assets/images/stickers/angry.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/images/stickers/angry.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.0 MiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/stickers/clap.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/images/stickers/clap.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.0 MiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/stickers/confuse.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/images/stickers/confuse.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 668 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/stickers/party.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/images/stickers/party.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.1 MiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/stickers/pray.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/images/stickers/pray.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 666 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/stickers/thumb_up.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/images/stickers/thumb_up.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 623 KiB | 
| @@ -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 | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								drift_schemas/app_database/drift_schema_v6.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								drift_schemas/app_database/drift_schema_v6.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -50,18 +50,18 @@ PODS: | |||||||
|   - Firebase/Messaging (12.2.0): |   - Firebase/Messaging (12.2.0): | ||||||
|     - Firebase/CoreOnly |     - Firebase/CoreOnly | ||||||
|     - FirebaseMessaging (~> 12.2.0) |     - FirebaseMessaging (~> 12.2.0) | ||||||
|   - firebase_analytics (12.0.1): |   - firebase_analytics (12.0.2): | ||||||
|     - firebase_core |     - firebase_core | ||||||
|     - FirebaseAnalytics (= 12.2.0) |     - FirebaseAnalytics (= 12.2.0) | ||||||
|     - Flutter |     - Flutter | ||||||
|   - firebase_core (4.1.0): |   - firebase_core (4.1.1): | ||||||
|     - Firebase/CoreOnly (= 12.2.0) |     - Firebase/CoreOnly (= 12.2.0) | ||||||
|     - Flutter |     - Flutter | ||||||
|   - firebase_crashlytics (5.0.1): |   - firebase_crashlytics (5.0.2): | ||||||
|     - Firebase/Crashlytics (= 12.2.0) |     - Firebase/Crashlytics (= 12.2.0) | ||||||
|     - firebase_core |     - firebase_core | ||||||
|     - Flutter |     - Flutter | ||||||
|   - firebase_messaging (16.0.1): |   - firebase_messaging (16.0.2): | ||||||
|     - Firebase/Messaging (= 12.2.0) |     - Firebase/Messaging (= 12.2.0) | ||||||
|     - firebase_core |     - firebase_core | ||||||
|     - Flutter |     - Flutter | ||||||
| @@ -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 | ||||||
| @@ -476,10 +476,10 @@ SPEC CHECKSUMS: | |||||||
|   file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be |   file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be | ||||||
|   file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 |   file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 | ||||||
|   Firebase: 26f6f8d460603af3df970ad505b16b15f5e2e9a1 |   Firebase: 26f6f8d460603af3df970ad505b16b15f5e2e9a1 | ||||||
|   firebase_analytics: 111ff65791a430356bd6c7e4d7339537fc6a15ae |   firebase_analytics: 8c78ce6224e0623152379d6cc7ef3d9098477b7e | ||||||
|   firebase_core: 3ff52146406557dddd01d570e807e203ec7e1302 |   firebase_core: dfc4bd142bee4bc53a5d482397ca322c2dd3165d | ||||||
|   firebase_crashlytics: 3637078b718a52dc9fb4d64e37c969e86b87ff6f |   firebase_crashlytics: e55dcf895eed0dd87c447dd5aff8db7f1bb8bbdb | ||||||
|   firebase_messaging: 3dcc998dd98e1e54af75d0cccae8606eba43553c |   firebase_messaging: 38c66c1184695b0c87abe51d40fc590718abed1a | ||||||
|   FirebaseAnalytics: e04e23bc070e3014aa5cf4980f9df7ce5cd79ec8 |   FirebaseAnalytics: e04e23bc070e3014aa5cf4980f9df7ce5cd79ec8 | ||||||
|   FirebaseCore: 311c48a147ad4a0ab7febbaed89e8025c67510cd |   FirebaseCore: 311c48a147ad4a0ab7febbaed89e8025c67510cd | ||||||
|   FirebaseCoreExtension: 73af080c22a2f7b44cefa391dc08f7e4ee162cb5 |   FirebaseCoreExtension: 73af080c22a2f7b44cefa391dc08f7e4ee162cb5 | ||||||
| @@ -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 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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"; | ||||||
|   | |||||||
| @@ -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,29 @@ 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, ignore if they already exist | ||||||
|  |         final columnsToAdd = [ | ||||||
|  |           chatMessages.updatedAt, | ||||||
|  |           chatMessages.deletedAt, | ||||||
|  |           chatMessages.type, | ||||||
|  |           chatMessages.meta, | ||||||
|  |           chatMessages.membersMentioned, | ||||||
|  |           chatMessages.editedAt, | ||||||
|  |           chatMessages.attachments, | ||||||
|  |           chatMessages.reactions, | ||||||
|  |           chatMessages.repliedMessageId, | ||||||
|  |           chatMessages.forwardedMessageId, | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         for (final column in columnsToAdd) { | ||||||
|  |           try { | ||||||
|  |             await m.addColumn(chatMessages, column); | ||||||
|  |           } catch (e) { | ||||||
|  |             // Column already exists, skip | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
| @@ -116,12 +139,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 +151,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 +183,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 +216,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
											
										
									
								
							| @@ -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, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -98,6 +98,7 @@ sealed class SnAccountStatus with _$SnAccountStatus { | |||||||
|     required bool isNotDisturb, |     required bool isNotDisturb, | ||||||
|     required bool isCustomized, |     required bool isCustomized, | ||||||
|     @Default("") String label, |     @Default("") String label, | ||||||
|  |     required Map<String, dynamic>? meta, | ||||||
|     required DateTime? clearedAt, |     required DateTime? clearedAt, | ||||||
|     required String accountId, |     required String accountId, | ||||||
|     required DateTime createdAt, |     required DateTime createdAt, | ||||||
|   | |||||||
| @@ -1053,7 +1053,7 @@ $SnVerificationMarkCopyWith<$Res>? get verification { | |||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnAccountStatus { | mixin _$SnAccountStatus { | ||||||
|  |  | ||||||
|  String get id; int get attitude; bool get isOnline; bool get isInvisible; bool get isNotDisturb; bool get isCustomized; String get label; DateTime? get clearedAt; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; |  String get id; int get attitude; bool get isOnline; bool get isInvisible; bool get isNotDisturb; bool get isCustomized; String get label; Map<String, dynamic>? get meta; DateTime? get clearedAt; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||||
| /// Create a copy of SnAccountStatus | /// Create a copy of SnAccountStatus | ||||||
| /// 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) | ||||||
| @@ -1066,16 +1066,16 @@ $SnAccountStatusCopyWith<SnAccountStatus> get copyWith => _$SnAccountStatusCopyW | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(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) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,clearedAt,accountId,createdAt,updatedAt,deletedAt); | int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,const DeepCollectionEquality().hash(meta),clearedAt,accountId,createdAt,updatedAt,deletedAt); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; |   return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, meta: $meta, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -1086,7 +1086,7 @@ abstract mixin class $SnAccountStatusCopyWith<$Res>  { | |||||||
|   factory $SnAccountStatusCopyWith(SnAccountStatus value, $Res Function(SnAccountStatus) _then) = _$SnAccountStatusCopyWithImpl; |   factory $SnAccountStatusCopyWith(SnAccountStatus value, $Res Function(SnAccountStatus) _then) = _$SnAccountStatusCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt |  String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map<String, dynamic>? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -1103,7 +1103,7 @@ class _$SnAccountStatusCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnAccountStatus | /// Create a copy of SnAccountStatus | ||||||
| /// 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? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? meta = freezed,Object? clearedAt = 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,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable | as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -1112,7 +1112,8 @@ as bool,isInvisible: null == isInvisible ? _self.isInvisible : isInvisible // ig | |||||||
| as bool,isNotDisturb: null == isNotDisturb ? _self.isNotDisturb : isNotDisturb // ignore: cast_nullable_to_non_nullable | as bool,isNotDisturb: null == isNotDisturb ? _self.isNotDisturb : isNotDisturb // ignore: cast_nullable_to_non_nullable | ||||||
| as bool,isCustomized: null == isCustomized ? _self.isCustomized : isCustomized // ignore: cast_nullable_to_non_nullable | as bool,isCustomized: null == isCustomized ? _self.isCustomized : isCustomized // ignore: cast_nullable_to_non_nullable | ||||||
| as bool,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable | as bool,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable | ||||||
| as String,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable | as String,meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable | ||||||
|  | as Map<String, dynamic>?,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // 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,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | as String,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 | ||||||
| @@ -1199,10 +1200,10 @@ return $default(_that);case _: | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  int attitude,  bool isOnline,  bool isInvisible,  bool isNotDisturb,  bool isCustomized,  String label,  DateTime? clearedAt,  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,  int attitude,  bool isOnline,  bool isInvisible,  bool isNotDisturb,  bool isCustomized,  String label,  Map<String, dynamic>? meta,  DateTime? clearedAt,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAccountStatus() when $default != null: | case _SnAccountStatus() when $default != null: | ||||||
| return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.meta,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
|   return orElse(); |   return orElse(); | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -1220,10 +1221,10 @@ return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.i | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  int attitude,  bool isOnline,  bool isInvisible,  bool isNotDisturb,  bool isCustomized,  String label,  DateTime? clearedAt,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  int attitude,  bool isOnline,  bool isInvisible,  bool isNotDisturb,  bool isCustomized,  String label,  Map<String, dynamic>? meta,  DateTime? clearedAt,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAccountStatus(): | case _SnAccountStatus(): | ||||||
| return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);} | return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.meta,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||||
| } | } | ||||||
| /// A variant of `when` that fallback to returning `null` | /// A variant of `when` that fallback to returning `null` | ||||||
| /// | /// | ||||||
| @@ -1237,10 +1238,10 @@ return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.i | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  int attitude,  bool isOnline,  bool isInvisible,  bool isNotDisturb,  bool isCustomized,  String label,  DateTime? clearedAt,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  int attitude,  bool isOnline,  bool isInvisible,  bool isNotDisturb,  bool isCustomized,  String label,  Map<String, dynamic>? meta,  DateTime? clearedAt,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAccountStatus() when $default != null: | case _SnAccountStatus() when $default != null: | ||||||
| return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.meta,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
|   return null; |   return null; | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -1252,7 +1253,7 @@ return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.i | |||||||
| @JsonSerializable() | @JsonSerializable() | ||||||
|  |  | ||||||
| class _SnAccountStatus implements SnAccountStatus { | class _SnAccountStatus implements SnAccountStatus { | ||||||
|   const _SnAccountStatus({required this.id, required this.attitude, required this.isOnline, required this.isInvisible, required this.isNotDisturb, required this.isCustomized, this.label = "", required this.clearedAt, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}); |   const _SnAccountStatus({required this.id, required this.attitude, required this.isOnline, required this.isInvisible, required this.isNotDisturb, required this.isCustomized, this.label = "", required final  Map<String, dynamic>? meta, required this.clearedAt, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _meta = meta; | ||||||
|   factory _SnAccountStatus.fromJson(Map<String, dynamic> json) => _$SnAccountStatusFromJson(json); |   factory _SnAccountStatus.fromJson(Map<String, dynamic> json) => _$SnAccountStatusFromJson(json); | ||||||
|  |  | ||||||
| @override final  String id; | @override final  String id; | ||||||
| @@ -1262,6 +1263,15 @@ class _SnAccountStatus implements SnAccountStatus { | |||||||
| @override final  bool isNotDisturb; | @override final  bool isNotDisturb; | ||||||
| @override final  bool isCustomized; | @override final  bool isCustomized; | ||||||
| @override@JsonKey() final  String label; | @override@JsonKey() final  String label; | ||||||
|  |  final  Map<String, dynamic>? _meta; | ||||||
|  | @override Map<String, dynamic>? get meta { | ||||||
|  |   final value = _meta; | ||||||
|  |   if (value == null) return null; | ||||||
|  |   if (_meta is EqualUnmodifiableMapView) return _meta; | ||||||
|  |   // ignore: implicit_dynamic_type | ||||||
|  |   return EqualUnmodifiableMapView(value); | ||||||
|  | } | ||||||
|  |  | ||||||
| @override final  DateTime? clearedAt; | @override final  DateTime? clearedAt; | ||||||
| @override final  String accountId; | @override final  String accountId; | ||||||
| @override final  DateTime createdAt; | @override final  DateTime createdAt; | ||||||
| @@ -1281,16 +1291,16 @@ Map<String, dynamic> toJson() { | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(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) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,clearedAt,accountId,createdAt,updatedAt,deletedAt); | int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,const DeepCollectionEquality().hash(_meta),clearedAt,accountId,createdAt,updatedAt,deletedAt); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; |   return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, meta: $meta, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -1301,7 +1311,7 @@ abstract mixin class _$SnAccountStatusCopyWith<$Res> implements $SnAccountStatus | |||||||
|   factory _$SnAccountStatusCopyWith(_SnAccountStatus value, $Res Function(_SnAccountStatus) _then) = __$SnAccountStatusCopyWithImpl; |   factory _$SnAccountStatusCopyWith(_SnAccountStatus value, $Res Function(_SnAccountStatus) _then) = __$SnAccountStatusCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt |  String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map<String, dynamic>? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -1318,7 +1328,7 @@ class __$SnAccountStatusCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnAccountStatus | /// Create a copy of SnAccountStatus | ||||||
| /// 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? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? meta = freezed,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|   return _then(_SnAccountStatus( |   return _then(_SnAccountStatus( | ||||||
| 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,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable | as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -1327,7 +1337,8 @@ as bool,isInvisible: null == isInvisible ? _self.isInvisible : isInvisible // ig | |||||||
| as bool,isNotDisturb: null == isNotDisturb ? _self.isNotDisturb : isNotDisturb // ignore: cast_nullable_to_non_nullable | as bool,isNotDisturb: null == isNotDisturb ? _self.isNotDisturb : isNotDisturb // ignore: cast_nullable_to_non_nullable | ||||||
| as bool,isCustomized: null == isCustomized ? _self.isCustomized : isCustomized // ignore: cast_nullable_to_non_nullable | as bool,isCustomized: null == isCustomized ? _self.isCustomized : isCustomized // ignore: cast_nullable_to_non_nullable | ||||||
| as bool,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable | as bool,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable | ||||||
| as String,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable | as String,meta: freezed == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable | ||||||
|  | as Map<String, dynamic>?,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // 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,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | as String,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 | ||||||
|   | |||||||
| @@ -158,6 +158,7 @@ _SnAccountStatus _$SnAccountStatusFromJson(Map<String, dynamic> json) => | |||||||
|       isNotDisturb: json['is_not_disturb'] as bool, |       isNotDisturb: json['is_not_disturb'] as bool, | ||||||
|       isCustomized: json['is_customized'] as bool, |       isCustomized: json['is_customized'] as bool, | ||||||
|       label: json['label'] as String? ?? "", |       label: json['label'] as String? ?? "", | ||||||
|  |       meta: json['meta'] as Map<String, dynamic>?, | ||||||
|       clearedAt: |       clearedAt: | ||||||
|           json['cleared_at'] == null |           json['cleared_at'] == null | ||||||
|               ? null |               ? null | ||||||
| @@ -180,6 +181,7 @@ Map<String, dynamic> _$SnAccountStatusToJson(_SnAccountStatus instance) => | |||||||
|       'is_not_disturb': instance.isNotDisturb, |       'is_not_disturb': instance.isNotDisturb, | ||||||
|       'is_customized': instance.isCustomized, |       'is_customized': instance.isCustomized, | ||||||
|       'label': instance.label, |       'label': instance.label, | ||||||
|  |       'meta': instance.meta, | ||||||
|       'cleared_at': instance.clearedAt?.toIso8601String(), |       'cleared_at': instance.clearedAt?.toIso8601String(), | ||||||
|       'account_id': instance.accountId, |       'account_id': instance.accountId, | ||||||
|       'created_at': instance.createdAt.toIso8601String(), |       'created_at': instance.createdAt.toIso8601String(), | ||||||
|   | |||||||
| @@ -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) => | ||||||
|   | |||||||
| @@ -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?, | ||||||
|   )); |   )); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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) => | ||||||
|   | |||||||
| @@ -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; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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, | ||||||
|   )); |   )); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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(), | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | import 'package:freezed_annotation/freezed_annotation.dart'; | ||||||
|  | import 'package:island/models/file_pool.dart'; | ||||||
|  |  | ||||||
| part 'file.freezed.dart'; | part 'file.freezed.dart'; | ||||||
| part 'file.g.dart'; | part 'file.g.dart'; | ||||||
| @@ -42,6 +43,7 @@ sealed class SnCloudFile with _$SnCloudFile { | |||||||
|     required String? description, |     required String? description, | ||||||
|     required Map<String, dynamic>? fileMeta, |     required Map<String, dynamic>? fileMeta, | ||||||
|     required Map<String, dynamic>? userMeta, |     required Map<String, dynamic>? userMeta, | ||||||
|  |     required SnFilePool? pool, | ||||||
|     @Default([]) List<int> sensitiveMarks, |     @Default([]) List<int> sensitiveMarks, | ||||||
|     required String? mimeType, |     required String? mimeType, | ||||||
|     required String? hash, |     required String? hash, | ||||||
|   | |||||||
| @@ -278,7 +278,7 @@ as bool, | |||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnCloudFile { | mixin _$SnCloudFile { | ||||||
|  |  | ||||||
|  String get id; String get name; String? get description; Map<String, dynamic>? get fileMeta; Map<String, dynamic>? get userMeta; List<int> get sensitiveMarks; String? get mimeType; String? get hash; int get size; DateTime? get uploadedAt; String? get uploadedTo; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; |  String get id; String get name; String? get description; Map<String, dynamic>? get fileMeta; Map<String, dynamic>? get userMeta; SnFilePool? get pool; List<int> get sensitiveMarks; String? get mimeType; String? get hash; int get size; DateTime? get uploadedAt; String? get uploadedTo; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||||
| /// Create a copy of SnCloudFile | /// Create a copy of SnCloudFile | ||||||
| /// 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) | ||||||
| @@ -291,16 +291,16 @@ $SnCloudFileCopyWith<SnCloudFile> get copyWith => _$SnCloudFileCopyWithImpl<SnCl | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnCloudFile&&(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.fileMeta, fileMeta)&&const DeepCollectionEquality().equals(other.userMeta, userMeta)&&const DeepCollectionEquality().equals(other.sensitiveMarks, sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnCloudFile&&(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.fileMeta, fileMeta)&&const DeepCollectionEquality().equals(other.userMeta, userMeta)&&(identical(other.pool, pool) || other.pool == pool)&&const DeepCollectionEquality().equals(other.sensitiveMarks, sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(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) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(fileMeta),const DeepCollectionEquality().hash(userMeta),const DeepCollectionEquality().hash(sensitiveMarks),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt); | int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(fileMeta),const DeepCollectionEquality().hash(userMeta),pool,const DeepCollectionEquality().hash(sensitiveMarks),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; |   return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, pool: $pool, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -311,11 +311,11 @@ abstract mixin class $SnCloudFileCopyWith<$Res>  { | |||||||
|   factory $SnCloudFileCopyWith(SnCloudFile value, $Res Function(SnCloudFile) _then) = _$SnCloudFileCopyWithImpl; |   factory $SnCloudFileCopyWith(SnCloudFile value, $Res Function(SnCloudFile) _then) = _$SnCloudFileCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt |  String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | $SnFilePoolCopyWith<$Res>? get pool; | ||||||
|  |  | ||||||
| } | } | ||||||
| /// @nodoc | /// @nodoc | ||||||
| @@ -328,14 +328,15 @@ class _$SnCloudFileCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnCloudFile | /// Create a copy of SnCloudFile | ||||||
| /// 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? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? pool = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,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,name: null == name ? _self.name : name // 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,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,fileMeta: freezed == fileMeta ? _self.fileMeta : fileMeta // ignore: cast_nullable_to_non_nullable | as String?,fileMeta: freezed == fileMeta ? _self.fileMeta : fileMeta // ignore: cast_nullable_to_non_nullable | ||||||
| as Map<String, dynamic>?,userMeta: freezed == userMeta ? _self.userMeta : userMeta // ignore: cast_nullable_to_non_nullable | as Map<String, dynamic>?,userMeta: freezed == userMeta ? _self.userMeta : userMeta // ignore: cast_nullable_to_non_nullable | ||||||
| as Map<String, dynamic>?,sensitiveMarks: null == sensitiveMarks ? _self.sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable | as Map<String, dynamic>?,pool: freezed == pool ? _self.pool : pool // ignore: cast_nullable_to_non_nullable | ||||||
|  | as SnFilePool?,sensitiveMarks: null == sensitiveMarks ? _self.sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable | ||||||
| as List<int>,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable | as List<int>,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable | as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable | as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -347,7 +348,19 @@ as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ign | |||||||
| as DateTime?, | as DateTime?, | ||||||
|   )); |   )); | ||||||
| } | } | ||||||
|  | /// Create a copy of SnCloudFile | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
|  | $SnFilePoolCopyWith<$Res>? get pool { | ||||||
|  |     if (_self.pool == null) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return $SnFilePoolCopyWith<$Res>(_self.pool!, (value) { | ||||||
|  |     return _then(_self.copyWith(pool: value)); | ||||||
|  |   }); | ||||||
|  | } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -426,10 +439,10 @@ return $default(_that);case _: | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String name,  String? description,  Map<String, dynamic>? fileMeta,  Map<String, dynamic>? userMeta,  List<int> sensitiveMarks,  String? mimeType,  String? hash,  int size,  DateTime? uploadedAt,  String? uploadedTo,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String name,  String? description,  Map<String, dynamic>? fileMeta,  Map<String, dynamic>? userMeta,  SnFilePool? pool,  List<int> sensitiveMarks,  String? mimeType,  String? hash,  int size,  DateTime? uploadedAt,  String? uploadedTo,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnCloudFile() when $default != null: | case _SnCloudFile() when $default != null: | ||||||
| return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.pool,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
|   return orElse(); |   return orElse(); | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -447,10 +460,10 @@ return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userM | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String name,  String? description,  Map<String, dynamic>? fileMeta,  Map<String, dynamic>? userMeta,  List<int> sensitiveMarks,  String? mimeType,  String? hash,  int size,  DateTime? uploadedAt,  String? uploadedTo,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String name,  String? description,  Map<String, dynamic>? fileMeta,  Map<String, dynamic>? userMeta,  SnFilePool? pool,  List<int> sensitiveMarks,  String? mimeType,  String? hash,  int size,  DateTime? uploadedAt,  String? uploadedTo,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnCloudFile(): | case _SnCloudFile(): | ||||||
| return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);} | return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.pool,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||||
| } | } | ||||||
| /// A variant of `when` that fallback to returning `null` | /// A variant of `when` that fallback to returning `null` | ||||||
| /// | /// | ||||||
| @@ -464,10 +477,10 @@ return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userM | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String name,  String? description,  Map<String, dynamic>? fileMeta,  Map<String, dynamic>? userMeta,  List<int> sensitiveMarks,  String? mimeType,  String? hash,  int size,  DateTime? uploadedAt,  String? uploadedTo,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String name,  String? description,  Map<String, dynamic>? fileMeta,  Map<String, dynamic>? userMeta,  SnFilePool? pool,  List<int> sensitiveMarks,  String? mimeType,  String? hash,  int size,  DateTime? uploadedAt,  String? uploadedTo,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnCloudFile() when $default != null: | case _SnCloudFile() when $default != null: | ||||||
| return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.pool,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||||
|   return null; |   return null; | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -479,7 +492,7 @@ return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userM | |||||||
| @JsonSerializable() | @JsonSerializable() | ||||||
|  |  | ||||||
| class _SnCloudFile implements SnCloudFile { | class _SnCloudFile implements SnCloudFile { | ||||||
|   const _SnCloudFile({required this.id, required this.name, required this.description, required final  Map<String, dynamic>? fileMeta, required final  Map<String, dynamic>? userMeta, final  List<int> sensitiveMarks = const [], required this.mimeType, required this.hash, required this.size, required this.uploadedAt, required this.uploadedTo, required this.createdAt, required this.updatedAt, required this.deletedAt}): _fileMeta = fileMeta,_userMeta = userMeta,_sensitiveMarks = sensitiveMarks; |   const _SnCloudFile({required this.id, required this.name, required this.description, required final  Map<String, dynamic>? fileMeta, required final  Map<String, dynamic>? userMeta, required this.pool, final  List<int> sensitiveMarks = const [], required this.mimeType, required this.hash, required this.size, required this.uploadedAt, required this.uploadedTo, required this.createdAt, required this.updatedAt, required this.deletedAt}): _fileMeta = fileMeta,_userMeta = userMeta,_sensitiveMarks = sensitiveMarks; | ||||||
|   factory _SnCloudFile.fromJson(Map<String, dynamic> json) => _$SnCloudFileFromJson(json); |   factory _SnCloudFile.fromJson(Map<String, dynamic> json) => _$SnCloudFileFromJson(json); | ||||||
|  |  | ||||||
| @override final  String id; | @override final  String id; | ||||||
| @@ -503,6 +516,7 @@ class _SnCloudFile implements SnCloudFile { | |||||||
|   return EqualUnmodifiableMapView(value); |   return EqualUnmodifiableMapView(value); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @override final  SnFilePool? pool; | ||||||
|  final  List<int> _sensitiveMarks; |  final  List<int> _sensitiveMarks; | ||||||
| @override@JsonKey() List<int> get sensitiveMarks { | @override@JsonKey() List<int> get sensitiveMarks { | ||||||
|   if (_sensitiveMarks is EqualUnmodifiableListView) return _sensitiveMarks; |   if (_sensitiveMarks is EqualUnmodifiableListView) return _sensitiveMarks; | ||||||
| @@ -532,16 +546,16 @@ Map<String, dynamic> toJson() { | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnCloudFile&&(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._fileMeta, _fileMeta)&&const DeepCollectionEquality().equals(other._userMeta, _userMeta)&&const DeepCollectionEquality().equals(other._sensitiveMarks, _sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnCloudFile&&(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._fileMeta, _fileMeta)&&const DeepCollectionEquality().equals(other._userMeta, _userMeta)&&(identical(other.pool, pool) || other.pool == pool)&&const DeepCollectionEquality().equals(other._sensitiveMarks, _sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(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) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(_fileMeta),const DeepCollectionEquality().hash(_userMeta),const DeepCollectionEquality().hash(_sensitiveMarks),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt); | int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(_fileMeta),const DeepCollectionEquality().hash(_userMeta),pool,const DeepCollectionEquality().hash(_sensitiveMarks),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; |   return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, pool: $pool, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -552,11 +566,11 @@ abstract mixin class _$SnCloudFileCopyWith<$Res> implements $SnCloudFileCopyWith | |||||||
|   factory _$SnCloudFileCopyWith(_SnCloudFile value, $Res Function(_SnCloudFile) _then) = __$SnCloudFileCopyWithImpl; |   factory _$SnCloudFileCopyWith(_SnCloudFile value, $Res Function(_SnCloudFile) _then) = __$SnCloudFileCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt |  String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @override $SnFilePoolCopyWith<$Res>? get pool; | ||||||
|  |  | ||||||
| } | } | ||||||
| /// @nodoc | /// @nodoc | ||||||
| @@ -569,14 +583,15 @@ class __$SnCloudFileCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnCloudFile | /// Create a copy of SnCloudFile | ||||||
| /// 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? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? pool = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|   return _then(_SnCloudFile( |   return _then(_SnCloudFile( | ||||||
| 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,name: null == name ? _self.name : name // 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,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,fileMeta: freezed == fileMeta ? _self._fileMeta : fileMeta // ignore: cast_nullable_to_non_nullable | as String?,fileMeta: freezed == fileMeta ? _self._fileMeta : fileMeta // ignore: cast_nullable_to_non_nullable | ||||||
| as Map<String, dynamic>?,userMeta: freezed == userMeta ? _self._userMeta : userMeta // ignore: cast_nullable_to_non_nullable | as Map<String, dynamic>?,userMeta: freezed == userMeta ? _self._userMeta : userMeta // ignore: cast_nullable_to_non_nullable | ||||||
| as Map<String, dynamic>?,sensitiveMarks: null == sensitiveMarks ? _self._sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable | as Map<String, dynamic>?,pool: freezed == pool ? _self.pool : pool // ignore: cast_nullable_to_non_nullable | ||||||
|  | as SnFilePool?,sensitiveMarks: null == sensitiveMarks ? _self._sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable | ||||||
| as List<int>,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable | as List<int>,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable | as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable | as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -589,7 +604,19 @@ as DateTime?, | |||||||
|   )); |   )); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /// Create a copy of SnCloudFile | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
|  | $SnFilePoolCopyWith<$Res>? get pool { | ||||||
|  |     if (_self.pool == null) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return $SnFilePoolCopyWith<$Res>(_self.pool!, (value) { | ||||||
|  |     return _then(_self.copyWith(pool: value)); | ||||||
|  |   }); | ||||||
|  | } | ||||||
| } | } | ||||||
|  |  | ||||||
| // dart format on | // dart format on | ||||||
|   | |||||||
| @@ -33,6 +33,10 @@ _SnCloudFile _$SnCloudFileFromJson(Map<String, dynamic> json) => _SnCloudFile( | |||||||
|   description: json['description'] as String?, |   description: json['description'] as String?, | ||||||
|   fileMeta: json['file_meta'] as Map<String, dynamic>?, |   fileMeta: json['file_meta'] as Map<String, dynamic>?, | ||||||
|   userMeta: json['user_meta'] as Map<String, dynamic>?, |   userMeta: json['user_meta'] as Map<String, dynamic>?, | ||||||
|  |   pool: | ||||||
|  |       json['pool'] == null | ||||||
|  |           ? null | ||||||
|  |           : SnFilePool.fromJson(json['pool'] as Map<String, dynamic>), | ||||||
|   sensitiveMarks: |   sensitiveMarks: | ||||||
|       (json['sensitive_marks'] as List<dynamic>?) |       (json['sensitive_marks'] as List<dynamic>?) | ||||||
|           ?.map((e) => (e as num).toInt()) |           ?.map((e) => (e as num).toInt()) | ||||||
| @@ -61,6 +65,7 @@ Map<String, dynamic> _$SnCloudFileToJson(_SnCloudFile instance) => | |||||||
|       'description': instance.description, |       'description': instance.description, | ||||||
|       'file_meta': instance.fileMeta, |       'file_meta': instance.fileMeta, | ||||||
|       'user_meta': instance.userMeta, |       'user_meta': instance.userMeta, | ||||||
|  |       'pool': instance.pool?.toJson(), | ||||||
|       'sensitive_marks': instance.sensitiveMarks, |       'sensitive_marks': instance.sensitiveMarks, | ||||||
|       'mime_type': instance.mimeType, |       'mime_type': instance.mimeType, | ||||||
|       'hash': instance.hash, |       'hash': instance.hash, | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								lib/models/file_pool.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								lib/models/file_pool.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | 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); | ||||||
|  | } | ||||||
							
								
								
									
										328
									
								
								lib/models/file_pool.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										328
									
								
								lib/models/file_pool.freezed.dart
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										47
									
								
								lib/models/file_pool.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								lib/models/file_pool.g.dart
									
									
									
									
									
										Normal 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(), | ||||||
|  |     }; | ||||||
| @@ -4,7 +4,9 @@ import 'dart:developer' as developer; | |||||||
| import 'dart:io'; | import 'dart:io'; | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
|  | import 'package:island/widgets/account/status.dart'; | ||||||
| import 'package:shelf/shelf.dart'; | 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'; | ||||||
| @@ -12,8 +14,6 @@ import 'package:web_socket_channel/web_socket_channel.dart'; | |||||||
|  |  | ||||||
| // Conditional imports for IPC server - use web stubs on web platform | // Conditional imports for IPC server - use web stubs on web platform | ||||||
| import 'ipc_server.dart' if (dart.library.html) 'ipc_server.web.dart'; | import 'ipc_server.dart' if (dart.library.html) 'ipc_server.web.dart'; | ||||||
| import 'ipc_server.windows.dart' if (dart.library.html) 'ipc_server.web.dart'; |  | ||||||
| import 'ipc_server.unix.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'; | ||||||
| @@ -112,11 +112,7 @@ class ActivityRpcServer { | |||||||
|     final shouldStartIpc = !Platform.isMacOS && !kIsWeb; |     final shouldStartIpc = !Platform.isMacOS && !kIsWeb; | ||||||
|     if (shouldStartIpc) { |     if (shouldStartIpc) { | ||||||
|       try { |       try { | ||||||
|         if (Platform.isWindows) { |         _ipcServer = MultiPlatformIpcServer(); | ||||||
|           _ipcServer = WindowsIpcServer(); |  | ||||||
|         } else { |  | ||||||
|           _ipcServer = UnixIpcServer(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Set up IPC handlers |         // Set up IPC handlers | ||||||
|         _ipcServer!.handlePacket = (socket, packet, _) { |         _ipcServer!.handlePacket = (socket, packet, _) { | ||||||
| @@ -345,7 +341,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'); | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -394,12 +392,35 @@ final rpcServerStateProvider = | |||||||
|         'message': (socket, dynamic data) async { |         'message': (socket, dynamic data) async { | ||||||
|           if (data['cmd'] == 'SET_ACTIVITY') { |           if (data['cmd'] == 'SET_ACTIVITY') { | ||||||
|             notifier.addActivity( |             notifier.addActivity( | ||||||
|               'Activity: ${data['args']['activity']['details'] ?? 'Unknown'}', |               'Activity: ${data['args']['activity']['details'] ?? ''}', | ||||||
|             ); |             ); | ||||||
|             final label = data['args']['activity']['details'] ?? 'Unknown'; |             final label = data['args']['activity']['details'] ?? ''; | ||||||
|             final appId = socket.clientId; |             final appId = socket.clientId; | ||||||
|  |             final meta = data['args']['activity']; | ||||||
|             try { |             try { | ||||||
|               await setRemoteActivityStatus(ref, label, appId); |               await setRemoteActivityStatus( | ||||||
|  |                 ref, | ||||||
|  |                 label, | ||||||
|  |                 appId, | ||||||
|  |                 meta, | ||||||
|  |               ); | ||||||
|  |               final now = DateTime.now(); | ||||||
|  |               final status = SnAccountStatus( | ||||||
|  |                 id: 'local_$appId', | ||||||
|  |                 attitude: 0, | ||||||
|  |                 isOnline: true, | ||||||
|  |                 isInvisible: false, | ||||||
|  |                 isNotDisturb: false, | ||||||
|  |                 isCustomized: true, | ||||||
|  |                 label: label, | ||||||
|  |                 meta: meta, | ||||||
|  |                 clearedAt: null, | ||||||
|  |                 accountId: 'me', | ||||||
|  |                 createdAt: now, | ||||||
|  |                 updatedAt: now, | ||||||
|  |                 deletedAt: null, | ||||||
|  |               ); | ||||||
|  |               ref.read(currentAccountStatusProvider.notifier).setStatus(status); | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|               developer.log( |               developer.log( | ||||||
|                 'Failed to set remote activity status: $e', |                 'Failed to set remote activity status: $e', | ||||||
| @@ -419,6 +440,7 @@ final rpcServerStateProvider = | |||||||
|           final appId = socket.clientId; |           final appId = socket.clientId; | ||||||
|           try { |           try { | ||||||
|             await unsetRemoteActivityStatus(ref, appId); |             await unsetRemoteActivityStatus(ref, appId); | ||||||
|  |             ref.read(currentAccountStatusProvider.notifier).clearStatus(); | ||||||
|           } catch (e) { |           } catch (e) { | ||||||
|             developer.log( |             developer.log( | ||||||
|               'Failed to unset remote activity status: $e', |               'Failed to unset remote activity status: $e', | ||||||
| @@ -439,6 +461,7 @@ Future<void> setRemoteActivityStatus( | |||||||
|   Ref ref, |   Ref ref, | ||||||
|   String label, |   String label, | ||||||
|   String appId, |   String appId, | ||||||
|  |   Map<String, dynamic> meta, | ||||||
| ) async { | ) async { | ||||||
|   final apiClient = ref.read(apiClientProvider); |   final apiClient = ref.read(apiClientProvider); | ||||||
|   await apiClient.post( |   await apiClient.post( | ||||||
| @@ -449,6 +472,7 @@ Future<void> setRemoteActivityStatus( | |||||||
|       'is_automated': true, |       'is_automated': true, | ||||||
|       'label': label, |       'label': label, | ||||||
|       'app_identifier': appId, |       'app_identifier': appId, | ||||||
|  |       'meta': meta, | ||||||
|     }, |     }, | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,10 @@ | |||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
|  | import 'dart:developer' as developer; | ||||||
|  | import 'dart:io'; | ||||||
| import 'dart:typed_data'; | import 'dart:typed_data'; | ||||||
|  | import 'package:dart_ipc/dart_ipc.dart'; | ||||||
|  | import 'package:path/path.dart' as path; | ||||||
|  |  | ||||||
| const String kRpcIpcLogPrefix = 'arRPC.ipc'; | const String kRpcIpcLogPrefix = 'arRPC.ipc'; | ||||||
|  |  | ||||||
| @@ -116,3 +120,178 @@ abstract class IpcSocketWrapper { | |||||||
|     return packets; |     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(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,203 +0,0 @@ | |||||||
| import 'dart:async'; |  | ||||||
| import 'dart:developer' as developer; |  | ||||||
| import 'dart:io'; |  | ||||||
| import 'package:path/path.dart' as path; |  | ||||||
| import 'ipc_server.dart'; |  | ||||||
|  |  | ||||||
| class UnixIpcServer extends IpcServer { |  | ||||||
|   ServerSocket? _ipcServer; |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Future<void> start() async { |  | ||||||
|     final ipcPath = await _findAvailableIpcPath(); |  | ||||||
|     _ipcServer = await ServerSocket.bind( |  | ||||||
|       InternetAddress(ipcPath, type: InternetAddressType.unix), |  | ||||||
|       0, |  | ||||||
|     ); |  | ||||||
|     developer.log('IPC listening at $ipcPath', name: kRpcIpcLogPrefix); |  | ||||||
|  |  | ||||||
|     _ipcServer!.listen((Socket socket) { |  | ||||||
|       _onIpcConnection(socket); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @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(); |  | ||||||
|     await _ipcServer?.close(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Handle new IPC connection |  | ||||||
|   void _onIpcConnection(Socket socket) { |  | ||||||
|     developer.log('New IPC connection!', name: kRpcIpcLogPrefix); |  | ||||||
|  |  | ||||||
|     final socketWrapper = UnixIpcSocketWrapper(socket); |  | ||||||
|     addSocket(socketWrapper); |  | ||||||
|  |  | ||||||
|     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); |  | ||||||
|         removeSocket(socketWrapper); |  | ||||||
|       }, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Handle incoming IPC data |  | ||||||
|   void _onIpcData(UnixIpcSocketWrapper socket, List<int> data) { |  | ||||||
|     try { |  | ||||||
|       socket.addData(data); |  | ||||||
|       final packets = socket.readPackets(); |  | ||||||
|       for (final packet in packets) { |  | ||||||
|         handlePacket?.call(socket, packet, {}); |  | ||||||
|       } |  | ||||||
|     } catch (e) { |  | ||||||
|       developer.log('IPC data error: $e', name: kRpcIpcLogPrefix); |  | ||||||
|       socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString()); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Handle IPC handshake |  | ||||||
|   void _onIpcHandshake( |  | ||||||
|     IpcSocketWrapper socket, |  | ||||||
|     Map<String, dynamic> params, |  | ||||||
|     Map<String, Function> handlers, |  | ||||||
|   ) { |  | ||||||
|     developer.log('IPC handshake: $params', name: kRpcIpcLogPrefix); |  | ||||||
|  |  | ||||||
|     final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1; |  | ||||||
|     final clientId = params['client_id']?.toString() ?? ''; |  | ||||||
|  |  | ||||||
|     if (ver != 1) { |  | ||||||
|       developer.log( |  | ||||||
|         'IPC unsupported version requested: $ver', |  | ||||||
|         name: kRpcIpcLogPrefix, |  | ||||||
|       ); |  | ||||||
|       socket.closeWithCode(IpcErrorCodes.invalidVersion); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (clientId.isEmpty) { |  | ||||||
|       developer.log('IPC client ID required', name: kRpcIpcLogPrefix); |  | ||||||
|       socket.closeWithCode(IpcErrorCodes.invalidClientId); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     socket.clientId = clientId; |  | ||||||
|  |  | ||||||
|     handlers['connection']?.call(socket); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   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'], |  | ||||||
|       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 ServerSocket.bind( |  | ||||||
|             InternetAddress(socketPath, type: InternetAddressType.unix), |  | ||||||
|             0, |  | ||||||
|           ); |  | ||||||
|           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', |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class UnixIpcSocketWrapper extends IpcSocketWrapper { |  | ||||||
|   final Socket socket; |  | ||||||
|  |  | ||||||
|   UnixIpcSocketWrapper(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(); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -56,8 +56,6 @@ class IpcSocketWrapper { | |||||||
|   List<dynamic> readPackets() => []; |   List<dynamic> readPackets() => []; | ||||||
| } | } | ||||||
|  |  | ||||||
| class WindowsIpcServer extends IpcServer {} | class MultiPlatformIpcServer extends IpcServer {} | ||||||
|  |  | ||||||
| class UnixIpcServer extends IpcServer {} | class MultiPlatformIpcSocketWrapper extends IpcSocketWrapper {} | ||||||
|  |  | ||||||
| class WindowsIpcSocketWrapper extends IpcSocketWrapper {} |  | ||||||
|   | |||||||
| @@ -1,273 +0,0 @@ | |||||||
| import 'dart:async'; |  | ||||||
| import 'dart:developer' as developer; |  | ||||||
| import 'dart:ffi'; |  | ||||||
| import 'dart:io'; |  | ||||||
| import 'dart:isolate'; |  | ||||||
| import 'package:ffi/ffi.dart'; |  | ||||||
| import 'package:win32/win32.dart'; |  | ||||||
| import 'ipc_server.dart'; |  | ||||||
|  |  | ||||||
| class WindowsIpcServer extends IpcServer { |  | ||||||
|   int? _pipeHandle; |  | ||||||
|   Timer? _ipcTimer; |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Future<void> start() async { |  | ||||||
|     final pipeName = r'\\.\pipe\discord-ipc'.toNativeUtf16(); |  | ||||||
|     try { |  | ||||||
|       _pipeHandle = CreateNamedPipe( |  | ||||||
|         pipeName, |  | ||||||
|         PIPE_ACCESS_DUPLEX, |  | ||||||
|         PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, |  | ||||||
|         PIPE_UNLIMITED_INSTANCES, |  | ||||||
|         4096, // Output buffer size |  | ||||||
|         4096, // Input buffer size |  | ||||||
|         0, // Default timeout |  | ||||||
|         nullptr, // Security attributes |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       if (_pipeHandle == INVALID_HANDLE_VALUE) { |  | ||||||
|         final error = GetLastError(); |  | ||||||
|         throw Exception('Failed to create named pipe: error code $error'); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       developer.log( |  | ||||||
|         'IPC named pipe created at \\\\.\\pipe\\discord-ipc', |  | ||||||
|         name: kRpcIpcLogPrefix, |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       // Start listening for connections in a separate isolate |  | ||||||
|       _listenWindowsIpc(); |  | ||||||
|     } finally { |  | ||||||
|       free(pipeName); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @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(); |  | ||||||
|  |  | ||||||
|     if (_pipeHandle != null) { |  | ||||||
|       try { |  | ||||||
|         CloseHandle(_pipeHandle!); |  | ||||||
|       } catch (e) { |  | ||||||
|         developer.log('Error closing named pipe: $e', name: kRpcIpcLogPrefix); |  | ||||||
|       } |  | ||||||
|       _pipeHandle = null; |  | ||||||
|     } |  | ||||||
|     _ipcTimer?.cancel(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Listen for Windows IPC connections in an isolate |  | ||||||
|   void _listenWindowsIpc() async { |  | ||||||
|     final receivePort = ReceivePort(); |  | ||||||
|     await Isolate.spawn(_windowsIpcIsolate, receivePort.sendPort); |  | ||||||
|  |  | ||||||
|     receivePort.listen((message) { |  | ||||||
|       if (message is int) { |  | ||||||
|         final socketWrapper = WindowsIpcSocketWrapper(message); |  | ||||||
|         addSocket(socketWrapper); |  | ||||||
|         developer.log( |  | ||||||
|           'New IPC connection on named pipe', |  | ||||||
|           name: kRpcIpcLogPrefix, |  | ||||||
|         ); |  | ||||||
|         _handleWindowsIpcData(socketWrapper); |  | ||||||
|         start(); // Create new pipe for next connection |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static void _windowsIpcIsolate(SendPort sendPort) { |  | ||||||
|     while (true) { |  | ||||||
|       final pipeHandle = CreateNamedPipe( |  | ||||||
|         r'\\.\pipe\discord-ipc'.toNativeUtf16(), |  | ||||||
|         PIPE_ACCESS_DUPLEX, |  | ||||||
|         PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, |  | ||||||
|         PIPE_UNLIMITED_INSTANCES, |  | ||||||
|         4096, |  | ||||||
|         4096, |  | ||||||
|         0, |  | ||||||
|         nullptr, |  | ||||||
|       ); |  | ||||||
|       if (pipeHandle == INVALID_HANDLE_VALUE) { |  | ||||||
|         developer.log( |  | ||||||
|           'Failed to create named pipe: ${GetLastError()}', |  | ||||||
|           name: kRpcIpcLogPrefix, |  | ||||||
|         ); |  | ||||||
|         break; |  | ||||||
|       } |  | ||||||
|       final connected = ConnectNamedPipe(pipeHandle, nullptr); |  | ||||||
|       if (connected != 0 || GetLastError() == ERROR_PIPE_CONNECTED) { |  | ||||||
|         sendPort.send(pipeHandle); |  | ||||||
|       } |  | ||||||
|       // Avoid tight loop |  | ||||||
|       sleep(Duration(milliseconds: 100)); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Handle Windows IPC data |  | ||||||
|   void _handleWindowsIpcData(WindowsIpcSocketWrapper socket) async { |  | ||||||
|     final startTime = DateTime.now(); |  | ||||||
|     final buffer = malloc.allocate<BYTE>(4096); |  | ||||||
|     final bytesRead = malloc.allocate<DWORD>(4); |  | ||||||
|     try { |  | ||||||
|       while (socket.pipeHandle != null) { |  | ||||||
|         final readStart = DateTime.now(); |  | ||||||
|         final success = ReadFile( |  | ||||||
|           socket.pipeHandle!, |  | ||||||
|           buffer.cast(), |  | ||||||
|           4096, |  | ||||||
|           bytesRead, |  | ||||||
|           nullptr, |  | ||||||
|         ); |  | ||||||
|         final readDuration = |  | ||||||
|             DateTime.now().difference(readStart).inMicroseconds; |  | ||||||
|         developer.log( |  | ||||||
|           'ReadFile took $readDuration microseconds', |  | ||||||
|           name: kRpcIpcLogPrefix, |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         if (success == FALSE && GetLastError() != ERROR_MORE_DATA) { |  | ||||||
|           developer.log( |  | ||||||
|             'IPC read error: ${GetLastError()}', |  | ||||||
|             name: kRpcIpcLogPrefix, |  | ||||||
|           ); |  | ||||||
|           socket.close(); |  | ||||||
|           break; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         final data = buffer.asTypedList(0); |  | ||||||
|         socket.addData(data); |  | ||||||
|         final packets = socket.readPackets(); |  | ||||||
|         for (final packet in packets) { |  | ||||||
|           handlePacket?.call(socket, packet, {}); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } catch (e) { |  | ||||||
|       developer.log('IPC data error: $e', name: kRpcIpcLogPrefix); |  | ||||||
|       socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString()); |  | ||||||
|     } finally { |  | ||||||
|       malloc.free(buffer); |  | ||||||
|       malloc.free(bytesRead); |  | ||||||
|       final totalDuration = DateTime.now().difference(startTime).inMicroseconds; |  | ||||||
|       developer.log( |  | ||||||
|         'handleWindowsIpcData took $totalDuration microseconds', |  | ||||||
|         name: kRpcIpcLogPrefix, |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Handle IPC handshake |  | ||||||
|   void _onIpcHandshake( |  | ||||||
|     IpcSocketWrapper socket, |  | ||||||
|     Map<String, dynamic> params, |  | ||||||
|     Map<String, Function> handlers, |  | ||||||
|   ) { |  | ||||||
|     developer.log('IPC handshake: $params', name: kRpcIpcLogPrefix); |  | ||||||
|  |  | ||||||
|     final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1; |  | ||||||
|     final clientId = params['client_id']?.toString() ?? ''; |  | ||||||
|  |  | ||||||
|     if (ver != 1) { |  | ||||||
|       developer.log( |  | ||||||
|         'IPC unsupported version requested: $ver', |  | ||||||
|         name: kRpcIpcLogPrefix, |  | ||||||
|       ); |  | ||||||
|       socket.closeWithCode(IpcErrorCodes.invalidVersion); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (clientId.isEmpty) { |  | ||||||
|       developer.log('IPC client ID required', name: kRpcIpcLogPrefix); |  | ||||||
|       socket.closeWithCode(IpcErrorCodes.invalidClientId); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     socket.clientId = clientId; |  | ||||||
|  |  | ||||||
|     handlers['connection']?.call(socket); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class WindowsIpcSocketWrapper extends IpcSocketWrapper { |  | ||||||
|   final int? pipeHandle; |  | ||||||
|  |  | ||||||
|   WindowsIpcSocketWrapper(this.pipeHandle); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   void send(Map<String, dynamic> msg) { |  | ||||||
|     developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix); |  | ||||||
|     final packet = IpcServer.encodeIpcPacket(IpcTypes.frame, msg); |  | ||||||
|     final buffer = malloc.allocate<BYTE>(packet.length); |  | ||||||
|     buffer.asTypedList(packet.length).setAll(0, packet); |  | ||||||
|     final bytesWritten = malloc.allocate<DWORD>(4); // DWORD is 4 bytes |  | ||||||
|     try { |  | ||||||
|       WriteFile( |  | ||||||
|         pipeHandle!, |  | ||||||
|         buffer.cast(), |  | ||||||
|         packet.length, |  | ||||||
|         bytesWritten, |  | ||||||
|         nullptr, |  | ||||||
|       ); |  | ||||||
|     } finally { |  | ||||||
|       malloc.free(buffer); |  | ||||||
|       malloc.free(bytesWritten); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   void sendPong(dynamic data) { |  | ||||||
|     final packet = IpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {}); |  | ||||||
|     final buffer = malloc.allocate<BYTE>(packet.length); |  | ||||||
|     buffer.asTypedList(packet.length).setAll(0, packet); |  | ||||||
|     final bytesWritten = malloc.allocate<DWORD>(4); // DWORD is 4 bytes |  | ||||||
|     try { |  | ||||||
|       WriteFile( |  | ||||||
|         pipeHandle!, |  | ||||||
|         buffer.cast(), |  | ||||||
|         packet.length, |  | ||||||
|         bytesWritten, |  | ||||||
|         nullptr, |  | ||||||
|       ); |  | ||||||
|     } finally { |  | ||||||
|       malloc.free(buffer); |  | ||||||
|       malloc.free(bytesWritten); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   void close() { |  | ||||||
|     if (pipeHandle != null) { |  | ||||||
|       CloseHandle(pipeHandle!); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   void closeWithCode(int code, [String message = '']) { |  | ||||||
|     final closeData = {'code': code, 'message': message}; |  | ||||||
|     final packet = IpcServer.encodeIpcPacket(IpcTypes.close, closeData); |  | ||||||
|     final buffer = malloc.allocate<BYTE>(packet.length); |  | ||||||
|     buffer.asTypedList(packet.length).setAll(0, packet); |  | ||||||
|     final bytesWritten = malloc.allocate<DWORD>(4); // DWORD is 4 bytes |  | ||||||
|     try { |  | ||||||
|       WriteFile( |  | ||||||
|         pipeHandle!, |  | ||||||
|         buffer.cast(), |  | ||||||
|         packet.length, |  | ||||||
|         bytesWritten, |  | ||||||
|         nullptr, |  | ||||||
|       ); |  | ||||||
|     } finally { |  | ||||||
|       malloc.free(buffer); |  | ||||||
|       malloc.free(bytesWritten); |  | ||||||
|     } |  | ||||||
|     CloseHandle(pipeHandle!); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										34
									
								
								lib/pods/chat_rooms.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								lib/pods/chat_rooms.dart
									
									
									
									
									
										Normal 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); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -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 = | ||||||
|   | |||||||
| @@ -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, | ||||||
|   )); |   )); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								lib/pods/file_pool.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								lib/pods/file_pool.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | 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'); | ||||||
|  |   return response.data | ||||||
|  |       .map((e) => SnFilePool.fromJson(e)) | ||||||
|  |       .cast<SnFilePool>() | ||||||
|  |       .toList(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | String? resolveDefaultPoolId(WidgetRef ref, List<SnFilePool> pools) { | ||||||
|  |   final settings = ref.watch(appSettingsNotifierProvider); | ||||||
|  |  | ||||||
|  |   final configuredId = settings.defaultPoolId; | ||||||
|  |   if (configuredId != null && pools.any((p) => p.id == configuredId)) { | ||||||
|  |     return configuredId; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return pools.firstOrNull?.id; | ||||||
|  | } | ||||||
| @@ -10,17 +10,19 @@ Future<void> resetDatabase(WidgetRef ref) async { | |||||||
|   if (kIsWeb) return; |   if (kIsWeb) return; | ||||||
|  |  | ||||||
|   final db = ref.read(databaseProvider); |   final db = ref.read(databaseProvider); | ||||||
|   final basepath = await getApplicationSupportDirectory(); |  | ||||||
|   final file = File(join(basepath.path, 'solar_network_data.sqlite')); |  | ||||||
|  |  | ||||||
|   // Close current database connection |   // Close current database connection | ||||||
|   db.close(); |   await db.close(); | ||||||
|  |  | ||||||
|   // Delete database file |   // Get the correct database file path | ||||||
|  |   final dbFolder = await getApplicationDocumentsDirectory(); | ||||||
|  |   final file = File(join(dbFolder.path, 'solar_network_data.sqlite')); | ||||||
|  |  | ||||||
|  |   // Delete database file if it exists | ||||||
|   if (await file.exists()) { |   if (await file.exists()) { | ||||||
|     await file.delete(); |     await file.delete(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Force refresh the database provider |   // Force refresh the database provider to create a new instance | ||||||
|   ref.invalidate(databaseProvider); |   ref.invalidate(databaseProvider); | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										852
									
								
								lib/pods/messages_notifier.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										852
									
								
								lib/pods/messages_notifier.dart
									
									
									
									
									
										Normal 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/chat_rooms.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); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -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 { | ||||||
| @@ -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); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart' show kIsWeb; | |||||||
| import 'package:go_router/go_router.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/about.dart'; | import 'package:island/screens/about.dart'; | ||||||
| import 'package:island/screens/account/credits.dart'; |  | ||||||
| import 'package:island/screens/developers/app_detail.dart'; | import 'package:island/screens/developers/app_detail.dart'; | ||||||
| import 'package:island/screens/developers/bot_detail.dart'; | import 'package:island/screens/developers/bot_detail.dart'; | ||||||
| import 'package:island/screens/developers/edit_app.dart'; | import 'package:island/screens/developers/edit_app.dart'; | ||||||
| @@ -19,6 +18,7 @@ import 'package:island/screens/developers/edit_project.dart'; | |||||||
| import 'package:island/screens/developers/new_project.dart'; | import 'package:island/screens/developers/new_project.dart'; | ||||||
| import 'package:island/screens/developers/project_detail.dart'; | import 'package:island/screens/developers/project_detail.dart'; | ||||||
| import 'package:island/screens/discovery/articles.dart'; | import 'package:island/screens/discovery/articles.dart'; | ||||||
|  | import 'package:island/screens/files/file_list.dart'; | ||||||
| import 'package:island/screens/posts/post_categories_list.dart'; | import 'package:island/screens/posts/post_categories_list.dart'; | ||||||
| import 'package:island/screens/posts/post_category_detail.dart'; | import 'package:island/screens/posts/post_category_detail.dart'; | ||||||
| import 'package:island/screens/posts/post_search.dart'; | import 'package:island/screens/posts/post_search.dart'; | ||||||
| @@ -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,10 +86,7 @@ Widget _tabPagesTransitionBuilder( | |||||||
| } | } | ||||||
|  |  | ||||||
| bool get _supportsAnalytics => | bool get _supportsAnalytics => | ||||||
|     kIsWeb || |     kIsWeb || Platform.isAndroid || Platform.isIOS || Platform.isMacOS; | ||||||
|     Platform.isAndroid || |  | ||||||
|     Platform.isIOS || |  | ||||||
|     Platform.isMacOS; |  | ||||||
|  |  | ||||||
| // Provider for the router | // Provider for the router | ||||||
| final routerProvider = Provider<GoRouter>((ref) { | final routerProvider = Provider<GoRouter>((ref) { | ||||||
| @@ -659,9 +656,9 @@ final routerProvider = Provider<GoRouter>((ref) { | |||||||
|                     builder: (context, state) => const WalletScreen(), |                     builder: (context, state) => const WalletScreen(), | ||||||
|                   ), |                   ), | ||||||
|                   GoRoute( |                   GoRoute( | ||||||
|                     name: 'socialCredits', |                     name: 'files', | ||||||
|                     path: '/account/credits', |                     path: '/account/files', | ||||||
|                     builder: (context, state) => const SocialCreditsScreen(), |                     builder: (context, state) => const FileListScreen(), | ||||||
|                   ), |                   ), | ||||||
|                   GoRoute( |                   GoRoute( | ||||||
|                     name: 'relationships', |                     name: 'relationships', | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -141,20 +141,22 @@ class AccountScreen extends HookConsumerWidget { | |||||||
|                 ], |                 ], | ||||||
|               ), |               ), | ||||||
|             ).padding(horizontal: 8), |             ).padding(horizontal: 8), | ||||||
|             GestureDetector( |             LevelingProgressCard( | ||||||
|               child: LevelingProgressCard( |               isCompact: true, | ||||||
|                 level: user.value!.profile.level, |               level: user.value!.profile.level, | ||||||
|                 experience: user.value!.profile.experience, |               experience: user.value!.profile.experience, | ||||||
|                 progress: user.value!.profile.levelingProgress, |               progress: user.value!.profile.levelingProgress, | ||||||
|               ), |  | ||||||
|               onTap: () { |               onTap: () { | ||||||
|                 context.pushNamed('leveling'); |                 context.pushNamed('leveling'); | ||||||
|               }, |               }, | ||||||
|             ).padding(horizontal: 12), |             ).padding(horizontal: 12), | ||||||
|  |             const SizedBox.shrink(), | ||||||
|             Row( |             Row( | ||||||
|  |               spacing: 8, | ||||||
|               children: [ |               children: [ | ||||||
|                 Expanded( |                 Expanded( | ||||||
|                   child: Card( |                   child: Card( | ||||||
|  |                     margin: EdgeInsets.zero, | ||||||
|                     child: InkWell( |                     child: InkWell( | ||||||
|                       borderRadius: BorderRadius.circular(8), |                       borderRadius: BorderRadius.circular(8), | ||||||
|                       child: Column( |                       child: Column( | ||||||
| @@ -181,6 +183,7 @@ class AccountScreen extends HookConsumerWidget { | |||||||
|                 ), |                 ), | ||||||
|                 Expanded( |                 Expanded( | ||||||
|                   child: Card( |                   child: Card( | ||||||
|  |                     margin: EdgeInsets.zero, | ||||||
|                     child: InkWell( |                     child: InkWell( | ||||||
|                       borderRadius: BorderRadius.circular(8), |                       borderRadius: BorderRadius.circular(8), | ||||||
|                       child: Column( |                       child: Column( | ||||||
| @@ -206,7 +209,73 @@ class AccountScreen extends HookConsumerWidget { | |||||||
|                   ).height(140), |                   ).height(140), | ||||||
|                 ), |                 ), | ||||||
|               ], |               ], | ||||||
|             ).padding(horizontal: 8), |             ).padding(horizontal: 12), | ||||||
|  |             const SizedBox.shrink(), | ||||||
|  |             Row( | ||||||
|  |               spacing: 8, | ||||||
|  |               children: [ | ||||||
|  |                 Expanded( | ||||||
|  |                   child: Card( | ||||||
|  |                     margin: EdgeInsets.zero, | ||||||
|  |                     child: InkWell( | ||||||
|  |                       borderRadius: BorderRadius.circular(8), | ||||||
|  |                       child: Column( | ||||||
|  |                         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                         children: [ | ||||||
|  |                           Icon(Symbols.settings, size: 28).padding(bottom: 8), | ||||||
|  |                           Text('appSettings').tr().fontSize(16).bold(), | ||||||
|  |                         ], | ||||||
|  |                       ).padding(horizontal: 16, vertical: 12), | ||||||
|  |                       onTap: () { | ||||||
|  |                         context.pushNamed('settings'); | ||||||
|  |                       }, | ||||||
|  |                     ), | ||||||
|  |                   ).height(120), | ||||||
|  |                 ), | ||||||
|  |                 Expanded( | ||||||
|  |                   child: Card( | ||||||
|  |                     margin: EdgeInsets.zero, | ||||||
|  |                     child: InkWell( | ||||||
|  |                       borderRadius: BorderRadius.circular(8), | ||||||
|  |                       child: Column( | ||||||
|  |                         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                         children: [ | ||||||
|  |                           Icon( | ||||||
|  |                             Symbols.person_edit, | ||||||
|  |                             size: 28, | ||||||
|  |                           ).padding(bottom: 8), | ||||||
|  |                           Text('updateYourProfile').tr().fontSize(16).bold(), | ||||||
|  |                         ], | ||||||
|  |                       ).padding(horizontal: 16, vertical: 12), | ||||||
|  |                       onTap: () { | ||||||
|  |                         context.pushNamed('profileUpdate'); | ||||||
|  |                       }, | ||||||
|  |                     ), | ||||||
|  |                   ).height(120), | ||||||
|  |                 ), | ||||||
|  |                 Expanded( | ||||||
|  |                   child: Card( | ||||||
|  |                     margin: EdgeInsets.zero, | ||||||
|  |                     child: InkWell( | ||||||
|  |                       borderRadius: BorderRadius.circular(8), | ||||||
|  |                       child: Column( | ||||||
|  |                         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                         children: [ | ||||||
|  |                           Icon( | ||||||
|  |                             Symbols.manage_accounts, | ||||||
|  |                             size: 28, | ||||||
|  |                           ).padding(bottom: 8), | ||||||
|  |                           Text('accountSettings').tr().fontSize(16).bold(), | ||||||
|  |                         ], | ||||||
|  |                       ).padding(horizontal: 16, vertical: 12), | ||||||
|  |                       onTap: () { | ||||||
|  |                         context.pushNamed('accountSettings'); | ||||||
|  |                       }, | ||||||
|  |                     ), | ||||||
|  |                   ).height(120), | ||||||
|  |                 ), | ||||||
|  |               ], | ||||||
|  |             ).padding(horizontal: 12), | ||||||
|             ListTile( |             ListTile( | ||||||
|               minTileHeight: 48, |               minTileHeight: 48, | ||||||
|               leading: const Icon(Symbols.notifications), |               leading: const Icon(Symbols.notifications), | ||||||
| @@ -235,6 +304,16 @@ class AccountScreen extends HookConsumerWidget { | |||||||
|                 context.pushNamed('wallet'); |                 context.pushNamed('wallet'); | ||||||
|               }, |               }, | ||||||
|             ), |             ), | ||||||
|  |             ListTile( | ||||||
|  |               minTileHeight: 48, | ||||||
|  |               leading: const Icon(Symbols.files), | ||||||
|  |               trailing: const Icon(Symbols.chevron_right), | ||||||
|  |               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||||
|  |               title: Text('files').tr(), | ||||||
|  |               onTap: () { | ||||||
|  |                 context.pushNamed('files'); | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|             ListTile( |             ListTile( | ||||||
|               minTileHeight: 48, |               minTileHeight: 48, | ||||||
|               leading: const Icon(Symbols.people), |               leading: const Icon(Symbols.people), | ||||||
| @@ -265,16 +344,6 @@ class AccountScreen extends HookConsumerWidget { | |||||||
|                 context.pushNamed('webFeedMarketplace'); |                 context.pushNamed('webFeedMarketplace'); | ||||||
|               }, |               }, | ||||||
|             ), |             ), | ||||||
|             ListTile( |  | ||||||
|               minTileHeight: 48, |  | ||||||
|               leading: const Icon(Symbols.star), |  | ||||||
|               trailing: const Icon(Symbols.chevron_right), |  | ||||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), |  | ||||||
|               title: Text('credits').tr(), |  | ||||||
|               onTap: () { |  | ||||||
|                 context.pushNamed('socialCredits'); |  | ||||||
|               }, |  | ||||||
|             ), |  | ||||||
|             ListTile( |             ListTile( | ||||||
|               minTileHeight: 48, |               minTileHeight: 48, | ||||||
|               title: Text('abuseReport').tr(), |               title: Text('abuseReport').tr(), | ||||||
| @@ -284,37 +353,6 @@ class AccountScreen extends HookConsumerWidget { | |||||||
|               onTap: () => context.pushNamed('reportList'), |               onTap: () => context.pushNamed('reportList'), | ||||||
|             ), |             ), | ||||||
|             const Divider(height: 1).padding(vertical: 8), |             const Divider(height: 1).padding(vertical: 8), | ||||||
|             ListTile( |  | ||||||
|               minTileHeight: 48, |  | ||||||
|               leading: const Icon(Symbols.settings), |  | ||||||
|               trailing: const Icon(Symbols.chevron_right), |  | ||||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), |  | ||||||
|               title: Text('appSettings').tr(), |  | ||||||
|               onTap: () { |  | ||||||
|                 context.pushNamed('settings'); |  | ||||||
|               }, |  | ||||||
|             ), |  | ||||||
|             ListTile( |  | ||||||
|               minTileHeight: 48, |  | ||||||
|               leading: const Icon(Symbols.person_edit), |  | ||||||
|               trailing: const Icon(Symbols.chevron_right), |  | ||||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), |  | ||||||
|               title: Text('updateYourProfile').tr(), |  | ||||||
|               onTap: () { |  | ||||||
|                 context.pushNamed('profileUpdate'); |  | ||||||
|               }, |  | ||||||
|             ), |  | ||||||
|             ListTile( |  | ||||||
|               minTileHeight: 48, |  | ||||||
|               leading: const Icon(Symbols.manage_accounts), |  | ||||||
|               trailing: const Icon(Symbols.chevron_right), |  | ||||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), |  | ||||||
|               title: Text('accountSettings').tr(), |  | ||||||
|               onTap: () { |  | ||||||
|                 context.pushNamed('accountSettings'); |  | ||||||
|               }, |  | ||||||
|             ), |  | ||||||
|             const Divider(height: 1).padding(vertical: 8), |  | ||||||
|             ListTile( |             ListTile( | ||||||
|               minTileHeight: 48, |               minTileHeight: 48, | ||||||
|               leading: const Icon(Symbols.info), |               leading: const Icon(Symbols.info), | ||||||
| @@ -333,6 +371,8 @@ class AccountScreen extends HookConsumerWidget { | |||||||
|               title: Text('debugOptions').tr(), |               title: Text('debugOptions').tr(), | ||||||
|               onTap: () { |               onTap: () { | ||||||
|                 showModalBottomSheet( |                 showModalBottomSheet( | ||||||
|  |                   useRootNavigator: true, | ||||||
|  |                   isScrollControlled: true, | ||||||
|                   context: context, |                   context: context, | ||||||
|                   builder: (context) => DebugSheet(), |                   builder: (context) => DebugSheet(), | ||||||
|                 ); |                 ); | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ import 'package:gap/gap.dart'; | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | 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/widgets/app_scaffold.dart'; |  | ||||||
| import 'package:material_symbols_icons/material_symbols_icons.dart'; | import 'package:material_symbols_icons/material_symbols_icons.dart'; | ||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | 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'; | ||||||
| @@ -59,94 +58,93 @@ class SocialCreditHistoryNotifier extends _$SocialCreditHistoryNotifier | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| class SocialCreditsScreen extends HookConsumerWidget { | class SocialCreditsTab extends HookConsumerWidget { | ||||||
|   const SocialCreditsScreen({super.key}); |   const SocialCreditsTab({super.key}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final socialCredits = ref.watch(socialCreditsProvider); |     final socialCredits = ref.watch(socialCreditsProvider); | ||||||
|  |     return Column( | ||||||
|     return AppScaffold( |       children: [ | ||||||
|       appBar: AppBar(title: Text('socialCredits').tr()), |         const Gap(8), | ||||||
|       body: Column( |         Card( | ||||||
|         children: [ |           margin: const EdgeInsets.only(left: 16, right: 16, top: 8), | ||||||
|           Card( |           child: socialCredits | ||||||
|             margin: EdgeInsets.only(left: 16, right: 16, top: 8), |               .when( | ||||||
|             child: socialCredits |                 data: | ||||||
|                 .when( |                     (credits) => Stack( | ||||||
|                   data: |                       children: [ | ||||||
|                       (credits) => Stack( |                         Column( | ||||||
|                         children: [ |                           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|                           Column( |                           children: [ | ||||||
|                             crossAxisAlignment: CrossAxisAlignment.start, |                             Text( | ||||||
|                             children: [ |                               credits < 100 | ||||||
|                               Text( |                                   ? 'socialCreditsLevelPoor'.tr() | ||||||
|                                 credits < 100 |                                   : credits < 150 | ||||||
|                                     ? 'socialCreditsLevelPoor'.tr() |                                   ? 'socialCreditsLevelNormal'.tr() | ||||||
|                                     : credits < 150 |                                   : credits < 200 | ||||||
|                                     ? 'socialCreditsLevelNormal'.tr() |                                   ? 'socialCreditsLevelGood'.tr() | ||||||
|                                     : credits < 200 |                                   : 'socialCreditsLevelExcellent'.tr(), | ||||||
|                                     ? 'socialCreditsLevelGood'.tr() |                             ).tr().bold().fontSize(20), | ||||||
|                                     : 'socialCreditsLevelExcellent'.tr(), |                             Text( | ||||||
|                               ).tr().bold().fontSize(20), |                               '${credits.toStringAsFixed(2)} pts', | ||||||
|                               Text( |                             ).fontSize(14), | ||||||
|                                 '${credits.toStringAsFixed(2)} pts', |                             const Gap(8), | ||||||
|                               ).fontSize(14), |                             LinearProgressIndicator(value: credits / 200), | ||||||
|                               const Gap(8), |                           ], | ||||||
|                               LinearProgressIndicator(value: credits / 200), |                         ), | ||||||
|                             ], |                         Positioned( | ||||||
|  |                           right: 0, | ||||||
|  |                           top: 0, | ||||||
|  |                           child: IconButton( | ||||||
|  |                             onPressed: () {}, | ||||||
|  |                             icon: const Icon(Symbols.info), | ||||||
|  |                             tooltip: 'socialCreditsDescription'.tr(), | ||||||
|                           ), |                           ), | ||||||
|                           Positioned( |                         ), | ||||||
|                             right: 0, |                       ], | ||||||
|                             top: 0, |                     ), | ||||||
|                             child: IconButton( |                 error: (_, _) => Text('Error loading credits'), | ||||||
|                               onPressed: () {}, |                 loading: () => const LinearProgressIndicator(), | ||||||
|                               icon: const Icon(Symbols.info), |               ) | ||||||
|                               tooltip: 'socialCreditsDescription'.tr(), |               .padding(horizontal: 20, vertical: 16), | ||||||
|                             ), |         ), | ||||||
|                           ), |         Expanded( | ||||||
|                         ], |           child: PagingHelperView( | ||||||
|  |             provider: socialCreditHistoryNotifierProvider, | ||||||
|  |             futureRefreshable: socialCreditHistoryNotifierProvider.future, | ||||||
|  |             notifierRefreshable: socialCreditHistoryNotifierProvider.notifier, | ||||||
|  |             contentBuilder: | ||||||
|  |                 (data, widgetCount, endItemView) => ListView.builder( | ||||||
|  |                   padding: EdgeInsets.zero, | ||||||
|  |                   itemCount: widgetCount, | ||||||
|  |                   itemBuilder: (context, index) { | ||||||
|  |                     if (index == widgetCount - 1) { | ||||||
|  |                       return endItemView; | ||||||
|  |                     } | ||||||
|  |                     final record = data.items[index]; | ||||||
|  |                     return ListTile( | ||||||
|  |                       contentPadding: const EdgeInsets.symmetric( | ||||||
|  |                         horizontal: 24, | ||||||
|                       ), |                       ), | ||||||
|                   error: (_, _) => Text('Error loading credits'), |                       title: Text(record.reason), | ||||||
|                   loading: () => const LinearProgressIndicator(), |                       subtitle: Text( | ||||||
|                 ) |                         DateFormat.yMMMd().format(record.createdAt), | ||||||
|                 .padding(horizontal: 20, vertical: 16), |                       ), | ||||||
|           ), |                       trailing: Text( | ||||||
|           Expanded( |                         record.delta > 0 | ||||||
|             child: PagingHelperView( |                             ? '+${record.delta}' | ||||||
|               provider: socialCreditHistoryNotifierProvider, |                             : '${record.delta}', | ||||||
|               futureRefreshable: socialCreditHistoryNotifierProvider.future, |                         style: TextStyle( | ||||||
|               notifierRefreshable: socialCreditHistoryNotifierProvider.notifier, |                           color: record.delta > 0 ? Colors.green : Colors.red, | ||||||
|               contentBuilder: |  | ||||||
|                   (data, widgetCount, endItemView) => ListView.builder( |  | ||||||
|                     padding: EdgeInsets.zero, |  | ||||||
|                     itemCount: widgetCount, |  | ||||||
|                     itemBuilder: (context, index) { |  | ||||||
|                       if (index == widgetCount - 1) { |  | ||||||
|                         return endItemView; |  | ||||||
|                       } |  | ||||||
|                       final record = data.items[index]; |  | ||||||
|                       return ListTile( |  | ||||||
|                         contentPadding: EdgeInsets.symmetric(horizontal: 24), |  | ||||||
|                         title: Text(record.reason), |  | ||||||
|                         subtitle: Text( |  | ||||||
|                           DateFormat.yMMMd().format(record.createdAt), |  | ||||||
|                         ), |                         ), | ||||||
|                         trailing: Text( |                       ), | ||||||
|                           record.delta > 0 |                     ); | ||||||
|                               ? '+${record.delta}' |                   }, | ||||||
|                               : '${record.delta}', |                 ), | ||||||
|                           style: TextStyle( |  | ||||||
|                             color: record.delta > 0 ? Colors.green : Colors.red, |  | ||||||
|                           ), |  | ||||||
|                         ), |  | ||||||
|                       ); |  | ||||||
|                     }, |  | ||||||
|                   ), |  | ||||||
|             ), |  | ||||||
|           ), |           ), | ||||||
|         ], |         ), | ||||||
|       ), |       ], | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,12 +4,12 @@ import 'package:dio/dio.dart'; | |||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:google_fonts/google_fonts.dart'; |  | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/account.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/models/wallet.dart'; | import 'package:island/models/wallet.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/pods/userinfo.dart'; | import 'package:island/pods/userinfo.dart'; | ||||||
|  | import 'package:island/screens/account/credits.dart'; | ||||||
| import 'package:island/services/responsive.dart'; | import 'package:island/services/responsive.dart'; | ||||||
| import 'package:island/services/time.dart'; | import 'package:island/services/time.dart'; | ||||||
| import 'package:island/widgets/account/leveling_progress.dart'; | import 'package:island/widgets/account/leveling_progress.dart'; | ||||||
| @@ -89,7 +89,7 @@ class LevelingScreen extends HookConsumerWidget { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     return DefaultTabController( |     return DefaultTabController( | ||||||
|       length: 2, |       length: 3, | ||||||
|       child: AppScaffold( |       child: AppScaffold( | ||||||
|         appBar: AppBar( |         appBar: AppBar( | ||||||
|           title: Text('levelingProgress'.tr()), |           title: Text('levelingProgress'.tr()), | ||||||
| @@ -104,6 +104,15 @@ class LevelingScreen extends HookConsumerWidget { | |||||||
|                   ), |                   ), | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|  |               Tab( | ||||||
|  |                 child: Text( | ||||||
|  |                   'socialCredits'.tr(), | ||||||
|  |                   textAlign: TextAlign.center, | ||||||
|  |                   style: TextStyle( | ||||||
|  |                     color: Theme.of(context).appBarTheme.foregroundColor!, | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|               Tab( |               Tab( | ||||||
|                 child: Text( |                 child: Text( | ||||||
|                   'stellarProgram'.tr(), |                   'stellarProgram'.tr(), | ||||||
| @@ -119,6 +128,7 @@ class LevelingScreen extends HookConsumerWidget { | |||||||
|         body: TabBarView( |         body: TabBarView( | ||||||
|           children: [ |           children: [ | ||||||
|             _buildLevelingTab(context, ref, user.value!), |             _buildLevelingTab(context, ref, user.value!), | ||||||
|  |             const SocialCreditsTab(), | ||||||
|             _buildStellarProgramTab(context, ref), |             _buildStellarProgramTab(context, ref), | ||||||
|           ], |           ], | ||||||
|         ), |         ), | ||||||
| @@ -138,7 +148,6 @@ class LevelingScreen extends HookConsumerWidget { | |||||||
|     return Center( |     return Center( | ||||||
|       child: Container( |       child: Container( | ||||||
|         padding: const EdgeInsets.symmetric(horizontal: 20), |         padding: const EdgeInsets.symmetric(horizontal: 20), | ||||||
|         constraints: const BoxConstraints(maxWidth: 480), |  | ||||||
|         child: CustomScrollView( |         child: CustomScrollView( | ||||||
|           slivers: [ |           slivers: [ | ||||||
|             const SliverGap(20), |             const SliverGap(20), | ||||||
| @@ -164,10 +173,33 @@ class LevelingScreen extends HookConsumerWidget { | |||||||
|             ), |             ), | ||||||
|             const SliverGap(16), |             const SliverGap(16), | ||||||
|  |  | ||||||
|             // Stairs visualization with fixed height and horizontal scroll |             SliverToBoxAdapter( | ||||||
|             SliverToBoxAdapter(child: _buildLevelStairs(context, currentLevel)), |               child: Card( | ||||||
|             const SliverGap(24), |                 margin: EdgeInsets.zero, | ||||||
|  |                 child: Column( | ||||||
|  |                   crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |                   children: [ | ||||||
|  |                     Text( | ||||||
|  |                       '${'levelingProgressLevel'.tr(args: [currentLevel.toString()])} / 120', | ||||||
|  |                       textAlign: TextAlign.start, | ||||||
|  |                       style: Theme.of(context).textTheme.bodySmall, | ||||||
|  |                     ), | ||||||
|  |                     const Gap(8), | ||||||
|  |                     LinearProgressIndicator( | ||||||
|  |                       value: currentLevel / 120, | ||||||
|  |                       minHeight: 10, | ||||||
|  |                       stopIndicatorRadius: 0, | ||||||
|  |                       trackGap: 0, | ||||||
|  |                       color: Theme.of(context).colorScheme.primary, | ||||||
|  |                       backgroundColor: | ||||||
|  |                           Theme.of(context).colorScheme.surfaceContainerHigh, | ||||||
|  |                       borderRadius: BorderRadius.circular(32), | ||||||
|  |                     ), | ||||||
|  |                   ], | ||||||
|  |                 ).padding(horizontal: 16, top: 16, bottom: 12), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |             const SliverGap(16), | ||||||
|             // Leveling History |             // Leveling History | ||||||
|             SliverToBoxAdapter( |             SliverToBoxAdapter( | ||||||
|               child: Text( |               child: Text( | ||||||
| @@ -239,137 +271,12 @@ class LevelingScreen extends HookConsumerWidget { | |||||||
|  |  | ||||||
|     return SingleChildScrollView( |     return SingleChildScrollView( | ||||||
|       padding: getTabbedPadding(context, horizontal: 20, vertical: 20), |       padding: getTabbedPadding(context, horizontal: 20, vertical: 20), | ||||||
|       child: Center( |       child: Column( | ||||||
|         child: ConstrainedBox( |         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|           constraints: const BoxConstraints(maxWidth: 480), |         children: [ | ||||||
|           child: Column( |           _buildMembershipSection(context, ref, stellarSubscription), | ||||||
|             crossAxisAlignment: CrossAxisAlignment.stretch, |           const Gap(16), | ||||||
|             children: [ |         ], | ||||||
|               _buildMembershipSection(context, ref, stellarSubscription), |  | ||||||
|               const Gap(16), |  | ||||||
|             ], |  | ||||||
|           ), |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildLevelStairs(BuildContext context, int currentLevel) { |  | ||||||
|     const totalLevels = 14; |  | ||||||
|     const stairHeight = 20.0; |  | ||||||
|     const stairWidth = 50.0; |  | ||||||
|     const containerHeight = 280.0; |  | ||||||
|  |  | ||||||
|     return Container( |  | ||||||
|       height: containerHeight, |  | ||||||
|       decoration: BoxDecoration( |  | ||||||
|         borderRadius: BorderRadius.circular(12), |  | ||||||
|         border: Border.all( |  | ||||||
|           color: Theme.of(context).colorScheme.outline.withOpacity(0.2), |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|       child: SingleChildScrollView( |  | ||||||
|         scrollDirection: Axis.horizontal, |  | ||||||
|         padding: const EdgeInsets.symmetric(horizontal: 16), |  | ||||||
|         child: SizedBox( |  | ||||||
|           width: (totalLevels * (stairWidth + 8)) + 40, |  | ||||||
|           height: containerHeight, |  | ||||||
|           child: CustomPaint( |  | ||||||
|             painter: LevelStairsPainter( |  | ||||||
|               currentLevel: currentLevel, |  | ||||||
|               totalLevels: totalLevels, |  | ||||||
|               primaryColor: Theme.of(context).colorScheme.primary, |  | ||||||
|               surfaceColor: Theme.of(context).colorScheme.surfaceContainerHigh, |  | ||||||
|               onSurfaceColor: Theme.of(context).colorScheme.onSurface, |  | ||||||
|               stairHeight: stairHeight, |  | ||||||
|               stairWidth: stairWidth, |  | ||||||
|             ), |  | ||||||
|             child: Stack( |  | ||||||
|               children: List.generate(totalLevels, (index) { |  | ||||||
|                 final level = index + 1; |  | ||||||
|                 final isCompleted = level <= currentLevel; |  | ||||||
|                 final isCurrent = level == currentLevel; |  | ||||||
|  |  | ||||||
|                 // Calculate position from bottom |  | ||||||
|                 final bottomPosition = 0.0; |  | ||||||
|                 final leftPosition = 20.0 + (index * (stairWidth + 8)); |  | ||||||
|  |  | ||||||
|                 // Make higher levels progressively taller |  | ||||||
|                 final progressiveHeight = |  | ||||||
|                     40.0 + (index * 15.0); // Base height + progressive increase |  | ||||||
|  |  | ||||||
|                 return Positioned( |  | ||||||
|                   left: leftPosition, |  | ||||||
|                   bottom: bottomPosition, |  | ||||||
|                   child: Container( |  | ||||||
|                     width: stairWidth, |  | ||||||
|                     height: progressiveHeight, |  | ||||||
|                     decoration: BoxDecoration( |  | ||||||
|                       color: |  | ||||||
|                           isCompleted |  | ||||||
|                               ? Theme.of(context).colorScheme.primary |  | ||||||
|                               : Theme.of( |  | ||||||
|                                 context, |  | ||||||
|                               ).colorScheme.surfaceContainerHigh, |  | ||||||
|                       borderRadius: const BorderRadius.only( |  | ||||||
|                         topLeft: Radius.circular(6), |  | ||||||
|                         topRight: Radius.circular(6), |  | ||||||
|                       ), |  | ||||||
|                       border: |  | ||||||
|                           isCurrent |  | ||||||
|                               ? Border.all( |  | ||||||
|                                 color: Theme.of(context).colorScheme.primary, |  | ||||||
|                                 width: 2, |  | ||||||
|                               ) |  | ||||||
|                               : null, |  | ||||||
|                       boxShadow: |  | ||||||
|                           isCurrent |  | ||||||
|                               ? [ |  | ||||||
|                                 BoxShadow( |  | ||||||
|                                   color: Theme.of( |  | ||||||
|                                     context, |  | ||||||
|                                   ).colorScheme.primary.withOpacity(0.3), |  | ||||||
|                                   blurRadius: 6, |  | ||||||
|                                   spreadRadius: 1, |  | ||||||
|                                 ), |  | ||||||
|                               ] |  | ||||||
|                               : null, |  | ||||||
|                     ), |  | ||||||
|                     child: Padding( |  | ||||||
|                       padding: const EdgeInsets.only(top: 8), |  | ||||||
|                       child: Column( |  | ||||||
|                         children: [ |  | ||||||
|                           Text( |  | ||||||
|                             level.toString(), |  | ||||||
|                             style: GoogleFonts.robotoMono( |  | ||||||
|                               fontSize: 14, |  | ||||||
|                               fontWeight: FontWeight.bold, |  | ||||||
|                               color: |  | ||||||
|                                   isCompleted |  | ||||||
|                                       ? Theme.of(context).colorScheme.onPrimary |  | ||||||
|                                       : Theme.of(context).colorScheme.onSurface, |  | ||||||
|                             ), |  | ||||||
|                           ), |  | ||||||
|                           if (isCurrent) ...[ |  | ||||||
|                             const Gap(4), |  | ||||||
|                             Container( |  | ||||||
|                               width: 4, |  | ||||||
|                               height: 4, |  | ||||||
|                               decoration: BoxDecoration( |  | ||||||
|                                 color: Theme.of(context).colorScheme.onPrimary, |  | ||||||
|                                 shape: BoxShape.circle, |  | ||||||
|                               ), |  | ||||||
|                             ), |  | ||||||
|                           ], |  | ||||||
|                         ], |  | ||||||
|                       ), |  | ||||||
|                     ), |  | ||||||
|                   ), |  | ||||||
|                 ); |  | ||||||
|               }), |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|         ), |  | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -268,7 +268,7 @@ class _AccountBadgesProviderElement | |||||||
| } | } | ||||||
|  |  | ||||||
| String _$accountAppbarForcegroundColorHash() => | String _$accountAppbarForcegroundColorHash() => | ||||||
|     r'8ee0cae10817b77fb09548a482f5247662b4374c'; |     r'127fcc7fd6ec6a41ac4a6975276b5271aa4fa7d0'; | ||||||
|  |  | ||||||
| /// See also [accountAppbarForcegroundColor]. | /// See also [accountAppbarForcegroundColor]. | ||||||
| @ProviderFor(accountAppbarForcegroundColor) | @ProviderFor(accountAppbarForcegroundColor) | ||||||
|   | |||||||
| @@ -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'; |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -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}); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
							
								
								
									
										221
									
								
								lib/screens/chat/public_room_preview.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								lib/screens/chat/public_room_preview.dart
									
									
									
									
									
										Normal 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
											
										
									
								
							| @@ -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); | ||||||
|  |                                       } | ||||||
|  |                                     } | ||||||
|                                   }, |                                   }, | ||||||
|                                 ), |                                 ), | ||||||
|                               ], |                               ], | ||||||
|   | |||||||
| @@ -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()])), | ||||||
|  |                   ), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|         ], |         ], | ||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
							
								
								
									
										556
									
								
								lib/screens/files/file_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										556
									
								
								lib/screens/files/file_list.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,556 @@ | |||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:fl_chart/fl_chart.dart'; | ||||||
|  | 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/models/file.dart'; | ||||||
|  | import 'package:island/pods/network.dart'; | ||||||
|  | import 'package:island/pods/file_pool.dart'; | ||||||
|  | import 'package:island/utils/format.dart'; | ||||||
|  | import 'package:island/widgets/alert.dart'; | ||||||
|  | import 'package:island/widgets/app_scaffold.dart'; | ||||||
|  | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
|  | import 'package:island/widgets/content/file_info_sheet.dart'; | ||||||
|  | import 'package:material_symbols_icons/symbols.dart'; | ||||||
|  | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
|  | import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||||
|  | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
|  | part 'file_list.g.dart'; | ||||||
|  |  | ||||||
|  | @riverpod | ||||||
|  | class CloudFileListNotifier extends _$CloudFileListNotifier | ||||||
|  |     with CursorPagingNotifierMixin<SnCloudFile> { | ||||||
|  |   String? _poolId; | ||||||
|  |   bool _includeRecycled = false; | ||||||
|  |  | ||||||
|  |   void setFilters(String? poolId, bool includeRecycled) { | ||||||
|  |     _poolId = poolId; | ||||||
|  |     _includeRecycled = includeRecycled; | ||||||
|  |     ref.invalidateSelf(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Future<CursorPagingData<SnCloudFile>> build() => fetch(cursor: null); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Future<CursorPagingData<SnCloudFile>> fetch({required String? cursor}) async { | ||||||
|  |     final client = ref.read(apiClientProvider); | ||||||
|  |     final offset = cursor == null ? 0 : int.parse(cursor); | ||||||
|  |     final take = 20; | ||||||
|  |  | ||||||
|  |     final queryParameters = <String, dynamic>{'offset': offset, 'take': take}; | ||||||
|  |  | ||||||
|  |     // Add filter parameters | ||||||
|  |     if (_poolId != null) { | ||||||
|  |       queryParameters['pool'] = _poolId!; | ||||||
|  |     } | ||||||
|  |     if (_includeRecycled) { | ||||||
|  |       queryParameters['recycled'] = 'true'; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     final response = await client.get( | ||||||
|  |       '/drive/files/me', | ||||||
|  |       queryParameters: queryParameters, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     final List<SnCloudFile> items = | ||||||
|  |         (response.data as List) | ||||||
|  |             .map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>)) | ||||||
|  |             .toList(); | ||||||
|  |     final total = int.parse(response.headers.value('X-Total') ?? '0'); | ||||||
|  |  | ||||||
|  |     final hasMore = offset + items.length < total; | ||||||
|  |     final nextCursor = hasMore ? (offset + items.length).toString() : null; | ||||||
|  |  | ||||||
|  |     return CursorPagingData( | ||||||
|  |       items: items, | ||||||
|  |       hasMore: hasMore, | ||||||
|  |       nextCursor: nextCursor, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @riverpod | ||||||
|  | Future<Map<String, dynamic>?> billingUsage(Ref ref) async { | ||||||
|  |   final client = ref.read(apiClientProvider); | ||||||
|  |   final response = await client.get('/drive/billing/usage'); | ||||||
|  |   return response.data; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @riverpod | ||||||
|  | Future<Map<String, dynamic>?> billingQuota(Ref ref) async { | ||||||
|  |   final client = ref.read(apiClientProvider); | ||||||
|  |   final response = await client.get('/drive/billing/quota'); | ||||||
|  |   return response.data; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class FileListScreen extends HookConsumerWidget { | ||||||
|  |   const FileListScreen({super.key}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     // Filter state | ||||||
|  |     final selectedPool = useState<String?>(null); | ||||||
|  |     final includeRecycled = useState(false); | ||||||
|  |  | ||||||
|  |     final usageAsync = ref.watch(billingUsageProvider); | ||||||
|  |     final quotaAsync = ref.watch(billingQuotaProvider); | ||||||
|  |  | ||||||
|  |     // Update notifier filters when state changes | ||||||
|  |     useEffect(() { | ||||||
|  |       final notifier = ref.read(cloudFileListNotifierProvider.notifier); | ||||||
|  |       notifier.setFilters(selectedPool.value, includeRecycled.value); | ||||||
|  |       return null; | ||||||
|  |     }, [selectedPool.value, includeRecycled.value]); | ||||||
|  |  | ||||||
|  |     return AppScaffold( | ||||||
|  |       appBar: AppBar(title: Text('Files'), leading: const PageBackButton()), | ||||||
|  |       body: usageAsync.when( | ||||||
|  |         data: | ||||||
|  |             (usage) => quotaAsync.when( | ||||||
|  |               data: | ||||||
|  |                   (quota) => _buildQuotaUI( | ||||||
|  |                     usage, | ||||||
|  |                     quota, | ||||||
|  |                     ref, | ||||||
|  |                     selectedPool, | ||||||
|  |                     includeRecycled, | ||||||
|  |                   ), | ||||||
|  |               loading: () => const Center(child: CircularProgressIndicator()), | ||||||
|  |               error: (e, _) => Center(child: Text('Error loading quota')), | ||||||
|  |             ), | ||||||
|  |         loading: () => const Center(child: CircularProgressIndicator()), | ||||||
|  |         error: (e, _) => Center(child: Text('Error loading usage')), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildQuotaUI( | ||||||
|  |     Map<String, dynamic>? usage, | ||||||
|  |     Map<String, dynamic>? quota, | ||||||
|  |     WidgetRef ref, | ||||||
|  |     ValueNotifier<String?> selectedPool, | ||||||
|  |     ValueNotifier<bool> includeRecycled, | ||||||
|  |   ) { | ||||||
|  |     if (usage == null) return const SizedBox.shrink(); | ||||||
|  |     return CustomScrollView( | ||||||
|  |       slivers: [ | ||||||
|  |         const SliverGap(8), | ||||||
|  |         SliverToBoxAdapter( | ||||||
|  |           child: Column( | ||||||
|  |             children: [ | ||||||
|  |               Row( | ||||||
|  |                 children: [ | ||||||
|  |                   Expanded( | ||||||
|  |                     child: _buildStatCard( | ||||||
|  |                       'All Uploads', | ||||||
|  |                       '${((usage['total_usage_bytes'] as num) / (1024 * 1024 * 1024)).toStringAsFixed(3)} GiB', | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                   Expanded( | ||||||
|  |                     child: _buildStatCard( | ||||||
|  |                       'All Files', | ||||||
|  |                       '${usage['total_file_count']}', | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |               Row( | ||||||
|  |                 children: [ | ||||||
|  |                   Expanded( | ||||||
|  |                     child: _buildStatCard( | ||||||
|  |                       'Quota', | ||||||
|  |                       '${usage['total_quota']} MiB', | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                   Expanded( | ||||||
|  |                     child: _buildStatCard( | ||||||
|  |                       'Used Quota', | ||||||
|  |                       '${((usage['used_quota'] as num) / (usage['total_quota'] as num) * 100).toStringAsFixed(2)}%', | ||||||
|  |                       progress: | ||||||
|  |                           (usage['used_quota'] as num) / | ||||||
|  |                           (usage['total_quota'] as num), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ).padding(horizontal: 8), | ||||||
|  |         ), | ||||||
|  |         SliverToBoxAdapter( | ||||||
|  |           child: Row( | ||||||
|  |             children: [ | ||||||
|  |               Expanded( | ||||||
|  |                 child: Card( | ||||||
|  |                   child: Padding( | ||||||
|  |                     padding: const EdgeInsets.all(16), | ||||||
|  |                     child: Column( | ||||||
|  |                       children: [ | ||||||
|  |                         const Text('Pool Usage'), | ||||||
|  |                         SizedBox( | ||||||
|  |                           height: 200, | ||||||
|  |                           child: PieChart(_buildPoolChartData(usage)), | ||||||
|  |                         ), | ||||||
|  |                       ], | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |               Expanded( | ||||||
|  |                 child: Card( | ||||||
|  |                   child: Padding( | ||||||
|  |                     padding: const EdgeInsets.all(16), | ||||||
|  |                     child: Column( | ||||||
|  |                       children: [ | ||||||
|  |                         const Text('Verbose Quota'), | ||||||
|  |                         SizedBox( | ||||||
|  |                           height: 200, | ||||||
|  |                           child: PieChart(_buildQuotaChartData(quota)), | ||||||
|  |                         ), | ||||||
|  |                       ], | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ).padding(horizontal: 8), | ||||||
|  |         ), | ||||||
|  |         const SliverGap(8), | ||||||
|  |         SliverToBoxAdapter( | ||||||
|  |           child: _buildFilters(ref, selectedPool, includeRecycled), | ||||||
|  |         ), | ||||||
|  |         const SliverGap(8), | ||||||
|  |         PagingHelperSliverView( | ||||||
|  |           provider: cloudFileListNotifierProvider, | ||||||
|  |           futureRefreshable: cloudFileListNotifierProvider.future, | ||||||
|  |           notifierRefreshable: cloudFileListNotifierProvider.notifier, | ||||||
|  |           contentBuilder: | ||||||
|  |               (data, widgetCount, endItemView) => SliverList.builder( | ||||||
|  |                 itemCount: widgetCount, | ||||||
|  |                 itemBuilder: (context, index) { | ||||||
|  |                   if (index == widgetCount - 1) { | ||||||
|  |                     return endItemView; | ||||||
|  |                   } | ||||||
|  |  | ||||||
|  |                   final item = data.items[index]; | ||||||
|  |                   final itemType = item.mimeType?.split('/').firstOrNull; | ||||||
|  |                   return ListTile( | ||||||
|  |                     leading: ClipRRect( | ||||||
|  |                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|  |                       child: SizedBox( | ||||||
|  |                         height: 48, | ||||||
|  |                         width: 48, | ||||||
|  |                         child: switch (itemType) { | ||||||
|  |                           'image' => CloudImageWidget(file: item), | ||||||
|  |                           'audio' => | ||||||
|  |                             const Icon(Symbols.audio_file, fill: 1).center(), | ||||||
|  |                           'video' => | ||||||
|  |                             const Icon(Symbols.video_file, fill: 1).center(), | ||||||
|  |                           _ => | ||||||
|  |                             const Icon(Symbols.body_system, fill: 1).center(), | ||||||
|  |                         }, | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                     title: | ||||||
|  |                         item.name.isEmpty | ||||||
|  |                             ? Text('untitled').tr().italic() | ||||||
|  |                             : Text( | ||||||
|  |                               item.name, | ||||||
|  |                               maxLines: 1, | ||||||
|  |                               overflow: TextOverflow.ellipsis, | ||||||
|  |                             ), | ||||||
|  |                     subtitle: Text(formatFileSize(item.size)), | ||||||
|  |                     onTap: () { | ||||||
|  |                       showModalBottomSheet( | ||||||
|  |                         useRootNavigator: true, | ||||||
|  |                         context: context, | ||||||
|  |                         isScrollControlled: true, | ||||||
|  |                         builder: (context) => FileInfoSheet(item: item), | ||||||
|  |                       ); | ||||||
|  |                     }, | ||||||
|  |                     trailing: IconButton( | ||||||
|  |                       icon: const Icon(Symbols.delete), | ||||||
|  |                       onPressed: () async { | ||||||
|  |                         final confirmed = await showConfirmAlert( | ||||||
|  |                           'confirmDeleteFile'.tr(), | ||||||
|  |                           'deleteFile'.tr(), | ||||||
|  |                         ); | ||||||
|  |                         if (!confirmed) return; | ||||||
|  |  | ||||||
|  |                         if (context.mounted) showLoadingModal(context); | ||||||
|  |                         try { | ||||||
|  |                           final client = ref.read(apiClientProvider); | ||||||
|  |                           await client.delete('/drive/files/${item.id}'); | ||||||
|  |                           ref.invalidate(cloudFileListNotifierProvider); | ||||||
|  |                         } catch (e) { | ||||||
|  |                           showSnackBar('failedToDeleteFile'.tr()); | ||||||
|  |                         } finally { | ||||||
|  |                           if (context.mounted) hideLoadingModal(context); | ||||||
|  |                         } | ||||||
|  |                       }, | ||||||
|  |                     ), | ||||||
|  |                   ); | ||||||
|  |                 }, | ||||||
|  |               ), | ||||||
|  |         ), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   PieChartData _buildPoolChartData(Map<String, dynamic> usage) { | ||||||
|  |     final pools = usage['pool_usages'] as List<dynamic>; | ||||||
|  |     final colors = [ | ||||||
|  |       Colors.blue, | ||||||
|  |       Colors.green, | ||||||
|  |       Colors.orange, | ||||||
|  |       Colors.red, | ||||||
|  |       Colors.purple, | ||||||
|  |     ]; | ||||||
|  |     return PieChartData( | ||||||
|  |       sections: | ||||||
|  |           pools.asMap().entries.map((entry) { | ||||||
|  |             final pool = entry.value as Map<String, dynamic>; | ||||||
|  |             final title = pool['pool_name'] as String; | ||||||
|  |             final truncatedTitle = | ||||||
|  |                 title.length > 8 ? '${title.substring(0, 8)}...' : title; | ||||||
|  |             return PieChartSectionData( | ||||||
|  |               value: (pool['usage_bytes'] as num).toDouble(), | ||||||
|  |               title: truncatedTitle, | ||||||
|  |               color: colors[entry.key % colors.length], | ||||||
|  |               radius: 60, | ||||||
|  |               titleStyle: const TextStyle( | ||||||
|  |                 fontSize: 12, | ||||||
|  |                 color: Colors.white, | ||||||
|  |                 fontWeight: FontWeight.bold, | ||||||
|  |               ), | ||||||
|  |             ); | ||||||
|  |           }).toList(), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   PieChartData _buildQuotaChartData(Map<String, dynamic>? quota) { | ||||||
|  |     if (quota == null) return PieChartData(sections: []); | ||||||
|  |     return PieChartData( | ||||||
|  |       sections: [ | ||||||
|  |         PieChartSectionData( | ||||||
|  |           value: (quota['based_quota'] as num).toDouble(), | ||||||
|  |           title: 'Base', | ||||||
|  |           color: Colors.green, | ||||||
|  |           radius: 60, | ||||||
|  |           titleStyle: const TextStyle( | ||||||
|  |             fontSize: 12, | ||||||
|  |             color: Colors.white, | ||||||
|  |             fontWeight: FontWeight.bold, | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |         PieChartSectionData( | ||||||
|  |           value: (quota['extra_quota'] as num).toDouble(), | ||||||
|  |           title: 'Extra', | ||||||
|  |           color: Colors.orange, | ||||||
|  |           radius: 60, | ||||||
|  |           titleStyle: const TextStyle( | ||||||
|  |             fontSize: 12, | ||||||
|  |             color: Colors.white, | ||||||
|  |             fontWeight: FontWeight.bold, | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildFilters( | ||||||
|  |     WidgetRef ref, | ||||||
|  |     ValueNotifier<String?> selectedPool, | ||||||
|  |     ValueNotifier<bool> includeRecycled, | ||||||
|  |   ) { | ||||||
|  |     final poolsAsync = ref.watch(poolsProvider); | ||||||
|  |  | ||||||
|  |     return Card( | ||||||
|  |       child: Padding( | ||||||
|  |         padding: const EdgeInsets.all(16), | ||||||
|  |         child: Column( | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |           children: [ | ||||||
|  |             Text( | ||||||
|  |               'filters'.tr(), | ||||||
|  |               style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), | ||||||
|  |             ), | ||||||
|  |             const Gap(16), | ||||||
|  |             LayoutBuilder( | ||||||
|  |               builder: (context, constraints) { | ||||||
|  |                 final isWide = constraints.maxWidth > 600; | ||||||
|  |                 return isWide | ||||||
|  |                     ? Row( | ||||||
|  |                       children: [ | ||||||
|  |                         Expanded( | ||||||
|  |                           flex: 2, | ||||||
|  |                           child: poolsAsync.when( | ||||||
|  |                             data: | ||||||
|  |                                 (pools) => DropdownButtonFormField<String?>( | ||||||
|  |                                   value: selectedPool.value, | ||||||
|  |                                   decoration: InputDecoration( | ||||||
|  |                                     labelText: 'Pool', | ||||||
|  |                                     border: const OutlineInputBorder(), | ||||||
|  |                                   ), | ||||||
|  |                                   items: [ | ||||||
|  |                                     DropdownMenuItem<String?>( | ||||||
|  |                                       value: null, | ||||||
|  |                                       child: Text('allPools'.tr()), | ||||||
|  |                                     ), | ||||||
|  |                                     ...pools.map( | ||||||
|  |                                       (pool) => DropdownMenuItem<String?>( | ||||||
|  |                                         value: pool.id, | ||||||
|  |                                         child: Text(pool.name), | ||||||
|  |                                       ), | ||||||
|  |                                     ), | ||||||
|  |                                   ], | ||||||
|  |                                   onChanged: | ||||||
|  |                                       (value) => selectedPool.value = value, | ||||||
|  |                                 ), | ||||||
|  |                             loading: () => const CircularProgressIndicator(), | ||||||
|  |                             error: (e, _) => const Text('Error loading pools'), | ||||||
|  |                           ), | ||||||
|  |                         ), | ||||||
|  |                         const Gap(8), | ||||||
|  |                         Expanded( | ||||||
|  |                           child: Row( | ||||||
|  |                             children: [ | ||||||
|  |                               Text('includeRecycled'.tr()), | ||||||
|  |                               const Gap(8), | ||||||
|  |                               Switch( | ||||||
|  |                                 value: includeRecycled.value, | ||||||
|  |                                 onChanged: | ||||||
|  |                                     (value) => includeRecycled.value = value, | ||||||
|  |                                 padding: EdgeInsets.zero, | ||||||
|  |                               ), | ||||||
|  |                             ], | ||||||
|  |                           ), | ||||||
|  |                         ), | ||||||
|  |                         const Gap(16), | ||||||
|  |                         IconButton( | ||||||
|  |                           icon: const Icon(Symbols.delete_sweep), | ||||||
|  |                           tooltip: 'deleteRecycledFiles'.tr(), | ||||||
|  |                           onPressed: | ||||||
|  |                               includeRecycled.value | ||||||
|  |                                   ? () => _deleteRecycledFiles(ref) | ||||||
|  |                                   : null, | ||||||
|  |                         ), | ||||||
|  |                       ], | ||||||
|  |                     ) | ||||||
|  |                     : Column( | ||||||
|  |                       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                       children: [ | ||||||
|  |                         poolsAsync.when( | ||||||
|  |                           data: | ||||||
|  |                               (pools) => DropdownButtonFormField<String?>( | ||||||
|  |                                 value: selectedPool.value, | ||||||
|  |                                 decoration: const InputDecoration( | ||||||
|  |                                   labelText: 'Pool', | ||||||
|  |                                   border: OutlineInputBorder(), | ||||||
|  |                                 ), | ||||||
|  |                                 items: [ | ||||||
|  |                                   DropdownMenuItem<String?>( | ||||||
|  |                                     value: null, | ||||||
|  |                                     child: Text('allPools'.tr()), | ||||||
|  |                                   ), | ||||||
|  |                                   ...pools.map( | ||||||
|  |                                     (pool) => DropdownMenuItem<String?>( | ||||||
|  |                                       value: pool.id, | ||||||
|  |                                       child: Text(pool.name), | ||||||
|  |                                     ), | ||||||
|  |                                   ), | ||||||
|  |                                 ], | ||||||
|  |                                 onChanged: | ||||||
|  |                                     (value) => selectedPool.value = value, | ||||||
|  |                               ), | ||||||
|  |                           loading: () => const CircularProgressIndicator(), | ||||||
|  |                           error: (e, _) => const Text('Error loading pools'), | ||||||
|  |                         ), | ||||||
|  |                         const Gap(16), | ||||||
|  |                         Row( | ||||||
|  |                           children: [ | ||||||
|  |                             Text('includeRecycled'.tr()), | ||||||
|  |                             const Gap(8), | ||||||
|  |                             Switch( | ||||||
|  |                               value: includeRecycled.value, | ||||||
|  |                               onChanged: | ||||||
|  |                                   (value) => includeRecycled.value = value, | ||||||
|  |                             ), | ||||||
|  |                             const Spacer(), | ||||||
|  |                             IconButton( | ||||||
|  |                               icon: const Icon(Symbols.delete_sweep), | ||||||
|  |                               tooltip: 'deleteRecycledFiles'.tr(), | ||||||
|  |                               onPressed: | ||||||
|  |                                   includeRecycled.value | ||||||
|  |                                       ? () => _deleteRecycledFiles(ref) | ||||||
|  |                                       : null, | ||||||
|  |                             ), | ||||||
|  |                           ], | ||||||
|  |                         ), | ||||||
|  |                       ], | ||||||
|  |                     ); | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ).padding(horizontal: 8); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> _deleteRecycledFiles(WidgetRef ref) async { | ||||||
|  |     final confirmed = await showConfirmAlert( | ||||||
|  |       'confirmDeleteRecycledFiles'.tr(), | ||||||
|  |       'deleteRecycledFiles'.tr(), | ||||||
|  |     ); | ||||||
|  |     if (!confirmed) return; | ||||||
|  |  | ||||||
|  |     if (ref.context.mounted) showLoadingModal(ref.context); | ||||||
|  |     try { | ||||||
|  |       final client = ref.read(apiClientProvider); | ||||||
|  |       await client.delete('/drive/files/recycled'); | ||||||
|  |       ref.invalidate(cloudFileListNotifierProvider); | ||||||
|  |       showSnackBar('recycledFilesDeleted'.tr()); | ||||||
|  |     } catch (e) { | ||||||
|  |       showSnackBar('failedToDeleteRecycledFiles'.tr()); | ||||||
|  |     } finally { | ||||||
|  |       if (ref.context.mounted) hideLoadingModal(ref.context); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildStatCard(String label, String value, {double? progress}) { | ||||||
|  |     return Card( | ||||||
|  |       child: Padding( | ||||||
|  |         padding: const EdgeInsets.all(16), | ||||||
|  |         child: Column( | ||||||
|  |           mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |           children: [ | ||||||
|  |             Text(label, style: const TextStyle(fontSize: 14)), | ||||||
|  |             Row( | ||||||
|  |               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
|  |               children: [ | ||||||
|  |                 Text( | ||||||
|  |                   value, | ||||||
|  |                   style: const TextStyle( | ||||||
|  |                     fontSize: 24, | ||||||
|  |                     fontWeight: FontWeight.bold, | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |                 if (progress != null) ...[ | ||||||
|  |                   const SizedBox(height: 8), | ||||||
|  |                   SizedBox( | ||||||
|  |                     width: 28, | ||||||
|  |                     height: 28, | ||||||
|  |                     child: CircularProgressIndicator(value: progress), | ||||||
|  |                   ), | ||||||
|  |                 ], | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										69
									
								
								lib/screens/files/file_list.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								lib/screens/files/file_list.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | // GENERATED CODE - DO NOT MODIFY BY HAND | ||||||
|  |  | ||||||
|  | part of 'file_list.dart'; | ||||||
|  |  | ||||||
|  | // ************************************************************************** | ||||||
|  | // RiverpodGenerator | ||||||
|  | // ************************************************************************** | ||||||
|  |  | ||||||
|  | String _$billingUsageHash() => r'58d8bc774868d60781574c85d6b25869a79c57aa'; | ||||||
|  |  | ||||||
|  | /// See also [billingUsage]. | ||||||
|  | @ProviderFor(billingUsage) | ||||||
|  | final billingUsageProvider = | ||||||
|  |     AutoDisposeFutureProvider<Map<String, dynamic>?>.internal( | ||||||
|  |       billingUsage, | ||||||
|  |       name: r'billingUsageProvider', | ||||||
|  |       debugGetCreateSourceHash: | ||||||
|  |           const bool.fromEnvironment('dart.vm.product') | ||||||
|  |               ? null | ||||||
|  |               : _$billingUsageHash, | ||||||
|  |       dependencies: null, | ||||||
|  |       allTransitiveDependencies: null, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  | @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||||
|  | // ignore: unused_element | ||||||
|  | typedef BillingUsageRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>; | ||||||
|  | String _$billingQuotaHash() => r'4ec5d728e439015800abb2d0d673b5a7329cc654'; | ||||||
|  |  | ||||||
|  | /// See also [billingQuota]. | ||||||
|  | @ProviderFor(billingQuota) | ||||||
|  | final billingQuotaProvider = | ||||||
|  |     AutoDisposeFutureProvider<Map<String, dynamic>?>.internal( | ||||||
|  |       billingQuota, | ||||||
|  |       name: r'billingQuotaProvider', | ||||||
|  |       debugGetCreateSourceHash: | ||||||
|  |           const bool.fromEnvironment('dart.vm.product') | ||||||
|  |               ? null | ||||||
|  |               : _$billingQuotaHash, | ||||||
|  |       dependencies: null, | ||||||
|  |       allTransitiveDependencies: null, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  | @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||||
|  | // ignore: unused_element | ||||||
|  | typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>; | ||||||
|  | String _$cloudFileListNotifierHash() => | ||||||
|  |     r'22c45a8ea23147a3835ba870ad2f0bb833f853ea'; | ||||||
|  |  | ||||||
|  | /// See also [CloudFileListNotifier]. | ||||||
|  | @ProviderFor(CloudFileListNotifier) | ||||||
|  | final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider< | ||||||
|  |   CloudFileListNotifier, | ||||||
|  |   CursorPagingData<SnCloudFile> | ||||||
|  | >.internal( | ||||||
|  |   CloudFileListNotifier.new, | ||||||
|  |   name: r'cloudFileListNotifierProvider', | ||||||
|  |   debugGetCreateSourceHash: | ||||||
|  |       const bool.fromEnvironment('dart.vm.product') | ||||||
|  |           ? null | ||||||
|  |           : _$cloudFileListNotifierHash, | ||||||
|  |   dependencies: null, | ||||||
|  |   allTransitiveDependencies: null, | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | typedef _$CloudFileListNotifier = | ||||||
|  |     AutoDisposeAsyncNotifier<CursorPagingData<SnCloudFile>>; | ||||||
|  | // ignore_for_file: type=lint | ||||||
|  | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | ||||||
| @@ -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); | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import 'package:island/screens/creators/publishers.dart'; | |||||||
| import 'package:island/screens/posts/compose_article.dart'; | import 'package:island/screens/posts/compose_article.dart'; | ||||||
| import 'package:island/services/responsive.dart'; | import 'package:island/services/responsive.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
|  | import 'package:island/widgets/attachment_uploader.dart'; | ||||||
| import 'package:island/widgets/content/attachment_preview.dart'; | import 'package:island/widgets/content/attachment_preview.dart'; | ||||||
| import 'package:island/widgets/content/cloud_files.dart'; | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
| import 'package:island/widgets/post/compose_shared.dart'; | import 'package:island/widgets/post/compose_shared.dart'; | ||||||
| @@ -225,8 +226,26 @@ class PostComposeScreen extends HookConsumerWidget { | |||||||
|           return AttachmentPreview( |           return AttachmentPreview( | ||||||
|             item: state.attachments.value[idx], |             item: state.attachments.value[idx], | ||||||
|             progress: progressMap[idx], |             progress: progressMap[idx], | ||||||
|             onRequestUpload: |             onRequestUpload: () async { | ||||||
|                 () => ComposeLogic.uploadAttachment(ref, state, idx), |               final config = await showModalBottomSheet<AttachmentUploadConfig>( | ||||||
|  |                 context: context, | ||||||
|  |                 isScrollControlled: true, | ||||||
|  |                 builder: | ||||||
|  |                     (context) => AttachmentUploaderSheet( | ||||||
|  |                       ref: ref, | ||||||
|  |                       state: state, | ||||||
|  |                       index: idx, | ||||||
|  |                     ), | ||||||
|  |               ); | ||||||
|  |               if (config != null) { | ||||||
|  |                 await ComposeLogic.uploadAttachment( | ||||||
|  |                   ref, | ||||||
|  |                   state, | ||||||
|  |                   idx, | ||||||
|  |                   poolId: config.poolId, | ||||||
|  |                 ); | ||||||
|  |               } | ||||||
|  |             }, | ||||||
|             onDelete: () => ComposeLogic.deleteAttachment(ref, state, idx), |             onDelete: () => ComposeLogic.deleteAttachment(ref, state, idx), | ||||||
|             onUpdate: |             onUpdate: | ||||||
|                 (value) => ComposeLogic.updateAttachment(state, value, idx), |                 (value) => ComposeLogic.updateAttachment(state, value, idx), | ||||||
| @@ -253,8 +272,27 @@ class PostComposeScreen extends HookConsumerWidget { | |||||||
|                 return AttachmentPreview( |                 return AttachmentPreview( | ||||||
|                   item: state.attachments.value[idx], |                   item: state.attachments.value[idx], | ||||||
|                   progress: progressMap[idx], |                   progress: progressMap[idx], | ||||||
|                   onRequestUpload: |                   onRequestUpload: () async { | ||||||
|                       () => ComposeLogic.uploadAttachment(ref, state, idx), |                     final config = | ||||||
|  |                         await showModalBottomSheet<AttachmentUploadConfig>( | ||||||
|  |                           context: context, | ||||||
|  |                           isScrollControlled: true, | ||||||
|  |                           builder: | ||||||
|  |                               (context) => AttachmentUploaderSheet( | ||||||
|  |                                 ref: ref, | ||||||
|  |                                 state: state, | ||||||
|  |                                 index: idx, | ||||||
|  |                               ), | ||||||
|  |                         ); | ||||||
|  |                     if (config != null) { | ||||||
|  |                       await ComposeLogic.uploadAttachment( | ||||||
|  |                         ref, | ||||||
|  |                         state, | ||||||
|  |                         idx, | ||||||
|  |                         poolId: config.poolId, | ||||||
|  |                       ); | ||||||
|  |                     } | ||||||
|  |                   }, | ||||||
|                   onDelete: |                   onDelete: | ||||||
|                       () => ComposeLogic.deleteAttachment(ref, state, idx), |                       () => ComposeLogic.deleteAttachment(ref, state, idx), | ||||||
|                   onUpdate: |                   onUpdate: | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ import 'package:island/models/post.dart'; | |||||||
| import 'package:island/screens/creators/publishers.dart'; | import 'package:island/screens/creators/publishers.dart'; | ||||||
| import 'package:island/services/responsive.dart'; | import 'package:island/services/responsive.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
|  | import 'package:island/widgets/attachment_uploader.dart'; | ||||||
| import 'package:island/screens/posts/post_detail.dart'; | import 'package:island/screens/posts/post_detail.dart'; | ||||||
| import 'package:island/widgets/content/attachment_preview.dart'; | import 'package:island/widgets/content/attachment_preview.dart'; | ||||||
| import 'package:island/widgets/content/cloud_files.dart'; | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
| @@ -345,12 +346,30 @@ class ArticleComposeScreen extends HookConsumerWidget { | |||||||
|                                       isCompact: true, |                                       isCompact: true, | ||||||
|                                       item: attachments[idx], |                                       item: attachments[idx], | ||||||
|                                       progress: progressMap[idx], |                                       progress: progressMap[idx], | ||||||
|                                       onRequestUpload: |                                       onRequestUpload: () async { | ||||||
|                                           () => ComposeLogic.uploadAttachment( |                                         final config = | ||||||
|  |                                             await showModalBottomSheet< | ||||||
|  |                                               AttachmentUploadConfig | ||||||
|  |                                             >( | ||||||
|  |                                               context: context, | ||||||
|  |                                               isScrollControlled: true, | ||||||
|  |                                               builder: | ||||||
|  |                                                   (context) => | ||||||
|  |                                                       AttachmentUploaderSheet( | ||||||
|  |                                                         ref: ref, | ||||||
|  |                                                         state: state, | ||||||
|  |                                                         index: idx, | ||||||
|  |                                                       ), | ||||||
|  |                                             ); | ||||||
|  |                                         if (config != null) { | ||||||
|  |                                           await ComposeLogic.uploadAttachment( | ||||||
|                                             ref, |                                             ref, | ||||||
|                                             state, |                                             state, | ||||||
|                                             idx, |                                             idx, | ||||||
|                                           ), |                                             poolId: config.poolId, | ||||||
|  |                                           ); | ||||||
|  |                                         } | ||||||
|  |                                       }, | ||||||
|                                       onUpdate: |                                       onUpdate: | ||||||
|                                           (value) => |                                           (value) => | ||||||
|                                               ComposeLogic.updateAttachment( |                                               ComposeLogic.updateAttachment( | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -400,7 +400,7 @@ class _PublisherSubscriptionStatusProviderElement | |||||||
| } | } | ||||||
|  |  | ||||||
| String _$publisherAppbarForcegroundColorHash() => | String _$publisherAppbarForcegroundColorHash() => | ||||||
|     r'd781a806a242aea5c1609ec98c97c52fdd9f7db1'; |     r'cd9a9816177a6eecc2bc354acebbbd48892ffdd7'; | ||||||
|  |  | ||||||
| /// See also [publisherAppbarForcegroundColor]. | /// See also [publisherAppbarForcegroundColor]. | ||||||
| @ProviderFor(publisherAppbarForcegroundColor) | @ProviderFor(publisherAppbarForcegroundColor) | ||||||
|   | |||||||
| @@ -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; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 { | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -12,14 +12,16 @@ 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/file_pool.dart'; | ||||||
|  |  | ||||||
| class SettingsScreen extends HookConsumerWidget { | class SettingsScreen extends HookConsumerWidget { | ||||||
|   const SettingsScreen({super.key}); |   const SettingsScreen({super.key}); | ||||||
| @@ -33,7 +35,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 +130,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: 'column', | ||||||
|  |                 child: Text('Column').fontSize(14), | ||||||
|  |               ), | ||||||
|  |               DropdownMenuItem<String>( | ||||||
|  |                 value: 'compact', | ||||||
|  |                 child: Text('Compact').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 +338,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 +412,71 @@ class SettingsScreen extends HookConsumerWidget { | |||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|       ), |       ), | ||||||
|  |  | ||||||
|  |       if (user.value != null) | ||||||
|  |         pools.when( | ||||||
|  |           data: (data) { | ||||||
|  |             final validPools = data; | ||||||
|  |             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: Tooltip( | ||||||
|  |                             message: p.name, | ||||||
|  |                             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 = [ | ||||||
|   | |||||||
							
								
								
									
										49
									
								
								lib/services/color_extraction.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								lib/services/color_extraction.dart
									
									
									
									
									
										Normal 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 []; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -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; | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										155
									
								
								lib/services/file_uploader.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								lib/services/file_uploader.dart
									
									
									
									
									
										Normal 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); | ||||||
|  | }); | ||||||
| @@ -226,7 +226,7 @@ Future<void> _putTokenToRemote( | |||||||
|   int provider, |   int provider, | ||||||
| ) async { | ) async { | ||||||
|   await apiClient.put( |   await apiClient.put( | ||||||
|     "/pusher/notifications/subscription", |     "/ring/notifications/subscription", | ||||||
|     data: {"provider": provider, "device_token": token}, |     data: {"provider": provider, "device_token": token}, | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -170,7 +170,7 @@ Future<void> _putTokenToRemote( | |||||||
|   int provider, |   int provider, | ||||||
| ) async { | ) async { | ||||||
|   await apiClient.put( |   await apiClient.put( | ||||||
|     "/pusher/notifications/subscription", |     "/ring/notifications/subscription", | ||||||
|     data: {"provider": provider, "device_token": token}, |     data: {"provider": provider, "device_token": token}, | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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() { | ||||||
|   | |||||||
| @@ -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() { | ||||||
|   | |||||||
| @@ -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'; | ||||||
|   | |||||||
| @@ -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(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -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'; | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										122
									
								
								lib/utils/activity_utils.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								lib/utils/activity_utils.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | |||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:island/models/account.dart'; | ||||||
|  |  | ||||||
|  | String? getActivityTitle(String? label, Map<String, dynamic>? meta) { | ||||||
|  |   if (meta == null) return label; | ||||||
|  |   if (meta['assets']?['large_text'] is String) { | ||||||
|  |     return meta['assets']?['large_text']; | ||||||
|  |   } | ||||||
|  |   return label; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | String? getActivitySubtitle(Map<String, dynamic>? meta) { | ||||||
|  |   if (meta == null) return null; | ||||||
|  |   if (meta['assets']?['small_text'] is String) { | ||||||
|  |     return meta['assets']?['small_text']; | ||||||
|  |   } | ||||||
|  |   return null; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | InlineSpan getActivityFullMessage(SnAccountStatus? status) { | ||||||
|  |   if (status?.meta == null) return TextSpan(text: 'No activity details available'); | ||||||
|  |   final meta = status!.meta!; | ||||||
|  |   final List<InlineSpan> spans = []; | ||||||
|  |   if (meta.containsKey('assets') && meta['assets'] is Map) { | ||||||
|  |     final assets = meta['assets'] as Map<String, dynamic>; | ||||||
|  |     if (assets.containsKey('large_text')) { | ||||||
|  |       spans.add(TextSpan(text: assets['large_text'], style: TextStyle(fontWeight: FontWeight.bold))); | ||||||
|  |     } | ||||||
|  |     if (assets.containsKey('small_text')) { | ||||||
|  |       if (spans.isNotEmpty) spans.add(TextSpan(text: '\n')); | ||||||
|  |       spans.add(TextSpan(text: assets['small_text'])); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   String normalText = ''; | ||||||
|  |   if (meta.containsKey('details')) { | ||||||
|  |     normalText += 'Details: ${meta['details']}\n'; | ||||||
|  |   } | ||||||
|  |   if (meta.containsKey('state')) { | ||||||
|  |     normalText += 'State: ${meta['state']}\n'; | ||||||
|  |   } | ||||||
|  |   if (meta.containsKey('timestamps') && meta['timestamps'] is Map) { | ||||||
|  |     final ts = meta['timestamps'] as Map<String, dynamic>; | ||||||
|  |     if (ts.containsKey('start') && ts['start'] is int) { | ||||||
|  |       final start = DateTime.fromMillisecondsSinceEpoch(ts['start'] * 1000); | ||||||
|  |       normalText += 'Started: ${start.toLocal()}\n'; | ||||||
|  |     } | ||||||
|  |     if (ts.containsKey('end') && ts['end'] is int) { | ||||||
|  |       final end = DateTime.fromMillisecondsSinceEpoch(ts['end'] * 1000); | ||||||
|  |       normalText += 'Ends: ${end.toLocal()}\n'; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (meta.containsKey('party') && meta['party'] is Map) { | ||||||
|  |     final party = meta['party'] as Map<String, dynamic>; | ||||||
|  |     if (party.containsKey('size') && party['size'] is List && party['size'].length >= 2) { | ||||||
|  |       final size = party['size'] as List; | ||||||
|  |       normalText += 'Party: ${size[0]}/${size[1]}\n'; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (meta.containsKey('instance')) { | ||||||
|  |     normalText += 'Instance: ${meta['instance']}\n'; | ||||||
|  |   } | ||||||
|  |   // Add other keys if present | ||||||
|  |   meta.forEach((key, value) { | ||||||
|  |     if (!['details', 'state', 'timestamps', 'assets', 'party', 'secrets', 'instance'].contains(key)) { | ||||||
|  |       normalText += '$key: $value\n'; | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   if (normalText.isNotEmpty) { | ||||||
|  |     if (spans.isNotEmpty) spans.add(TextSpan(text: '\n')); | ||||||
|  |     spans.add(TextSpan(text: normalText.trimRight())); | ||||||
|  |   } | ||||||
|  |   return TextSpan(children: spans); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Widget buildActivityDetails(SnAccountStatus? status) { | ||||||
|  |   if (status?.meta == null) return Text('No activity details available'); | ||||||
|  |   final meta = status!.meta!; | ||||||
|  |   final List<Widget> children = []; | ||||||
|  |   if (meta.containsKey('assets') && meta['assets'] is Map) { | ||||||
|  |     final assets = meta['assets'] as Map<String, dynamic>; | ||||||
|  |     if (assets.containsKey('large_text')) { | ||||||
|  |       children.add(Text(assets['large_text'])); | ||||||
|  |     } | ||||||
|  |     if (assets.containsKey('small_text')) { | ||||||
|  |       children.add(Text(assets['small_text'])); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (meta.containsKey('details')) { | ||||||
|  |     children.add(Text('Details: ${meta['details']}')); | ||||||
|  |   } | ||||||
|  |   if (meta.containsKey('state')) { | ||||||
|  |     children.add(Text('State: ${meta['state']}')); | ||||||
|  |   } | ||||||
|  |   if (meta.containsKey('timestamps') && meta['timestamps'] is Map) { | ||||||
|  |     final ts = meta['timestamps'] as Map<String, dynamic>; | ||||||
|  |     if (ts.containsKey('start') && ts['start'] is int) { | ||||||
|  |       final start = DateTime.fromMillisecondsSinceEpoch(ts['start'] * 1000); | ||||||
|  |       children.add(Text('Started: ${start.toLocal()}')); | ||||||
|  |     } | ||||||
|  |     if (ts.containsKey('end') && ts['end'] is int) { | ||||||
|  |       final end = DateTime.fromMillisecondsSinceEpoch(ts['end'] * 1000); | ||||||
|  |       children.add(Text('Ends: ${end.toLocal()}')); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (meta.containsKey('party') && meta['party'] is Map) { | ||||||
|  |     final party = meta['party'] as Map<String, dynamic>; | ||||||
|  |     if (party.containsKey('size') && party['size'] is List && party['size'].length >= 2) { | ||||||
|  |       final size = party['size'] as List; | ||||||
|  |       children.add(Text('Party: ${size[0]}/${size[1]}')); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (meta.containsKey('instance')) { | ||||||
|  |     children.add(Text('Instance: ${meta['instance']}')); | ||||||
|  |   } | ||||||
|  |   // Add other keys if present | ||||||
|  |   children.addAll(meta.entries.where((e) => !['details', 'state', 'timestamps', 'assets', 'party', 'secrets', 'instance'].contains(e.key)).map((e) => Text('${e.key}: ${e.value}'))); | ||||||
|  |   return Column( | ||||||
|  |     crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |     mainAxisSize: MainAxisSize.min, | ||||||
|  |     children: children, | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -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,117 @@ 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(), |           ), | ||||||
|  |         ], | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -147,6 +147,7 @@ class AccountProfileCard extends HookConsumerWidget { | |||||||
|                       if (data.badges.isNotEmpty) |                       if (data.badges.isNotEmpty) | ||||||
|                         BadgeList(badges: data.badges).padding(top: 12), |                         BadgeList(badges: data.badges).padding(top: 12), | ||||||
|                       LevelingProgressCard( |                       LevelingProgressCard( | ||||||
|  |                         isCompact: true, | ||||||
|                         level: data.profile.level, |                         level: data.profile.level, | ||||||
|                         experience: data.profile.experience, |                         experience: data.profile.experience, | ||||||
|                         progress: data.profile.levelingProgress, |                         progress: data.profile.levelingProgress, | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; | |||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:island/models/activity.dart'; | import 'package:island/models/activity.dart'; | ||||||
| import 'package:island/services/time.dart'; | import 'package:island/services/time.dart'; | ||||||
|  | import 'package:island/utils/activity_utils.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
| @@ -75,7 +76,10 @@ class EventDetailsWidget extends StatelessWidget { | |||||||
|                       child: Column( |                       child: Column( | ||||||
|                         crossAxisAlignment: CrossAxisAlignment.start, |                         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|                         children: [ |                         children: [ | ||||||
|                           Text(status.label), |                           if ((getActivityTitle(status.label, status.meta) ?? status.label).isNotEmpty) | ||||||
|  |                             Text(getActivityTitle(status.label, status.meta) ?? status.label), | ||||||
|  |                           if (getActivitySubtitle(status.meta) != null) | ||||||
|  |                             Text(getActivitySubtitle(status.meta)!).fontSize(11).opacity(0.8), | ||||||
|                           Text( |                           Text( | ||||||
|                             '${status.createdAt.formatSystem()} - ${status.clearedAt?.formatSystem() ?? 'present'.tr()}', |                             '${status.createdAt.formatSystem()} - ${status.clearedAt?.formatSystem() ?? 'present'.tr()}', | ||||||
|                           ).fontSize(11).opacity(0.8), |                           ).fontSize(11).opacity(0.8), | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:google_fonts/google_fonts.dart'; |  | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
| @@ -8,50 +7,152 @@ class LevelingProgressCard extends StatelessWidget { | |||||||
|   final int level; |   final int level; | ||||||
|   final int experience; |   final int experience; | ||||||
|   final double progress; |   final double progress; | ||||||
|  |   final VoidCallback? onTap; | ||||||
|  |   final bool isCompact; | ||||||
|  |  | ||||||
|   const LevelingProgressCard({ |   const LevelingProgressCard({ | ||||||
|     super.key, |     super.key, | ||||||
|     required this.level, |     required this.level, | ||||||
|     required this.experience, |     required this.experience, | ||||||
|     required this.progress, |     required this.progress, | ||||||
|  |     this.onTap, | ||||||
|  |     this.isCompact = false, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return Card( |     // Calculate level stage (1-12, each stage covers 10 levels) | ||||||
|  |     int stage = ((level - 1) ~/ 10) + 1; | ||||||
|  |     stage = stage.clamp(1, 12); // Ensure stage is within 1-12 | ||||||
|  |  | ||||||
|  |     // Define colors for each stage | ||||||
|  |     const List<Color> stageColors = [ | ||||||
|  |       Colors.green, | ||||||
|  |       Colors.blue, | ||||||
|  |       Colors.teal, | ||||||
|  |       Colors.cyan, | ||||||
|  |       Colors.indigo, | ||||||
|  |       Colors.lime, | ||||||
|  |       Colors.yellow, | ||||||
|  |       Colors.amber, | ||||||
|  |       Colors.orange, | ||||||
|  |       Colors.deepOrange, | ||||||
|  |       Colors.pink, | ||||||
|  |       Colors.red, | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     Color stageColor = stageColors[stage - 1]; | ||||||
|  |  | ||||||
|  |     // Compact mode adjustments | ||||||
|  |     final double levelFontSize = isCompact ? 14 : 18; | ||||||
|  |     final double stageFontSize = isCompact ? 13 : 14; | ||||||
|  |     final double experienceFontSize = isCompact ? 12 : 14; | ||||||
|  |     final double progressHeight = isCompact ? 6 : 10; | ||||||
|  |     final double horizontalPadding = isCompact ? 16 : 20; | ||||||
|  |     final double verticalPadding = isCompact ? 12 : 16; | ||||||
|  |     final double gapSize = isCompact ? 4 : 8; | ||||||
|  |     final double rowSpacing = 12; | ||||||
|  |  | ||||||
|  |     final cardContent = Card( | ||||||
|       margin: EdgeInsets.zero, |       margin: EdgeInsets.zero, | ||||||
|       child: Column( |       shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), | ||||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, |       child: InkWell( | ||||||
|         children: [ |         onTap: onTap, | ||||||
|           Row( |         borderRadius: BorderRadius.circular(12), | ||||||
|             spacing: 8, |         child: Container( | ||||||
|             crossAxisAlignment: CrossAxisAlignment.baseline, |           decoration: BoxDecoration( | ||||||
|             mainAxisAlignment: MainAxisAlignment.spaceBetween, |             borderRadius: BorderRadius.circular(12), | ||||||
|             textBaseline: TextBaseline.alphabetic, |             gradient: LinearGradient( | ||||||
|             children: [ |               colors: [ | ||||||
|               Text( |                 stageColor.withOpacity(0.1), | ||||||
|                 'levelingProgressLevel'.tr(args: [level.toString()]), |                 Theme.of(context).colorScheme.surface, | ||||||
|                 style: GoogleFonts.robotoMono(), |               ], | ||||||
|               ).fontSize(13).bold(), |               begin: Alignment.topLeft, | ||||||
|               Text( |               end: Alignment.bottomRight, | ||||||
|                 'levelingProgressExperience'.tr(args: [experience.toString()]), |  | ||||||
|                 style: GoogleFonts.robotoMono(), |  | ||||||
|               ).fontSize(13), |  | ||||||
|             ], |  | ||||||
|           ), |  | ||||||
|           const Gap(8), |  | ||||||
|           Tooltip( |  | ||||||
|             message: '${(progress).toStringAsFixed(1)}%', |  | ||||||
|             child: LinearProgressIndicator( |  | ||||||
|               minHeight: 4, |  | ||||||
|               value: progress / 100, |  | ||||||
|               color: Theme.of(context).colorScheme.primary, |  | ||||||
|               backgroundColor: |  | ||||||
|                   Theme.of(context).colorScheme.surfaceContainerHigh, |  | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|         ], |           child: Column( | ||||||
|       ).padding(horizontal: 16, vertical: 12), |             crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |             children: [ | ||||||
|  |               Row( | ||||||
|  |                 spacing: rowSpacing, | ||||||
|  |                 crossAxisAlignment: CrossAxisAlignment.baseline, | ||||||
|  |                 mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
|  |                 textBaseline: TextBaseline.alphabetic, | ||||||
|  |                 children: [ | ||||||
|  |                   Expanded( | ||||||
|  |                     child: Text( | ||||||
|  |                       'levelingProgressLevel'.tr(args: [level.toString()]), | ||||||
|  |                       style: TextStyle( | ||||||
|  |                         color: stageColor, | ||||||
|  |                         fontWeight: FontWeight.bold, | ||||||
|  |                         fontSize: levelFontSize, | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                   Row( | ||||||
|  |                     mainAxisSize: MainAxisSize.min, | ||||||
|  |                     children: [ | ||||||
|  |                       Text( | ||||||
|  |                         'levelingStage$stage'.tr(), | ||||||
|  |                         style: TextStyle( | ||||||
|  |                           color: stageColor.withOpacity(0.7), | ||||||
|  |                           fontWeight: FontWeight.w500, | ||||||
|  |                           fontSize: stageFontSize, | ||||||
|  |                         ), | ||||||
|  |                       ), | ||||||
|  |                       if (onTap != null) ...[ | ||||||
|  |                         const Gap(4), | ||||||
|  |                         Icon( | ||||||
|  |                           Icons.arrow_forward_ios, | ||||||
|  |                           size: isCompact ? 10 : 12, | ||||||
|  |                           color: stageColor.withOpacity(0.7), | ||||||
|  |                         ), | ||||||
|  |                       ], | ||||||
|  |                     ], | ||||||
|  |                   ), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |               Gap(gapSize), | ||||||
|  |               Row( | ||||||
|  |                 spacing: rowSpacing, | ||||||
|  |                 crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |                 children: [ | ||||||
|  |                   Expanded( | ||||||
|  |                     child: Tooltip( | ||||||
|  |                       message: '${progress.toStringAsFixed(1)}%', | ||||||
|  |                       child: LinearProgressIndicator( | ||||||
|  |                         minHeight: progressHeight, | ||||||
|  |                         value: progress, | ||||||
|  |                         borderRadius: BorderRadius.circular(32), | ||||||
|  |                         backgroundColor: Theme.of( | ||||||
|  |                           context, | ||||||
|  |                         ).colorScheme.surfaceContainerLow.withOpacity(0.75), | ||||||
|  |                         color: stageColor, | ||||||
|  |                         stopIndicatorRadius: 0, | ||||||
|  |                         trackGap: 0, | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                   Text( | ||||||
|  |                     'levelingProgressExperience'.tr( | ||||||
|  |                       args: [experience.toString()], | ||||||
|  |                     ), | ||||||
|  |                     style: TextStyle( | ||||||
|  |                       color: Theme.of( | ||||||
|  |                         context, | ||||||
|  |                       ).colorScheme.onSurface.withOpacity(0.8), | ||||||
|  |                       fontSize: experienceFontSize, | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ).padding(horizontal: horizontalPadding, vertical: verticalPadding), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  |     return cardContent; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,8 +4,10 @@ import 'package:flutter/material.dart'; | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | 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/pods/userinfo.dart'; | ||||||
| import 'package:island/screens/account/profile.dart'; | import 'package:island/screens/account/profile.dart'; | ||||||
| import 'package:island/services/time.dart'; | import 'package:island/services/time.dart'; | ||||||
|  | import 'package:island/utils/activity_utils.dart'; | ||||||
| import 'package:island/widgets/account/status_creation.dart'; | import 'package:island/widgets/account/status_creation.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| @@ -13,8 +15,31 @@ import 'package:styled_widget/styled_widget.dart'; | |||||||
|  |  | ||||||
| part 'status.g.dart'; | part 'status.g.dart'; | ||||||
|  |  | ||||||
|  | class CurrentAccountStatusNotifier extends StateNotifier<SnAccountStatus?> { | ||||||
|  |   CurrentAccountStatusNotifier() : super(null); | ||||||
|  |  | ||||||
|  |   void setStatus(SnAccountStatus status) { | ||||||
|  |     state = status; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void clearStatus() { | ||||||
|  |     state = null; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | final currentAccountStatusProvider = StateNotifierProvider<CurrentAccountStatusNotifier, SnAccountStatus?>((ref) { | ||||||
|  |   return CurrentAccountStatusNotifier(); | ||||||
|  | }); | ||||||
|  |  | ||||||
| @riverpod | @riverpod | ||||||
| Future<SnAccountStatus?> accountStatus(Ref ref, String uname) async { | Future<SnAccountStatus?> accountStatus(Ref ref, String uname) async { | ||||||
|  |   final userInfo = ref.watch(userInfoProvider); | ||||||
|  |   if (uname == 'me' || (userInfo.value != null && uname == userInfo.value!.name)) { | ||||||
|  |     final local = ref.watch(currentAccountStatusProvider); | ||||||
|  |     if (local != null) { | ||||||
|  |       return local; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|   final apiClient = ref.watch(apiClientProvider); |   final apiClient = ref.watch(apiClientProvider); | ||||||
|   try { |   try { | ||||||
|     final resp = await apiClient.get('/id/accounts/$uname/statuses'); |     final resp = await apiClient.get('/id/accounts/$uname/statuses'); | ||||||
| @@ -110,7 +135,11 @@ class AccountStatusWidget extends HookConsumerWidget { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final status = ref.watch(accountStatusProvider(uname)); |     final userInfo = ref.watch(userInfoProvider); | ||||||
|  |     final localStatus = ref.watch(currentAccountStatusProvider); | ||||||
|  |     final status = (uname == 'me' || (userInfo.value != null && uname == userInfo.value!.name && localStatus != null)) | ||||||
|  |         ? AsyncValue.data(localStatus) | ||||||
|  |         : ref.watch(accountStatusProvider(uname)); | ||||||
|     final account = ref.watch(accountProvider(uname)); |     final account = ref.watch(accountProvider(uname)); | ||||||
|  |  | ||||||
|     return Padding( |     return Padding( | ||||||
| @@ -133,10 +162,31 @@ class AccountStatusWidget extends HookConsumerWidget { | |||||||
|             ).padding(right: 4), |             ).padding(right: 4), | ||||||
|           if (status.value?.isCustomized ?? false) |           if (status.value?.isCustomized ?? false) | ||||||
|             Flexible( |             Flexible( | ||||||
|               child: Text( |               child: GestureDetector( | ||||||
|                 status.value?.label ?? 'unknown'.tr(), |                 onLongPress: () { | ||||||
|                 maxLines: 1, |                   showDialog( | ||||||
|                 overflow: TextOverflow.ellipsis, |                     context: context, | ||||||
|  |                     builder: (context) => AlertDialog( | ||||||
|  |                       title: Text('Activity Details'), | ||||||
|  |                       content: buildActivityDetails(status.value), | ||||||
|  |                       actions: [ | ||||||
|  |                         TextButton( | ||||||
|  |                           onPressed: () => Navigator.of(context).pop(), | ||||||
|  |                           child: Text('Close'), | ||||||
|  |                         ), | ||||||
|  |                       ], | ||||||
|  |                     ), | ||||||
|  |                   ); | ||||||
|  |                 }, | ||||||
|  |                 child: Tooltip( | ||||||
|  |                   richMessage: getActivityFullMessage(status.value), | ||||||
|  |                   child: Text( | ||||||
|  |                     getActivityTitle(status.value?.label, status.value?.meta) ?? | ||||||
|  |                         'unknown'.tr(), | ||||||
|  |                     maxLines: 1, | ||||||
|  |                     overflow: TextOverflow.ellipsis, | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|               ), |               ), | ||||||
|             ) |             ) | ||||||
|           else |           else | ||||||
| @@ -148,7 +198,13 @@ class AccountStatusWidget extends HookConsumerWidget { | |||||||
|                     overflow: TextOverflow.ellipsis, |                     overflow: TextOverflow.ellipsis, | ||||||
|                   ).tr(), |                   ).tr(), | ||||||
|             ), |             ), | ||||||
|           if (!(status.value?.isOnline ?? false) && |           if (getActivitySubtitle(status.value?.meta) != null) | ||||||
|  |             Flexible( | ||||||
|  |               child: Text( | ||||||
|  |                 getActivitySubtitle(status.value?.meta)!, | ||||||
|  |               ).opacity(0.75), | ||||||
|  |             ) | ||||||
|  |           else if (!(status.value?.isOnline ?? false) && | ||||||
|               account.value?.profile.lastSeenAt != null) |               account.value?.profile.lastSeenAt != null) | ||||||
|             Flexible( |             Flexible( | ||||||
|               child: Text( |               child: Text( | ||||||
|   | |||||||
							
								
								
									
										363
									
								
								lib/widgets/attachment_uploader.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										363
									
								
								lib/widgets/attachment_uploader.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,363 @@ | |||||||
|  | import 'dart:typed_data'; | ||||||
|  |  | ||||||
|  | import 'package:cross_file/cross_file.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:island/models/file.dart'; | ||||||
|  | import 'package:island/models/file_pool.dart'; | ||||||
|  | import 'package:island/pods/file_pool.dart'; | ||||||
|  | import 'package:island/widgets/content/attachment_preview.dart'; | ||||||
|  | import 'package:island/widgets/content/sheet.dart'; | ||||||
|  | import 'package:island/widgets/post/compose_shared.dart'; | ||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:gap/gap.dart'; | ||||||
|  | import 'package:material_symbols_icons/symbols.dart'; | ||||||
|  | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
|  | class AttachmentUploadConfig { | ||||||
|  |   final String poolId; | ||||||
|  |   final bool hasConstraints; | ||||||
|  |  | ||||||
|  |   const AttachmentUploadConfig({ | ||||||
|  |     required this.poolId, | ||||||
|  |     required this.hasConstraints, | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class AttachmentUploaderSheet extends StatefulWidget { | ||||||
|  |   final WidgetRef ref; | ||||||
|  |   final ComposeState state; | ||||||
|  |   final int index; | ||||||
|  |  | ||||||
|  |   const AttachmentUploaderSheet({ | ||||||
|  |     super.key, | ||||||
|  |     required this.ref, | ||||||
|  |     required this.state, | ||||||
|  |     required this.index, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<AttachmentUploaderSheet> createState() => | ||||||
|  |       _AttachmentUploaderSheetState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> { | ||||||
|  |   String? selectedPoolId; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     final attachment = widget.state.attachments.value[widget.index]; | ||||||
|  |  | ||||||
|  |     return SheetScaffold( | ||||||
|  |       titleText: 'uploadAttachment'.tr(), | ||||||
|  |       child: FutureBuilder<List<SnFilePool>>( | ||||||
|  |         future: widget.ref.read(poolsProvider.future), | ||||||
|  |         builder: (context, snapshot) { | ||||||
|  |           if (snapshot.connectionState == ConnectionState.waiting) { | ||||||
|  |             return const Center(child: CircularProgressIndicator()); | ||||||
|  |           } | ||||||
|  |           if (snapshot.hasError) { | ||||||
|  |             return Center(child: Text('errorLoadingPools'.tr())); | ||||||
|  |           } | ||||||
|  |           final pools = snapshot.data!; | ||||||
|  |           selectedPoolId ??= resolveDefaultPoolId(widget.ref, pools); | ||||||
|  |  | ||||||
|  |           return Column( | ||||||
|  |             children: [ | ||||||
|  |               Expanded( | ||||||
|  |                 child: SingleChildScrollView( | ||||||
|  |                   padding: const EdgeInsets.all(16), | ||||||
|  |                   child: Column( | ||||||
|  |                     crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                     children: [ | ||||||
|  |                       DropdownButtonFormField<String>( | ||||||
|  |                         value: selectedPoolId, | ||||||
|  |                         items: | ||||||
|  |                             pools.map((pool) { | ||||||
|  |                               return DropdownMenuItem<String>( | ||||||
|  |                                 value: pool.id, | ||||||
|  |                                 child: Text(pool.name), | ||||||
|  |                               ); | ||||||
|  |                             }).toList(), | ||||||
|  |                         onChanged: (value) { | ||||||
|  |                           setState(() { | ||||||
|  |                             selectedPoolId = value; | ||||||
|  |                           }); | ||||||
|  |                         }, | ||||||
|  |                         decoration: InputDecoration( | ||||||
|  |                           labelText: 'selectPool'.tr(), | ||||||
|  |                           border: const OutlineInputBorder(), | ||||||
|  |                           hintText: 'choosePool'.tr(), | ||||||
|  |                         ), | ||||||
|  |                       ), | ||||||
|  |                       const Gap(16), | ||||||
|  |                       FutureBuilder<int?>( | ||||||
|  |                         future: _getFileSize(attachment), | ||||||
|  |                         builder: (context, sizeSnapshot) { | ||||||
|  |                           if (!sizeSnapshot.hasData) { | ||||||
|  |                             return const SizedBox.shrink(); | ||||||
|  |                           } | ||||||
|  |                           final fileSize = sizeSnapshot.data!; | ||||||
|  |                           final selectedPool = pools.firstWhere( | ||||||
|  |                             (p) => p.id == selectedPoolId, | ||||||
|  |                           ); | ||||||
|  |  | ||||||
|  |                           // Check file size limit | ||||||
|  |                           final maxFileSize = | ||||||
|  |                               selectedPool.policyConfig?['max_file_size'] | ||||||
|  |                                   as int?; | ||||||
|  |                           final fileSizeExceeded = | ||||||
|  |                               maxFileSize != null && fileSize > maxFileSize; | ||||||
|  |  | ||||||
|  |                           // Check accepted types | ||||||
|  |                           final acceptTypes = | ||||||
|  |                               selectedPool.policyConfig?['accept_types'] | ||||||
|  |                                   as List?; | ||||||
|  |                           final mimeType = | ||||||
|  |                               attachment.data.mimeType ?? | ||||||
|  |                               ComposeLogic.getMimeTypeFromFileType( | ||||||
|  |                                 attachment.type, | ||||||
|  |                               ); | ||||||
|  |                           final typeAccepted = | ||||||
|  |                               acceptTypes == null || | ||||||
|  |                               acceptTypes.isEmpty || | ||||||
|  |                               acceptTypes.any( | ||||||
|  |                                 (type) => mimeType.startsWith(type), | ||||||
|  |                               ); | ||||||
|  |  | ||||||
|  |                           final hasIssues = fileSizeExceeded || !typeAccepted; | ||||||
|  |  | ||||||
|  |                           return Column( | ||||||
|  |                             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                             children: [ | ||||||
|  |                               if (hasIssues) ...[ | ||||||
|  |                                 Container( | ||||||
|  |                                   padding: const EdgeInsets.all(12), | ||||||
|  |                                   decoration: BoxDecoration( | ||||||
|  |                                     color: | ||||||
|  |                                         Theme.of( | ||||||
|  |                                           context, | ||||||
|  |                                         ).colorScheme.errorContainer, | ||||||
|  |                                     borderRadius: BorderRadius.circular(8), | ||||||
|  |                                   ), | ||||||
|  |                                   child: Column( | ||||||
|  |                                     crossAxisAlignment: | ||||||
|  |                                         CrossAxisAlignment.start, | ||||||
|  |                                     children: [ | ||||||
|  |                                       Row( | ||||||
|  |                                         children: [ | ||||||
|  |                                           Icon( | ||||||
|  |                                             Symbols.warning, | ||||||
|  |                                             size: 18, | ||||||
|  |                                             color: | ||||||
|  |                                                 Theme.of( | ||||||
|  |                                                   context, | ||||||
|  |                                                 ).colorScheme.error, | ||||||
|  |                                           ), | ||||||
|  |                                           const Gap(8), | ||||||
|  |                                           Text( | ||||||
|  |                                             'uploadConstraints'.tr(), | ||||||
|  |                                             style: Theme.of( | ||||||
|  |                                               context, | ||||||
|  |                                             ).textTheme.bodyMedium?.copyWith( | ||||||
|  |                                               color: | ||||||
|  |                                                   Theme.of( | ||||||
|  |                                                     context, | ||||||
|  |                                                   ).colorScheme.error, | ||||||
|  |                                               fontWeight: FontWeight.w600, | ||||||
|  |                                             ), | ||||||
|  |                                           ), | ||||||
|  |                                         ], | ||||||
|  |                                       ), | ||||||
|  |                                       if (fileSizeExceeded) ...[ | ||||||
|  |                                         const Gap(4), | ||||||
|  |                                         Text( | ||||||
|  |                                           'fileSizeExceeded'.tr( | ||||||
|  |                                             args: [ | ||||||
|  |                                               _formatFileSize(maxFileSize), | ||||||
|  |                                             ], | ||||||
|  |                                           ), | ||||||
|  |                                           style: Theme.of( | ||||||
|  |                                             context, | ||||||
|  |                                           ).textTheme.bodySmall?.copyWith( | ||||||
|  |                                             color: | ||||||
|  |                                                 Theme.of( | ||||||
|  |                                                   context, | ||||||
|  |                                                 ).colorScheme.error, | ||||||
|  |                                           ), | ||||||
|  |                                         ), | ||||||
|  |                                       ], | ||||||
|  |                                       if (!typeAccepted) ...[ | ||||||
|  |                                         const Gap(4), | ||||||
|  |                                         Text( | ||||||
|  |                                           'fileTypeNotAccepted'.tr(), | ||||||
|  |                                           style: Theme.of( | ||||||
|  |                                             context, | ||||||
|  |                                           ).textTheme.bodySmall?.copyWith( | ||||||
|  |                                             color: | ||||||
|  |                                                 Theme.of( | ||||||
|  |                                                   context, | ||||||
|  |                                                 ).colorScheme.error, | ||||||
|  |                                           ), | ||||||
|  |                                         ), | ||||||
|  |                                       ], | ||||||
|  |                                     ], | ||||||
|  |                                   ), | ||||||
|  |                                 ), | ||||||
|  |                                 const Gap(12), | ||||||
|  |                               ], | ||||||
|  |                               Row( | ||||||
|  |                                 spacing: 6, | ||||||
|  |                                 children: [ | ||||||
|  |                                   const Icon( | ||||||
|  |                                     Symbols.account_balance_wallet, | ||||||
|  |                                     size: 18, | ||||||
|  |                                   ), | ||||||
|  |                                   Expanded( | ||||||
|  |                                     child: Text( | ||||||
|  |                                       'quotaCostInfo'.tr( | ||||||
|  |                                         args: [ | ||||||
|  |                                           _formatQuotaCost( | ||||||
|  |                                             fileSize, | ||||||
|  |                                             selectedPool, | ||||||
|  |                                           ), | ||||||
|  |                                         ], | ||||||
|  |                                       ), | ||||||
|  |                                       style: | ||||||
|  |                                           Theme.of( | ||||||
|  |                                             context, | ||||||
|  |                                           ).textTheme.bodyMedium, | ||||||
|  |                                     ).fontSize(13), | ||||||
|  |                                   ), | ||||||
|  |                                 ], | ||||||
|  |                               ).padding(horizontal: 4), | ||||||
|  |                             ], | ||||||
|  |                           ); | ||||||
|  |                         }, | ||||||
|  |                       ), | ||||||
|  |                       const Gap(4), | ||||||
|  |                       Row( | ||||||
|  |                         spacing: 6, | ||||||
|  |                         children: [ | ||||||
|  |                           const Icon(Symbols.info, size: 18), | ||||||
|  |                           Text( | ||||||
|  |                             'attachmentPreview'.tr(), | ||||||
|  |                             style: Theme.of(context).textTheme.titleMedium, | ||||||
|  |                           ).fontSize(13), | ||||||
|  |                         ], | ||||||
|  |                       ).padding(horizontal: 4), | ||||||
|  |                       const Gap(8), | ||||||
|  |                       AttachmentPreview(item: attachment, isCompact: true), | ||||||
|  |                     ], | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |               Padding( | ||||||
|  |                 padding: const EdgeInsets.all(16), | ||||||
|  |                 child: Row( | ||||||
|  |                   mainAxisAlignment: MainAxisAlignment.end, | ||||||
|  |                   children: [ | ||||||
|  |                     TextButton.icon( | ||||||
|  |                       onPressed: () => Navigator.pop(context), | ||||||
|  |                       icon: const Icon(Symbols.close), | ||||||
|  |                       label: Text('cancel').tr(), | ||||||
|  |                     ), | ||||||
|  |                     const Gap(8), | ||||||
|  |                     TextButton.icon( | ||||||
|  |                       onPressed: () => _confirmUpload(), | ||||||
|  |                       icon: const Icon(Symbols.upload), | ||||||
|  |                       label: Text('upload').tr(), | ||||||
|  |                     ), | ||||||
|  |                   ], | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ); | ||||||
|  |         }, | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<AttachmentUploadConfig?> _getUploadConfig() async { | ||||||
|  |     final attachment = widget.state.attachments.value[widget.index]; | ||||||
|  |     final fileSize = await _getFileSize(attachment); | ||||||
|  |  | ||||||
|  |     if (fileSize == null) return null; | ||||||
|  |  | ||||||
|  |     // Get the selected pool to check constraints | ||||||
|  |     final pools = await widget.ref.read(poolsProvider.future); | ||||||
|  |     final selectedPool = pools.firstWhere((p) => p.id == selectedPoolId); | ||||||
|  |  | ||||||
|  |     // Check constraints | ||||||
|  |     final maxFileSize = selectedPool.policyConfig?['max_file_size'] as int?; | ||||||
|  |     final fileSizeExceeded = maxFileSize != null && fileSize > maxFileSize; | ||||||
|  |  | ||||||
|  |     final acceptTypes = selectedPool.policyConfig?['accept_types'] as List?; | ||||||
|  |     final mimeType = | ||||||
|  |         attachment.data.mimeType ?? | ||||||
|  |         ComposeLogic.getMimeTypeFromFileType(attachment.type); | ||||||
|  |     final typeAccepted = | ||||||
|  |         acceptTypes == null || | ||||||
|  |         acceptTypes.isEmpty || | ||||||
|  |         acceptTypes.any((type) => mimeType.startsWith(type)); | ||||||
|  |  | ||||||
|  |     final hasConstraints = fileSizeExceeded || !typeAccepted; | ||||||
|  |  | ||||||
|  |     return AttachmentUploadConfig( | ||||||
|  |       poolId: selectedPoolId!, | ||||||
|  |       hasConstraints: hasConstraints, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> _confirmUpload() async { | ||||||
|  |     final config = await _getUploadConfig(); | ||||||
|  |     if (config != null && mounted) { | ||||||
|  |       Navigator.pop(context, config); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<int?> _getFileSize(UniversalFile attachment) async { | ||||||
|  |     if (attachment.data is XFile) { | ||||||
|  |       try { | ||||||
|  |         return await (attachment.data as XFile).length(); | ||||||
|  |       } catch (e) { | ||||||
|  |         return null; | ||||||
|  |       } | ||||||
|  |     } else if (attachment.data is SnCloudFile) { | ||||||
|  |       return (attachment.data as SnCloudFile).size; | ||||||
|  |     } else if (attachment.data is List<int>) { | ||||||
|  |       return (attachment.data as List<int>).length; | ||||||
|  |     } else if (attachment.data is Uint8List) { | ||||||
|  |       return (attachment.data as Uint8List).length; | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   String _formatNumber(int number) { | ||||||
|  |     if (number >= 1000000) { | ||||||
|  |       return '${(number / 1000000).toStringAsFixed(1)}M'; | ||||||
|  |     } else if (number >= 1000) { | ||||||
|  |       return '${(number / 1000).toStringAsFixed(1)}K'; | ||||||
|  |     } else { | ||||||
|  |       return number.toString(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   String _formatFileSize(int bytes) { | ||||||
|  |     if (bytes >= 1073741824) { | ||||||
|  |       return '${(bytes / 1073741824).toStringAsFixed(1)} GB'; | ||||||
|  |     } else if (bytes >= 1048576) { | ||||||
|  |       return '${(bytes / 1048576).toStringAsFixed(1)} MB'; | ||||||
|  |     } else if (bytes >= 1024) { | ||||||
|  |       return '${(bytes / 1024).toStringAsFixed(1)} KB'; | ||||||
|  |     } else { | ||||||
|  |       return '$bytes bytes'; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   String _formatQuotaCost(int fileSize, SnFilePool pool) { | ||||||
|  |     final costMultiplier = pool.billingConfig?['cost_multiplier'] ?? 1.0; | ||||||
|  |     final quotaCost = ((fileSize / 1024 / 1024) * costMultiplier).round(); | ||||||
|  |     return _formatNumber(quotaCost); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										330
									
								
								lib/widgets/chat/chat_input.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										330
									
								
								lib/widgets/chat/chat_input.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,330 @@ | |||||||
|  | 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; | ||||||
|  |   final Map<String, Map<int, double>> attachmentProgress; | ||||||
|  |  | ||||||
|  |   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, | ||||||
|  |     required this.attachmentProgress, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @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], | ||||||
|  |                       progress: attachmentProgress['chat-upload']?[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), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										172
									
								
								lib/widgets/chat/message_content.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								lib/widgets/chat/message_content.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | |||||||
|  | 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: 16, | ||||||
|  |             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( | ||||||
|  |               fontSize: 13, | ||||||
|  |               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: 16, | ||||||
|  |               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.bodyMedium!.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), | ||||||
|  |         ), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										69
									
								
								lib/widgets/chat/message_indicators.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								lib/widgets/chat/message_indicators.dart
									
									
									
									
									
										Normal 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
											
										
									
								
							
							
								
								
									
										87
									
								
								lib/widgets/chat/message_list_tile.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								lib/widgets/chat/message_list_tile.dart
									
									
									
									
									
										Normal 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, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										158
									
								
								lib/widgets/chat/message_sender_info.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								lib/widgets/chat/message_sender_info.dart
									
									
									
									
									
										Normal 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(), | ||||||
|  |                 ), | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										223
									
								
								lib/widgets/chat/public_room_preview.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								lib/widgets/chat/public_room_preview.dart
									
									
									
									
									
										Normal 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: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), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user