diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6ec6d4d..90c7c6f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -46,12 +46,37 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 84228ef..422f9d2 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -987,7 +987,7 @@ INFOPLIST_FILE = SolianNotificationService/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1029,7 +1029,7 @@ INFOPLIST_FILE = SolianNotificationService/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1068,7 +1068,7 @@ INFOPLIST_FILE = SolianNotificationService/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = SolianNotificationService; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index eae3471..a8d5ad0 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -11,6 +11,21 @@ import UIKit ) -> Bool { UNUserNotificationCenter.current().delegate = notifyDelegate + let replyableMessageCategory = UNNotificationCategory( + identifier: "REPLYABLE_MESSAGE", + actions: [ + UNTextInputNotificationAction( + identifier: "reply_action", + title: "Reply", + options: [] + ), + ], + intentIdentifiers: [], + options: [] + ) + + UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory]) + GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/ios/Runner/NotifyDelegate.swift b/ios/Runner/NotifyDelegate.swift index 21c4a59..f611767 100644 --- a/ios/Runner/NotifyDelegate.swift +++ b/ios/Runner/NotifyDelegate.swift @@ -10,40 +10,51 @@ import Alamofire class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - if let textResponse = response as? UNTextInputNotificationResponse { - let content = response.notification.request.content - guard let metadata = content.userInfo["meta"] as? [AnyHashable: Any] else { - return - } - - var token: String? = UserDefaults.standard.getFlutterToken() - if token == nil { - return - } - - let serverUrl = UserDefaults.standard.getServerUrl() - let url = "\(serverUrl)/chat/\(metadata["room_id"] ?? "")/messages" - - let parameters: [String: Any?] = [ - "content": textResponse.userText, - "replied_message_id": metadata["message_id"] - ] - - AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: HTTPHeaders( - [HTTPHeader(name: "Authorization", value: "AtField \(token!)")] - )) - .validate() - .responseString { response in - switch response.result { - case .success(_): - break - case .failure(let error): - print("Failed to send chat reply message: \(error)") - break - } - } + guard let textResponse = response as? UNTextInputNotificationResponse else { + completionHandler() + return + } + + let content = response.notification.request.content + + // Only handle replies for new messages + guard let notificationType = content.userInfo["type"] as? String, notificationType == "messages.new" else { + completionHandler() + return + } + + guard let metadata = content.userInfo["meta"] as? [AnyHashable: Any] else { + completionHandler() + return } - completionHandler() + guard let token = UserDefaults.standard.getFlutterToken() else { + completionHandler() + return + } + + let serverUrl = UserDefaults.standard.getServerUrl() + let url = "\(serverUrl)/chat/\(metadata["room_id"] ?? "")/messages" + + let parameters: [String: Any?] = [ + "content": textResponse.userText, + "replied_message_id": metadata["message_id"] + ] + + AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: HTTPHeaders( + [HTTPHeader(name: "Authorization", value: "AtField \(token)")] + )) + .validate() + .responseString { response in + switch response.result { + case .success(_): + break + case .failure(let error): + print("Failed to send chat reply message: \(error)") + break + } + // Call completion handler after network request is finished + completionHandler() + } } } diff --git a/ios/SolianNotificationService/NotificationService.swift b/ios/SolianNotificationService/NotificationService.swift index 3a5d18e..47944b0 100644 --- a/ios/SolianNotificationService/NotificationService.swift +++ b/ios/SolianNotificationService/NotificationService.swift @@ -60,21 +60,7 @@ class NotificationService: UNNotificationServiceExtension { let pfpIdentifier = meta["pfp"] as? String - let replyableMessageCategory = UNNotificationCategory( - identifier: content.categoryIdentifier, - actions: [ - UNTextInputNotificationAction( - identifier: "reply_action", - title: "Reply", - options: [] - ), - ], - intentIdentifiers: [], - options: [] - ) - - UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory]) - content.categoryIdentifier = replyableMessageCategory.identifier + content.categoryIdentifier = "REPLYABLE_MESSAGE" let metaCopy = meta as? [String: Any] ?? [:] let pfpUrl = pfpIdentifier != nil ? getAttachmentUrl(for: pfpIdentifier!) : nil diff --git a/lib/route.dart b/lib/route.dart index 6abd41d..3144e47 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -53,7 +53,10 @@ final routerProvider = Provider((ref) { // Standalone routes without bottom navigation GoRoute( path: '/posts/compose', - builder: (context, state) => const PostComposeScreen(), + builder: + (context, state) => PostComposeScreen( + initialState: state.extra as PostComposeInitialState?, + ), ), GoRoute( path: '/posts/:id/edit', diff --git a/lib/screens/posts/pub_profile.dart b/lib/screens/posts/pub_profile.dart index 4a65dc6..9c79b0a 100644 --- a/lib/screens/posts/pub_profile.dart +++ b/lib/screens/posts/pub_profile.dart @@ -54,25 +54,26 @@ Future publisherSubscriptionStatus( @riverpod Future publisherAppbarForcegroundColor(Ref ref, String pubName) async { - final publisher = await ref.watch(publisherProvider(pubName).future); - if (publisher.background == null) return null; - final palette = await PaletteGenerator.fromImageProvider( - CloudImageWidget.provider( - fileId: publisher.background!.id, - serverUrl: ref.watch(serverUrlProvider), - ), - ); - final dominantColor = palette.dominantColor?.color; - if (dominantColor == null) return null; - return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; + try { + final publisher = await ref.watch(publisherProvider(pubName).future); + if (publisher.background == null) return null; + final palette = await PaletteGenerator.fromImageProvider( + CloudImageWidget.provider( + fileId: publisher.background!.id, + serverUrl: ref.watch(serverUrlProvider), + ), + ); + final dominantColor = palette.dominantColor?.color; + if (dominantColor == null) return null; + return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; + } catch (_) { + return null; + } } class PublisherProfileScreen extends HookConsumerWidget { final String name; - const PublisherProfileScreen({ - super.key, - required this.name, - }); + const PublisherProfileScreen({super.key, required this.name}); @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/lib/widgets/content/cloud_file_collection.dart b/lib/widgets/content/cloud_file_collection.dart index f367227..a3b2ff3 100644 --- a/lib/widgets/content/cloud_file_collection.dart +++ b/lib/widgets/content/cloud_file_collection.dart @@ -186,13 +186,7 @@ class CloudFileZoomIn extends HookConsumerWidget { Future saveToGallery() async { try { // Show loading indicator - final scaffold = ScaffoldMessenger.of(context); - scaffold.showSnackBar( - const SnackBar( - content: Text('Saving image to gallery...'), - duration: Duration(seconds: 1), - ), - ); + showSnackBar('Saving image to gallery...'); // Get the image URL final client = ref.watch(apiClientProvider); @@ -209,12 +203,7 @@ class CloudFileZoomIn extends HookConsumerWidget { await Gal.putImage(filePath, album: 'Solar Network'); // Show success message - scaffold.showSnackBar( - const SnackBar( - content: Text('Image saved to gallery'), - duration: Duration(seconds: 2), - ), - ); + showSnackBar('Image saved to gallery'); } catch (e) { showErrorAlert(e); } diff --git a/lib/widgets/publisher/publisher_card.dart b/lib/widgets/publisher/publisher_card.dart index 1132c02..d77f368 100644 --- a/lib/widgets/publisher/publisher_card.dart +++ b/lib/widgets/publisher/publisher_card.dart @@ -29,7 +29,7 @@ class PublisherCard extends ConsumerWidget { clipBehavior: Clip.antiAlias, child: InkWell( onTap: () { - context.push('/publishers/${publisher.id}'); + context.push('/publishers/${publisher.name}'); }, child: AspectRatio( aspectRatio: 16 / 7, diff --git a/lib/widgets/share/share_sheet.dart b/lib/widgets/share/share_sheet.dart index 49e3ff6..b0ff17d 100644 --- a/lib/widgets/share/share_sheet.dart +++ b/lib/widgets/share/share_sheet.dart @@ -13,6 +13,7 @@ import 'package:island/pods/network.dart'; import 'package:island/pods/config.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/services/file.dart'; +import 'package:mime/mime.dart'; import 'dart:io'; import 'package:path/path.dart' as path; @@ -149,9 +150,9 @@ class _ShareSheetState extends ConsumerState { case ShareContentType.file: if (widget.content.files != null) { // Convert XFiles to UniversalFiles - for (final xFile in widget.content.files!) { - final file = File(xFile.path); - final mimeType = xFile.mimeType; + for (final file in widget.content.files!) { + var mimeType = file.mimeType; + mimeType ??= lookupMimeType(file.path); UniversalFileType fileType; if (mimeType?.startsWith('image/') == true) { diff --git a/pubspec.lock b/pubspec.lock index b7b094c..e4ea605 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1470,7 +1470,7 @@ packages: source: hosted version: "1.16.0" mime: - dependency: transitive + dependency: "direct main" description: name: mime sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" @@ -1785,10 +1785,10 @@ packages: dependency: transitive description: name: record_linux - sha256: "29e7735b05c1944bb6c9b72a36c08d4a1b24117e712d6a9523c003bde12bf484" + sha256: "0626678a092c75ce6af1e32fe7fd1dea709b92d308bc8e3b6d6348e2430beb95" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" record_macos: dependency: transitive description: @@ -2254,10 +2254,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.0" table_calendar: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7a08991..33a1b33 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -126,6 +126,7 @@ dependencies: git: url: https://github.com/lionelmennig/textfield_tags.git ref: fixes/allow-controller-re-registration + mime: ^2.0.0 dev_dependencies: flutter_test: