import Flutter import WidgetKit import UIKit import WatchConnectivity import AppIntents import flutter_app_intents @main @objc class AppDelegate: FlutterAppDelegate { let notifyDelegate = NotifyDelegate() private static var sharedWatchConnectivityService: WatchConnectivityService? override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { syncDefaultsToGroup() WidgetCenter.shared.reloadAllTimelines() UNUserNotificationCenter.current().delegate = notifyDelegate let replyableMessageCategory = UNNotificationCategory( identifier: "CHAT_MESSAGE", actions: [ UNTextInputNotificationAction( identifier: "reply_action", title: "Reply", options: [] ), ], intentIdentifiers: [], options: [] ) UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory]) GeneratedPluginRegistrant.register(with: self) // Setup widget sync method channel setupWidgetSyncChannel() // Always initialize and retain a strong reference if WCSession.isSupported() { AppDelegate.sharedWatchConnectivityService = WatchConnectivityService.shared } else { print("[iOS] WCSession not supported on this device.") } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } private func setupWidgetSyncChannel() { let controller = window?.rootViewController as? FlutterViewController let channel = FlutterMethodChannel(name: "dev.solsynth.solian/widget", binaryMessenger: controller!.binaryMessenger) channel.setMethodCallHandler { [weak self] (call, result) in if call.method == "syncToWidget" { syncDefaultsToGroup() WidgetCenter.shared.reloadAllTimelines() result(true) } else { result(FlutterMethodNotImplemented) } } } override func applicationDidEnterBackground(_ application: UIApplication) { syncDefaultsToGroup() WidgetCenter.shared.reloadAllTimelines() } override func applicationWillTerminate(_ application: UIApplication) { syncDefaultsToGroup() } } final class WatchConnectivityService: NSObject, WCSessionDelegate { static let shared = WatchConnectivityService() private let session: WCSession = .default private override init() { super.init() print("[iOS] Activating WCSession...") session.delegate = self session.activate() } // MARK: - WCSessionDelegate func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { if let error = error { print("[iOS] WCSession activation failed: \(error.localizedDescription)") } else { print("[iOS] WCSession activated with state: \(activationState.rawValue)") if activationState == .activated { sendDataToWatch() } } } func sessionDidBecomeInactive(_ session: WCSession) {} func sessionDidDeactivate(_ session: WCSession) { session.activate() } func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { print("[iOS] Received message: \(message)") if let request = message["request"] as? String, request == "data" { let token = UserDefaults.standard.getFlutterToken() let serverUrl = UserDefaults.standard.getServerUrl() var data: [String: Any] = ["serverUrl": serverUrl] if let token = token { data["token"] = token } print("[iOS] Replying with data: \(data)") replyHandler(data) } } func sendDataToWatch() { guard session.activationState == .activated else { return } let token = UserDefaults.standard.getFlutterToken() let serverUrl = UserDefaults.standard.getServerUrl() var data: [String: Any] = ["serverUrl": serverUrl] if let token = token { data["token"] = token } do { try session.updateApplicationContext(data) print("[iOS] Sent application context: \(data)") } catch { print("[iOS] Failed to send application context: \(error.localizedDescription)") } } } // MARK: - App Intents @available(iOS 16.0, *) struct OpenChatIntent: AppIntent { static var title: LocalizedStringResource = "Open Chat" static var description = IntentDescription("Open a specific chat room") static var isDiscoverable = true static var openAppWhenRun = true @Parameter(title: "Channel ID") var channelId: String? func perform() async throws -> some IntentResult & ReturnsValue & OpensIntent { let plugin = FlutterAppIntentsPlugin.shared let result = await plugin.handleIntentInvocation( identifier: "open_chat", parameters: ["channelId": channelId ?? ""] ) if let success = result["success"] as? Bool, success { let value = result["value"] as? String ?? "Chat opened" return .result(value: value) } else { let errorMessage = result["error"] as? String ?? "Failed to open chat" throw AppIntentError.executionFailed(errorMessage) } } } @available(iOS 16.0, *) struct OpenPostIntent: AppIntent { static var title: LocalizedStringResource = "Open Post" static var description = IntentDescription("Open a specific post") static var isDiscoverable = true static var openAppWhenRun = true @Parameter(title: "Post ID") var postId: String? func perform() async throws -> some IntentResult & ReturnsValue & OpensIntent { let plugin = FlutterAppIntentsPlugin.shared let result = await plugin.handleIntentInvocation( identifier: "open_post", parameters: ["postId": postId ?? ""] ) if let success = result["success"] as? Bool, success { let value = result["value"] as? String ?? "Post opened" return .result(value: value) } else { let errorMessage = result["error"] as? String ?? "Failed to open post" throw AppIntentError.executionFailed(errorMessage) } } } @available(iOS 16.0, *) struct OpenComposeIntent: AppIntent { static var title: LocalizedStringResource = "Open Compose" static var description = IntentDescription("Open compose post screen") static var isDiscoverable = true static var openAppWhenRun = true func perform() async throws -> some IntentResult & ReturnsValue & OpensIntent { let plugin = FlutterAppIntentsPlugin.shared let result = await plugin.handleIntentInvocation( identifier: "open_compose", parameters: [:] ) if let success = result["success"] as? Bool, success { let value = result["value"] as? String ?? "Compose screen opened" return .result(value: value) } else { let errorMessage = result["error"] as? String ?? "Failed to open compose" throw AppIntentError.executionFailed(errorMessage) } } } @available(iOS 16.0, *) struct ComposePostIntent: AppIntent { static var title: LocalizedStringResource = "Compose Post" static var description = IntentDescription("Create a new post") static var isDiscoverable = true static var openAppWhenRun = true func perform() async throws -> some IntentResult & ReturnsValue & OpensIntent { let plugin = FlutterAppIntentsPlugin.shared let result = await plugin.handleIntentInvocation( identifier: "compose_post", parameters: [:] ) if let success = result["success"] as? Bool, success { let value = result["value"] as? String ?? "Compose screen opened" return .result(value: value) } else { let errorMessage = result["error"] as? String ?? "Failed to compose post" throw AppIntentError.executionFailed(errorMessage) } } } @available(iOS 16.0, *) struct SearchContentIntent: AppIntent { static var title: LocalizedStringResource = "Search Content" static var description = IntentDescription("Search for content") static var isDiscoverable = true static var openAppWhenRun = true @Parameter(title: "Search Query") var query: String? func perform() async throws -> some IntentResult & ReturnsValue & OpensIntent { let plugin = FlutterAppIntentsPlugin.shared let result = await plugin.handleIntentInvocation( identifier: "search_content", parameters: ["query": query ?? ""] ) if let success = result["success"] as? Bool, success { let value = result["value"] as? String ?? "Search opened" return .result(value: value) } else { let errorMessage = result["error"] as? String ?? "Failed to search" throw AppIntentError.executionFailed(errorMessage) } } } @available(iOS 16.0, *) struct ViewNotificationsIntent: AppIntent { static var title: LocalizedStringResource = "View Notifications" static var description = IntentDescription("View notifications") static var isDiscoverable = true static var openAppWhenRun = true func perform() async throws -> some IntentResult & ReturnsValue & OpensIntent { let plugin = FlutterAppIntentsPlugin.shared let result = await plugin.handleIntentInvocation( identifier: "view_notifications", parameters: [:] ) if let success = result["success"] as? Bool, success { let value = result["value"] as? String ?? "Notifications opened" return .result(value: value) } else { let errorMessage = result["error"] as? String ?? "Failed to view notifications" throw AppIntentError.executionFailed(errorMessage) } } } @available(iOS 16.0, *) struct CheckNotificationsIntent: AppIntent { static var title: LocalizedStringResource = "Check Notifications" static var description = IntentDescription("Check notification count") static var isDiscoverable = true static var openAppWhenRun = false func perform() async throws -> some IntentResult & ReturnsValue & ProvidesDialog { let plugin = FlutterAppIntentsPlugin.shared let result = await plugin.handleIntentInvocation( identifier: "check_notifications", parameters: [:] ) if let success = result["success"] as? Bool, success { let value = result["value"] as? String ?? "You have new notifications" return .result( value: value, dialog: "\(value)" ) } else { let errorMessage = result["error"] as? String ?? "Failed to check notifications" throw AppIntentError.executionFailed(errorMessage) } } } @available(iOS 16.0, *) struct SendMessageIntent: AppIntent { static var title: LocalizedStringResource = "Send Message" static var description = IntentDescription("Send a message to a chat channel") static var isDiscoverable = true static var openAppWhenRun = false @Parameter(title: "Channel ID") var channelId: String? @Parameter(title: "Message Content") var content: String? func perform() async throws -> some IntentResult & ReturnsValue & ProvidesDialog { guard let channelId = channelId, !channelId.isEmpty else { throw AppIntentError.executionFailed("Channel ID is required") } guard let content = content, !content.isEmpty else { throw AppIntentError.executionFailed("Message content is required") } let plugin = FlutterAppIntentsPlugin.shared let result = await plugin.handleIntentInvocation( identifier: "send_message", parameters: ["channelId": channelId, "content": content] ) if let success = result["success"] as? Bool, success { let value = result["value"] as? String ?? "Message sent" return .result( value: value, dialog: "\(value)" ) } else { let errorMessage = result["error"] as? String ?? "Failed to send message" throw AppIntentError.executionFailed(errorMessage) } } } @available(iOS 16.0, *) struct ReadMessagesIntent: AppIntent { static var title: LocalizedStringResource = "Read Messages" static var description = IntentDescription("Read recent messages from a chat channel") static var isDiscoverable = true static var openAppWhenRun = false @Parameter(title: "Channel ID") var channelId: String? @Parameter(title: "Number of Messages", default: "5") var limit: String? func perform() async throws -> some IntentResult & ReturnsValue & ProvidesDialog { guard let channelId = channelId, !channelId.isEmpty else { throw AppIntentError.executionFailed("Channel ID is required") } let limit = limit ?? "5" var parameters: [String: Any] = ["channelId": channelId] parameters["limit"] = limit let plugin = FlutterAppIntentsPlugin.shared let result = await plugin.handleIntentInvocation( identifier: "read_messages", parameters: parameters ) if let success = result["success"] as? Bool, success { let value = result["value"] as? String ?? "Messages retrieved" return .result( value: value, dialog: "\(value)" ) } else { let errorMessage = result["error"] as? String ?? "Failed to read messages" throw AppIntentError.executionFailed(errorMessage) } } } @available(iOS 16.0, *) struct CheckUnreadChatsIntent: AppIntent { static var title: LocalizedStringResource = "Check Unread Chats" static var description = IntentDescription("Check number of unread chat messages") static var isDiscoverable = true static var openAppWhenRun = false func perform() async throws -> some IntentResult & ReturnsValue & ProvidesDialog { let plugin = FlutterAppIntentsPlugin.shared let result = await plugin.handleIntentInvocation( identifier: "check_unread_chats", parameters: [:] ) if let success = result["success"] as? Bool, success { let value = result["value"] as? String ?? "No unread messages" return .result( value: value, dialog: "\(value)" ) } else { let errorMessage = result["error"] as? String ?? "Failed to check unread chats" throw AppIntentError.executionFailed(errorMessage) } } } @available(iOS 16.0, *) struct MarkNotificationsReadIntent: AppIntent { static var title: LocalizedStringResource = "Mark Notifications Read" static var description = IntentDescription("Mark all notifications as read") static var isDiscoverable = true static var openAppWhenRun = false func perform() async throws -> some IntentResult & ReturnsValue & ProvidesDialog { let plugin = FlutterAppIntentsPlugin.shared let result = await plugin.handleIntentInvocation( identifier: "mark_notifications_read", parameters: [:] ) if let success = result["success"] as? Bool, success { let value = result["value"] as? String ?? "Notifications marked as read" return .result( value: value, dialog: "\(value)" ) } else { let errorMessage = result["error"] as? String ?? "Failed to mark notifications as read" throw AppIntentError.executionFailed(errorMessage) } } } enum AppIntentError: Error { case executionFailed(String) } @available(iOS 16.0, *) struct AppShortcuts: AppShortcutsProvider { static var appShortcuts: [AppShortcut] { return [ // Open chat AppShortcut( intent: OpenChatIntent(), phrases: [ "Open chat with \(.applicationName)", "Go to chat using \(.applicationName)", "Show chat in \(.applicationName)" ] ), // Open post AppShortcut( intent: OpenPostIntent(), phrases: [ "Open post with \(.applicationName)", "Show post using \(.applicationName)" ] ), // Compose AppShortcut( intent: OpenComposeIntent(), phrases: [ "Open compose with \(.applicationName)", "New post using \(.applicationName)", "Write post in \(.applicationName)" ] ), // Search AppShortcut( intent: SearchContentIntent(), phrases: [ "Search in \(.applicationName)", "Find content using \(.applicationName)" ] ), // Check notifications AppShortcut( intent: CheckNotificationsIntent(), phrases: [ "Check notifications with \(.applicationName)", "Get notifications using \(.applicationName)", "Do I have notifications in \(.applicationName)" ] ), // Send message AppShortcut( intent: SendMessageIntent(), phrases: [ "Send message with \(.applicationName)", "Post message using \(.applicationName)", "Send text using \(.applicationName)" ] ), // Read messages AppShortcut( intent: ReadMessagesIntent(), phrases: [ "Read messages with \(.applicationName)", "Get chat using \(.applicationName)", "Show messages with \(.applicationName)" ] ), // Check unread chats AppShortcut( intent: CheckUnreadChatsIntent(), phrases: [ "Check unread chats with \(.applicationName)", "Do I have messages using \(.applicationName)", "Get unread messages with \(.applicationName)" ] ), // Mark notifications read AppShortcut( intent: MarkNotificationsReadIntent(), phrases: [ "Mark notifications read with \(.applicationName)", "Clear notifications using \(.applicationName)", "Mark all read with \(.applicationName)" ] ) ] } }