From 311420e1f714c85345aa998af73b2066e2e26b61 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 31 May 2025 19:16:47 +0800 Subject: [PATCH] :rocket: Launch 3.0.0+97 w/ NSE --- ios/NotificationService/Info.plist | 17 ++ .../NotificationService.swift | 223 ++++++++++++++ ios/Podfile | 10 +- ios/Podfile.lock | 6 +- ios/Runner.xcodeproj/project.pbxproj | 277 +++++++++++++++++- ios/Runner/Runner.entitlements | 2 + ios/Runner/Services/CloudFile.swift | 14 + lib/screens/auth/tabs.dart | 3 - lib/screens/chat/chat.dart | 1 + lib/screens/settings.dart | 1 + lib/widgets/content/image.dart | 54 +++- lib/widgets/content/image.native.dart | 53 ---- lib/widgets/content/image.web.dart | 42 --- pubspec.yaml | 2 +- 14 files changed, 601 insertions(+), 104 deletions(-) create mode 100644 ios/NotificationService/Info.plist create mode 100644 ios/NotificationService/NotificationService.swift create mode 100644 ios/Runner/Services/CloudFile.swift delete mode 100644 lib/widgets/content/image.native.dart delete mode 100644 lib/widgets/content/image.web.dart diff --git a/ios/NotificationService/Info.plist b/ios/NotificationService/Info.plist new file mode 100644 index 0000000..ad04f44 --- /dev/null +++ b/ios/NotificationService/Info.plist @@ -0,0 +1,17 @@ + + + + + NSUserActivityTypes + + INSendMessageIntent + + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + + diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift new file mode 100644 index 0000000..fba494e --- /dev/null +++ b/ios/NotificationService/NotificationService.swift @@ -0,0 +1,223 @@ +// +// NotificationService.swift +// NotificationService +// +// Created by LittleSheep on 2025/5/31. +// + +import UserNotifications +import Intents +import Kingfisher +import UniformTypeIdentifiers + +enum ParseNotificationPayloadError: Error { + case missingMetadata(String) + case missingAvatarUrl(String) +} + +class NotificationService: UNNotificationServiceExtension { + + private var contentHandler: ((UNNotificationContent) -> Void)? + private var bestAttemptContent: UNMutableNotificationContent? + + override func didReceive( + _ request: UNNotificationRequest, + withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void + ) { + self.contentHandler = contentHandler + guard let bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent else { + contentHandler(request.content) + return + } + self.bestAttemptContent = bestAttemptContent + + do { + try processNotification(request: request, content: bestAttemptContent) + } catch { + contentHandler(bestAttemptContent) + } + } + + override func serviceExtensionTimeWillExpire() { + if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { + contentHandler(bestAttemptContent) + } + } + + private func processNotification(request: UNNotificationRequest, content: UNMutableNotificationContent) throws { + switch content.categoryIdentifier { + case "messaging.message", "messaging.callStart": + try handleMessagingNotification(request: request, content: content) + default: + try handleDefaultNotification(content: content) + } + } + + private func handleMessagingNotification(request: UNNotificationRequest, content: UNMutableNotificationContent) throws { + guard let metadata = content.userInfo["metadata"] as? [AnyHashable: Any] else { + throw ParseNotificationPayloadError.missingMetadata("The notification has no metadata.") + } + + guard let pfpIdentifier = metadata["pfp"] as? String else { + throw ParseNotificationPayloadError.missingAvatarUrl("The notification has no pfp.") + } + + let replyableMessageCategory = UNNotificationCategory( + identifier: content.categoryIdentifier, + actions: [ + UNTextInputNotificationAction( + identifier: "reply_action", + title: "Reply", + options: [] + ), + ], + intentIdentifiers: [], + options: [] + ) + + UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory]) + content.categoryIdentifier = replyableMessageCategory.identifier + + let metadataCopy = metadata as? [String: String] ?? [:] + let pfpUrl = getAttachmentUrl(for: pfpIdentifier) + + let targetSize = 512 + let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit) + + KingfisherManager.shared.retrieveImage(with: URL(string: pfpUrl)!, options: [.processor(scaleProcessor)], completionHandler: { result in + var image: Data? + switch result { + case .success(let value): + image = value.image.pngData() + case .failure(let error): + print("Unable to get pfp url: \(error)") + } + + let handle = INPersonHandle(value: "\(metadataCopy["user_id"] ?? "")", type: .unknown) + let sender = INPerson( + personHandle: handle, + nameComponents: PersonNameComponents(nickname: "\(metadataCopy["sender_name"] ?? "")"), + displayName: content.title, + image: image == nil ? nil : INImage(imageData: image!), + contactIdentifier: nil, + customIdentifier: nil + ) + + let intent = self.createMessageIntent(with: sender, metadata: metadataCopy, body: content.body) + self.donateInteraction(for: intent) + let updatedContent = try? request.content.updating(from: intent) + self.contentHandler?(updatedContent ?? content) + }) + } + + private func handleDefaultNotification(content: UNMutableNotificationContent) throws { + guard let metadata = content.userInfo["metadata"] as? [AnyHashable: Any] else { + throw ParseNotificationPayloadError.missingMetadata("The notification has no metadata.") + } + + if let imageIdentifier = metadata["image"] as? String { + attachMedia(to: content, withIdentifier: [imageIdentifier], fileType: UTType.webP, doScaleDown: true) + } else if let pfpIdentifier = metadata["pfp"] as? String { + attachMedia(to: content, withIdentifier: [pfpIdentifier], fileType: UTType.webP, doScaleDown: true) + } else if let imagesIdentifier = metadata["images"] as? Array { + attachMedia(to: content, withIdentifier: imagesIdentifier, fileType: UTType.webP, doScaleDown: true) + } else { + contentHandler?(content) + } + } + + private func attachMedia(to content: UNMutableNotificationContent, withIdentifier identifier: Array, fileType type: UTType?, doScaleDown scaleDown: Bool = false) { + let attachmentUrls = identifier.compactMap { element in + return getAttachmentUrl(for: element) + } + + guard !attachmentUrls.isEmpty else { + print("Invalid URLs for attachments: \(attachmentUrls)") + return + } + + let targetSize = 800 + let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit) + + for attachmentUrl in attachmentUrls { + guard let remoteUrl = URL(string: attachmentUrl) else { + print("Invalid URL for attachment: \(attachmentUrl)") + continue // Skip this URL and move to the next one + } + + KingfisherManager.shared.retrieveImage(with: remoteUrl, options: scaleDown ? [ + .processor(scaleProcessor) + ] : nil) { [weak self] result in + guard let self = self else { return } + + switch result { + case .success(let retrievalResult): + // The image is either retrieved from cache or downloaded + let tempDirectory = FileManager.default.temporaryDirectory + let cachedFileUrl = tempDirectory.appendingPathComponent(UUID().uuidString) // Unique identifier for each file + + do { + // Write the image data to a temporary file for UNNotificationAttachment + try retrievalResult.image.pngData()?.write(to: cachedFileUrl) + self.attachLocalMedia(to: content, fileType: type?.identifier, from: cachedFileUrl, withIdentifier: attachmentUrl) + } catch { + print("Failed to write media to temporary file: \(error.localizedDescription)") + self.contentHandler?(content) + } + + case .failure(let error): + print("Failed to retrieve image: \(error.localizedDescription)") + self.contentHandler?(content) + } + } + } + } + + private func attachLocalMedia(to content: UNMutableNotificationContent, fileType type: String?, from localUrl: URL, withIdentifier identifier: String) { + do { + let attachment = try UNNotificationAttachment(identifier: identifier, url: localUrl, options: [ + UNNotificationAttachmentOptionsTypeHintKey: type as Any, + UNNotificationAttachmentOptionsThumbnailHiddenKey: 0, + ]) + content.attachments = [attachment] + } catch let error as NSError { + // Log detailed error information + print("Failed to create attachment from file at \(localUrl.path)") + print("Error: \(error.localizedDescription)") + + // Check specific error codes if needed + if error.domain == NSCocoaErrorDomain { + switch error.code { + case NSFileReadNoSuchFileError: + print("File does not exist at \(localUrl.path)") + case NSFileReadNoPermissionError: + print("No permission to read file at \(localUrl.path)") + default: + print("Unhandled file error: \(error.code)") + } + } + } + + // Call content handler regardless of success or failure + self.contentHandler?(content) + } + + private func createMessageIntent(with sender: INPerson, metadata: [AnyHashable: Any], body: String) -> INSendMessageIntent { + INSendMessageIntent( + recipients: nil, + outgoingMessageType: .outgoingMessageText, + content: body, + speakableGroupName: metadata["room_name"] != nil ? INSpeakableString(spokenPhrase: metadata["room_name"] as! String) : nil, + conversationIdentifier: "\(metadata["room_id"] ?? "")", + serviceName: nil, + sender: sender, + attachments: nil + ) + } + + private func donateInteraction(for intent: INIntent) { + let interaction = INInteraction(intent: intent, response: nil) + interaction.direction = .incoming + interaction.donate(completion: nil) + } +} diff --git a/ios/Podfile b/ios/Podfile index 4929876..57f564f 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -29,13 +29,19 @@ flutter_ios_podfile_setup target 'Runner' do use_frameworks! - - pod 'Kingfisher', '~> 8.0' + use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do inherit! :search_paths end + + target 'NotificationService' do + inherit! :search_paths + pod 'Kingfisher', '~> 8.0' + pod 'Alamofire' + end end post_install do |installer| diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 823f4a2..26f683b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,4 +1,5 @@ PODS: + - Alamofire (5.10.2) - connectivity_plus (0.0.1): - Flutter - croppy (0.0.1): @@ -187,6 +188,7 @@ PODS: - WebRTC-SDK (125.6422.07) DEPENDENCIES: + - Alamofire - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - croppy (from `.symlinks/plugins/croppy/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) @@ -218,6 +220,7 @@ DEPENDENCIES: SPEC REPOS: trunk: + - Alamofire - DKImagePickerController - DKPhotoGallery - Firebase @@ -294,6 +297,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: + Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30 device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe @@ -340,6 +344,6 @@ SPEC CHECKSUMS: wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 WebRTC-SDK: dff00a3892bc570b6014e046297782084071657e -PODFILE CHECKSUM: 2608312fddeea6d787ca3aa478c756da8f4ca66d +PODFILE CHECKSUM: c8120fa04387477e9e62f36b6c37495c69fca3bb COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index aae37a6..770800c 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,8 +10,10 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 73268D1C2DEAFD670076E970 /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73268D152DEAFD670076E970 /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 755557017FD1B99AFC4F9127 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29812C17FFBE7DBBC7203981 /* Pods_RunnerTests.framework */; }; + 8529D00678947B00A0162116 /* Pods_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -27,9 +29,27 @@ remoteGlobalIDString = 97C146ED1CF9000F007C117D; remoteInfo = Runner; }; + 73268D1A2DEAFD670076E970 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 73268D142DEAFD670076E970; + remoteInfo = NotificationService; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 73268D1C2DEAFD670076E970 /* NotificationService.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -47,12 +67,16 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 14DFD79BE7C26E51B117583C /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 192FDACE67D7CB6AED15C634 /* Pods-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.debug.xcconfig"; sourceTree = ""; }; 1C14F71D23E4371602065522 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 252A83CE6862573BB856ED8E /* Pods-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.release.xcconfig"; sourceTree = ""; }; 29812C17FFBE7DBBC7203981 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2D2457F8B2E6EF9C0F935035 /* Pods-NotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.profile.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.profile.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3A1C47BD29CC6AC2587D4DBE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 73268D152DEAFD670076E970 /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -66,10 +90,47 @@ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9AE244813FCDFAA941430393 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; A499FDB2082EB000933AA8C5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E6B10A9A85BECA2E576C91FF /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; F6D834CA86410B09796B312B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 73268D212DEAFD670076E970 /* Exceptions for "NotificationService" folder in "NotificationService" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 73268D142DEAFD670076E970 /* NotificationService */; + }; + 73268D2B2DEB013D0076E970 /* Exceptions for "Services" folder in "NotificationService" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + CloudFile.swift, + ); + target = 73268D142DEAFD670076E970 /* NotificationService */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 73268D162DEAFD670076E970 /* NotificationService */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 73268D212DEAFD670076E970 /* Exceptions for "NotificationService" folder in "NotificationService" target */, + ); + path = NotificationService; + sourceTree = ""; + }; + 73268D272DEB012A0076E970 /* Services */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 73268D2B2DEB013D0076E970 /* Exceptions for "Services" folder in "NotificationService" target */, + ); + path = Services; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 1DFF8FEBBD0CF5A990600776 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -79,6 +140,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 73268D122DEAFD670076E970 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8529D00678947B00A0162116 /* Pods_NotificationService.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -103,6 +172,7 @@ children = ( F6D834CA86410B09796B312B /* Pods_Runner.framework */, 29812C17FFBE7DBBC7203981 /* Pods_RunnerTests.framework */, + AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */, ); name = Frameworks; sourceTree = ""; @@ -116,6 +186,9 @@ 14DFD79BE7C26E51B117583C /* Pods-RunnerTests.debug.xcconfig */, 14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */, E6B10A9A85BECA2E576C91FF /* Pods-RunnerTests.profile.xcconfig */, + 192FDACE67D7CB6AED15C634 /* Pods-NotificationService.debug.xcconfig */, + 252A83CE6862573BB856ED8E /* Pods-NotificationService.release.xcconfig */, + 2D2457F8B2E6EF9C0F935035 /* Pods-NotificationService.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -136,6 +209,7 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + 73268D162DEAFD670076E970 /* NotificationService */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, 91E124CE95BCB4DCD890160D /* Pods */, @@ -149,6 +223,7 @@ children = ( 97C146EE1CF9000F007C117D /* Runner.app */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + 73268D152DEAFD670076E970 /* NotificationService.appex */, ); name = Products; sourceTree = ""; @@ -156,6 +231,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 73268D272DEB012A0076E970 /* Services */, 737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -191,6 +267,27 @@ productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + 73268D142DEAFD670076E970 /* NotificationService */ = { + isa = PBXNativeTarget; + buildConfigurationList = 73268D222DEAFD670076E970 /* Build configuration list for PBXNativeTarget "NotificationService" */; + buildPhases = ( + 042F7A1E06D2751BE114E578 /* [CP] Check Pods Manifest.lock */, + 73268D112DEAFD670076E970 /* Sources */, + 73268D122DEAFD670076E970 /* Frameworks */, + 73268D132DEAFD670076E970 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 73268D162DEAFD670076E970 /* NotificationService */, + ); + name = NotificationService; + productName = NotificationService; + productReference = 73268D152DEAFD670076E970 /* NotificationService.appex */; + productType = "com.apple.product-type.app-extension"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; @@ -199,6 +296,7 @@ 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, + 73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, @@ -208,6 +306,10 @@ buildRules = ( ); dependencies = ( + 73268D1B2DEAFD670076E970 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 73268D272DEB012A0076E970 /* Services */, ); name = Runner; productName = Runner; @@ -221,6 +323,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 1640; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { @@ -228,6 +331,9 @@ CreatedOnToolsVersion = 14.0; TestTargetID = 97C146ED1CF9000F007C117D; }; + 73268D142DEAFD670076E970 = { + CreatedOnToolsVersion = 16.4; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; @@ -235,7 +341,6 @@ }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -243,12 +348,14 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + preferredProjectObjectVersion = 77; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, 331C8080294A63A400263BE5 /* RunnerTests */, + 73268D142DEAFD670076E970 /* NotificationService */, ); }; /* End PBXProject section */ @@ -261,6 +368,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 73268D132DEAFD670076E970 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -276,6 +390,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 042F7A1E06D2751BE114E578 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-NotificationService-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -396,6 +532,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 73268D112DEAFD670076E970 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -413,6 +556,11 @@ target = 97C146ED1CF9000F007C117D /* Runner */; targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; }; + 73268D1B2DEAFD670076E970 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 73268D142DEAFD670076E970 /* NotificationService */; + targetProxy = 73268D1A2DEAFD670076E970 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -562,6 +710,123 @@ }; name = Profile; }; + 73268D1E2DEAFD670076E970 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 192FDACE67D7CB6AED15C634 /* Pods-NotificationService.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = W7HPZ53V6B; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NotificationService/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NotificationService; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 73268D1F2DEAFD670076E970 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 252A83CE6862573BB856ED8E /* Pods-NotificationService.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = W7HPZ53V6B; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NotificationService/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NotificationService; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 73268D202DEAFD670076E970 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2D2457F8B2E6EF9C0F935035 /* Pods-NotificationService.profile.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = W7HPZ53V6B; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NotificationService/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NotificationService; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -737,6 +1002,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 73268D222DEAFD670076E970 /* Build configuration list for PBXNativeTarget "NotificationService" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 73268D1E2DEAFD670076E970 /* Debug */, + 73268D1F2DEAFD670076E970 /* Release */, + 73268D202DEAFD670076E970 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 903def2..29326e3 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -4,5 +4,7 @@ aps-environment development + com.apple.developer.usernotifications.communication + diff --git a/ios/Runner/Services/CloudFile.swift b/ios/Runner/Services/CloudFile.swift new file mode 100644 index 0000000..f50a3cc --- /dev/null +++ b/ios/Runner/Services/CloudFile.swift @@ -0,0 +1,14 @@ +// +// CloudFile.swift +// Runner +// +// Created by LittleSheep on 2025/5/31. +// + +import Foundation + +func getAttachmentUrl(for identifier: String) -> String { + let serverBaseUrl = "https://api.sn.solsynth.dev" + + return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/cgi/uc/attachments/\(identifier)" +} diff --git a/lib/screens/auth/tabs.dart b/lib/screens/auth/tabs.dart index 566da58..2de5dfd 100644 --- a/lib/screens/auth/tabs.dart +++ b/lib/screens/auth/tabs.dart @@ -45,7 +45,6 @@ class TabsNavigationWidget extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final useHorizontalLayout = isWideScreen(context); - final useExpandableLayout = isWidestScreen(context); final currentRoute = ref.watch(currentRouteProvider); final notificationUnreadCount = ref.watch( @@ -112,8 +111,6 @@ class TabsNavigationWidget extends HookConsumerWidget { Gap(MediaQuery.of(context).padding.top + 8), Expanded( child: NavigationRail( - minExtendedWidth: 200, - extended: useExpandableLayout, selectedIndex: activeIndex, onDestinationSelected: (index) { router.replace(routes[index]); diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index 3a180aa..be77fb2 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -147,6 +147,7 @@ class ChatRoomListTile extends HookConsumerWidget { : room.name ?? '', ), subtitle: buildSubtitle(), + trailing: trailing, // Add this line onTap: () async { // Clear unread count if there are unread messages ref.read(chatSummaryProvider.future).then((summary) { diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 0eec43d..1fa6dbe 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -147,6 +147,7 @@ class SettingsScreen extends HookConsumerWidget { title: Text('settingsColorScheme').tr(), content: SingleChildScrollView( child: ColorPicker( + enableAlpha: false, pickerColor: selectedColor, onColorChanged: (color) { selectedColor = color; diff --git a/lib/widgets/content/image.dart b/lib/widgets/content/image.dart index 33291e3..524d2fc 100644 --- a/lib/widgets/content/image.dart +++ b/lib/widgets/content/image.dart @@ -1 +1,53 @@ -export 'image.native.dart' if (dart.library.html) 'image.web.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_blurhash/flutter_blurhash.dart'; + +class UniversalImage extends StatelessWidget { + final String uri; + final String? blurHash; + final BoxFit fit; + final double? width; + final double? height; + final bool noCacheOptimization; + + const UniversalImage({ + super.key, + required this.uri, + this.blurHash, + this.fit = BoxFit.cover, + this.width, + this.height, + this.noCacheOptimization = false, + }); + + @override + Widget build(BuildContext context) { + int? cacheWidth; + int? cacheHeight; + if (width != null && height != null && !noCacheOptimization) { + final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; + cacheWidth = width != null ? (width! * devicePixelRatio).round() : null; + cacheHeight = + height != null ? (height! * devicePixelRatio).round() : null; + } + + return SizedBox( + width: width, + height: height, + child: Stack( + fit: StackFit.expand, + children: [ + if (blurHash != null) BlurHash(hash: blurHash!), + CachedNetworkImage( + imageUrl: uri, + fit: fit, + width: width, + height: height, + memCacheHeight: cacheHeight, + memCacheWidth: cacheWidth, + ), + ], + ), + ); + } +} diff --git a/lib/widgets/content/image.native.dart b/lib/widgets/content/image.native.dart deleted file mode 100644 index 524d2fc..0000000 --- a/lib/widgets/content/image.native.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_blurhash/flutter_blurhash.dart'; - -class UniversalImage extends StatelessWidget { - final String uri; - final String? blurHash; - final BoxFit fit; - final double? width; - final double? height; - final bool noCacheOptimization; - - const UniversalImage({ - super.key, - required this.uri, - this.blurHash, - this.fit = BoxFit.cover, - this.width, - this.height, - this.noCacheOptimization = false, - }); - - @override - Widget build(BuildContext context) { - int? cacheWidth; - int? cacheHeight; - if (width != null && height != null && !noCacheOptimization) { - final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; - cacheWidth = width != null ? (width! * devicePixelRatio).round() : null; - cacheHeight = - height != null ? (height! * devicePixelRatio).round() : null; - } - - return SizedBox( - width: width, - height: height, - child: Stack( - fit: StackFit.expand, - children: [ - if (blurHash != null) BlurHash(hash: blurHash!), - CachedNetworkImage( - imageUrl: uri, - fit: fit, - width: width, - height: height, - memCacheHeight: cacheHeight, - memCacheWidth: cacheWidth, - ), - ], - ), - ); - } -} diff --git a/lib/widgets/content/image.web.dart b/lib/widgets/content/image.web.dart deleted file mode 100644 index 9c355b9..0000000 --- a/lib/widgets/content/image.web.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:web/web.dart' as web; -import 'package:flutter/material.dart'; - -class UniversalImage extends StatelessWidget { - final String uri; - final String? blurHash; - final BoxFit fit; - final double? width; - final double? height; - // No cache optimization for web - final bool noCacheOptimization; - - const UniversalImage({ - super.key, - required this.uri, - this.blurHash, - this.fit = BoxFit.cover, - this.width, - this.height, - this.noCacheOptimization = false, - }); - - @override - Widget build(BuildContext context) { - return HtmlElementView.fromTagName( - tagName: 'img', - onElementCreated: (element) { - element as web.HTMLImageElement; - element.src = uri; - element.style.width = width?.toString() ?? '100%'; - element.style.height = height?.toString() ?? '100%'; - element.style.objectFit = switch (fit) { - BoxFit.cover || BoxFit.fitWidth || BoxFit.fitHeight => 'cover', - BoxFit.fill => 'fill', - BoxFit.contain => 'contain', - BoxFit.none => 'none', - _ => 'cover', - }; - }, - ); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index d1cdb8e..5c64907 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 3.0.0+96 +version: 3.0.0+97 environment: sdk: ^3.7.2