diff --git a/.sample.env.json b/.sample.env.json index 6a15e5bdf..5ba1e0421 100644 --- a/.sample.env.json +++ b/.sample.env.json @@ -5,5 +5,6 @@ "SENTRY_DSN": "", "INTERCOM_IOS_KEY": "", "INTERCOM_ANDROID_KEY": "", - "INTERCOM_APP_ID": "" + "INTERCOM_APP_ID": "", + "OPENBOOK_SOCIAL_API_URL": "" } \ No newline at end of file diff --git a/assets/lang/en.json b/assets/lang/en.json index 857a3dc16..e98dfbae8 100644 --- a/assets/lang/en.json +++ b/assets/lang/en.json @@ -10,6 +10,11 @@ "AUTH.CREATE_ACC.PASTE_LINK": "Paste your registration link below", "AUTH.CREATE_ACC.PASTE_PASSWORD_RESET_LINK": "Paste your password reset link below", "AUTH.CREATE_ACC.PASTE_LINK_HELP_TEXT": "Use the link from the Join Openbook button in your invitation email.", + "AUTH.CREATE_ACC.REQUEST_INVITE": "No invite? Request one here.", + "AUTH.CREATE_ACC.SUBSCRIBE": "Request", + "AUTH.CREATE_ACC.SUBSCRIBE_TO_WAITLIST_TEXT": "Request an invite!", + "AUTH.CREATE_ACC.CONGRATULATIONS": "Congratulations!", + "AUTH.CREATE_ACC.YOUR_SUBSCRIBED": "You're {0} on the waitlist.", "AUTH.CREATE_ACC.ALMOST_THERE": "Almost there...", "AUTH.CREATE_ACC.WHAT_NAME": "What's your name?", "AUTH.CREATE_ACC.NAME_PLACEHOLDER": "James Bond", diff --git a/assets/lang/es.json b/assets/lang/es.json index 27196cd4a..610748923 100644 --- a/assets/lang/es.json +++ b/assets/lang/es.json @@ -6,6 +6,11 @@ "AUTH.CREATE_ACC.PASTE_LINK": "Paste your registration link below", "AUTH.CREATE_ACC.PASTE_PASSWORD_RESET_LINK": "Paste your password reset link below", "AUTH.CREATE_ACC.PASTE_LINK_HELP_TEXT": "Use the link from the Join Openbook button in your invitation email.", + "AUTH.CREATE_ACC.REQUEST_INVITE": "Need an invite? Request it here.", + "AUTH.CREATE_ACC.SUBSCRIBE": "Subscribe", + "AUTH.CREATE_ACC.SUBSCRIBE_TO_WAITLIST_TEXT": "Join the waitlist and get early access!", + "AUTH.CREATE_ACC.CONGRATULATIONS": "Congratulations!", + "AUTH.CREATE_ACC.YOUR_SUBSCRIBED": "You're {0} on the waitlist.", "AUTH.OR": "o", "AUTH.CREATE_ACC.LETS_GET_STARTED": "Comenzemos", "AUTH.CREATE_ACC.WELCOME_TO_ALPHA": "Welcome to the Alpha!", diff --git a/ios/OneSignalNotificationServiceExtension-Bridging-Header.h b/ios/OneSignalNotificationServiceExtension-Bridging-Header.h new file mode 100644 index 000000000..1b2cb5d6d --- /dev/null +++ b/ios/OneSignalNotificationServiceExtension-Bridging-Header.h @@ -0,0 +1,4 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + diff --git a/ios/Podfile b/ios/Podfile index 90e4a17b9..afc86d6fc 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -27,6 +27,7 @@ def parse_KV_file(file, separator='=') end target 'Runner' do + use_frameworks! # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock # referring to absolute paths on developers' machines. system('rm -rf .symlinks') @@ -55,6 +56,7 @@ target 'Runner' do end target 'OneSignalNotificationServiceExtension' do + use_frameworks! pod 'OneSignal', '>= 2.9.5', '< 3.0' end @@ -62,7 +64,7 @@ post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['ENABLE_BITCODE'] = 'NO' - config.build_settings['SWIFT_VERSION'] = '3.2' # <--- add this + config.build_settings['SWIFT_VERSION'] = '5' # <--- add this end end end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c7f60247d..122e1fa26 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,4 +1,9 @@ PODS: + - BSGridCollectionViewLayout (1.2.3) + - BSImagePicker (2.10.0): + - BSGridCollectionViewLayout (= 1.2.3) + - BSImageView (= 1.0.3) + - BSImageView (1.0.3) - device_info (0.0.1): - Flutter - Flutter (1.0.0) @@ -11,17 +16,20 @@ PODS: - FMDB/standard (2.7.5) - image_cropper (0.0.1): - Flutter - - TOCropViewController (~> 2.4.0) + - TOCropViewController (~> 2.5.0) - image_picker (0.0.1): - Flutter - Intercom (5.2.1) - intercom_flutter (1.0.7): - Flutter - Intercom - - onesignal (1.0.5): + - multi_image_picker (4.3.3): + - BSImagePicker (~> 2.10.0) + - Flutter + - OneSignal (2.10.0) + - onesignalflutter (1.0.5): - Flutter - OneSignal (< 3.0, >= 2.9.5) - - OneSignal (2.9.5) - path_provider (0.0.1): - Flutter - share (0.5.2): @@ -31,7 +39,7 @@ PODS: - sqflite (0.0.1): - Flutter - FMDB (~> 2.7.2) - - TOCropViewController (2.4.0) + - TOCropViewController (2.5.0) - uni_links (0.0.1): - Flutter - url_launcher (0.0.1): @@ -41,14 +49,15 @@ PODS: DEPENDENCIES: - device_info (from `.symlinks/plugins/device_info/ios`) - - Flutter (from `.symlinks/flutter/ios-release`) + - Flutter (from `.symlinks/flutter/ios`) - flutter_exif_rotation (from `.symlinks/plugins/flutter_exif_rotation/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - image_cropper (from `.symlinks/plugins/image_cropper/ios`) - image_picker (from `.symlinks/plugins/image_picker/ios`) - intercom_flutter (from `.symlinks/plugins/intercom_flutter/ios`) + - multi_image_picker (from `.symlinks/plugins/multi_image_picker/ios`) - OneSignal (< 3.0, >= 2.9.5) - - onesignal (from `.symlinks/plugins/onesignal/ios`) + - onesignalflutter (from `.symlinks/plugins/onesignalflutter/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) - share (from `.symlinks/plugins/share/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) @@ -59,6 +68,9 @@ DEPENDENCIES: SPEC REPOS: https://github.com/cocoapods/specs.git: + - BSGridCollectionViewLayout + - BSImagePicker + - BSImageView - FMDB - Intercom - OneSignal @@ -68,7 +80,7 @@ EXTERNAL SOURCES: device_info: :path: ".symlinks/plugins/device_info/ios" Flutter: - :path: ".symlinks/flutter/ios-release" + :path: ".symlinks/flutter/ios" flutter_exif_rotation: :path: ".symlinks/plugins/flutter_exif_rotation/ios" flutter_secure_storage: @@ -79,8 +91,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_picker/ios" intercom_flutter: :path: ".symlinks/plugins/intercom_flutter/ios" - onesignal: - :path: ".symlinks/plugins/onesignal/ios" + multi_image_picker: + :path: ".symlinks/plugins/multi_image_picker/ios" + onesignalflutter: + :path: ".symlinks/plugins/onesignalflutter/ios" path_provider: :path: ".symlinks/plugins/path_provider/ios" share: @@ -97,26 +111,30 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/video_player/ios" SPEC CHECKSUMS: + BSGridCollectionViewLayout: 568273e113fbc815f868f1898ef0465aee68f955 + BSImagePicker: fa0c15b6740e8aa7a7b7f0fe38a71ad4cfa0ec4a + BSImageView: a149459433a2687157d034c78e059d30ac7f2544 device_info: 76ce0b32e13034d1883be4a382433648f9dcee63 Flutter: 9d0fac939486c9aba2809b7982dfdbb47a7b0296 flutter_exif_rotation: 458778023267a1f0157ae8d9483474749990ce24 flutter_secure_storage: dbcc8ff35d99569c3a4d3b483afa3339416c1a78 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - image_cropper: 43c1f7f5ea92b68f43cae9340f55c84bdaad54bb + image_cropper: e0a40e80b2107490926d0f15e3b42f44867c15b4 image_picker: ee00aab0487cedc80a304085219503cc6d0f2e22 Intercom: 53f07c24f0ca18c1e910750e79b75f9c9867338f intercom_flutter: e281b619d2fc03dccf171089c87c01a17e1ed8b1 - OneSignal: ccdeb961882f8668305e5b694e2cb7cb325fc907 - onesignal: c2122c20ffcb03d65445f3e0b49273c10f9c37a6 + multi_image_picker: 83e69aae993ddffd49db61a1d970e2916e554103 + OneSignal: 0f5ff711d9f25da54885e4ab06ef0abc221a46ef + onesignalflutter: 872db11e9c18bcc9225e06c1cc7280872813783a path_provider: 09407919825bfe3c2deae39453b7a5b44f467873 share: 222b5dcc8031238af9d7de91149df65bad1aef75 shared_preferences: 5a1d487c427ee18fcd3ea1f2a131569481834b53 sqflite: d1612813fa7db7c667bed9f1d1b508deffc56999 - TOCropViewController: 368d8df3ea43b62c3dc5a61f11b9048274d240bd + TOCropViewController: d86078d3a57a70c116bf3db7c89c80046a0e5fdf uni_links: 5ee5240df5cbffc52d9e7f8017a576b6a6bc5141 url_launcher: 92b89c1029a0373879933c21642958c874539095 video_player: 906796a841943c8d370ac7c13b18039aa9b56498 -PODFILE CHECKSUM: 6f8026df7eb2040a45b4a2313fc73d43e59d6bd6 +PODFILE CHECKSUM: d3e4257a4755deb71f0bcbcc7c1e0639177bc4ed COCOAPODS: 1.5.3 diff --git a/ios/Runner-Bridging-Header.h b/ios/Runner-Bridging-Header.h new file mode 100644 index 000000000..1b2cb5d6d --- /dev/null +++ b/ios/Runner-Bridging-Header.h @@ -0,0 +1,4 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index be5dcb8af..67300bdb5 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -8,29 +8,11 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 26B90459017411DF809A7507 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 970D6175355421F353EA64C9 /* libPods-Runner.a */; }; + 30C0AB83114DC50252134799 /* Pods_OneSignalNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2F8A8BC5DC89A5ADEF49D1 /* Pods_OneSignalNotificationServiceExtension.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 542BD8834B6D6BE2F3E671A0 /* libPods-OneSignalNotificationServiceExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A4BA49A905C89E90E88FD37 /* libPods-OneSignalNotificationServiceExtension.a */; }; - 8902A1072236D5BF005F914D /* libFMDB.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8928E31E22356450001DB32A /* libFMDB.a */; }; - 8902A1082236D5BF005F914D /* libintercom_flutter.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8902A1062236D5BF005F914D /* libintercom_flutter.a */; }; - 8902A1092236D5BF005F914D /* libsqflite.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8928E32022356450001DB32A /* libsqflite.a */; }; - 8925094F222F0E0900455D87 /* libdevice_info.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8925094A222F0E0900455D87 /* libdevice_info.a */; }; - 898053B722047AD000E47AD9 /* libflutter_secure_storage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8980539E22047AAF00E47AD9 /* libflutter_secure_storage.a */; }; - 898053B822047AD000E47AD9 /* libimage_cropper.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 898053A022047AAF00E47AD9 /* libimage_cropper.a */; }; - 898053B922047AD000E47AD9 /* libimage_picker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 898053A222047AAF00E47AD9 /* libimage_picker.a */; }; - 898053BA22047AD000E47AD9 /* libonesignal.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 898053A422047AAF00E47AD9 /* libonesignal.a */; }; - 898053BB22047AD000E47AD9 /* libpath_provider.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 898053A622047AAF00E47AD9 /* libpath_provider.a */; }; - 898053BC22047AD000E47AD9 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 898053AA22047AAF00E47AD9 /* libPods-Runner.a */; }; - 898053BE22047AD000E47AD9 /* libTOCropViewController.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 898053AE22047AAF00E47AD9 /* libTOCropViewController.a */; }; - 898053BF22047AD000E47AD9 /* libuni_links.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 898053B222047AAF00E47AD9 /* libuni_links.a */; }; - 898053C022047AD000E47AD9 /* liburl_launcher.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 898053B422047AAF00E47AD9 /* liburl_launcher.a */; }; - 898053C122047AD000E47AD9 /* libvideo_player.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 898053B622047AAF00E47AD9 /* libvideo_player.a */; }; - 898053C322047AF100E47AD9 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 898053C222047AF100E47AD9 /* UserNotifications.framework */; }; - 898053C522047AF800E47AD9 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 898053C422047AF700E47AD9 /* SystemConfiguration.framework */; }; - 898053C622047B3B00E47AD9 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 898053C722047B4200E47AD9 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; + 89A543B022A7FFB500A4C0BB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 898053AA22047AAF00E47AD9 /* Pods_Runner.framework */; }; 89ABAE522203425900049DFB /* NotificationService.m in Sources */ = {isa = PBXBuildFile; fileRef = 89ABAE512203425900049DFB /* NotificationService.m */; }; 89ABAE562203425900049DFB /* OneSignalNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 89ABAE4E2203425900049DFB /* OneSignalNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; @@ -40,6 +22,7 @@ 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 */; }; + D9A32CBE0439E00B856D454C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BEBB57E8B03FDA9D0A698867 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -92,13 +75,6 @@ remoteGlobalIDString = D6D2B1288A9474AE589E5F0AED80524B; remoteInfo = image_picker; }; - 898053A322047AAF00E47AD9 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 8980538D22047AAE00E47AD9 /* Pods.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = BEB67C58BB1B431F81FB28DF8658A597; - remoteInfo = onesignal; - }; 898053A522047AAF00E47AD9 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 8980538D22047AAE00E47AD9 /* Pods.xcodeproj */; @@ -155,6 +131,13 @@ remoteGlobalIDString = F223BD1D37BD4412AC0C0C61C7615399; remoteInfo = video_player; }; + 8986A1FD22A7F7E7009824AF /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8980538D22047AAE00E47AD9 /* Pods.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = B71D6799C47D234A25EDBDA75F0C660D; + remoteInfo = onesignalflutter; + }; 89ABAE542203425900049DFB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; @@ -162,6 +145,41 @@ remoteGlobalIDString = 89ABAE4D2203425900049DFB; remoteInfo = OneSignalNotificationServiceExtension; }; + 89B81DB322A7F41F00B3E9E0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8980538D22047AAE00E47AD9 /* Pods.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = E17ED85F3FB0A1819C818427E1B96D7D; + remoteInfo = BSGridCollectionViewLayout; + }; + 89B81DB522A7F41F00B3E9E0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8980538D22047AAE00E47AD9 /* Pods.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = F0A7A725902A8FF9A7D6AB717B2F7501; + remoteInfo = BSImagePicker; + }; + 89B81DB722A7F41F00B3E9E0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8980538D22047AAE00E47AD9 /* Pods.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 95ED3BFAFDF702EF9348BDAB4EE5507C; + remoteInfo = "BSImagePicker-BSImagePicker"; + }; + 89B81DB922A7F41F00B3E9E0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8980538D22047AAE00E47AD9 /* Pods.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 03FE8D1D31B74F15F3AEC7ACEEE84D4F; + remoteInfo = BSImageView; + }; + 89B81DBB22A7F41F00B3E9E0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8980538D22047AAE00E47AD9 /* Pods.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = BFBFB5926D8AB35D39D19DC735F22517; + remoteInfo = multi_image_picker; + }; 89CF474322419BFD001BC50D /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 8980538D22047AAE00E47AD9 /* Pods.xcodeproj */; @@ -220,7 +238,6 @@ 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 4130CBCE97AB39E4070E45EF /* Pods-OneSignalNotificationServiceExtension.debug-development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.debug-development.xcconfig"; path = "Pods/Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.debug-development.xcconfig"; sourceTree = ""; }; 4E993998490EDCE06AC2B3F0 /* Pods-OneSignalNotificationServiceExtension.debug-production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.debug-production.xcconfig"; path = "Pods/Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.debug-production.xcconfig"; sourceTree = ""; }; - 7A4BA49A905C89E90E88FD37 /* libPods-OneSignalNotificationServiceExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-OneSignalNotificationServiceExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -236,8 +253,9 @@ 89ABAE502203425900049DFB /* NotificationService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotificationService.h; sourceTree = ""; }; 89ABAE512203425900049DFB /* NotificationService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationService.m; sourceTree = ""; }; 89ABAE532203425900049DFB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 89B81DC622A7F4EA00B3E9E0 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 89B81DC722A7F4EA00B3E9E0 /* OneSignalNotificationServiceExtension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OneSignalNotificationServiceExtension-Bridging-Header.h"; sourceTree = ""; }; 89DBC2DD2203430700F80685 /* OneSignalNotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OneSignalNotificationServiceExtension.entitlements; sourceTree = ""; }; - 970D6175355421F353EA64C9 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; @@ -250,7 +268,9 @@ A6C34D3821BE816E00882F1E /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; AAF4B8238CF43C30122544EF /* Pods-OneSignalNotificationServiceExtension.release-production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.release-production.xcconfig"; path = "Pods/Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.release-production.xcconfig"; sourceTree = ""; }; B23196F4E53E7786037AC8B5 /* Pods-OneSignalNotificationServiceExtension.release-development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.release-development.xcconfig"; path = "Pods/Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.release-development.xcconfig"; sourceTree = ""; }; + BEBB57E8B03FDA9D0A698867 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CBD573B1F3D19BD6C5F5624A /* Pods-OneSignalNotificationServiceExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.release.xcconfig"; path = "Pods/Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.release.xcconfig"; sourceTree = ""; }; + CE2F8A8BC5DC89A5ADEF49D1 /* Pods_OneSignalNotificationServiceExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_OneSignalNotificationServiceExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E6C0DC9AEB28225FDDAABF70 /* Pods-OneSignalNotificationServiceExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.profile.xcconfig"; path = "Pods/Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.profile.xcconfig"; sourceTree = ""; }; E73A042BEC81027B2EBB8967 /* Pods-OneSignalNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -260,25 +280,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 8902A1072236D5BF005F914D /* libFMDB.a in Frameworks */, - 8902A1082236D5BF005F914D /* libintercom_flutter.a in Frameworks */, - 8902A1092236D5BF005F914D /* libsqflite.a in Frameworks */, - 8925094F222F0E0900455D87 /* libdevice_info.a in Frameworks */, - 898053C722047B4200E47AD9 /* App.framework in Frameworks */, - 898053C622047B3B00E47AD9 /* Flutter.framework in Frameworks */, - 898053C522047AF800E47AD9 /* SystemConfiguration.framework in Frameworks */, - 898053C322047AF100E47AD9 /* UserNotifications.framework in Frameworks */, - 898053B722047AD000E47AD9 /* libflutter_secure_storage.a in Frameworks */, - 898053B822047AD000E47AD9 /* libimage_cropper.a in Frameworks */, - 898053B922047AD000E47AD9 /* libimage_picker.a in Frameworks */, - 898053BA22047AD000E47AD9 /* libonesignal.a in Frameworks */, - 898053BB22047AD000E47AD9 /* libpath_provider.a in Frameworks */, - 898053BC22047AD000E47AD9 /* libPods-Runner.a in Frameworks */, - 898053BE22047AD000E47AD9 /* libTOCropViewController.a in Frameworks */, - 898053BF22047AD000E47AD9 /* libuni_links.a in Frameworks */, - 898053C022047AD000E47AD9 /* liburl_launcher.a in Frameworks */, - 898053C122047AD000E47AD9 /* libvideo_player.a in Frameworks */, - 542BD8834B6D6BE2F3E671A0 /* libPods-OneSignalNotificationServiceExtension.a in Frameworks */, + 89A543B022A7FFB500A4C0BB /* Pods_Runner.framework in Frameworks */, + 30C0AB83114DC50252134799 /* Pods_OneSignalNotificationServiceExtension.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -288,7 +291,7 @@ files = ( 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - 26B90459017411DF809A7507 /* libPods-Runner.a in Frameworks */, + D9A32CBE0439E00B856D454C /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -320,8 +323,8 @@ 8980538622047A2300E47AD9 /* Runner */, 89805384220479C200E47AD9 /* video_player */, 898053822204791900E47AD9 /* VideoPlayerPlugin.h */, - 7A4BA49A905C89E90E88FD37 /* libPods-OneSignalNotificationServiceExtension.a */, - 970D6175355421F353EA64C9 /* libPods-Runner.a */, + CE2F8A8BC5DC89A5ADEF49D1 /* Pods_OneSignalNotificationServiceExtension.framework */, + BEBB57E8B03FDA9D0A698867 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -336,25 +339,30 @@ 8980538E22047AAE00E47AD9 /* Products */ = { isa = PBXGroup; children = ( - 8925094A222F0E0900455D87 /* libdevice_info.a */, - 89CF474422419BFD001BC50D /* libflutter_exif_rotation.a */, - 8980539E22047AAF00E47AD9 /* libflutter_secure_storage.a */, - 8928E31E22356450001DB32A /* libFMDB.a */, - 898053A022047AAF00E47AD9 /* libimage_cropper.a */, - 898053A222047AAF00E47AD9 /* libimage_picker.a */, - 8902A1062236D5BF005F914D /* libintercom_flutter.a */, - 898053A422047AAF00E47AD9 /* libonesignal.a */, - 898053A622047AAF00E47AD9 /* libpath_provider.a */, - 898053A822047AAF00E47AD9 /* libPods-OneSignalNotificationServiceExtension.a */, - 898053AA22047AAF00E47AD9 /* libPods-Runner.a */, - 89EE3231227B56510094ACB0 /* libshare.a */, - 89EE3233227B56510094ACB0 /* libshared_preferences.a */, - 8928E32022356450001DB32A /* libsqflite.a */, - 898053AE22047AAF00E47AD9 /* libTOCropViewController.a */, + 89B81DB422A7F41F00B3E9E0 /* BSGridCollectionViewLayout.framework */, + 89B81DB622A7F41F00B3E9E0 /* BSImagePicker.framework */, + 89B81DB822A7F41F00B3E9E0 /* BSImagePicker.bundle */, + 89B81DBA22A7F41F00B3E9E0 /* BSImageView.framework */, + 8925094A222F0E0900455D87 /* device_info.framework */, + 89CF474422419BFD001BC50D /* flutter_exif_rotation.framework */, + 8980539E22047AAF00E47AD9 /* flutter_secure_storage.framework */, + 8928E31E22356450001DB32A /* FMDB.framework */, + 898053A022047AAF00E47AD9 /* image_cropper.framework */, + 898053A222047AAF00E47AD9 /* image_picker.framework */, + 8902A1062236D5BF005F914D /* intercom_flutter.framework */, + 89B81DBC22A7F41F00B3E9E0 /* multi_image_picker.framework */, + 8986A1FE22A7F7E7009824AF /* onesignalflutter.framework */, + 898053A622047AAF00E47AD9 /* path_provider.framework */, + 898053A822047AAF00E47AD9 /* Pods_OneSignalNotificationServiceExtension.framework */, + 898053AA22047AAF00E47AD9 /* Pods_Runner.framework */, + 89EE3231227B56510094ACB0 /* share.framework */, + 89EE3233227B56510094ACB0 /* shared_preferences.framework */, + 8928E32022356450001DB32A /* sqflite.framework */, + 898053AE22047AAF00E47AD9 /* TOCropViewController.framework */, 898053B022047AAF00E47AD9 /* TOCropViewControllerBundle.bundle */, - 898053B222047AAF00E47AD9 /* libuni_links.a */, - 898053B422047AAF00E47AD9 /* liburl_launcher.a */, - 898053B622047AAF00E47AD9 /* libvideo_player.a */, + 898053B222047AAF00E47AD9 /* uni_links.framework */, + 898053B422047AAF00E47AD9 /* url_launcher.framework */, + 898053B622047AAF00E47AD9 /* video_player.framework */, ); name = Products; sourceTree = ""; @@ -392,6 +400,8 @@ 97C146EF1CF9000F007C117D /* Products */, 0532A0D3A2BF06AB149735E4 /* Pods */, 5ABC6138995F2182C962F35D /* Frameworks */, + 89B81DC622A7F4EA00B3E9E0 /* Runner-Bridging-Header.h */, + 89B81DC722A7F4EA00B3E9E0 /* OneSignalNotificationServiceExtension-Bridging-Header.h */, ); sourceTree = ""; }; @@ -490,6 +500,7 @@ 89ABAE4D2203425900049DFB = { CreatedOnToolsVersion = 10.1; DevelopmentTeam = GAR7B57RXU; + LastSwiftMigration = 1020; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.ApplicationGroups.iOS = { @@ -500,6 +511,7 @@ 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; DevelopmentTeam = GAR7B57RXU; + LastSwiftMigration = 1020; SystemCapabilities = { com.apple.ApplicationGroups.iOS = { enabled = 1; @@ -548,87 +560,80 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ - 8902A1062236D5BF005F914D /* libintercom_flutter.a */ = { + 8902A1062236D5BF005F914D /* intercom_flutter.framework */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = libintercom_flutter.a; + fileType = wrapper.framework; + path = intercom_flutter.framework; remoteRef = 8902A1052236D5BF005F914D /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 8925094A222F0E0900455D87 /* libdevice_info.a */ = { + 8925094A222F0E0900455D87 /* device_info.framework */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = libdevice_info.a; + fileType = wrapper.framework; + path = device_info.framework; remoteRef = 89250949222F0E0900455D87 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 8928E31E22356450001DB32A /* libFMDB.a */ = { + 8928E31E22356450001DB32A /* FMDB.framework */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = libFMDB.a; + fileType = wrapper.framework; + path = FMDB.framework; remoteRef = 8928E31D22356450001DB32A /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 8928E32022356450001DB32A /* libsqflite.a */ = { + 8928E32022356450001DB32A /* sqflite.framework */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = libsqflite.a; + fileType = wrapper.framework; + path = sqflite.framework; remoteRef = 8928E31F22356450001DB32A /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 8980539E22047AAF00E47AD9 /* libflutter_secure_storage.a */ = { + 8980539E22047AAF00E47AD9 /* flutter_secure_storage.framework */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = libflutter_secure_storage.a; + fileType = wrapper.framework; + path = flutter_secure_storage.framework; remoteRef = 8980539D22047AAF00E47AD9 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 898053A022047AAF00E47AD9 /* libimage_cropper.a */ = { + 898053A022047AAF00E47AD9 /* image_cropper.framework */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = libimage_cropper.a; + fileType = wrapper.framework; + path = image_cropper.framework; remoteRef = 8980539F22047AAF00E47AD9 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 898053A222047AAF00E47AD9 /* libimage_picker.a */ = { + 898053A222047AAF00E47AD9 /* image_picker.framework */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = libimage_picker.a; + fileType = wrapper.framework; + path = image_picker.framework; remoteRef = 898053A122047AAF00E47AD9 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 898053A422047AAF00E47AD9 /* libonesignal.a */ = { + 898053A622047AAF00E47AD9 /* path_provider.framework */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = libonesignal.a; - remoteRef = 898053A322047AAF00E47AD9 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 898053A622047AAF00E47AD9 /* libpath_provider.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libpath_provider.a; + fileType = wrapper.framework; + path = path_provider.framework; remoteRef = 898053A522047AAF00E47AD9 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 898053A822047AAF00E47AD9 /* libPods-OneSignalNotificationServiceExtension.a */ = { + 898053A822047AAF00E47AD9 /* Pods_OneSignalNotificationServiceExtension.framework */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libPods-OneSignalNotificationServiceExtension.a"; + fileType = wrapper.framework; + path = Pods_OneSignalNotificationServiceExtension.framework; remoteRef = 898053A722047AAF00E47AD9 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 898053AA22047AAF00E47AD9 /* libPods-Runner.a */ = { + 898053AA22047AAF00E47AD9 /* Pods_Runner.framework */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = "libPods-Runner.a"; + fileType = wrapper.framework; + path = Pods_Runner.framework; remoteRef = 898053A922047AAF00E47AD9 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 898053AE22047AAF00E47AD9 /* libTOCropViewController.a */ = { + 898053AE22047AAF00E47AD9 /* TOCropViewController.framework */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = libTOCropViewController.a; + fileType = wrapper.framework; + path = TOCropViewController.framework; remoteRef = 898053AD22047AAF00E47AD9 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -639,45 +644,87 @@ remoteRef = 898053AF22047AAF00E47AD9 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 898053B222047AAF00E47AD9 /* libuni_links.a */ = { + 898053B222047AAF00E47AD9 /* uni_links.framework */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = libuni_links.a; + fileType = wrapper.framework; + path = uni_links.framework; remoteRef = 898053B122047AAF00E47AD9 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 898053B422047AAF00E47AD9 /* liburl_launcher.a */ = { + 898053B422047AAF00E47AD9 /* url_launcher.framework */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = liburl_launcher.a; + fileType = wrapper.framework; + path = url_launcher.framework; remoteRef = 898053B322047AAF00E47AD9 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 898053B622047AAF00E47AD9 /* libvideo_player.a */ = { + 898053B622047AAF00E47AD9 /* video_player.framework */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = libvideo_player.a; + fileType = wrapper.framework; + path = video_player.framework; remoteRef = 898053B522047AAF00E47AD9 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 89CF474422419BFD001BC50D /* libflutter_exif_rotation.a */ = { + 8986A1FE22A7F7E7009824AF /* onesignalflutter.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = onesignalflutter.framework; + remoteRef = 8986A1FD22A7F7E7009824AF /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 89B81DB422A7F41F00B3E9E0 /* BSGridCollectionViewLayout.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = BSGridCollectionViewLayout.framework; + remoteRef = 89B81DB322A7F41F00B3E9E0 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 89B81DB622A7F41F00B3E9E0 /* BSImagePicker.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = BSImagePicker.framework; + remoteRef = 89B81DB522A7F41F00B3E9E0 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 89B81DB822A7F41F00B3E9E0 /* BSImagePicker.bundle */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = BSImagePicker.bundle; + remoteRef = 89B81DB722A7F41F00B3E9E0 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 89B81DBA22A7F41F00B3E9E0 /* BSImageView.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = BSImageView.framework; + remoteRef = 89B81DB922A7F41F00B3E9E0 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 89B81DBC22A7F41F00B3E9E0 /* multi_image_picker.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = multi_image_picker.framework; + remoteRef = 89B81DBB22A7F41F00B3E9E0 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 89CF474422419BFD001BC50D /* flutter_exif_rotation.framework */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = libflutter_exif_rotation.a; + fileType = wrapper.framework; + path = flutter_exif_rotation.framework; remoteRef = 89CF474322419BFD001BC50D /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 89EE3231227B56510094ACB0 /* libshare.a */ = { + 89EE3231227B56510094ACB0 /* share.framework */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = libshare.a; + fileType = wrapper.framework; + path = share.framework; remoteRef = 89EE3230227B56510094ACB0 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 89EE3233227B56510094ACB0 /* libshared_preferences.a */ = { + 89EE3233227B56510094ACB0 /* shared_preferences.framework */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = libshared_preferences.a; + fileType = wrapper.framework; + path = shared_preferences.framework; remoteRef = 89EE3232227B56510094ACB0 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -714,13 +761,11 @@ "${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", "${PODS_ROOT}/Intercom/Intercom/Intercom.framework/Versions/A/Resources/Intercom.bundle", "${PODS_ROOT}/Intercom/Intercom/Intercom.framework/Versions/A/Resources/IntercomTranslations.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Intercom.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/IntercomTranslations.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -734,11 +779,45 @@ ); inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/../.symlinks/flutter/ios-release/Flutter.framework", + "${BUILT_PRODUCTS_DIR}/BSGridCollectionViewLayout/BSGridCollectionViewLayout.framework", + "${BUILT_PRODUCTS_DIR}/BSImagePicker/BSImagePicker.framework", + "${BUILT_PRODUCTS_DIR}/BSImageView/BSImageView.framework", + "${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework", + "${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework", + "${BUILT_PRODUCTS_DIR}/TOCropViewController/TOCropViewController.framework", + "${BUILT_PRODUCTS_DIR}/device_info/device_info.framework", + "${BUILT_PRODUCTS_DIR}/flutter_exif_rotation/flutter_exif_rotation.framework", + "${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework", + "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework", + "${BUILT_PRODUCTS_DIR}/multi_image_picker/multi_image_picker.framework", + "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", + "${BUILT_PRODUCTS_DIR}/share/share.framework", + "${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework", + "${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework", + "${BUILT_PRODUCTS_DIR}/uni_links/uni_links.framework", + "${BUILT_PRODUCTS_DIR}/url_launcher/url_launcher.framework", + "${BUILT_PRODUCTS_DIR}/video_player/video_player.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BSGridCollectionViewLayout.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BSImagePicker.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BSImageView.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/TOCropViewController.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_exif_rotation.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/multi_image_picker.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/uni_links.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_player.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -915,6 +994,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; @@ -933,6 +1013,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = social.openbook.app; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner-Bridging-Header.h"; + SWIFT_VERSION = 5; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; @@ -999,6 +1081,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; @@ -1017,6 +1100,9 @@ ); PRODUCT_BUNDLE_IDENTIFIER = social.openbook.app; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; @@ -1077,6 +1163,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; @@ -1095,6 +1182,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = social.openbook.app; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner-Bridging-Header.h"; + SWIFT_VERSION = 5; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; @@ -1161,6 +1250,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; @@ -1179,6 +1269,9 @@ ); PRODUCT_BUNDLE_IDENTIFIER = social.openbook.app; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; @@ -1239,6 +1332,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; @@ -1257,6 +1351,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = social.openbook.app; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner-Bridging-Header.h"; + SWIFT_VERSION = 5; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; @@ -1268,6 +1364,7 @@ buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; @@ -1289,6 +1386,9 @@ PRODUCT_BUNDLE_IDENTIFIER = social.openbook.app.OneSignalNotificationServiceExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "OneSignalNotificationServiceExtension-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -1299,6 +1399,7 @@ buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; @@ -1320,6 +1421,9 @@ PRODUCT_BUNDLE_IDENTIFIER = social.openbook.app.OneSignalNotificationServiceExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "OneSignalNotificationServiceExtension-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5; TARGETED_DEVICE_FAMILY = "1,2"; }; name = "Debug-production"; @@ -1330,6 +1434,7 @@ buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; @@ -1351,6 +1456,9 @@ PRODUCT_BUNDLE_IDENTIFIER = social.openbook.app.OneSignalNotificationServiceExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "OneSignalNotificationServiceExtension-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5; TARGETED_DEVICE_FAMILY = "1,2"; }; name = "Debug-development"; @@ -1361,6 +1469,7 @@ buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; @@ -1381,6 +1490,8 @@ PRODUCT_BUNDLE_IDENTIFIER = social.openbook.app.OneSignalNotificationServiceExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "OneSignalNotificationServiceExtension-Bridging-Header.h"; + SWIFT_VERSION = 5; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -1391,6 +1502,7 @@ buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; @@ -1411,6 +1523,8 @@ PRODUCT_BUNDLE_IDENTIFIER = social.openbook.app.OneSignalNotificationServiceExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "OneSignalNotificationServiceExtension-Bridging-Header.h"; + SWIFT_VERSION = 5; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Profile; @@ -1421,6 +1535,7 @@ buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; @@ -1441,6 +1556,8 @@ PRODUCT_BUNDLE_IDENTIFIER = social.openbook.app.OneSignalNotificationServiceExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "OneSignalNotificationServiceExtension-Bridging-Header.h"; + SWIFT_VERSION = 5; TARGETED_DEVICE_FAMILY = "1,2"; }; name = "Release-production"; @@ -1451,6 +1568,7 @@ buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; @@ -1471,6 +1589,8 @@ PRODUCT_BUNDLE_IDENTIFIER = social.openbook.app.OneSignalNotificationServiceExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "OneSignalNotificationServiceExtension-Bridging-Header.h"; + SWIFT_VERSION = 5; TARGETED_DEVICE_FAMILY = "1,2"; }; name = "Release-development"; @@ -1586,6 +1706,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; @@ -1604,6 +1725,9 @@ ); PRODUCT_BUNDLE_IDENTIFIER = social.openbook.app; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; @@ -1614,6 +1738,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; @@ -1632,6 +1757,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = social.openbook.app; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner-Bridging-Header.h"; + SWIFT_VERSION = 5; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; diff --git a/lib/libs/str_utils.dart b/lib/libs/str_utils.dart index c8bbde1ef..a08591b72 100644 --- a/lib/libs/str_utils.dart +++ b/lib/libs/str_utils.dart @@ -1 +1 @@ -String capitalize(String s) => s[0].toUpperCase() + s.substring(1); +String toCapital(String s) => s[0].toUpperCase() + s.substring(1); diff --git a/lib/libs/type_to_str.dart b/lib/libs/type_to_str.dart new file mode 100644 index 000000000..c52d1809a --- /dev/null +++ b/lib/libs/type_to_str.dart @@ -0,0 +1,22 @@ +import 'package:Openbook/libs/str_utils.dart'; +import 'package:Openbook/models/community.dart'; +import 'package:Openbook/models/post.dart'; +import 'package:Openbook/models/post_comment.dart'; +import 'package:Openbook/models/user.dart'; + +String modelTypeToString(dynamic modelInstance, {bool capitalize = false}) { + String result; + if (modelInstance is Post) { + result = 'post'; + } else if (modelInstance is PostComment) { + result = 'post comment'; + } else if (modelInstance is Community) { + result = 'community'; + } else if (modelInstance is User) { + result = 'user'; + } else { + result = 'item'; + } + + return capitalize ? toCapital(result) : result; +} diff --git a/lib/main.dart b/lib/main.dart index ce1a840b5..81debfa4c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,6 +16,8 @@ import 'package:Openbook/pages/auth/reset_password/verify_reset_password_link_st import 'package:Openbook/pages/auth/login.dart'; import 'package:Openbook/pages/auth/splash.dart'; import 'package:Openbook/pages/home/home.dart'; +import 'package:Openbook/pages/waitlist/subscribe_done_step.dart'; +import 'package:Openbook/pages/waitlist/subscribe_email_step.dart'; import 'package:Openbook/provider.dart'; import 'package:Openbook/pages/auth/create_account/name_step.dart'; import 'package:Openbook/plugins/desktop/error-reporting.dart'; @@ -34,6 +36,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { + var textTheme = _defaultTextTheme(); return OpenbookProvider( key: openbookProviderKey, child: OBToast( @@ -62,7 +65,10 @@ class MyApp extends StatelessWidget { // or press Run > Flutter Hot Reload in IntelliJ). Notice that the // counter didn't reset back to zero; the application is not restarted. primarySwatch: Colors.grey, - fontFamily: 'NunitoSans'), + fontFamily: 'NunitoSans', + textTheme: textTheme, + primaryTextTheme: textTheme, + accentTextTheme: textTheme), routes: { /// The openbookProvider uses services available in the context /// Their connection must be bootstrapped but no other way to execute @@ -130,6 +136,15 @@ class MyApp extends StatelessWidget { '/auth/password_reset_success_step': (BuildContext context) { bootstrapOpenbookProviderInContext(context); return OBAuthPasswordResetSuccessPage(); + }, + '/waitlist/subscribe_email_step': (BuildContext context) { + bootstrapOpenbookProviderInContext(context); + return OBWaitlistSubscribePage(); + }, + '/waitlist/subscribe_done_step': (BuildContext context) { + bootstrapOpenbookProviderInContext(context); + WaitlistSubscribeArguments args = ModalRoute.of(context).settings.arguments; + return OBWaitlistSubscribeDoneStep(count: args.count); } }), ), @@ -238,3 +253,28 @@ bool get isInDebugMode { bool get isOnDesktop { return Platform.isLinux || Platform.isMacOS || Platform.isWindows; } + +TextTheme _defaultTextTheme() { + // This text theme is merged with the default theme in the `TextData` + // constructor. This makes sure that the emoji font is used as fallback for + // every text that uses the default theme. + var style; + if (isOnDesktop) { + style = new TextStyle(fontFamilyFallback: ['Emoji']); + } + return new TextTheme( + body1: style, + body2: style, + button: style, + caption: style, + display1: style, + display2: style, + display3: style, + display4: style, + headline: style, + overline: style, + subhead: style, + subtitle: style, + title: style, + ); +} diff --git a/lib/models/community.dart b/lib/models/community.dart index 010497d8f..c6571b1ca 100644 --- a/lib/models/community.dart +++ b/lib/models/community.dart @@ -53,6 +53,8 @@ class Community extends UpdatableModel { String userAdjective; String usersAdjective; int membersCount; + int pendingModeratedObjectsCount; + CommunityType type; // Whether the user has been invited to the community @@ -63,6 +65,8 @@ class Community extends UpdatableModel { bool isFavorite; + bool isReported; + bool invitesEnabled; CategoriesList categories; @@ -88,12 +92,14 @@ class Community extends UpdatableModel { this.cover, this.isInvited, this.isCreator, + this.isReported, this.moderators, this.memberships, this.administrators, this.isFavorite, this.invitesEnabled, this.membersCount, + this.pendingModeratedObjectsCount, this.categories}); bool hasDescription() { @@ -202,6 +208,14 @@ class Community extends UpdatableModel { usersAdjective = json['users_adjective']; } + if (json.containsKey('is_reported')) { + isReported = json['is_reported']; + } + + if (json.containsKey('pending_moderated_objects_count')) { + pendingModeratedObjectsCount = json['pending_moderated_objects_count']; + } + if (json.containsKey('color')) { color = json['color']; } @@ -247,6 +261,11 @@ class Community extends UpdatableModel { notifyUpdate(); } } + + void setIsReported(isReported) { + this.isReported = isReported; + notifyUpdate(); + } } class CommunityFactory extends UpdatableModelFactory { @@ -265,8 +284,10 @@ class CommunityFactory extends UpdatableModelFactory { avatar: json['avatar'], isInvited: json['is_invited'], isCreator: json['is_creator'], + isReported: json['is_reported'], isFavorite: json['is_favorite'], invitesEnabled: json['invites_enabled'], + pendingModeratedObjectsCount: json['pending_moderated_objects_count'], cover: json['cover'], color: json['color'], memberships: parseMemberships(json['memberships']), diff --git a/lib/models/moderation/moderated_object.dart b/lib/models/moderation/moderated_object.dart new file mode 100644 index 000000000..d08f9f872 --- /dev/null +++ b/lib/models/moderation/moderated_object.dart @@ -0,0 +1,293 @@ +import 'package:Openbook/libs/str_utils.dart'; +import 'package:Openbook/models/community.dart'; +import 'package:Openbook/models/moderation/moderation_category.dart'; +import 'package:Openbook/models/post.dart'; +import 'package:Openbook/models/post_comment.dart'; +import 'package:Openbook/models/updatable_model.dart'; +import 'package:Openbook/models/user.dart'; +import 'package:dcache/dcache.dart'; +import 'package:meta/meta.dart'; + +class ModeratedObject extends UpdatableModel { + static final factory = ModeratedObjectFactory(); + + factory ModeratedObject.fromJSON(Map json) { + return factory.fromJson(json); + } + + static String objectTypePost = 'P'; + static String objectTypePostComment = 'PC'; + static String objectTypeCommunity = 'C'; + static String objectTypeUser = 'U'; + static String statusPending = 'P'; + static String statusApproved = 'A'; + static String statusRejected = 'R'; + + final int id; + final Community community; + + dynamic contentObject; + ModeratedObjectType type; + ModeratedObjectStatus status; + ModerationCategory category; + + String description; + bool verified; + int reportsCount; + + ModeratedObject( + {this.id, + this.community, + this.contentObject, + this.type, + this.status, + this.reportsCount, + this.category, + this.description, + this.verified}); + + @override + void updateFromJson(Map json) { + if (json.containsKey('description')) { + description = json['description']; + } + + if (json.containsKey('category')) { + category = factory.parseModerationCategory(json['category']); + } + + if (json.containsKey('verified')) { + verified = json['verified']; + } + + if (json.containsKey('reports_count')) { + reportsCount = json['reports_count']; + } + + if (json.containsKey('status')) { + status = factory.parseStatus(json['status']); + } + + if (json.containsKey('type')) { + type = factory.parseType(json['object_type']); + } + + if (json.containsKey('content_object')) { + contentObject = factory.parseContentObject( + contentObjectData: json['content_object'], type: type); + } + } + + void setIsVerified(bool isVerified) { + verified = isVerified; + notifyUpdate(); + } + + bool isVerified() { + return verified; + } + + void setIsApproved() { + setStatus(ModeratedObjectStatus.approved); + } + + void setIsRejected() { + setStatus(ModeratedObjectStatus.rejected); + } + + void setStatus(ModeratedObjectStatus newStatus) { + status = newStatus; + notifyUpdate(); + } +} + +class ModeratedObjectFactory extends UpdatableModelFactory { + @override + SimpleCache cache = + SimpleCache(storage: UpdatableModelSimpleStorage(size: 120)); + + @override + ModeratedObject makeFromJson(Map json) { + ModeratedObjectType type = parseType(json['object_type']); + ModeratedObjectStatus status = parseStatus(json['status']); + ModerationCategory category = parseModerationCategory(json['category']); + Community community = parseCommunity(json['community']); + + return ModeratedObject( + id: json['id'], + community: community, + category: category, + description: json['description'], + reportsCount: json['reports_count'], + status: status, + type: type, + contentObject: parseContentObject( + contentObjectData: json['content_object'], type: type), + verified: json['verified']); + } + + Community parseCommunity(Map communityData) { + if (communityData == null) return null; + return Community.fromJSON(communityData); + } + + ModerationCategory parseModerationCategory(Map moderationCategoryData) { + if (moderationCategoryData == null) return null; + return ModerationCategory.fromJson(moderationCategoryData); + } + + ModeratedObjectType parseType(String moderatedObjectTypeStr) { + if (moderatedObjectTypeStr == null) return null; + + ModeratedObjectType moderatedObjectType; + if (moderatedObjectTypeStr == ModeratedObject.objectTypeCommunity) { + moderatedObjectType = ModeratedObjectType.community; + } else if (moderatedObjectTypeStr == ModeratedObject.objectTypePost) { + moderatedObjectType = ModeratedObjectType.post; + } else if (moderatedObjectTypeStr == + ModeratedObject.objectTypePostComment) { + moderatedObjectType = ModeratedObjectType.postComment; + } else if (moderatedObjectTypeStr == ModeratedObject.objectTypeUser) { + moderatedObjectType = ModeratedObjectType.user; + } else { + // Don't throw as we might introduce new moderatedObjects on the API which might not be yet in code + print('Unsupported moderatedObject type'); + } + + return moderatedObjectType; + } + + ModeratedObjectStatus parseStatus(String moderatedObjectStatusStr) { + if (moderatedObjectStatusStr == null) return null; + + ModeratedObjectStatus moderatedObjectStatus; + if (moderatedObjectStatusStr == ModeratedObject.statusPending) { + moderatedObjectStatus = ModeratedObjectStatus.pending; + } else if (moderatedObjectStatusStr == ModeratedObject.statusApproved) { + moderatedObjectStatus = ModeratedObjectStatus.approved; + } else if (moderatedObjectStatusStr == ModeratedObject.statusRejected) { + moderatedObjectStatus = ModeratedObjectStatus.rejected; + } else { + // Don't throw as we might introduce new moderatedObjects on the API which might not be yet in code + print('Unsupported moderatedObject status'); + } + + return moderatedObjectStatus; + } + + String convertStatusToString(ModeratedObjectStatus moderatedObjectStatus) { + if (moderatedObjectStatus == null) return null; + + switch (moderatedObjectStatus) { + case ModeratedObjectStatus.approved: + return ModeratedObject.statusApproved; + case ModeratedObjectStatus.rejected: + return ModeratedObject.statusRejected; + case ModeratedObjectStatus.pending: + return ModeratedObject.statusPending; + default: + return ''; + } + } + + String convertStatusToHumanReadableString( + ModeratedObjectStatus moderatedObjectStatus, + {capitalize = false}) { + if (moderatedObjectStatus == null) return null; + + String result; + + switch (moderatedObjectStatus) { + case ModeratedObjectStatus.approved: + result = 'approved'; + break; + case ModeratedObjectStatus.rejected: + result = 'rejected'; + break; + case ModeratedObjectStatus.pending: + result = 'pending'; + break; + default: + } + + return capitalize ? toCapital(result) : result; + } + + String convertTypeToString(ModeratedObjectType moderatedObjectType) { + if (moderatedObjectType == null) return null; + + switch (moderatedObjectType) { + case ModeratedObjectType.community: + return ModeratedObject.objectTypeCommunity; + case ModeratedObjectType.user: + return ModeratedObject.objectTypeUser; + case ModeratedObjectType.post: + return ModeratedObject.objectTypePost; + case ModeratedObjectType.postComment: + return ModeratedObject.objectTypePostComment; + default: + return ''; + } + } + + String convertTypeToHumanReadableString( + ModeratedObjectType moderatedObjectType, + {capitalize = false}) { + if (moderatedObjectType == null) return null; + + String result = 'object'; + + switch (moderatedObjectType) { + case ModeratedObjectType.community: + result = 'community'; + break; + case ModeratedObjectType.user: + result = 'user'; + break; + case ModeratedObjectType.post: + result = 'post'; + break; + case ModeratedObjectType.postComment: + result = 'post comment'; + break; + default: + } + + return capitalize ? toCapital(result) : result; + } + + dynamic parseContentObject( + {@required Map contentObjectData, @required ModeratedObjectType type}) { + if (contentObjectData == null) return null; + + dynamic contentObject; + switch (type) { + case ModeratedObjectType.post: + contentObject = Post.fromJson(contentObjectData); + break; + case ModeratedObjectType.postComment: + contentObject = PostComment.fromJSON(contentObjectData); + break; + case ModeratedObjectType.community: + contentObject = Community.fromJSON(contentObjectData); + break; + case ModeratedObjectType.user: + contentObject = User.fromJson(contentObjectData); + break; + default: + } + return contentObject; + } + + DateTime parseCreated(String created) { + return DateTime.parse(created).toLocal(); + } +} + +enum ModeratedObjectType { post, postComment, user, community } + +enum ModeratedObjectStatus { + approved, + rejected, + pending, +} diff --git a/lib/models/moderation/moderated_object_list.dart b/lib/models/moderation/moderated_object_list.dart new file mode 100644 index 000000000..6f4203000 --- /dev/null +++ b/lib/models/moderation/moderated_object_list.dart @@ -0,0 +1,18 @@ +import 'package:Openbook/models/moderation/moderated_object.dart'; + +class ModeratedObjectsList { + final List moderatedObjects; + + ModeratedObjectsList({ + this.moderatedObjects, + }); + + factory ModeratedObjectsList.fromJson(List parsedJson) { + List moderatedObjects = + parsedJson.map((moderatedObjectJson) => ModeratedObject.fromJSON(moderatedObjectJson)).toList(); + + return new ModeratedObjectsList( + moderatedObjects: moderatedObjects, + ); + } +} diff --git a/lib/models/moderation/moderated_object_log.dart b/lib/models/moderation/moderated_object_log.dart new file mode 100644 index 000000000..f0f26c453 --- /dev/null +++ b/lib/models/moderation/moderated_object_log.dart @@ -0,0 +1,184 @@ +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/moderation/moderation_category.dart'; +import 'package:Openbook/models/user.dart'; +import 'package:meta/meta.dart'; + +class ModeratedObjectLog { + static String descriptionChangedLogType = 'DC'; + static String statusChangedLogType = 'AC'; + static String verifiedChangedLogType = 'VC'; + static String categoryChangedLogType = 'CC'; + + final int id; + final String description; + final bool verified; + final User actor; + final DateTime created; + ModeratedObjectLogType logType; + + dynamic contentObject; + + ModeratedObjectLog( + {this.verified, + this.id, + this.description, + this.contentObject, + this.actor, + this.created, + this.logType}); + + factory ModeratedObjectLog.fromJson(Map parsedJson) { + ModeratedObjectLogType logType = parseType(parsedJson['log_type']); + + return ModeratedObjectLog( + verified: parsedJson['verified'], + id: parsedJson['id'], + description: parsedJson['description'], + actor: parseActor( + parsedJson['actor'], + ), + logType: logType, + created: parseCreated(parsedJson['created']), + contentObject: parseContentObject( + contentObjectData: parsedJson['content_object'], logType: logType)); + } + + static User parseActor(Map rawActor) { + if (rawActor == null) return null; + return User.fromJson(rawActor); + } + + static DateTime parseCreated(String created) { + if (created == null) return null; + return DateTime.parse(created).toLocal(); + } + + static dynamic parseContentObject( + {@required Map contentObjectData, + @required ModeratedObjectLogType logType}) { + if (contentObjectData == null) return null; + + dynamic contentObject; + switch (logType) { + case ModeratedObjectLogType.categoryChanged: + contentObject = + ModeratedObjectCategoryChangedLog.fromJson(contentObjectData); + break; + case ModeratedObjectLogType.descriptionChanged: + contentObject = + ModeratedObjectDescriptionChangedLog.fromJson(contentObjectData); + break; + case ModeratedObjectLogType.verifiedChanged: + contentObject = + ModeratedObjectVerifiedChangedLog.fromJson(contentObjectData); + break; + case ModeratedObjectLogType.statusChanged: + contentObject = + ModeratedObjectStatusChangedLog.fromJson(contentObjectData); + break; + default: + } + + return contentObject; + } + + static ModeratedObjectLogType parseType(String moderatedObjectTypeStr) { + if (moderatedObjectTypeStr == null) return null; + + ModeratedObjectLogType moderatedObjectLogType; + if (moderatedObjectTypeStr == + ModeratedObjectLog.descriptionChangedLogType) { + moderatedObjectLogType = ModeratedObjectLogType.descriptionChanged; + } else if (moderatedObjectTypeStr == + ModeratedObjectLog.statusChangedLogType) { + moderatedObjectLogType = ModeratedObjectLogType.statusChanged; + } else if (moderatedObjectTypeStr == + ModeratedObjectLog.verifiedChangedLogType) { + moderatedObjectLogType = ModeratedObjectLogType.verifiedChanged; + } else if (moderatedObjectTypeStr == + ModeratedObjectLog.categoryChangedLogType) { + moderatedObjectLogType = ModeratedObjectLogType.categoryChanged; + } else { + print('Unsupported moderatedObjectLog type'); + } + + return moderatedObjectLogType; + } +} + +enum ModeratedObjectLogType { + descriptionChanged, + verifiedChanged, + statusChanged, + categoryChanged, +} + +class ModeratedObjectCategoryChangedLog { + final ModerationCategory changedFrom; + final ModerationCategory changedTo; + + ModeratedObjectCategoryChangedLog({this.changedFrom, this.changedTo}); + + factory ModeratedObjectCategoryChangedLog.fromJson( + Map parsedJson) { + return ModeratedObjectCategoryChangedLog( + changedFrom: parseCategory(parsedJson['changed_from']), + changedTo: parseCategory(parsedJson['changed_to']), + ); + } + + static ModerationCategory parseCategory(Map rawModerationCategory) { + if (rawModerationCategory == null) return null; + return ModerationCategory.fromJson(rawModerationCategory); + } +} + +class ModeratedObjectDescriptionChangedLog { + final String changedFrom; + final String changedTo; + + ModeratedObjectDescriptionChangedLog({this.changedFrom, this.changedTo}); + + factory ModeratedObjectDescriptionChangedLog.fromJson( + Map parsedJson) { + return ModeratedObjectDescriptionChangedLog( + changedFrom: parsedJson['changed_from'], + changedTo: parsedJson['changed_to'], + ); + } +} + +class ModeratedObjectVerifiedChangedLog { + final bool changedFrom; + final bool changedTo; + + ModeratedObjectVerifiedChangedLog({this.changedFrom, this.changedTo}); + + factory ModeratedObjectVerifiedChangedLog.fromJson( + Map parsedJson) { + return ModeratedObjectVerifiedChangedLog( + changedFrom: parsedJson['changed_from'], + changedTo: parsedJson['changed_to'], + ); + } +} + +class ModeratedObjectStatusChangedLog { + final ModeratedObjectStatus changedFrom; + final ModeratedObjectStatus changedTo; + + ModeratedObjectStatusChangedLog({this.changedFrom, this.changedTo}); + + factory ModeratedObjectStatusChangedLog.fromJson( + Map parsedJson) { + return ModeratedObjectStatusChangedLog( + changedFrom: parseStatus(parsedJson['changed_from']), + changedTo: parseStatus(parsedJson['changed_to']), + ); + } + + static ModeratedObjectStatus parseStatus(String rawModerationStatus) { + if (rawModerationStatus == null) return null; + return ModeratedObject.factory.parseStatus(rawModerationStatus); + } +} diff --git a/lib/models/moderation/moderated_object_log_list.dart b/lib/models/moderation/moderated_object_log_list.dart new file mode 100644 index 000000000..d4c70550a --- /dev/null +++ b/lib/models/moderation/moderated_object_log_list.dart @@ -0,0 +1,20 @@ +import 'package:Openbook/models/moderation/moderated_object_log.dart'; + +class ModeratedObjectLogsList { + final List moderatedObjectLogs; + + ModeratedObjectLogsList({ + this.moderatedObjectLogs, + }); + + factory ModeratedObjectLogsList.fromJson(List parsedJson) { + List moderatedObjectLogs = parsedJson + .map((moderatedObjectLogJson) => + ModeratedObjectLog.fromJson(moderatedObjectLogJson)) + .toList(); + + return new ModeratedObjectLogsList( + moderatedObjectLogs: moderatedObjectLogs, + ); + } +} diff --git a/lib/models/moderation/moderation_category.dart b/lib/models/moderation/moderation_category.dart new file mode 100644 index 000000000..49b2fb032 --- /dev/null +++ b/lib/models/moderation/moderation_category.dart @@ -0,0 +1,51 @@ +class ModerationCategory { + static final severityCritical = 'C'; + static final severityHigh = 'H'; + static final severityMedium = 'M'; + static final severityLow = 'L'; + + static ModerationCategorySeverity parseType(String notificationTypeStr) { + if (notificationTypeStr == null) return null; + + ModerationCategorySeverity notificationType; + if (notificationTypeStr == ModerationCategory.severityCritical) { + notificationType = ModerationCategorySeverity.critical; + } else if (notificationTypeStr == ModerationCategory.severityHigh) { + notificationType = ModerationCategorySeverity.high; + } else if (notificationTypeStr == ModerationCategory.severityMedium) { + notificationType = ModerationCategorySeverity.medium; + } else if (notificationTypeStr == ModerationCategory.severityLow) { + notificationType = ModerationCategorySeverity.low; + } else { + // Don't throw as we might introduce new notifications on the API which might not be yet in code + print('Unsupported notification type'); + } + + return notificationType; + } + + final int id; + final ModerationCategorySeverity severity; + final String name; + final String title; + final String description; + + ModerationCategory( + {this.id, this.severity, this.name, this.title, this.description}); + + factory ModerationCategory.fromJson(Map parsedJson) { + return ModerationCategory( + id: parsedJson['id'], + name: parsedJson['name'], + title: parsedJson['title'], + description: parsedJson['description'], + severity: parseType(parsedJson['type'])); + } +} + +enum ModerationCategorySeverity { + critical, + high, + medium, + low, +} diff --git a/lib/models/moderation/moderation_category_list.dart b/lib/models/moderation/moderation_category_list.dart new file mode 100644 index 000000000..df40140c7 --- /dev/null +++ b/lib/models/moderation/moderation_category_list.dart @@ -0,0 +1,20 @@ +import 'package:Openbook/models/moderation/moderation_category.dart'; + +class ModerationCategoriesList { + final List moderationCategories; + + ModerationCategoriesList({ + this.moderationCategories, + }); + + factory ModerationCategoriesList.fromJson(List parsedJson) { + List moderationCategories = parsedJson + .map((moderationCategoryJson) => + ModerationCategory.fromJson(moderationCategoryJson)) + .toList(); + + return new ModerationCategoriesList( + moderationCategories: moderationCategories, + ); + } +} diff --git a/lib/models/moderation/moderation_penalty.dart b/lib/models/moderation/moderation_penalty.dart new file mode 100644 index 000000000..e9039d738 --- /dev/null +++ b/lib/models/moderation/moderation_penalty.dart @@ -0,0 +1,88 @@ +import 'package:Openbook/libs/str_utils.dart'; +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/moderation/moderation_category.dart'; +import 'package:Openbook/models/user.dart'; + +class ModerationPenalty { + static String moderationPenaltyTypeSuspension = 'S'; + + final int id; + final User user; + final DateTime expiration; + final ModeratedObject moderatedObject; + final ModerationPenaltyType type; + + ModerationPenalty({ + this.user, + this.expiration, + this.moderatedObject, + this.id, + this.type, + }); + + factory ModerationPenalty.fromJson(Map parsedJson) { + return ModerationPenalty( + id: parsedJson['id'], + user: parseUser( + parsedJson['user'], + ), + type: parseType( + parsedJson['type'], + ), + moderatedObject: parseModeratedObject( + parsedJson['moderated_object'], + ), + expiration: parseExpiration(parsedJson['expiration'])); + } + + static User parseUser(Map rawActor) { + if (rawActor == null) return null; + return User.fromJson(rawActor); + } + + static ModerationCategory parseCategory(Map rawModerationCategory) { + if (rawModerationCategory == null) return null; + return ModerationCategory.fromJson(rawModerationCategory); + } + + static DateTime parseExpiration(String expiration) { + if (expiration == null) return null; + return DateTime.parse(expiration).toLocal(); + } + + static ModeratedObject parseModeratedObject(Map rawModeratedObject) { + if (rawModeratedObject == null) return null; + return ModeratedObject.fromJSON(rawModeratedObject); + } + + static ModerationPenaltyType parseType(String moderationPenaltyTypeStr) { + if (moderationPenaltyTypeStr == null) return null; + + ModerationPenaltyType moderationPenaltyType; + if (moderationPenaltyTypeStr == + ModerationPenalty.moderationPenaltyTypeSuspension) { + moderationPenaltyType = ModerationPenaltyType.suspension; + } else { + // Don't throw as we might introduce new moderation penalties on the API which might not be yet in code + print('Unsupported moderation penalty type'); + } + + return moderationPenaltyType; + } + + static String convertModerationPenaltyTypeToHumanReadableString( + ModerationPenaltyType type, + {bool capitalize}) { + String result; + switch (type) { + case ModerationPenaltyType.suspension: + result = 'Account suspension'; + break; + default: + result = 'unknown'; + } + return capitalize ? toCapital(result) : result; + } +} + +enum ModerationPenaltyType { suspension } diff --git a/lib/models/moderation/moderation_penalty_list.dart b/lib/models/moderation/moderation_penalty_list.dart new file mode 100644 index 000000000..7dae428a0 --- /dev/null +++ b/lib/models/moderation/moderation_penalty_list.dart @@ -0,0 +1,20 @@ +import 'package:Openbook/models/moderation/moderation_penalty.dart'; + +class ModerationPenaltiesList { + final List moderationPenalties; + + ModerationPenaltiesList({ + this.moderationPenalties, + }); + + factory ModerationPenaltiesList.fromJson(List parsedJson) { + List moderationPenalties = parsedJson + .map((moderationPenaltyJson) => + ModerationPenalty.fromJson(moderationPenaltyJson)) + .toList(); + + return new ModerationPenaltiesList( + moderationPenalties: moderationPenalties, + ); + } +} diff --git a/lib/models/moderation/moderation_report.dart b/lib/models/moderation/moderation_report.dart new file mode 100644 index 000000000..01d754e21 --- /dev/null +++ b/lib/models/moderation/moderation_report.dart @@ -0,0 +1,40 @@ +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/moderation/moderation_category.dart'; +import 'package:Openbook/models/user.dart'; + +class ModerationReport { + final ModerationCategory category; + final String description; + final User reporter; + final DateTime created; + + ModerationReport( + {this.description, this.reporter, this.category, this.created}); + + factory ModerationReport.fromJson(Map parsedJson) { + return ModerationReport( + description: parsedJson['description'], + reporter: parseReporter( + parsedJson['reporter'], + ), + category: parseCategory( + parsedJson['category'], + ), + created: parseCreated(parsedJson['created'])); + } + + static User parseReporter(Map rawActor) { + if (rawActor == null) return null; + return User.fromJson(rawActor); + } + + static ModerationCategory parseCategory(Map rawModerationCategory) { + if (rawModerationCategory == null) return null; + return ModerationCategory.fromJson(rawModerationCategory); + } + + static DateTime parseCreated(String created) { + if (created == null) return null; + return DateTime.parse(created).toLocal(); + } +} diff --git a/lib/models/moderation/moderation_report_list.dart b/lib/models/moderation/moderation_report_list.dart new file mode 100644 index 000000000..f1ef97f19 --- /dev/null +++ b/lib/models/moderation/moderation_report_list.dart @@ -0,0 +1,20 @@ +import 'package:Openbook/models/moderation/moderation_report.dart'; + +class ModerationReportsList { + final List moderationReports; + + ModerationReportsList({ + this.moderationReports, + }); + + factory ModerationReportsList.fromJson(List parsedJson) { + List moderationReports = parsedJson + .map((moderationReportJson) => + ModerationReport.fromJson(moderationReportJson)) + .toList(); + + return new ModerationReportsList( + moderationReports: moderationReports, + ); + } +} diff --git a/lib/models/notifications/notification.dart b/lib/models/notifications/notification.dart index 6e6a10184..dcfa4f6a7 100644 --- a/lib/models/notifications/notification.dart +++ b/lib/models/notifications/notification.dart @@ -3,6 +3,7 @@ import 'package:Openbook/models/notifications/connection_confirmed_notification. import 'package:Openbook/models/notifications/connection_request_notification.dart'; import 'package:Openbook/models/notifications/follow_notification.dart'; import 'package:Openbook/models/notifications/post_comment_notification.dart'; +import 'package:Openbook/models/notifications/post_comment_reply_notification.dart'; import 'package:Openbook/models/notifications/post_reaction_notification.dart'; import 'package:Openbook/models/updatable_model.dart'; import 'package:Openbook/models/user.dart'; @@ -30,6 +31,7 @@ class OBNotification extends UpdatableModel { static final factory = NotificationFactory(); static final postReaction = 'PR'; static final postComment = 'PC'; + static final postCommentReply = 'PCR'; static final connectionRequest = 'CR'; static final connectionConfirmed = 'CC'; static final follow = 'F'; @@ -105,6 +107,8 @@ class NotificationFactory extends UpdatableModelFactory { notificationType = NotificationType.postReaction; } else if (notificationTypeStr == OBNotification.postComment) { notificationType = NotificationType.postComment; + } else if (notificationTypeStr == OBNotification.postCommentReply) { + notificationType = NotificationType.postCommentReply; } else if (notificationTypeStr == OBNotification.connectionRequest) { notificationType = NotificationType.connectionRequest; } else if (notificationTypeStr == OBNotification.connectionConfirmed) { @@ -141,6 +145,9 @@ class NotificationFactory extends UpdatableModelFactory { case NotificationType.postComment: contentObject = PostCommentNotification.fromJson(contentObjectData); break; + case NotificationType.postCommentReply: + contentObject = PostCommentReplyNotification.fromJson(contentObjectData); + break; case NotificationType.postReaction: contentObject = PostReactionNotification.fromJson(contentObjectData); break; @@ -160,6 +167,7 @@ class NotificationFactory extends UpdatableModelFactory { enum NotificationType { postReaction, postComment, + postCommentReply, connectionRequest, connectionConfirmed, follow, diff --git a/lib/models/notifications/post_comment_reply_notification.dart b/lib/models/notifications/post_comment_reply_notification.dart new file mode 100644 index 000000000..7c9cc61fc --- /dev/null +++ b/lib/models/notifications/post_comment_reply_notification.dart @@ -0,0 +1,25 @@ +import 'package:Openbook/models/post_comment.dart'; + +class PostCommentReplyNotification { + final int id; + final PostComment postComment; + final PostComment parentComment; + + PostCommentReplyNotification({this.id, this.postComment, this.parentComment}); + + factory PostCommentReplyNotification.fromJson(Map json) { + return PostCommentReplyNotification( + id: json['id'], + postComment: _parsePostComment(json['post_comment']), + parentComment: _parsePostComment(json['parent_comment']) + ); + } + + static PostComment _parsePostComment(Map postCommentData) { + return PostComment.fromJSON(postCommentData); + } + + int getPostCreatorId() { + return postComment.getPostCreatorId(); + } +} diff --git a/lib/models/post.dart b/lib/models/post.dart index 1a02dbd2a..27f91adc8 100644 --- a/lib/models/post.dart +++ b/lib/models/post.dart @@ -38,6 +38,7 @@ class Post extends UpdatableModel { bool isEncircled; bool isEdited; bool isClosed; + bool isReported; static final factory = PostFactory(); @@ -70,6 +71,7 @@ class Post extends UpdatableModel { this.isMuted, this.isEncircled, this.isClosed, + this.isReported, this.isEdited}) : super(); @@ -102,6 +104,8 @@ class Post extends UpdatableModel { if (json.containsKey('is_closed')) isClosed = json['is_closed']; + if (json.containsKey('is_reported')) isReported = json['is_reported']; + if (json.containsKey('image')) image = factory.parseImage(json['image']); if (json.containsKey('video')) video = factory.parseVideo(json['video']); @@ -277,6 +281,11 @@ class Post extends UpdatableModel { this.notifyUpdate(); } + void setIsReported(isReported) { + this.isReported = isReported; + notifyUpdate(); + } + void _setReactionsEmojiCounts(PostReactionsEmojiCountList emojiCounts) { reactionsEmojiCounts = emojiCounts; } @@ -299,6 +308,7 @@ class PostFactory extends UpdatableModelFactory { reactionsCount: json['reactions_count'], commentsCount: json['comments_count'], isMuted: json['is_muted'], + isReported: json['is_reported'], areCommentsEnabled: json['comments_enabled'], publicReactions: json['public_reactions'], creator: parseCreator(json['creator']), diff --git a/lib/models/post_comment.dart b/lib/models/post_comment.dart index 76d751efd..a475cfc83 100644 --- a/lib/models/post_comment.dart +++ b/lib/models/post_comment.dart @@ -1,4 +1,5 @@ import 'package:Openbook/models/post.dart'; +import 'package:Openbook/models/post_comment_list.dart'; import 'package:Openbook/models/user.dart'; import 'package:dcache/dcache.dart'; import 'package:timeago/timeago.dart' as timeago; @@ -7,11 +8,15 @@ import 'package:Openbook/models/updatable_model.dart'; class PostComment extends UpdatableModel { final int id; int creatorId; + int repliesCount; DateTime created; String text; User commenter; + PostComment parentComment; + PostCommentList replies; Post post; bool isEdited; + bool isReported; static convertPostCommentSortTypeToString(PostCommentsSortType type) { String result; @@ -45,14 +50,19 @@ class PostComment extends UpdatableModel { factory.clearCache(); } - PostComment( - {this.id, - this.created, - this.text, - this.creatorId, - this.commenter, - this.post, - this.isEdited}); + PostComment({ + this.id, + this.created, + this.text, + this.creatorId, + this.commenter, + this.post, + this.isEdited, + this.isReported, + this.parentComment, + this.replies, + this.repliesCount, + }); static final factory = PostCommentFactory(); @@ -70,10 +80,18 @@ class PostComment extends UpdatableModel { creatorId = json['creator_id']; } + if (json.containsKey('replies_count')) { + repliesCount = json['replies_count']; + } + if (json.containsKey('is_edited')) { isEdited = json['is_edited']; } + if (json.containsKey('is_reported')) { + isReported = json['is_reported']; + } + if (json.containsKey('text')) { text = json['text']; } @@ -85,6 +103,14 @@ class PostComment extends UpdatableModel { if (json.containsKey('created')) { created = factory.parseCreated(json['created']); } + + if (json.containsKey('parent_comment')) { + parentComment = factory.parseParentComment(json['parent_comment']); + } + + if (json.containsKey('replies')) { + replies = factory.parseCommentReplies(json['replies']); + } } String getRelativeCreated() { @@ -103,6 +129,15 @@ class PostComment extends UpdatableModel { return this.commenter.getProfileAvatar(); } + bool hasReplies() { + return repliesCount != null && repliesCount > 0 && replies != null; + } + + List getPostCommentReplies() { + if (replies == null) return []; + return replies.comments; + } + int getCommenterId() { return this.commenter.id; } @@ -110,6 +145,11 @@ class PostComment extends UpdatableModel { int getPostCreatorId() { return post.getCreatorId(); } + + void setIsReported(isReported) { + this.isReported = isReported; + notifyUpdate(); + } } class PostCommentFactory extends UpdatableModelFactory { @@ -125,7 +165,11 @@ class PostCommentFactory extends UpdatableModelFactory { created: parseCreated(json['created']), commenter: parseUser(json['commenter']), post: parsePost(json['post']), + repliesCount: json['replies_count'], + replies: parseCommentReplies(json['replies']), + parentComment: parseParentComment(json['parent_comment']), isEdited: json['is_edited'], + isReported: json['is_reported'], text: json['text']); } @@ -143,6 +187,17 @@ class PostCommentFactory extends UpdatableModelFactory { if (userData == null) return null; return User.fromJson(userData); } + + PostComment parseParentComment(Map commentData) { + if (commentData == null) return null; + return PostComment.fromJSON(commentData); + } + + PostCommentList parseCommentReplies(List repliesData) { + if (repliesData == null) return null; + return PostCommentList.fromJson(repliesData); + } + } enum PostCommentsSortType { asc, dec } diff --git a/lib/models/user.dart b/lib/models/user.dart index 5fefffed1..732b0a94d 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -27,10 +27,14 @@ class User extends UpdatableModel { int unreadNotificationsCount; int postsCount; int inviteCount; + int pendingCommunitiesModeratedObjectsCount; + int activeModerationPenaltiesCount; bool areGuidelinesAccepted; bool isFollowing; bool isConnected; + bool isReported; bool isBlocked; + bool isGlobalModerator; bool isFullyConnected; bool isPendingConnectionConfirmation; bool isMemberOfCommunities; @@ -85,13 +89,17 @@ class User extends UpdatableModel { this.inviteCount, this.isFollowing, this.isBlocked, + this.isGlobalModerator, this.isConnected, + this.isReported, this.isFullyConnected, this.isMemberOfCommunities, this.connectedCircles, this.followLists, this.communitiesMemberships, this.communitiesInvites, + this.activeModerationPenaltiesCount, + this.pendingCommunitiesModeratedObjectsCount, this.areGuidelinesAccepted}); void updateFromJson(Map json) { @@ -117,6 +125,12 @@ class User extends UpdatableModel { } if (json.containsKey('followers_count')) followersCount = json['followers_count']; + if (json.containsKey('pending_communities_moderated_objects_count')) + pendingCommunitiesModeratedObjectsCount = + json['pending_communities_moderated_objects_count']; + if (json.containsKey('active_moderation_penalties_count')) + activeModerationPenaltiesCount = + json['active_moderation_penalties_count']; if (json.containsKey('following_count')) followingCount = json['following_count']; if (json.containsKey('unread_notifications_count')) @@ -125,7 +139,10 @@ class User extends UpdatableModel { if (json.containsKey('invite_count')) inviteCount = json['invite_count']; if (json.containsKey('is_following')) isFollowing = json['is_following']; if (json.containsKey('is_connected')) isConnected = json['is_connected']; + if (json.containsKey('is_global_moderator')) + isGlobalModerator = json['is_global_moderator']; if (json.containsKey('is_blocked')) isBlocked = json['is_blocked']; + if (json.containsKey('is_reported')) isReported = json['is_reported']; if (json.containsKey('connections_circle_id')) connectionsCircleId = json['connections_circle_id']; if (json.containsKey('is_fully_connected')) @@ -296,6 +313,21 @@ class User extends UpdatableModel { } } + bool hasPendingCommunitiesModeratedObjects() { + return pendingCommunitiesModeratedObjectsCount != null && + pendingCommunitiesModeratedObjectsCount > 0; + } + + bool hasActiveModerationPenaltiesCount() { + return activeModerationPenaltiesCount != null && + activeModerationPenaltiesCount > 0; + } + + void setIsReported(isReported) { + this.isReported = isReported; + notifyUpdate(); + } + bool canDisableOrEnableCommentsForPost(Post post) { User loggedInUser = this; bool _canDisableOrEnableComments = false; @@ -318,7 +350,8 @@ class User extends UpdatableModel { if (post.hasCommunity()) { Community postCommunity = post.community; - if (postCommunity.isAdministrator(loggedInUser) || postCommunity.isModerator(loggedInUser)) { + if (postCommunity.isAdministrator(loggedInUser) || + postCommunity.isModerator(loggedInUser)) { _canCloseOrOpenPost = true; } } @@ -329,7 +362,8 @@ class User extends UpdatableModel { User loggedInUser = this; bool _canCloseOrOpenPost = false; - if (community.isAdministrator(loggedInUser) || community.isModerator(loggedInUser)) { + if (community.isAdministrator(loggedInUser) || + community.isModerator(loggedInUser)) { _canCloseOrOpenPost = true; } @@ -340,7 +374,8 @@ class User extends UpdatableModel { User loggedInUser = this; bool _canBanOrUnban = false; - if (community.isAdministrator(loggedInUser) || community.isModerator(loggedInUser)) { + if (community.isAdministrator(loggedInUser) || + community.isModerator(loggedInUser)) { _canBanOrUnban = true; } @@ -392,26 +427,32 @@ class User extends UpdatableModel { return _canComment; } - bool canDeletePost(Post post) { + bool isStaffForCommunity(Community community) { User loggedInUser = this; - bool loggedInUserIsPostCreator = loggedInUser.id == post.getCreatorId(); bool loggedInUserIsCommunityAdministrator = false; bool loggedInUserIsCommunityModerator = false; - bool _canDelete = false; - if (post.hasCommunity()) { - Community postCommunity = post.community; + loggedInUserIsCommunityAdministrator = + community.isAdministrator(loggedInUser); + + loggedInUserIsCommunityModerator = community.isModerator(loggedInUser); - loggedInUserIsCommunityAdministrator = - postCommunity.isAdministrator(loggedInUser); + return loggedInUserIsCommunityModerator || + loggedInUserIsCommunityAdministrator; + } - loggedInUserIsCommunityModerator = - postCommunity.isModerator(loggedInUser); + bool canDeletePost(Post post) { + User loggedInUser = this; + bool loggedInUserIsPostCreator = loggedInUser.id == post.getCreatorId(); + bool _canDelete = false; + bool loggedInUserIsStaffForCommunity = false; + + if (post.hasCommunity()) { + loggedInUserIsStaffForCommunity = + this.isStaffForCommunity(post.community); } - if (loggedInUserIsPostCreator || - loggedInUserIsCommunityAdministrator || - loggedInUserIsCommunityModerator) { + if (loggedInUserIsPostCreator || loggedInUserIsStaffForCommunity) { _canDelete = true; } @@ -421,35 +462,55 @@ class User extends UpdatableModel { bool canEditPost(Post post) { User loggedInUser = this; bool loggedInUserIsPostCreator = loggedInUser.id == post.getCreatorId(); - return loggedInUserIsPostCreator; + + return loggedInUserIsPostCreator && !post.isClosed; } - bool canEditPostComment(PostComment postComment) { + bool canEditPostComment(PostComment postComment, Post post) { User loggedInUser = this; User postCommenter = postComment.commenter; + bool loggedInUserIsStaffForCommunity = false; + bool loggedInUserIsCommenter = loggedInUser.id == postCommenter.id; + bool loggedInUserIsCommenterForOpenPost = + loggedInUserIsCommenter && !post.isClosed && post.areCommentsEnabled; + + if (post.hasCommunity()) { + loggedInUserIsStaffForCommunity = isStaffForCommunity(post.community); + } - return loggedInUser.id == postCommenter.id; + return loggedInUserIsCommenterForOpenPost || + (loggedInUserIsStaffForCommunity && loggedInUserIsCommenter); } - bool canDeletePostComment(Post post, PostComment postComment) { + bool canReportPostComment(PostComment postComment) { User loggedInUser = this; User postCommenter = postComment.commenter; - bool loggedInUserIsCommunityAdministrator = false; - bool loggedInUserIsCommunityModerator = false; - if (post.hasCommunity()) { - Community postCommunity = post.community; + return loggedInUser.id != postCommenter.id; + } - loggedInUserIsCommunityAdministrator = - postCommunity.isAdministrator(loggedInUser); + bool canReplyPostComment(PostComment postComment) { + return postComment.parentComment == null; + } - loggedInUserIsCommunityModerator = - postCommunity.isModerator(loggedInUser); + bool canDeletePostComment(Post post, PostComment postComment) { + User loggedInUser = this; + User postCommenter = postComment.commenter; + bool loggedInUserIsPostCreator = loggedInUser.id == post.getCreatorId(); + bool userIsCreatorOfNonCommunityPost = + loggedInUserIsPostCreator && !post.hasCommunity(); + bool loggedInUserIsStaffForCommunity = false; + bool loggedInUserIsCommenterForOpenPost = + (loggedInUser.id == postCommenter.id) && !post.isClosed; + + if (post.hasCommunity()) { + loggedInUserIsStaffForCommunity = + this.isStaffForCommunity(post.community); } - return (loggedInUser.id == postCommenter.id || - loggedInUserIsCommunityModerator || - loggedInUserIsCommunityAdministrator); + return (loggedInUserIsCommenterForOpenPost || + loggedInUserIsStaffForCommunity || + userIsCreatorOfNonCommunityPost); } bool canBlockOrUnblockUser(User user) { @@ -471,12 +532,18 @@ class UserFactory extends UpdatableModelFactory { postsCount: json['posts_count'], inviteCount: json['invite_count'], unreadNotificationsCount: json['unread_notifications_count'], + pendingCommunitiesModeratedObjectsCount: + json['pending_communities_moderated_objects_count'], + activeModerationPenaltiesCount: + json['active_moderation_penalties_count'], email: json['email'], username: json['username'], followingCount: json['following_count'], isFollowing: json['is_following'], isConnected: json['is_connected'], + isGlobalModerator: json['is_global_moderator'], isBlocked: json['is_blocked'], + isReported: json['is_reported'], isFullyConnected: json['is_fully_connected'], isMemberOfCommunities: json['is_member_of_communities'], profile: parseUserProfile(json['profile']), diff --git a/lib/pages/auth/create_account/create_account.dart b/lib/pages/auth/create_account/create_account.dart index 05834d03b..89422430d 100644 --- a/lib/pages/auth/create_account/create_account.dart +++ b/lib/pages/auth/create_account/create_account.dart @@ -53,7 +53,8 @@ class OBAuthCreateAccountPageState extends State { _buildLinkForm(), const SizedBox( height: 20.0 - ) + ), + _buildRequestInvite(context: context) ], ))), ), @@ -195,4 +196,25 @@ class OBAuthCreateAccountPageState extends State { ]), ); } + + Widget _buildRequestInvite({@required BuildContext context}) { + String requestInviteText = _localizationService.trans('AUTH.CREATE_ACC.REQUEST_INVITE'); + + return OBSecondaryButton( + isFullWidth: true, + isLarge: true, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + requestInviteText, + style: TextStyle(fontSize: 18.0, color: Colors.white), + ) + ], + ), + onPressed: () { + Navigator.pushNamed(context, '/waitlist/subscribe_email_step'); + }, + ); + } } diff --git a/lib/pages/home/bottom_sheets/community_actions.dart b/lib/pages/home/bottom_sheets/community_actions.dart index e9128b43c..91c2184dd 100644 --- a/lib/pages/home/bottom_sheets/community_actions.dart +++ b/lib/pages/home/bottom_sheets/community_actions.dart @@ -8,6 +8,7 @@ import 'package:Openbook/widgets/icon.dart'; import 'package:Openbook/widgets/theming/primary_color_container.dart'; import 'package:Openbook/widgets/theming/text.dart'; import 'package:Openbook/widgets/tiles/actions/favorite_community_tile.dart'; +import 'package:Openbook/widgets/tiles/actions/report_community_tile.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -65,12 +66,11 @@ class OBCommunityActionsBottomSheetState } if (!isCommunityAdministrator && !isCommunityModerator) { - communityActions.add(ListTile( - leading: const OBIcon(OBIcons.reportCommunity), - title: const OBText( - 'Report community', - ), - onTap: _onWantsToReportCommunity, + communityActions.add(OBReportCommunityTile( + community: community, + onWantsToReportCommunity: () { + Navigator.of(context).pop(); + }, )); } diff --git a/lib/pages/home/bottom_sheets/photo_picker.dart b/lib/pages/home/bottom_sheets/photo_picker.dart deleted file mode 100644 index 85fc24ca5..000000000 --- a/lib/pages/home/bottom_sheets/photo_picker.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'dart:io'; - -import 'package:Openbook/provider.dart'; -import 'package:Openbook/services/image_picker.dart'; -import 'package:Openbook/services/toast.dart'; -import 'package:Openbook/widgets/icon.dart'; -import 'package:Openbook/widgets/theming/primary_color_container.dart'; -import 'package:Openbook/widgets/theming/text.dart'; -import 'package:flutter/material.dart'; - -class OBPhotoPickerBottomSheet extends StatelessWidget { - final OBImageType imageType; - - const OBPhotoPickerBottomSheet({Key key, this.imageType = OBImageType.post}) - : super(key: key); - - @override - Widget build(BuildContext context) { - ImagePickerService imagePickerService = - OpenbookProvider.of(context).imagePickerService; - ToastService toastService = OpenbookProvider.of(context).toastService; - - List photoPickerActions = [ - ListTile( - leading: const OBIcon(OBIcons.gallery), - title: const OBText( - 'From gallery', - ), - onTap: () async { - try { - File image = await imagePickerService.pickImage( - imageType: imageType, source: ImageSource.gallery); - Navigator.pop(context, image); - } on ImageTooLargeException catch (e) { - int limit = e.getLimitInMB(); - toastService.error(message: 'Image too large (limit: $limit MB)', context: context); - } - }, - ), - ListTile( - leading: const OBIcon(OBIcons.camera), - title: const OBText( - 'From camera', - ), - onTap: () async { - try { - File image = await imagePickerService.pickImage( - imageType: imageType, source: ImageSource.camera); - Navigator.pop(context, image); - } on ImageTooLargeException catch (e) { - int limit = e.getLimitInMB(); - toastService.error(message: 'Image too large (limit: $limit MB)', context: context); - } - }, - ) - ]; - - return OBPrimaryColorContainer( - mainAxisSize: MainAxisSize.min, - child: Padding( - padding: EdgeInsets.only(bottom: 16), - child: Column( - children: photoPickerActions, - mainAxisSize: MainAxisSize.min, - ), - ) - ); - } -} diff --git a/lib/pages/home/bottom_sheets/post_actions.dart b/lib/pages/home/bottom_sheets/post_actions.dart index a1eafc55a..884399acf 100644 --- a/lib/pages/home/bottom_sheets/post_actions.dart +++ b/lib/pages/home/bottom_sheets/post_actions.dart @@ -1,5 +1,3 @@ -import 'dart:io'; -import 'package:Openbook/models/community.dart'; import 'package:Openbook/models/post.dart'; import 'package:Openbook/models/user.dart'; import 'package:Openbook/provider.dart'; @@ -13,12 +11,13 @@ import 'package:Openbook/widgets/theming/text.dart'; import 'package:Openbook/widgets/tiles/actions/close_post_tile.dart'; import 'package:Openbook/widgets/tiles/actions/disable_comments_post_tile.dart'; import 'package:Openbook/widgets/tiles/actions/mute_post_tile.dart'; +import 'package:Openbook/widgets/tiles/actions/report_post_tile.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class OBPostActionsBottomSheet extends StatefulWidget { final Post post; - final OnPostReported onPostReported; + final ValueChanged onPostReported; final OnPostDeleted onPostDeleted; const OBPostActionsBottomSheet( @@ -69,42 +68,39 @@ class OBPostActionsBottomSheetState extends State { )); } + if (loggedInUser.canCloseOrOpenPost(post)) { + postActions.add(OBClosePostTile( + post: post, + onClosePost: _dismiss, + onOpenPost: _dismiss, + )); + } - if (loggedInUser.canCloseOrOpenPost(post)) { - postActions.add(OBClosePostTile( - post: post, - onClosePost: _dismiss, - onOpenPost: _dismiss, - )); - } - - if (loggedInUser.canEditPost(post)) { - postActions.add(ListTile( - leading: const OBIcon(OBIcons.editPost), - title: const OBText( - 'Edit post', - ), - onTap: _onWantsToEditPost, - )); - } + if (loggedInUser.canEditPost(post)) { + postActions.add(ListTile( + leading: const OBIcon(OBIcons.editPost), + title: const OBText( + 'Edit post', + ), + onTap: _onWantsToEditPost, + )); + } - if (loggedInUser.canDeletePost(post)) { - postActions.add(ListTile( - leading: const OBIcon(OBIcons.deletePost), - title: const OBText( - 'Delete post', - ), - onTap: _onWantsToDeletePost, - )); - } else { - postActions.add(ListTile( - leading: const OBIcon(OBIcons.reportPost), - title: const OBText( - 'Report post', - ), - onTap: _onWantsToReportPost, - )); - } + if (loggedInUser.canDeletePost(post)) { + postActions.add(ListTile( + leading: const OBIcon(OBIcons.deletePost), + title: const OBText( + 'Delete post', + ), + onTap: _onWantsToDeletePost, + )); + } else { + postActions.add(OBReportPostTile( + post: widget.post, + onWantsToReportPost: _dismiss, + onPostReported: widget.onPostReported, + )); + } return OBPrimaryColorContainer( mainAxisSize: MainAxisSize.min, @@ -149,15 +145,9 @@ class OBPostActionsBottomSheetState extends State { } } - void _onWantsToReportPost() async { - _toastService.error(message: 'Not implemented yet', context: context); - _dismiss(); - } - void _dismiss() { Navigator.pop(context); } } -typedef OnPostReported(Post post); typedef OnPostDeleted(Post post); diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index 991201141..5ccce390e 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -422,7 +422,8 @@ class OBHomePageState extends ReceiveShareState _pushNotificationSubscription = _pushNotificationsService.pushNotification .listen(_onPushNotification); - if (!newUser.areGuidelinesAccepted) { + if (!newUser.areGuidelinesAccepted != null && + !newUser.areGuidelinesAccepted) { _modalService.openAcceptGuidelines(context: context); } } diff --git a/lib/pages/home/modals/create_post/create_post.dart b/lib/pages/home/modals/create_post/create_post.dart index da21b8548..11e8d7796 100644 --- a/lib/pages/home/modals/create_post/create_post.dart +++ b/lib/pages/home/modals/create_post/create_post.dart @@ -10,6 +10,7 @@ import 'package:Openbook/pages/home/modals/create_post/widgets/remaining_post_ch import 'package:Openbook/provider.dart'; import 'package:Openbook/services/bottom_sheet.dart'; import 'package:Openbook/services/httpie.dart'; +import 'package:Openbook/services/image_picker.dart'; import 'package:Openbook/services/navigation_service.dart'; import 'package:Openbook/services/toast.dart'; import 'package:Openbook/services/user.dart'; @@ -43,7 +44,7 @@ class CreatePostModal extends StatefulWidget { class CreatePostModalState extends State { ValidationService _validationService; NavigationService _navigationService; - BottomSheetService _bottomSheetService; + ImagePickerService _imagePickerService; ToastService _toastService; UserService _userService; @@ -80,7 +81,9 @@ class CreatePostModalState extends State { _isPostTextAllowedLength = false; _hasImage = false; _hasVideo = false; - _postItemsWidgets = [OBCreatePostText(controller: _textController, focusNode: _focusNode)]; + _postItemsWidgets = [ + OBCreatePostText(controller: _textController, focusNode: _focusNode) + ]; if (widget.community != null) _postItemsWidgets.add(OBPostCommunityPreviewer( @@ -104,7 +107,7 @@ class CreatePostModalState extends State { var openbookProvider = OpenbookProvider.of(context); _validationService = openbookProvider.validationService; _navigationService = openbookProvider.navigationService; - _bottomSheetService = openbookProvider.bottomSheetService; + _imagePickerService = openbookProvider.imagePickerService; _userService = openbookProvider.userService; _toastService = openbookProvider.toastService; @@ -262,7 +265,7 @@ class CreatePostModalState extends State { onPressed: () async { _unfocusTextField(); File pickedPhoto = - await _bottomSheetService.showPhotoPicker(context: context); + await _imagePickerService.pickImage(imageType: OBImageType.post); if (pickedPhoto != null) _setPostImage(pickedPhoto); }, ), diff --git a/lib/pages/home/modals/edit_user_profile/edit_user_profile.dart b/lib/pages/home/modals/edit_user_profile/edit_user_profile.dart index 2dda697d7..606f7c6df 100644 --- a/lib/pages/home/modals/edit_user_profile/edit_user_profile.dart +++ b/lib/pages/home/modals/edit_user_profile/edit_user_profile.dart @@ -314,33 +314,19 @@ class OBEditUserProfileModalState extends State { context: context, builder: (BuildContext context) { List listTiles = [ - new ListTile( - leading: new Icon(Icons.camera_alt), - title: new Text('Camera'), - onTap: () async { - try { - var image = await _imagePickerService.pickImage( - source: ImageSource.camera, imageType: imageType); - _onUserImageSelected(image: image, imageType: imageType); - //if (image != null) createAccountBloc.avatar.add(image); - } on ImageTooLargeException catch(e) { - int limit = e.getLimitInMB(); - toastService.error(message: 'Image too large (limit: $limit MB)', context: context); - } - Navigator.pop(context); - }, - ), new ListTile( leading: new Icon(Icons.photo_library), - title: new Text('Gallery'), + title: new Text('Pick image'), onTap: () async { try { - var image = await _imagePickerService.pickImage( - source: ImageSource.gallery, imageType: imageType); + var image = await _imagePickerService.pickImage(imageType: imageType); + _onUserImageSelected(image: image, imageType: imageType); - } on ImageTooLargeException catch(e) { + } on ImageTooLargeException catch (e) { int limit = e.getLimitInMB(); - toastService.error(message: 'Image too large (limit: $limit MB)', context: context); + toastService.error( + message: 'Image too large (limit: $limit MB)', + context: context); } Navigator.pop(context); }, diff --git a/lib/pages/home/modals/post_comment/post-comment-reply-expanded.dart b/lib/pages/home/modals/post_comment/post-comment-reply-expanded.dart new file mode 100644 index 000000000..1bf8f56e0 --- /dev/null +++ b/lib/pages/home/modals/post_comment/post-comment-reply-expanded.dart @@ -0,0 +1,204 @@ +import 'package:Openbook/models/post.dart'; +import 'package:Openbook/models/post_comment.dart'; +import 'package:Openbook/pages/home/modals/create_post/widgets/create_post_text.dart'; +import 'package:Openbook/pages/home/modals/create_post/widgets/remaining_post_characters.dart'; +import 'package:Openbook/pages/home/pages/post_comments/widgets/post_comment/widgets/post_comment_tile.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/httpie.dart'; +import 'package:Openbook/services/navigation_service.dart'; +import 'package:Openbook/services/toast.dart'; +import 'package:Openbook/services/user.dart'; +import 'package:Openbook/services/validation.dart'; +import 'package:Openbook/widgets/avatars/logged_in_user_avatar.dart'; +import 'package:Openbook/widgets/avatars/avatar.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart'; +import 'package:Openbook/widgets/theming/post_divider.dart'; +import 'package:Openbook/widgets/theming/primary_color_container.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + + +class OBPostCommentReplyExpandedModal extends StatefulWidget { + final Post post; + final PostComment postComment; + final Function(PostComment) onReplyAdded; + final Function(PostComment) onReplyDeleted; + + const OBPostCommentReplyExpandedModal({Key key, this.post, this.postComment, this.onReplyAdded, this.onReplyDeleted}) : super(key: key); + + @override + State createState() { + return OBPostCommentReplyExpandedModalState(); + } +} + +class OBPostCommentReplyExpandedModalState extends State { + ValidationService _validationService; + NavigationService _navigationService; + ToastService _toastService; + UserService _userService; + + TextEditingController _textController; + int _charactersCount; + bool _isPostCommentTextAllowedLength; + List _postCommentItemsWidgets; + + @override + void initState() { + super.initState(); + _textController = TextEditingController(); + _textController.addListener(_onPostCommentTextChanged); + _charactersCount = 0; + _isPostCommentTextAllowedLength = false; + String hintText = 'Your reply...'; + _postCommentItemsWidgets = [OBCreatePostText(controller: _textController, hintText: hintText)]; + + } + + @override + void dispose() { + super.dispose(); + _textController.removeListener(_onPostCommentTextChanged); + } + + @override + Widget build(BuildContext context) { + var openbookProvider = OpenbookProvider.of(context); + _validationService = openbookProvider.validationService; + _navigationService = openbookProvider.navigationService; + _userService = openbookProvider.userService; + _toastService = openbookProvider.toastService; + + return CupertinoPageScaffold( + backgroundColor: Colors.transparent, + navigationBar: _buildNavigationBar(), + child: OBPrimaryColorContainer( + child: Column( + children: [_buildPostCommentContent()], + ))); + } + + Widget _buildNavigationBar() { + bool isPrimaryActionButtonIsEnabled = + (_isPostCommentTextAllowedLength && _charactersCount > 0); + + return OBThemedNavigationBar( + leading: GestureDetector( + child: const OBIcon(OBIcons.close), + onTap: () { + Navigator.pop(context); + }, + ), + title: 'Reply comment', + trailing: + _buildPrimaryActionButton(isEnabled: isPrimaryActionButtonIsEnabled), + ); + } + + Widget _buildPrimaryActionButton({bool isEnabled}) { + Widget primaryButton; + + if (isEnabled) { + primaryButton = GestureDetector( + onTap: _onWantsToReplyComment, + child: const OBText('Post'), + ); + } else { + primaryButton = Opacity( + opacity: 0.5, + child: const OBText('Post'), + ); + } + + return primaryButton; + } + + void _onWantsToReplyComment() async { + PostComment comment; + if (widget.postComment != null) { + comment = await _userService.replyPostComment( + post: widget.post, + postComment: widget.postComment, + text: _textController.text); + } + if (comment != null) { + // Remove modal + if (widget.onReplyAdded != null) widget.onReplyAdded(comment); + Navigator.pop(context, comment); + } + } + + Widget _buildPostCommentContent() { + return Expanded( + child: Padding( + padding: EdgeInsets.only(left: 0.0, top: 20.0), + child: Column( + children: [ + OBPostCommentTile(post:widget.post, postComment: widget.postComment), + OBPostDivider(), + Padding( + padding: EdgeInsets.only(left: 20.0, top: 10.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + OBLoggedInUserAvatar( + size: OBAvatarSize.medium, + ), + const SizedBox( + height: 12.0, + ), + OBRemainingPostCharacters( + maxCharacters: ValidationService.POST_COMMENT_MAX_LENGTH, + currentCharacters: _charactersCount, + ), + ], + ), + Expanded( + child: SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: Padding( + padding: + EdgeInsets.only(left: 20.0, right: 20.0, bottom: 30.0, top: 0.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _postCommentItemsWidgets)), + ), + ) + ], + ), + ) + ], + ) + )); + } + + void _onPostCommentTextChanged() { + String text = _textController.text; + setState(() { + _charactersCount = text.length; + _isPostCommentTextAllowedLength = + _validationService.isPostCommentAllowedLength(text); + }); + } + + void _onError(error) async { + if (error is HttpieConnectionRefusedError) { + _toastService.error( + message: error.toHumanReadableMessage(), context: context); + } else if (error is HttpieRequestError) { + String errorMessage = await error.toHumanReadableMessage(); + _toastService.error(message: errorMessage, context: context); + } else { + _toastService.error(message: 'Unknown error', context: context); + throw error; + } + } + + void _unfocusTextField() { + FocusScope.of(context).requestFocus(new FocusNode()); + } +} diff --git a/lib/pages/home/modals/post_comment/post-commenter-expanded.dart b/lib/pages/home/modals/post_comment/post-commenter-expanded.dart index 95dceb9a2..8cfef1e1d 100644 --- a/lib/pages/home/modals/post_comment/post-commenter-expanded.dart +++ b/lib/pages/home/modals/post_comment/post-commenter-expanded.dart @@ -117,20 +117,24 @@ class OBPostCommenterExpandedModalState extends State { UserService _userService; ToastService _toastService; ValidationService _validationService; - BottomSheetService _bottomSheetService; + ImagePickerService _imagePickerService; ThemeValueParserService _themeValueParserService; bool _requestInProgress; @@ -121,7 +121,7 @@ class OBSaveCommunityModalState extends State { _userService = openbookProvider.userService; _toastService = openbookProvider.toastService; _validationService = openbookProvider.validationService; - _bottomSheetService = openbookProvider.bottomSheetService; + _imagePickerService = openbookProvider.imagePickerService; _themeValueParserService = openbookProvider.themeValueParserService; var themeService = openbookProvider.themeService; @@ -399,8 +399,8 @@ class OBSaveCommunityModalState extends State { } void _pickNewAvatar() async { - File newAvatar = await _bottomSheetService.showPhotoPicker( - context: context, imageType: OBImageType.avatar); + File newAvatar = + await _imagePickerService.pickImage(imageType: OBImageType.avatar); if (newAvatar != null) _setAvatarFile(newAvatar); } @@ -443,8 +443,8 @@ class OBSaveCommunityModalState extends State { } void _pickNewCover() async { - File newCover = await _bottomSheetService.showPhotoPicker( - context: context, imageType: OBImageType.cover); + File newCover = + await _imagePickerService.pickImage(imageType: OBImageType.cover); if (newCover != null) _setCoverFile(newCover); } diff --git a/lib/pages/home/pages/communities/widgets/my_communities/widgets/my_communities_group.dart b/lib/pages/home/pages/communities/widgets/my_communities/widgets/my_communities_group.dart index fa28969cd..c5712f4eb 100644 --- a/lib/pages/home/pages/communities/widgets/my_communities/widgets/my_communities_group.dart +++ b/lib/pages/home/pages/communities/widgets/my_communities/widgets/my_communities_group.dart @@ -190,7 +190,7 @@ class OBMyCommunitiesGroupState extends State { _navigationService.navigateToBlankPageWithWidget( context: context, key: Key('obMyCommunitiesGroup' + widget.groupItemName), - navBarTitle: capitalize(widget.groupName), + navBarTitle: toCapital(widget.groupName), widget: _buildSeeAllGroupItemsPage()); } diff --git a/lib/pages/home/pages/community/pages/manage_community/manage_community.dart b/lib/pages/home/pages/community/pages/manage_community/manage_community.dart index 3dee52d50..fc3a4782d 100644 --- a/lib/pages/home/pages/community/pages/manage_community/manage_community.dart +++ b/lib/pages/home/pages/community/pages/manage_community/manage_community.dart @@ -89,6 +89,21 @@ class OBManageCommunityPage extends StatelessWidget { )); } + if (loggedInUser.canBanOrUnbanUsersInCommunity(community)) { + menuListTiles.add(ListTile( + leading: const OBIcon(OBIcons.communityModerators), + title: const OBText('Moderation reports'), + subtitle: const OBText( + 'Review the community moderation reports.', + style: listItemSubtitleStyle, + ), + onTap: () { + navigationService.navigateToCommunityModeratedObjects( + context: context, community: community); + }, + )); + } + if (loggedInUser.canCloseOrOpenPostsInCommunity(community)) { menuListTiles.add(ListTile( leading: const OBIcon(OBIcons.closePost), diff --git a/lib/pages/home/pages/menu/menu.dart b/lib/pages/home/pages/menu/menu.dart index 919253a0c..daddc1da0 100644 --- a/lib/pages/home/pages/menu/menu.dart +++ b/lib/pages/home/pages/menu/menu.dart @@ -1,10 +1,12 @@ import 'package:Openbook/models/user.dart'; import 'package:Openbook/pages/home/lib/poppable_page_controller.dart'; +import 'package:Openbook/widgets/badges/badge.dart'; import 'package:Openbook/widgets/icon.dart'; import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart'; import 'package:Openbook/provider.dart'; import 'package:Openbook/widgets/theming/primary_color_container.dart'; import 'package:Openbook/widgets/theming/text.dart'; +import 'package:Openbook/widgets/tile_group_title.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -29,97 +31,181 @@ class OBMainMenuPage extends StatelessWidget { child: OBPrimaryColorContainer( child: Column( children: [ - Expanded( - child: ListView( - physics: const ClampingScrollPhysics(), - // Important: Remove any padding from the ListView. - padding: EdgeInsets.zero, - children: [ - ListTile( - leading: const OBIcon(OBIcons.circles), - title: const OBText('My circles'), - onTap: () { - navigationService.navigateToConnectionsCircles( - context: context); - }, - ), - ListTile( - leading: const OBIcon(OBIcons.lists), - title: const OBText('My lists'), - onTap: () { - navigationService.navigateToFollowsLists(context: context); - }, - ), - ListTile( - leading: const OBIcon(OBIcons.followers), - title: const OBText('My followers'), - onTap: () { - navigationService.navigateToFollowersPage(context: context); - }, - ), - ListTile( - leading: const OBIcon(OBIcons.following), - title: const OBText('My following'), - onTap: () { - navigationService.navigateToFollowingPage(context: context); - }, - ), - ListTile( - leading: const OBIcon(OBIcons.invite), - title: const OBText('My invites'), - onTap: () { - navigationService.navigateToUserInvites(context: context); - }, - ), - ListTile( - leading: const OBIcon(OBIcons.settings), - title: OBText('Settings'), - onTap: () { - navigationService.navigateToSettingsPage(context: context); - }, - ), - ListTile( - leading: const OBIcon(OBIcons.themes), - title: OBText('Themes'), - onTap: () { - navigationService.navigateToThemesPage(context: context); - }, - ), - StreamBuilder( - stream: userService.loggedInUserChange, - initialData: userService.getLoggedInUser(), + StreamBuilder( + stream: userService.loggedInUserChange, + initialData: userService.getLoggedInUser(), + builder: (BuildContext context, + AsyncSnapshot loggedInUserSnapshot) { + User loggedInUser = loggedInUserSnapshot.data; + + if (loggedInUser == null) return const SizedBox(); + + return StreamBuilder( + stream: loggedInUserSnapshot.data.updateSubject, + initialData: loggedInUserSnapshot.data, builder: - (BuildContext context, AsyncSnapshot snapshot) { - User loggedInUser = snapshot.data; + (BuildContext context, AsyncSnapshot userSnapshot) { + User user = userSnapshot.data; - if (loggedInUser == null) return const SizedBox(); + return Expanded( + child: ListView( + physics: const ClampingScrollPhysics(), + // Important: Remove any padding from the ListView. + padding: EdgeInsets.zero, + children: [ + OBTileGroupTitle( + title: 'My Openbook', + ), + ListTile( + leading: const OBIcon(OBIcons.circles), + title: const OBText('My circles'), + onTap: () { + navigationService.navigateToConnectionsCircles( + context: context); + }, + ), + ListTile( + leading: const OBIcon(OBIcons.lists), + title: const OBText('My lists'), + onTap: () { + navigationService.navigateToFollowsLists( + context: context); + }, + ), + ListTile( + leading: const OBIcon(OBIcons.followers), + title: const OBText('My followers'), + onTap: () { + navigationService.navigateToFollowersPage( + context: context); + }, + ), + ListTile( + leading: const OBIcon(OBIcons.following), + title: const OBText('My following'), + onTap: () { + navigationService.navigateToFollowingPage( + context: context); + }, + ), + ListTile( + leading: const OBIcon(OBIcons.invite), + title: const OBText('My invites'), + onTap: () { + navigationService.navigateToUserInvites( + context: context); + }, + ), + ListTile( + leading: const OBIcon(OBIcons.communityModerators), + title: OBText('My pending moderation tasks'), + onTap: () async { + await navigationService + .navigateToMyModerationTasksPage( + context: context); + userService.refreshUser(); + }, + trailing: OBBadge( + size: 25, + count: user.pendingCommunitiesModeratedObjectsCount, + ), + ), + ListTile( + leading: const OBIcon(OBIcons.moderationPenalties), + title: OBText('My moderation penalties'), + onTap: () async { + await navigationService + .navigateToMyModerationPenaltiesPage( + context: context); + userService.refreshUser(); + }, + trailing: OBBadge( + size: 25, + count: user.activeModerationPenaltiesCount, + ), + ), + OBTileGroupTitle( + title: 'App & Account', + ), + ListTile( + leading: const OBIcon(OBIcons.settings), + title: OBText('Settings'), + onTap: () { + navigationService.navigateToSettingsPage( + context: context); + }, + ), + ListTile( + leading: const OBIcon(OBIcons.themes), + title: OBText('Themes'), + onTap: () { + navigationService.navigateToThemesPage( + context: context); + }, + ), + StreamBuilder( + stream: userService.loggedInUserChange, + initialData: userService.getLoggedInUser(), + builder: (BuildContext context, + AsyncSnapshot snapshot) { + User loggedInUser = snapshot.data; - return ListTile( - leading: const OBIcon(OBIcons.help), - title: OBText(localizationService.trans('DRAWER.HELP')), - onTap: () async { - intercomService.displayMessenger(); - }, - ); - }, - ), - ListTile( - leading: const OBIcon(OBIcons.link), - title: OBText('Useful links'), - onTap: () { - navigationService.navigateToUsefulLinksPage( - context: context); - }, - ), - ListTile( - leading: const OBIcon(OBIcons.logout), - title: OBText(localizationService.trans('DRAWER.LOGOUT')), - onTap: () { - userService.logout(); + if (loggedInUser == null) return const SizedBox(); + + return ListTile( + leading: const OBIcon(OBIcons.help), + title: OBText( + localizationService.trans('DRAWER.HELP')), + onTap: () async { + intercomService.displayMessenger(); + }, + ); + }, + ), + StreamBuilder( + stream: userService.loggedInUserChange, + initialData: userService.getLoggedInUser(), + builder: (BuildContext context, + AsyncSnapshot snapshot) { + User loggedInUser = snapshot.data; + + if (loggedInUser == null || + !(loggedInUser.isGlobalModerator ?? false)) + return const SizedBox(); + + return ListTile( + leading: const OBIcon(OBIcons.globalModerator), + title: OBText('Global moderation'), + onTap: () async { + navigationService + .navigateToGlobalModeratedObjects( + context: context); + }, + ); + }, + ), + ListTile( + leading: const OBIcon(OBIcons.link), + title: OBText('Useful links'), + onTap: () { + navigationService.navigateToUsefulLinksPage( + context: context); + }, + ), + ListTile( + leading: const OBIcon(OBIcons.logout), + title: OBText( + localizationService.trans('DRAWER.LOGOUT')), + onTap: () { + userService.logout(); + }, + ) + ], + )); }, - ) - ], - )) + ); + }, + ), ], ), ), diff --git a/lib/pages/home/pages/menu/pages/my_moderation_penalties/my_moderation_penalties.dart b/lib/pages/home/pages/menu/pages/my_moderation_penalties/my_moderation_penalties.dart new file mode 100644 index 000000000..95368f1a9 --- /dev/null +++ b/lib/pages/home/pages/menu/pages/my_moderation_penalties/my_moderation_penalties.dart @@ -0,0 +1,86 @@ +import 'dart:async'; + +import 'package:Openbook/models/moderation/moderation_penalty_list.dart'; +import 'package:Openbook/models/moderation/moderation_penalty.dart'; +import 'package:Openbook/pages/home/pages/menu/pages/my_moderation_penalties/widgets/moderation_penalty/moderation_penalty.dart'; +import 'package:Openbook/widgets/http_list.dart'; +import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart'; +import 'package:Openbook/widgets/page_scaffold.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/user.dart'; +import 'package:Openbook/widgets/theming/primary_color_container.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class OBMyModerationPenaltiesPage extends StatefulWidget { + @override + State createState() { + return OBMyModerationPenaltiesPageState(); + } +} + +class OBMyModerationPenaltiesPageState + extends State { + UserService _userService; + + OBHttpListController _httpListController; + bool _needsBootstrap; + + @override + void initState() { + super.initState(); + _httpListController = OBHttpListController(); + _needsBootstrap = true; + } + + @override + Widget build(BuildContext context) { + if (_needsBootstrap) { + var provider = OpenbookProvider.of(context); + _userService = provider.userService; + _needsBootstrap = false; + } + + return OBCupertinoPageScaffold( + navigationBar: OBThemedNavigationBar( + title: 'Moderation penalties', + ), + child: OBPrimaryColorContainer( + child: OBHttpList( + padding: EdgeInsets.all(15), + controller: _httpListController, + listItemBuilder: _buildModerationPenaltyListItem, + listRefresher: _refreshModerationPenalties, + listOnScrollLoader: _loadMoreModerationPenalties, + resourceSingularName: 'moderation penalty', + resourcePluralName: 'moderation penalties', + ), + ), + ); + } + + Widget _buildModerationPenaltyListItem( + BuildContext context, ModerationPenalty moderationPenalty) { + return OBModerationPenaltyTile( + moderationPenalty: moderationPenalty, + ); + } + + Future> _refreshModerationPenalties() async { + ModerationPenaltiesList moderationPenalties = + await _userService.getModerationPenalties(); + return moderationPenalties.moderationPenalties; + } + + Future> _loadMoreModerationPenalties( + List moderationPenaltiesList) async { + var lastModerationPenalty = moderationPenaltiesList.last; + var lastModerationPenaltyId = lastModerationPenalty.id; + var moreModerationPenalties = (await _userService.getModerationPenalties( + maxId: lastModerationPenaltyId, + count: 10, + )) + .moderationPenalties; + return moreModerationPenalties; + } +} diff --git a/lib/pages/home/pages/menu/pages/my_moderation_penalties/widgets/moderation_penalty/moderation_penalty.dart b/lib/pages/home/pages/menu/pages/my_moderation_penalties/widgets/moderation_penalty/moderation_penalty.dart new file mode 100644 index 000000000..720ea7c02 --- /dev/null +++ b/lib/pages/home/pages/menu/pages/my_moderation_penalties/widgets/moderation_penalty/moderation_penalty.dart @@ -0,0 +1,80 @@ +import 'package:Openbook/models/moderation/moderation_penalty.dart'; +import 'package:Openbook/pages/home/pages/menu/pages/my_moderation_penalties/widgets/moderation_penalty/widgets/moderation_penalty_actions.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_category/moderated_object_category.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/widgets/moderated_object/widgets/moderated_object_preview.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:Openbook/widgets/tile_group_title.dart'; +import 'package:Openbook/widgets/tiles/moderated_object_status_tile.dart'; +import 'package:flutter/material.dart'; + +class OBModerationPenaltyTile extends StatelessWidget { + final ModerationPenalty moderationPenalty; + + const OBModerationPenaltyTile({Key key, @required this.moderationPenalty}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OBTileGroupTitle( + title: 'Object', + ), + OBModeratedObjectPreview( + moderatedObject: moderationPenalty.moderatedObject, + ), + const SizedBox( + height: 10, + ), + OBModeratedObjectCategory( + moderatedObject: moderationPenalty.moderatedObject, + isEditable: false, + ), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + OBTileGroupTitle( + title: 'Status', + ), + OBModeratedObjectStatusTile( + moderatedObject: moderationPenalty.moderatedObject, + ), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OBTileGroupTitle( + title: 'Type', + ), + ListTile( + title: OBText(ModerationPenalty + .convertModerationPenaltyTypeToHumanReadableString( + moderationPenalty.type, + capitalize: true)), + ) + ], + ), + ) + ], + ), + OBTileGroupTitle( + title: 'Expiration', + ), + ListTile( + title: OBText(moderationPenalty.expiration.toString()), + ), + OBModerationPenaltyActions( + moderationPenalty: moderationPenalty, + ) + ], + ); + } +} diff --git a/lib/pages/home/pages/menu/pages/my_moderation_penalties/widgets/moderation_penalty/widgets/moderation_penalty_actions.dart b/lib/pages/home/pages/menu/pages/my_moderation_penalties/widgets/moderation_penalty/widgets/moderation_penalty_actions.dart new file mode 100644 index 000000000..8e6345f03 --- /dev/null +++ b/lib/pages/home/pages/menu/pages/my_moderation_penalties/widgets/moderation_penalty/widgets/moderation_penalty_actions.dart @@ -0,0 +1,52 @@ +import 'package:Openbook/models/community.dart'; +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/moderation/moderation_penalty.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/widgets/buttons/button.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:flutter/material.dart'; + +class OBModerationPenaltyActions extends StatelessWidget { + final ModerationPenalty moderationPenalty; + + OBModerationPenaltyActions({@required this.moderationPenalty}); + + @override + Widget build(BuildContext context) { + List moderationPenaltyActions = [ + Expanded( + child: OBButton( + type: OBButtonType.highlight, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const OBIcon( + OBIcons.chat, + customSize: 20.0, + ), + const SizedBox( + width: 10.0, + ), + const OBText('Chat with the team'), + ], + ), + onPressed: () { + OpenbookProviderState openbookProvider = + OpenbookProvider.of(context); + openbookProvider.intercomService.displayMessenger(); + })), + ]; + + return Padding( + padding: EdgeInsets.only(left: 20.0, top: 10.0, right: 20.0), + child: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.max, + children: moderationPenaltyActions, + ) + ], + )); + } +} diff --git a/lib/pages/home/pages/menu/pages/my_moderation_tasks/my_moderation_tasks.dart b/lib/pages/home/pages/menu/pages/my_moderation_tasks/my_moderation_tasks.dart new file mode 100644 index 000000000..c4107e295 --- /dev/null +++ b/lib/pages/home/pages/menu/pages/my_moderation_tasks/my_moderation_tasks.dart @@ -0,0 +1,112 @@ +import 'dart:async'; + +import 'package:Openbook/models/communities_list.dart'; +import 'package:Openbook/models/community.dart'; +import 'package:Openbook/services/navigation_service.dart'; +import 'package:Openbook/widgets/badges/badge.dart'; +import 'package:Openbook/widgets/http_list.dart'; +import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart'; +import 'package:Openbook/widgets/page_scaffold.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/user.dart'; +import 'package:Openbook/widgets/theming/primary_color_container.dart'; +import 'package:Openbook/widgets/tiles/community_tile.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class OBMyModerationTasksPage extends StatefulWidget { + @override + State createState() { + return OBMyModerationTasksPageState(); + } +} + +class OBMyModerationTasksPageState extends State { + UserService _userService; + NavigationService _navigationService; + + OBHttpListController _httpListController; + bool _needsBootstrap; + + @override + void initState() { + super.initState(); + _httpListController = OBHttpListController(); + _needsBootstrap = true; + } + + @override + Widget build(BuildContext context) { + if (_needsBootstrap) { + var provider = OpenbookProvider.of(context); + _userService = provider.userService; + _navigationService = provider.navigationService; + _needsBootstrap = false; + } + + return OBCupertinoPageScaffold( + navigationBar: OBThemedNavigationBar( + title: 'Pending moderation tasks', + ), + child: OBPrimaryColorContainer( + child: OBHttpList( + padding: EdgeInsets.all(15), + controller: _httpListController, + listItemBuilder: _buildPendingModeratedObjectsCommunityListItem, + listRefresher: _refreshPendingModeratedObjectsCommunities, + listOnScrollLoader: _loadMorePendingModeratedObjectsCommunities, + resourceSingularName: 'pending moderation task', + resourcePluralName: 'pending moderation tasks', + ), + ), + ); + } + + Widget _buildPendingModeratedObjectsCommunityListItem( + BuildContext context, Community community) { + return GestureDetector( + onTap: () => + _onPendingModeratedObjectsCommunityListItemPressed(community), + child: Row( + children: [ + Expanded( + child: OBCommunityTile(community), + ), + SizedBox( + width: 20, + ), + OBBadge( + size: 25, + count: community.pendingModeratedObjectsCount, + ) + ], + ), + ); + } + + void _onPendingModeratedObjectsCommunityListItemPressed(Community community) { + _navigationService.navigateToCommunityModeratedObjects( + community: community, context: context); + } + + Future> _refreshPendingModeratedObjectsCommunities() async { + CommunitiesList pendingModeratedObjectsCommunities = + await _userService.getPendingModeratedObjectsCommunities(); + return pendingModeratedObjectsCommunities.communities; + } + + Future> _loadMorePendingModeratedObjectsCommunities( + List pendingModeratedObjectsCommunitiesList) async { + var lastPendingModeratedObjectsCommunity = + pendingModeratedObjectsCommunitiesList.last; + var lastPendingModeratedObjectsCommunityId = + lastPendingModeratedObjectsCommunity.id; + var morePendingModeratedObjectsCommunities = + (await _userService.getPendingModeratedObjectsCommunities( + maxId: lastPendingModeratedObjectsCommunityId, + count: 10, + )) + .communities; + return morePendingModeratedObjectsCommunities; + } +} diff --git a/lib/pages/home/pages/menu/pages/useful_links.dart b/lib/pages/home/pages/menu/pages/useful_links.dart index e5cae994e..c8fc5ee3a 100644 --- a/lib/pages/home/pages/menu/pages/useful_links.dart +++ b/lib/pages/home/pages/menu/pages/useful_links.dart @@ -28,7 +28,7 @@ class OBUsefulLinksPage extends StatelessWidget { children: [ ListTile( leading: const OBIcon(OBIcons.guide), - title: OBText('Community guidelines'), + title: OBText('Openbook guidelines'), subtitle: OBSecondaryText( 'The guidelines we\'re all expected to follow for a healthy and friendly co-existence.'), onTap: () { @@ -70,9 +70,9 @@ class OBUsefulLinksPage extends StatelessWidget { ), ListTile( leading: const OBIcon(OBIcons.guide), - title: OBText('Community guide'), + title: OBText('Openbook handbook'), subtitle: OBSecondaryText( - 'An introduction to the Openbook Experience by @meep'), + 'A book with everything there is to know about using the platform'), onTap: () { urlLauncherService.launchUrl('https://openbook.support/'); }, diff --git a/lib/pages/home/pages/menu/pages/user_invites/user_invites.dart b/lib/pages/home/pages/menu/pages/user_invites/user_invites.dart index 2328ae817..f04e065a3 100644 --- a/lib/pages/home/pages/menu/pages/user_invites/user_invites.dart +++ b/lib/pages/home/pages/menu/pages/user_invites/user_invites.dart @@ -104,18 +104,20 @@ class OBUserInvitesPageState extends State { OBPrimaryColorContainer( child: Column( children: [ - _hasAcceptedInvites || _hasPendingInvites - ? _buildInvitesList() - : _buildNoInvitesFallback() - ], - )), + _hasAcceptedInvites || _hasPendingInvites + ? _buildInvitesList() + : _buildNoInvitesFallback() + ], + ) + ), ], - )); + ), + ); } Widget _buildInvitesList() { return Expanded( - child: RefreshIndicator( + child: RefreshIndicator( key: _refreshIndicatorKey, onRefresh: _refreshInvites, child: ListView( @@ -155,8 +157,9 @@ class OBUserInvitesPageState extends State { ), ], ) - ]), - ), + ] + ), + ) ); } @@ -170,11 +173,20 @@ class OBUserInvitesPageState extends State { String assetImage = hasInvites ? 'assets/images/stickers/perplexed-owl.png' : 'assets/images/stickers/owl-instructor.png'; + + Function _onPressed = hasInvites + ? _onWantsToCreateInvite + : _refreshInvites; + + String buttonText = hasInvites + ? 'Invite a friend' + : 'Refresh'; + return OBButtonAlert( text: message, - onPressed: _refreshInvites, - buttonText: 'Refresh', - buttonIcon: OBIcons.refresh, + onPressed: _onPressed, + buttonText: buttonText, + buttonIcon: hasInvites ? OBIcons.add : OBIcons.refresh, isLoading: _refreshInProgress, assetImage: assetImage, ); @@ -182,7 +194,9 @@ class OBUserInvitesPageState extends State { Widget _onUserInviteDeletedCallback( BuildContext context, UserInvite userInvite) { - _refreshUser(); + setState(() { + if (userInvite.createdUser == null) _user.inviteCount += 1; + }); } void _bootstrap() async { @@ -194,8 +208,8 @@ class OBUserInvitesPageState extends State { try { await Future.wait([ _refreshUser(), - _acceptedInvitesGroupController.refresh(), - _pendingInvitesGroupController.refresh() + _hasAcceptedInvites ?_acceptedInvitesGroupController.refresh() : _refreshAcceptedInvites(), + _hasPendingInvites ? _pendingInvitesGroupController.refresh() : _refreshPendingInvites(), ]); _scrollToTop(); } catch (error) { @@ -286,7 +300,7 @@ class OBUserInvitesPageState extends State { } void _showNoInvitesLeft() { - _toastService.error(message: 'You have no invites left', context: context); + _toastService.error(message: 'You have no invites available', context: context); } void _onUserInviteCreated(UserInvite createdUserInvite) { diff --git a/lib/pages/home/pages/menu/pages/user_invites/widgets/my_invite_group.dart b/lib/pages/home/pages/menu/pages/user_invites/widgets/my_invite_group.dart index 44a76bdac..a79febbe0 100644 --- a/lib/pages/home/pages/menu/pages/user_invites/widgets/my_invite_group.dart +++ b/lib/pages/home/pages/menu/pages/user_invites/widgets/my_invite_group.dart @@ -152,6 +152,7 @@ class OBMyInvitesGroupState extends State { var onUserInviteDeletedCallback = () { _removeUserInvite(userInvite); widget.inviteGroupListItemDeleteCallback(context, userInvite); + if (_inviteGroupList.length == 0) _refreshInvites(); }; return OBUserInviteTile( @@ -209,7 +210,7 @@ class OBMyInvitesGroupState extends State { _navigationService.navigateToBlankPageWithWidget( context: context, key: Key('obMyUserInvitesGroup' + widget.groupItemName), - navBarTitle: capitalize(widget.groupName), + navBarTitle: toCapital(widget.groupName), widget: _buildSeeAllGroupItemsPage()); } diff --git a/lib/pages/home/pages/moderated_objects/modals/moderated_objects_filters/moderated_objects_filters.dart b/lib/pages/home/pages/moderated_objects/modals/moderated_objects_filters/moderated_objects_filters.dart new file mode 100644 index 000000000..e97fa33c0 --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/modals/moderated_objects_filters/moderated_objects_filters.dart @@ -0,0 +1,300 @@ +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/moderated_objects.dart'; +import 'package:Openbook/widgets/fields/checkbox_field.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/widgets/moderated_object_status_circle.dart'; +import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart'; +import 'package:Openbook/widgets/buttons/button.dart'; +import 'package:Openbook/widgets/theming/primary_color_container.dart'; +import 'package:Openbook/widgets/tile_group_title.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class OBModeratedObjectsFiltersModal extends StatefulWidget { + final OBModeratedObjectsPageController moderatedObjectsPageController; + + const OBModeratedObjectsFiltersModal( + {Key key, @required this.moderatedObjectsPageController}) + : super(key: key); + + @override + OBModeratedObjectsFiltersModalState createState() { + return OBModeratedObjectsFiltersModalState(); + } +} + +class OBModeratedObjectsFiltersModalState + extends State { + bool _requestInProgress; + + List _types; + List _selectedTypes; + List _statuses = [ + ModeratedObjectStatus.approved, + ModeratedObjectStatus.rejected, + ModeratedObjectStatus.pending, + ]; + List _selectedStatuses; + bool _onlyVerified; + + @override + void initState() { + super.initState(); + _requestInProgress = false; + + OBModeratedObjectsFilters currentFilters = + widget.moderatedObjectsPageController.getFilters(); + + if (widget.moderatedObjectsPageController.hasCommunity()) { + _types = [ + ModeratedObjectType.post, + ModeratedObjectType.postComment, + ]; + } else { + _types = [ + ModeratedObjectType.post, + ModeratedObjectType.postComment, + ModeratedObjectType.community, + ModeratedObjectType.user, + ]; + } + + _selectedTypes = currentFilters.types.toList(); + _selectedStatuses = currentFilters.statuses.toList(); + + _onlyVerified = currentFilters.onlyVerified; + } + + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: _buildNavigationBar(), + child: OBPrimaryColorContainer( + child: Column( + children: [ + Expanded( + child: _buildFilters(), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 20), + child: Row( + children: [ + Expanded( + child: OBButton( + size: OBButtonSize.large, + type: OBButtonType.highlight, + child: Text('Reset'), + onPressed: _onWantsToResetFilters, + ), + ), + const SizedBox( + width: 20, + ), + Expanded( + child: OBButton( + size: OBButtonSize.large, + child: _buildApplyFiltersText(), + onPressed: _onWantsToApplyFilters, + isLoading: _requestInProgress, + ), + ) + ], + ), + ) + ], + ))); + } + + Widget _buildApplyFiltersText() { + String text = 'Apply filters'; + int filterCount = _countFilters(); + if (filterCount > 0) { + String friendlyCount = filterCount.toString(); + text += ' ($friendlyCount)'; + } + return Text(text); + } + + Widget _buildFilters() { + return ListView( + children: [ + OBTileGroupTitle( + title: 'Type', + ), + ListView.builder( + itemBuilder: _buildTypeListTile, + shrinkWrap: true, + itemCount: _types.length, + ), + OBTileGroupTitle( + title: 'Status', + ), + ListView.builder( + itemBuilder: _buildStatusListTile, + shrinkWrap: true, + itemCount: _statuses.length, + ), + OBTileGroupTitle( + title: 'Other', + ), + _buildIsVerifiedListTile() + ], + ); + } + + Widget _buildTypeListTile(BuildContext context, int index) { + ModeratedObjectType type = _types[index]; + String typeString = ModeratedObject.factory + .convertTypeToHumanReadableString(type, capitalize: true); + return OBCheckboxField( + titleStyle: TextStyle(fontWeight: FontWeight.normal), + onTap: () { + _onTypePressed(type); + }, + title: typeString, + value: _selectedTypes.contains(type), + ); + } + + Widget _buildStatusListTile(BuildContext context, int index) { + ModeratedObjectStatus status = _statuses[index]; + String statusString = ModeratedObject.factory + .convertStatusToHumanReadableString(status, capitalize: true); + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: EdgeInsets.only(left: 15), + child: OBModeratedObjectStatusCircle( + status: status, + ), + ), + Expanded( + child: OBCheckboxField( + titleStyle: TextStyle(fontWeight: FontWeight.normal), + onTap: () { + _onStatusPressed(status); + }, + title: statusString, + value: _selectedStatuses.contains(status), + ), + ) + ], + ); + } + + Widget _buildIsVerifiedListTile() { + return Row( + children: [ + Padding( + padding: EdgeInsets.only(left: 15), + child: OBIcon(OBIcons.verify), + ), + Expanded( + child: OBCheckboxField( + titleStyle: TextStyle(fontWeight: FontWeight.normal), + title: 'Verified', + value: _onlyVerified, + onTap: () { + setState(() { + _onlyVerified = !_onlyVerified; + }); + }, + ), + ) + ], + ); + } + + Widget _buildNavigationBar() { + return OBThemedNavigationBar( + leading: GestureDetector( + child: const OBIcon(OBIcons.close), + onTap: () { + Navigator.pop(context); + }, + ), + title: 'Moderation Filters'); + } + + void _onWantsToApplyFilters() async { + _setRequestInProgress(true); + await widget.moderatedObjectsPageController.setFilters( + OBModeratedObjectsFilters( + types: _selectedTypes, + statuses: _selectedStatuses, + onlyVerified: _onlyVerified)); + _setRequestInProgress(false); + Navigator.pop(context); + } + + void _onWantsToResetFilters() async { + OBModeratedObjectsFilters _defaultFilters = + OBModeratedObjectsFilters.makeDefault( + isGlobalModeration: + !widget.moderatedObjectsPageController.hasCommunity()); + setState(() { + _selectedTypes = _defaultFilters.types; + _selectedStatuses = _defaultFilters.statuses; + _onlyVerified = _defaultFilters.onlyVerified; + }); + } + + void _onTypePressed(ModeratedObjectType pressedType) { + if (_selectedTypes.contains(pressedType)) { + if (_selectedTypes.length == 1) return; + // Remove + _removeSelectedType(pressedType); + } else { + // Add + _addSelectedType(pressedType); + } + } + + void _addSelectedType(ModeratedObjectType type) { + setState(() { + _selectedTypes.add(type); + }); + } + + void _removeSelectedType(ModeratedObjectType type) { + setState(() { + _selectedTypes.remove(type); + }); + } + + void _onStatusPressed(ModeratedObjectStatus pressedStatus) { + if (_selectedStatuses.contains(pressedStatus)) { + if (_selectedStatuses.length == 1) return; + // Remove + _removeSelectedStatus(pressedStatus); + } else { + // Add + _addSelectedStatus(pressedStatus); + } + } + + void _addSelectedStatus(ModeratedObjectStatus status) { + setState(() { + _selectedStatuses.add(status); + }); + } + + void _removeSelectedStatus(ModeratedObjectStatus status) { + setState(() { + _selectedStatuses.remove(status); + }); + } + + void _setRequestInProgress(bool requestInProgress) { + setState(() { + _requestInProgress = requestInProgress; + }); + } + + int _countFilters() { + return _selectedStatuses.length + + _selectedTypes.length + + (_onlyVerified ? 1 : 0); + } +} diff --git a/lib/pages/home/pages/moderated_objects/moderated_objects.dart b/lib/pages/home/pages/moderated_objects/moderated_objects.dart new file mode 100644 index 000000000..b97d3c05d --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/moderated_objects.dart @@ -0,0 +1,420 @@ +import 'package:Openbook/models/community.dart'; +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/moderation/moderated_object_list.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/widgets/moderated_object/moderated_object.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/widgets/no_moderated_objects.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/modal_service.dart'; +import 'package:Openbook/services/toast.dart'; +import 'package:Openbook/services/user.dart'; +import 'package:Openbook/widgets/badges/badge.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/widgets/icon_button.dart'; +import 'package:Openbook/widgets/load_more.dart'; +import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart'; +import 'package:Openbook/widgets/theming/primary_color_container.dart'; +import 'package:Openbook/widgets/tiles/loading_indicator_tile.dart'; +import 'package:Openbook/widgets/tiles/retry_tile.dart'; +import 'package:async/async.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class OBModeratedObjectsPage extends StatefulWidget { + final Community community; + + const OBModeratedObjectsPage({Key key, this.community}) : super(key: key); + + @override + OBModeratedObjectsPageState createState() { + return OBModeratedObjectsPageState(); + } +} + +class OBModeratedObjectsPageState extends State { + static int itemsLoadMoreCount = 10; + + OBModeratedObjectsPageController _controller; + + Community _community; + OBModeratedObjectsFilters _filters; + ScrollController _scrollController; + final GlobalKey _refreshIndicatorKey = + GlobalKey(); + List _moderatedObjects; + + UserService _userService; + ToastService _toastService; + ModalService _modalService; + + CancelableOperation _loadMoreOperation; + CancelableOperation _refreshModeratedObjectsOperation; + + bool _needsBootstrap; + bool _moreModeratedObjectsToLoad; + bool _refreshModeratedObjectsInProgress; + + @override + void initState() { + super.initState(); + _community = widget.community; + _filters = OBModeratedObjectsFilters.makeDefault( + isGlobalModeration: widget.community == null); + _controller = OBModeratedObjectsPageController(state: this); + _scrollController = ScrollController(); + _moderatedObjects = []; + _needsBootstrap = true; + _moreModeratedObjectsToLoad = true; + _refreshModeratedObjectsInProgress = true; + } + + @override + void dispose() { + super.dispose(); + if (_loadMoreOperation != null) _loadMoreOperation.cancel(); + if (_refreshModeratedObjectsOperation != null) + _refreshModeratedObjectsOperation.cancel(); + } + + @override + Widget build(BuildContext context) { + if (_needsBootstrap) { + var openbookProvider = OpenbookProvider.of(context); + _userService = openbookProvider.userService; + _toastService = openbookProvider.toastService; + _modalService = openbookProvider.modalService; + _bootstrap(); + _needsBootstrap = false; + } + + return CupertinoPageScaffold( + backgroundColor: Color.fromARGB(0, 0, 0, 0), + navigationBar: OBThemedNavigationBar( + title: widget.community != null + ? 'Community moderated objects' + : 'Globally moderated objects', + trailing: _buildFiltersButton(), + ), + child: OBPrimaryColorContainer( + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: RefreshIndicator( + key: _refreshIndicatorKey, + child: LoadMore( + whenEmptyLoad: false, + isFinish: !_moreModeratedObjectsToLoad, + delegate: OBModeratedObjectsPageLoadMoreDelegate(), + child: ListView.builder( + controller: _scrollController, + physics: const ClampingScrollPhysics(), + padding: EdgeInsets.all(0), + itemCount: _moderatedObjects.length + 1, + itemBuilder: _buildModeratedObject, + ), + onLoadMore: _loadMoreModeratedObjects), + onRefresh: _refreshModeratedObjects), + ) + ], + ), + )); + } + + Widget _buildModeratedObject(BuildContext context, int index) { + if (index == 0) { + Widget moderatedObjectItem; + + if (_moderatedObjects.isEmpty && !_refreshModeratedObjectsInProgress) { + moderatedObjectItem = OBNoModeratedObjects( + onWantsToRefreshModeratedObjects: _refresh, + ); + } else { + moderatedObjectItem = const SizedBox( + height: 20, + ); + } + + return moderatedObjectItem; + } + + int moderatedObjectIndex = index - 1; + + var moderatedObject = _moderatedObjects[moderatedObjectIndex]; + + return OBModeratedObject( + moderatedObject: moderatedObject, + community: widget.community, + key: Key(moderatedObject.id.toString())); + } + + Widget _buildFiltersButton() { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + OBBadge( + count: _filters.count(), + ), + const SizedBox( + width: 10, + ), + OBIconButton( + OBIcons.filter, + themeColor: OBIconThemeColor.primaryAccent, + onPressed: _onWantsToOpenFilters, + ) + ], + ); + } + + void _onWantsToOpenFilters() { + _modalService.openModeratedObjectsFilters( + context: context, moderatedObjectsPageController: _controller); + } + + void _bootstrap() { + Future.delayed(Duration(milliseconds: 100), () { + _refresh(); + }); + } + + Future setFilters(OBModeratedObjectsFilters filters) { + _filters = filters; + _refresh(); + } + + void scrollToTop() { + if (_scrollController.hasClients) { + if (_scrollController.offset == 0) { + _refresh(); + } + + _scrollController.animateTo( + 0.0, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 300), + ); + } + } + + Future _refresh() async { + _refreshIndicatorKey.currentState.show(); + } + + Future _refreshModeratedObjects() async { + _setRefreshModeratedObjectsInProgress(true); + try { + if (widget.community == null) { + _refreshModeratedObjectsOperation = CancelableOperation.fromFuture( + _userService.getGlobalModeratedObjects( + count: itemsLoadMoreCount, + verified: _filters.onlyVerified, + statuses: _filters.statuses, + types: _filters.types)); + } else { + _refreshModeratedObjectsOperation = CancelableOperation.fromFuture( + _userService.getCommunityModeratedObjects( + community: widget.community, + count: itemsLoadMoreCount, + verified: _filters.onlyVerified, + statuses: _filters.statuses, + types: _filters.types)); + } + + ModeratedObjectsList moderatedObjectList = + await _refreshModeratedObjectsOperation.value; + _setModeratedObjects(moderatedObjectList.moderatedObjects); + } catch (error) { + _onError(error); + } finally { + _setRefreshModeratedObjectsInProgress(false); + } + } + + Future _loadMoreModeratedObjects() async { + if (_loadMoreOperation != null) _loadMoreOperation.cancel(); + + var lastModeratedObjectId; + if (_moderatedObjects.isNotEmpty) { + ModeratedObject lastModeratedObject = _moderatedObjects.last; + lastModeratedObjectId = lastModeratedObject.id; + } + + try { + if (widget.community == null) { + _loadMoreOperation = CancelableOperation.fromFuture( + _userService.getGlobalModeratedObjects( + maxId: lastModeratedObjectId, + count: itemsLoadMoreCount, + verified: _filters.onlyVerified, + statuses: _filters.statuses, + types: _filters.types)); + } else { + _loadMoreOperation = CancelableOperation.fromFuture( + _userService.getCommunityModeratedObjects( + community: _community, + maxId: lastModeratedObjectId, + count: itemsLoadMoreCount, + verified: _filters.onlyVerified, + statuses: _filters.statuses, + types: _filters.types)); + } + + var moreModeratedObjects = + (await _loadMoreOperation.value).moderatedObjects; + + if (moreModeratedObjects.length == 0) { + _setMoreModeratedObjectsToLoad(false); + } else { + setState(() { + _moderatedObjects.addAll(moreModeratedObjects); + }); + } + return true; + } catch (error) { + _onError(error); + } finally { + _loadMoreOperation = null; + } + + return false; + } + + void _setMoreModeratedObjectsToLoad(bool moreModeratedObjectsToLoad) { + setState(() { + _moreModeratedObjectsToLoad = moreModeratedObjectsToLoad; + }); + } + + void _setModeratedObjects(List moderatedObjects) { + setState(() { + _moderatedObjects = moderatedObjects; + }); + } + + void _setRefreshModeratedObjectsInProgress( + bool refreshModeratedObjectsInProgress) { + setState(() { + _refreshModeratedObjectsInProgress = refreshModeratedObjectsInProgress; + }); + } + + OBModeratedObjectsFilters getFilters() { + return _filters.clone(); + } + + bool hasCommunity() { + return _community != null; + } + + void _onError(error) async { + if (error is HttpieConnectionRefusedError) { + _toastService.error( + message: error.toHumanReadableMessage(), context: context); + } else if (error is HttpieRequestError) { + String errorMessage = await error.toHumanReadableMessage(); + _toastService.error(message: errorMessage, context: context); + } else { + _toastService.error(message: 'Unknown error', context: context); + throw error; + } + } +} + +class OBModeratedObjectsFilters { + final List types; + final List statuses; + final bool onlyVerified; + + static OBModeratedObjectsFilters makeDefault({@required isGlobalModeration}) { + List filterTypes = [ + ModeratedObjectType.postComment, + ModeratedObjectType.post, + ]; + List filterStatuses = [ + ModeratedObjectStatus.pending + ]; + + if (isGlobalModeration) { + filterTypes + .addAll([ModeratedObjectType.user, ModeratedObjectType.community]); + filterStatuses.addAll( + [ModeratedObjectStatus.approved, ModeratedObjectStatus.rejected]); + } + + return OBModeratedObjectsFilters( + statuses: filterStatuses, types: filterTypes, onlyVerified: false); + } + + OBModeratedObjectsFilters( + {@required this.types, + @required this.statuses, + @required this.onlyVerified}); + + OBModeratedObjectsFilters clone() { + return OBModeratedObjectsFilters( + types: types.toList(), + statuses: statuses.toList(), + onlyVerified: onlyVerified); + } + + int count() { + return statuses.length + types.length + (onlyVerified ? 1 : 0); + } +} + +class OBModeratedObjectsPageController { + OBModeratedObjectsPageState state; + + OBModeratedObjectsPageController({this.state}); + + void attach({OBModeratedObjectsPageState state}) { + state = state; + } + + Future setFilters(OBModeratedObjectsFilters filters) async { + return state.setFilters(filters); + } + + OBModeratedObjectsFilters getFilters() { + return state.getFilters(); + } + + void scrollToTop() { + state.scrollToTop(); + } + + bool hasCommunity() { + return state.hasCommunity(); + } +} + +class OBModeratedObjectsPageLoadMoreDelegate extends LoadMoreDelegate { + final VoidCallback onWantsToRetryLoading; + + const OBModeratedObjectsPageLoadMoreDelegate({this.onWantsToRetryLoading}); + + @override + Widget buildChild(LoadMoreStatus status, + {LoadMoreTextBuilder builder = DefaultLoadMoreTextBuilder.chinese}) { + String text = builder(status); + + if (status == LoadMoreStatus.fail) { + return OBRetryTile( + text: 'Tap to retry loading items', + onWantsToRetry: onWantsToRetryLoading, + ); + } + if (status == LoadMoreStatus.idle) { + // No clue why is this even a state. + return const SizedBox(); + } + if (status == LoadMoreStatus.loading) { + return OBLoadingIndicatorTile(); + } + if (status == LoadMoreStatus.nomore) { + return const SizedBox(); + } + + return Text(text); + } +} diff --git a/lib/pages/home/pages/moderated_objects/pages/moderated_object_community_review.dart b/lib/pages/home/pages/moderated_objects/pages/moderated_object_community_review.dart new file mode 100644 index 000000000..cd3c70ada --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/pages/moderated_object_community_review.dart @@ -0,0 +1,258 @@ +import 'package:Openbook/models/community.dart'; +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/moderation/moderation_category.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_category/moderated_object_category.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_description/moderated_object_description.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/moderated_object_logs.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_reports_preview/moderated_object_reports_preview.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_status/moderated_object_status.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/widgets/moderated_object/widgets/moderated_object_preview.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/toast.dart'; +import 'package:Openbook/services/user.dart'; +import 'package:Openbook/widgets/buttons/button.dart'; +import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart'; +import 'package:Openbook/widgets/page_scaffold.dart'; +import 'package:Openbook/widgets/theming/primary_color_container.dart'; +import 'package:Openbook/widgets/tile_group_title.dart'; +import 'package:async/async.dart'; +import 'package:flutter/material.dart'; + +class OBModeratedObjectCommunityReviewPage extends StatefulWidget { + final ModeratedObject moderatedObject; + final Community community; + + const OBModeratedObjectCommunityReviewPage( + {Key key, @required this.moderatedObject, @required this.community}) + : super(key: key); + + @override + OBModeratedObjectCommunityReviewPageState createState() { + return OBModeratedObjectCommunityReviewPageState(); + } +} + +class OBModeratedObjectCommunityReviewPageState + extends State { + bool _requestInProgress; + bool _isEditable; + + UserService _userService; + ToastService _toastService; + bool _needsBootstrap; + + CancelableOperation _requestOperation; + OBModeratedObjectLogsController _logsController; + + @override + void initState() { + super.initState(); + _needsBootstrap = true; + _isEditable = false; + _logsController = OBModeratedObjectLogsController(); + _requestInProgress = false; + } + + @override + void dispose() { + super.dispose(); + if (_requestOperation != null) _requestOperation.cancel(); + } + + @override + Widget build(BuildContext context) { + if (_needsBootstrap) { + OpenbookProviderState openbookProvider = OpenbookProvider.of(context); + _userService = openbookProvider.userService; + _bootstrap(); + _needsBootstrap = false; + } + + return OBCupertinoPageScaffold( + navigationBar: OBThemedNavigationBar( + title: 'Review moderated object', + ), + child: OBPrimaryColorContainer( + child: Column( + children: [ + Expanded( + child: ListView( + children: [ + OBTileGroupTitle( + title: 'Object', + ), + OBModeratedObjectPreview( + moderatedObject: widget.moderatedObject, + ), + SizedBox( + height: 10, + ), + OBModeratedObjectDescription( + isEditable: _isEditable, + moderatedObject: widget.moderatedObject, + onDescriptionChanged: _onDescriptionChanged), + OBModeratedObjectCategory( + isEditable: _isEditable, + moderatedObject: widget.moderatedObject, + onCategoryChanged: _onCategoryChanged), + OBModeratedObjectStatus( + moderatedObject: widget.moderatedObject, + isEditable: false, + ), + OBModeratedObjectReportsPreview( + isEditable: _isEditable, + moderatedObject: widget.moderatedObject, + ), + OBModeratedObjectLogs( + moderatedObject: widget.moderatedObject, + controller: _logsController, + ) + ], + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 20), + child: _buildPrimaryActions(), + ) + ], + )), + ); + } + + Widget _buildPrimaryActions() { + List actions = []; + + if (widget.moderatedObject.verified) { + actions.add(_buildVerifiedButton()); + } else if (widget.moderatedObject.status != ModeratedObjectStatus.pending) { + if (widget.moderatedObject.status == ModeratedObjectStatus.approved) { + actions.add(_buildRejectButton()); + } else if (widget.moderatedObject.status == + ModeratedObjectStatus.rejected) { + actions.add(_buildApproveButton()); + } + } else { + actions.addAll([ + _buildRejectButton(), + const SizedBox( + width: 20, + ), + _buildApproveButton() + ]); + } + + return Row( + children: actions, + ); + } + + Widget _buildRejectButton() { + return Expanded( + child: OBButton( + size: OBButtonSize.large, + type: OBButtonType.danger, + child: Text('Reject'), + onPressed: _onWantsToRejectModeratedObject, + isLoading: _requestInProgress, + ), + ); + } + + Widget _buildApproveButton() { + return Expanded( + child: OBButton( + size: OBButtonSize.large, + child: Text('Approve'), + type: OBButtonType.success, + onPressed: _onWantsToApproveModeratedObject, + isLoading: _requestInProgress, + ), + ); + } + + Widget _buildVerifiedButton() { + return Expanded( + child: OBButton( + size: OBButtonSize.large, + type: OBButtonType.highlight, + child: Text('This item has been verified'), + onPressed: null, + ), + ); + } + + void _onDescriptionChanged(String newDescription) { + _refreshLogs(); + } + + void _onCategoryChanged(ModerationCategory newCategory) { + _refreshLogs(); + } + + void _refreshLogs() { + _logsController.refreshLogs(); + } + + void _onWantsToApproveModeratedObject() async { + _setRequestInProgress(true); + + try { + _requestOperation = CancelableOperation.fromFuture( + _userService.approveModeratedObject(widget.moderatedObject)); + await _requestOperation.value; + widget.moderatedObject.setIsApproved(); + _updateIsEditable(); + } catch (error) { + _onError(error); + } finally { + _setRequestInProgress(false); + } + } + + void _onWantsToRejectModeratedObject() async { + _setRequestInProgress(true); + + try { + _requestOperation = CancelableOperation.fromFuture( + _userService.rejectModeratedObject(widget.moderatedObject)); + await _requestOperation.value; + widget.moderatedObject.setIsRejected(); + _updateIsEditable(); + } catch (error) { + _onError(error); + } finally { + _setRequestInProgress(false); + } + } + + void _setRequestInProgress(requestInProgress) { + setState(() { + _requestInProgress = requestInProgress; + }); + } + + void _bootstrap() { + _isEditable = + widget.moderatedObject.status == ModeratedObjectStatus.pending; + } + + void _updateIsEditable() { + setState(() { + _isEditable = + widget.moderatedObject.status == ModeratedObjectStatus.pending; + }); + } + + void _onError(error) async { + if (error is HttpieConnectionRefusedError) { + _toastService.error( + message: error.toHumanReadableMessage(), context: context); + } else if (error is HttpieRequestError) { + String errorMessage = await error.toHumanReadableMessage(); + _toastService.error(message: errorMessage, context: context); + } else { + _toastService.error(message: 'Unknown error', context: context); + throw error; + } + } +} diff --git a/lib/pages/home/pages/moderated_objects/pages/moderated_object_global_review.dart b/lib/pages/home/pages/moderated_objects/pages/moderated_object_global_review.dart new file mode 100644 index 000000000..2e52c5222 --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/pages/moderated_object_global_review.dart @@ -0,0 +1,257 @@ +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/moderation/moderation_category.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_category/moderated_object_category.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_description/moderated_object_description.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/moderated_object_logs.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_reports_preview/moderated_object_reports_preview.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_status/moderated_object_status.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/widgets/moderated_object/widgets/moderated_object_preview.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/toast.dart'; +import 'package:Openbook/services/user.dart'; +import 'package:Openbook/widgets/buttons/button.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart'; +import 'package:Openbook/widgets/page_scaffold.dart'; +import 'package:Openbook/widgets/theming/primary_color_container.dart'; +import 'package:Openbook/widgets/tile_group_title.dart'; +import 'package:async/async.dart'; +import 'package:flutter/material.dart'; +import 'package:pigment/pigment.dart'; + +class OBModeratedObjectGlobalReviewPage extends StatefulWidget { + final ModeratedObject moderatedObject; + + const OBModeratedObjectGlobalReviewPage( + {Key key, @required this.moderatedObject}) + : super(key: key); + + @override + OBModeratedObjectGlobalReviewPageState createState() { + return OBModeratedObjectGlobalReviewPageState(); + } +} + +class OBModeratedObjectGlobalReviewPageState + extends State { + bool _requestInProgress; + bool _isEditable; + + UserService _userService; + ToastService _toastService; + bool _needsBootstrap; + + CancelableOperation _requestOperation; + OBModeratedObjectLogsController _logsController; + + @override + void initState() { + super.initState(); + _needsBootstrap = true; + _isEditable = false; + _requestInProgress = false; + _logsController = OBModeratedObjectLogsController(); + } + + @override + Widget build(BuildContext context) { + if (_needsBootstrap) { + OpenbookProviderState openbookProvider = OpenbookProvider.of(context); + _userService = openbookProvider.userService; + _toastService = openbookProvider.toastService; + _bootstrap(); + _needsBootstrap = false; + } + + return OBCupertinoPageScaffold( + navigationBar: OBThemedNavigationBar( + title: 'Review moderated object', + ), + child: OBPrimaryColorContainer( + child: Column( + children: [ + Expanded( + child: ListView( + children: [ + OBTileGroupTitle( + title: 'Object', + ), + OBModeratedObjectPreview( + moderatedObject: widget.moderatedObject, + ), + SizedBox( + height: 10, + ), + OBModeratedObjectDescription( + isEditable: _isEditable, + moderatedObject: widget.moderatedObject, + onDescriptionChanged: _onDescriptionChanged), + OBModeratedObjectCategory( + isEditable: _isEditable, + moderatedObject: widget.moderatedObject, + onCategoryChanged: _onCategoryChanged), + OBModeratedObjectStatus( + moderatedObject: widget.moderatedObject, + isEditable: _isEditable, + onStatusChanged: _onStatusChanged, + ), + OBModeratedObjectReportsPreview( + isEditable: _isEditable, + moderatedObject: widget.moderatedObject, + ), + OBModeratedObjectLogs( + moderatedObject: widget.moderatedObject, + controller: _logsController, + ) + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), + child: _buildPrimaryActions(), + ) + ], + ), + ), + ); + } + + Widget _buildPrimaryActions() { + return StreamBuilder( + stream: widget.moderatedObject.updateSubject, + initialData: widget.moderatedObject, + builder: (BuildContext context, AsyncSnapshot snapshot) { + List actions = []; + + if (snapshot.data.verified) { + actions.add(Expanded( + child: OBButton( + size: OBButtonSize.large, + type: OBButtonType.danger, + child: Row( + children: [ + const OBIcon( + OBIcons.unverify, + color: Colors.white, + customSize: 18, + ), + const SizedBox( + width: 10, + ), + Text('Unverify') + ], + ), + onPressed: _onWantsToUnverifyModeratedObject, + isLoading: _requestInProgress, + ), + )); + } else { + actions.add(Expanded( + child: OBButton( + size: OBButtonSize.large, + child: Row( + children: [ + const OBIcon( + OBIcons.verify, + color: Colors.white, + customSize: 18, + ), + const SizedBox( + width: 10, + ), + Text('Verify') + ], + ), + onPressed: _onWantsToVerifyModeratedObject, + isLoading: _requestInProgress, + color: Pigment.fromString('#5e9bff'), + ), + )); + } + + return Row( + mainAxisSize: MainAxisSize.max, + children: actions, + ); + }, + ); + } + + void _onDescriptionChanged(String newDescription) { + _refreshLogs(); + } + + void _onCategoryChanged(ModerationCategory newCategory) { + _refreshLogs(); + } + + void _onStatusChanged(ModeratedObjectStatus newStatus) { + _refreshLogs(); + } + + void _refreshLogs() { + _logsController.refreshLogs(); + } + + void _onWantsToVerifyModeratedObject() async { + _setRequestInProgress(true); + try { + _requestOperation = CancelableOperation.fromFuture( + _userService.verifyModeratedObject(widget.moderatedObject)); + await _requestOperation.value; + widget.moderatedObject.setIsVerified(true); + _updateIsEditable(); + _refreshLogs(); + } catch (error) { + _onError(error); + } finally { + _setRequestInProgress(false); + } + } + + void _onWantsToUnverifyModeratedObject() async { + _setRequestInProgress(true); + + try { + _requestOperation = CancelableOperation.fromFuture( + _userService.unverifyModeratedObject(widget.moderatedObject)); + await _requestOperation.value; + widget.moderatedObject.setIsVerified(false); + _updateIsEditable(); + _refreshLogs(); + } catch (error) { + _onError(error); + } finally { + _setRequestInProgress(false); + } + } + + void _bootstrap() { + _isEditable = !widget.moderatedObject.verified; + } + + void _updateIsEditable() { + setState(() { + _isEditable = !widget.moderatedObject.verified; + }); + } + + void _onError(error) async { + if (error is HttpieConnectionRefusedError) { + _toastService.error( + message: error.toHumanReadableMessage(), context: context); + } else if (error is HttpieRequestError) { + String errorMessage = await error.toHumanReadableMessage(); + _toastService.error(message: errorMessage, context: context); + } else { + _toastService.error(message: 'Unknown error', context: context); + throw error; + } + } + + void _setRequestInProgress(requestInProgress) { + setState(() { + _requestInProgress = requestInProgress; + }); + } +} diff --git a/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_category/modals/moderated_object_update_category.dart b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_category/modals/moderated_object_update_category.dart new file mode 100644 index 000000000..e3840bb06 --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_category/modals/moderated_object_update_category.dart @@ -0,0 +1,184 @@ +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/moderation/moderation_category.dart'; +import 'package:Openbook/models/moderation/moderation_category_list.dart'; +import 'package:Openbook/services/toast.dart'; +import 'package:Openbook/services/user.dart'; +import 'package:Openbook/widgets/buttons/button.dart'; +import 'package:Openbook/widgets/checkbox.dart'; +import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/widgets/page_scaffold.dart'; +import 'package:Openbook/widgets/progress_indicator.dart'; +import 'package:Openbook/widgets/theming/primary_color_container.dart'; +import 'package:Openbook/widgets/theming/secondary_text.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class OBModeratedObjectUpdateCategoryModal extends StatefulWidget { + final ModeratedObject moderatedObject; + + const OBModeratedObjectUpdateCategoryModal( + {Key key, @required this.moderatedObject}) + : super(key: key); + + @override + OBModeratedObjectUpdateCategoryModalState createState() { + return OBModeratedObjectUpdateCategoryModalState(); + } +} + +class OBModeratedObjectUpdateCategoryModalState + extends State { + UserService _userService; + ToastService _toastService; + List _moderationCategories = []; + ModerationCategory _selectedModerationCategory; + bool _needsBootstrap; + bool _requestInProgress; + + @override + void initState() { + super.initState(); + _needsBootstrap = true; + _requestInProgress = false; + _selectedModerationCategory = widget.moderatedObject.category; + } + + @override + Widget build(BuildContext context) { + if (_needsBootstrap) { + var openbookProvider = OpenbookProvider.of(context); + _toastService = openbookProvider.toastService; + _userService = openbookProvider.userService; + _bootstrap(); + _needsBootstrap = false; + } + + return OBCupertinoPageScaffold( + navigationBar: _buildNavigationBar(), + child: OBPrimaryColorContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _moderationCategories.isEmpty + ? _buildProgressIndicator() + : _buildModerationCategories(), + ], + ), + )); + } + + Widget _buildProgressIndicator() { + return Expanded( + child: Center( + child: OBProgressIndicator(), + ), + ); + } + + Widget _buildModerationCategories() { + return Expanded( + child: ListView.separated( + itemBuilder: _buildModerationCategoryTile, + padding: EdgeInsets.symmetric(vertical: 20, horizontal: 10), + separatorBuilder: (context, index) { + return const Divider(); + }, + itemCount: _moderationCategories.length, + ), + ); + } + + Widget _buildModerationCategoryTile(context, index) { + ModerationCategory category = _moderationCategories[index]; + + return GestureDetector( + key: Key(category.id.toString()), + onTap: () => _setSelectedModerationCategory(category), + child: Row( + children: [ + Expanded( + child: ListTile( + title: OBText( + category.title, + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: OBSecondaryText(category.description), + //trailing: OBIcon(OBIcons.chevronRight), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 20), + child: OBCheckbox( + value: _selectedModerationCategory.id == category.id, + ), + ) + ], + ), + ); + } + + void _setSelectedModerationCategory(ModerationCategory category) { + setState(() { + _selectedModerationCategory = category; + }); + } + + void _saveModerationCategory() async{ + _setRequestInProgress(true); + try { + await _userService.updateModeratedObject(widget.moderatedObject, + category: _selectedModerationCategory); + Navigator.of(context).pop(_selectedModerationCategory); + } catch (error) { + _onError(error); + } finally { + _setRequestInProgress(false); + } + } + + void _onError(error) async { + if (error is HttpieConnectionRefusedError) { + _toastService.error( + message: error.toHumanReadableMessage(), context: context); + } else if (error is HttpieRequestError) { + String errorMessage = await error.toHumanReadableMessage(); + _toastService.error(message: errorMessage, context: context); + } else { + _toastService.error(message: 'Unknown error', context: context); + throw error; + } + } + + Widget _buildNavigationBar() { + return OBThemedNavigationBar( + title: 'Update category', + trailing: OBButton( + isLoading: _requestInProgress, + size: OBButtonSize.small, + onPressed: _saveModerationCategory, + child: Text('Save'), + ), + ); + } + + void _bootstrap() async { + var moderationCategories = await _userService.getModerationCategories(); + _setModerationCategories(moderationCategories); + } + + _setModerationCategories(ModerationCategoriesList moderationCategoriesList) { + setState(() { + _moderationCategories = moderationCategoriesList.moderationCategories; + }); + } + + _setRequestInProgress(bool requestInProgress) { + setState(() { + _requestInProgress = requestInProgress; + }); + } +} + +typedef OnObjectReported(dynamic object); diff --git a/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_category/moderated_object_category.dart b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_category/moderated_object_category.dart new file mode 100644 index 000000000..1b7e8adc1 --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_category/moderated_object_category.dart @@ -0,0 +1,59 @@ +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/moderation/moderation_category.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:Openbook/widgets/tile_group_title.dart'; +import 'package:Openbook/widgets/tiles/moderation_category_tile.dart'; +import 'package:flutter/material.dart'; + +class OBModeratedObjectCategory extends StatelessWidget { + final bool isEditable; + final ModeratedObject moderatedObject; + final ValueChanged onCategoryChanged; + + const OBModeratedObjectCategory( + {Key key, + @required this.moderatedObject, + @required this.isEditable, + this.onCategoryChanged}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + OBTileGroupTitle( + title: 'Category', + ), + StreamBuilder( + initialData: moderatedObject, + stream: moderatedObject.updateSubject, + builder: + (BuildContext context, AsyncSnapshot snapshot) { + return OBModerationCategoryTile( + category: snapshot.data.category, + onPressed: (category) async { + if (!isEditable) return; + OpenbookProviderState openbookProvider = + OpenbookProvider.of(context); + ModerationCategory newModerationCategory = + await openbookProvider.modalService + .openModeratedObjectUpdateCategory( + context: context, moderatedObject: moderatedObject); + if (newModerationCategory != null && onCategoryChanged != null) + onCategoryChanged(newModerationCategory); + }, + trailing: isEditable ? const OBIcon( + OBIcons.edit, + themeColor: OBIconThemeColor.secondaryText, + ) : null, + ); + }, + ), + ], + ); + } +} diff --git a/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_description/modals/moderated_object_update_description.dart b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_description/modals/moderated_object_update_description.dart new file mode 100644 index 000000000..43c5ba675 --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_description/modals/moderated_object_update_description.dart @@ -0,0 +1,178 @@ +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/httpie.dart'; +import 'package:Openbook/services/toast.dart'; +import 'package:Openbook/services/user.dart'; +import 'package:Openbook/services/validation.dart'; +import 'package:Openbook/widgets/buttons/button.dart'; +import 'package:Openbook/widgets/fields/text_form_field.dart'; +import 'package:Openbook/widgets/page_scaffold.dart'; +import 'package:Openbook/widgets/theming/primary_color_container.dart'; +import 'package:async/async.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class OBModeratedObjectUpdateDescriptionModal extends StatefulWidget { + final ModeratedObject moderatedObject; + + const OBModeratedObjectUpdateDescriptionModal( + {Key key, @required this.moderatedObject}) + : super(key: key); + + @override + OBModeratedObjectUpdateDescriptionModalState createState() { + return OBModeratedObjectUpdateDescriptionModalState(); + } +} + +class OBModeratedObjectUpdateDescriptionModalState + extends State { + UserService _userService; + ToastService _toastService; + ValidationService _validationService; + + bool _requestInProgress; + bool _formWasSubmitted; + bool _formValid; + + GlobalKey _formKey; + + TextEditingController _descriptionController; + + CancelableOperation _editDescriptionOperation; + + @override + void initState() { + super.initState(); + _formValid = true; + _requestInProgress = false; + _formWasSubmitted = false; + _descriptionController = + TextEditingController(text: widget.moderatedObject.description); + _formKey = GlobalKey(); + + _descriptionController.addListener(_updateFormValid); + } + + @override + void dispose() { + super.dispose(); + if (_editDescriptionOperation != null) _editDescriptionOperation.cancel(); + } + + @override + Widget build(BuildContext context) { + var openbookProvider = OpenbookProvider.of(context); + _userService = openbookProvider.userService; + _toastService = openbookProvider.toastService; + _validationService = openbookProvider.validationService; + + return OBCupertinoPageScaffold( + navigationBar: _buildNavigationBar(), + child: OBPrimaryColorContainer( + child: SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + children: [ + OBTextFormField( + textCapitalization: + TextCapitalization.sentences, + size: OBTextFormFieldSize.large, + autofocus: true, + controller: _descriptionController, + decoration: InputDecoration( + labelText: 'Report description', + hintText: + 'e.g. The report item was found to...'), + validator: (String description) { + if (!_formWasSubmitted) return null; + return _validationService + .validateModeratedObjectDescription( + description); + }), + ], + )), + ], + )), + ), + )); + } + + Widget _buildNavigationBar() { + return OBThemedNavigationBar( + title: 'Edit description', + trailing: OBButton( + isDisabled: !_formValid, + isLoading: _requestInProgress, + size: OBButtonSize.small, + onPressed: _submitForm, + child: Text('Save'), + )); + } + + bool _validateForm() { + return _formKey.currentState.validate(); + } + + bool _updateFormValid() { + var formValid = _validateForm(); + _setFormValid(formValid); + return formValid; + } + + void _submitForm() async { + _formWasSubmitted = true; + + var formIsValid = _updateFormValid(); + if (!formIsValid) return; + _setRequestInProgress(true); + try { + _editDescriptionOperation = CancelableOperation.fromFuture( + _userService.updateModeratedObject(widget.moderatedObject, + description: _descriptionController.text)); + + await _editDescriptionOperation.value; + + Navigator.of(context).pop(_descriptionController.text); + } catch (error) { + _onError(error); + } finally { + _setRequestInProgress(false); + _editDescriptionOperation = null; + } + } + + void _onError(error) async { + if (error is HttpieConnectionRefusedError) { + _toastService.error( + message: error.toHumanReadableMessage(), context: context); + } else if (error is HttpieRequestError) { + String errorMessage = await error.toHumanReadableMessage(); + _toastService.error(message: errorMessage, context: context); + } else { + _toastService.error(message: 'Unknown error', context: context); + throw error; + } + } + + void _setRequestInProgress(bool requestInProgress) { + setState(() { + _requestInProgress = requestInProgress; + }); + } + + void _setFormValid(bool formValid) { + setState(() { + _formValid = formValid; + }); + } +} diff --git a/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_description/moderated_object_description.dart b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_description/moderated_object_description.dart new file mode 100644 index 000000000..93fb2e16d --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_description/moderated_object_description.dart @@ -0,0 +1,63 @@ +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/widgets/theming/secondary_text.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:Openbook/widgets/tile_group_title.dart'; +import 'package:flutter/material.dart'; + +class OBModeratedObjectDescription extends StatelessWidget { + final bool isEditable; + final ModeratedObject moderatedObject; + final ValueChanged onDescriptionChanged; + + const OBModeratedObjectDescription( + {Key key, + @required this.moderatedObject, + @required this.isEditable, + this.onDescriptionChanged}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + OBTileGroupTitle( + title: 'Description', + ), + ListTile( + onTap: () async { + if(!isEditable) return; + OpenbookProviderState openbookProvider = + OpenbookProvider.of(context); + String newDescription = await openbookProvider.modalService + .openModeratedObjectUpdateDescription( + context: context, moderatedObject: moderatedObject); + if (newDescription != null && onDescriptionChanged != null) + onDescriptionChanged(newDescription); + }, + title: StreamBuilder( + initialData: moderatedObject, + stream: moderatedObject.updateSubject, + builder: (BuildContext context, + AsyncSnapshot snapshot) { + String description = snapshot.data.description; + return description != null + ? OBText(snapshot.data.description) + : OBSecondaryText( + 'No description', + style: TextStyle(fontStyle: FontStyle.italic), + ); + }, + ), + trailing: isEditable ? const OBIcon( + OBIcons.edit, + themeColor: OBIconThemeColor.secondaryText, + ) : null, + ) + ], + ); + } +} diff --git a/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/moderated_object_logs.dart b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/moderated_object_logs.dart new file mode 100644 index 000000000..dcdfc57b1 --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/moderated_object_logs.dart @@ -0,0 +1,161 @@ +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/moderation/moderated_object_log.dart'; +import 'package:Openbook/models/moderation/moderated_object_log_list.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/moderated_object_log_tile.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/toast.dart'; +import 'package:Openbook/services/user.dart'; +import 'package:Openbook/widgets/progress_indicator.dart'; +import 'package:Openbook/widgets/theming/divider.dart'; +import 'package:Openbook/widgets/theming/secondary_text.dart'; +import 'package:Openbook/widgets/tile_group_title.dart'; +import 'package:async/async.dart'; +import 'package:flutter/material.dart'; + +class OBModeratedObjectLogs extends StatefulWidget { + final ModeratedObject moderatedObject; + final OBModeratedObjectLogsController controller; + + const OBModeratedObjectLogs( + {Key key, @required this.moderatedObject, this.controller}) + : super(key: key); + + @override + OBModeratedObjectLogsState createState() { + return OBModeratedObjectLogsState(); + } +} + +class OBModeratedObjectLogsState extends State { + static int logssCount = 5; + + bool _needsBootstrap; + UserService _userService; + ToastService _toastService; + + CancelableOperation _refreshLogsOperation; + bool _refreshInProgress; + List _logs; + + @override + void initState() { + super.initState(); + _needsBootstrap = true; + _refreshInProgress = false; + _logs = []; + if (widget.controller != null) widget.controller.attach(this); + } + + @override + void dispose() { + super.dispose(); + if (_refreshLogsOperation != null) _refreshLogsOperation.cancel(); + } + + @override + Widget build(BuildContext context) { + if (_needsBootstrap) { + OpenbookProviderState openbookProvider = OpenbookProvider.of(context); + _userService = openbookProvider.userService; + _toastService = openbookProvider.toastService; + _refreshLogs(); + _needsBootstrap = false; + _refreshInProgress = true; + } + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OBTileGroupTitle( + title: 'Logs', + ), + OBDivider(), + _refreshInProgress + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.all(20), + child: OBProgressIndicator(), + ) + ], + ) + : _logs.length > 0 + ? ListView.builder( + padding: EdgeInsets.all(0), + itemBuilder: _buildModerationLog, + itemCount: _logs.length, + shrinkWrap: true, + ) + : ListTile( + title: OBSecondaryText( + 'No logs available', + style: TextStyle(fontStyle: FontStyle.italic), + ), + ), + ], + ); + } + + Widget _buildModerationLog(BuildContext context, int index) { + ModeratedObjectLog log = _logs[index]; + return OBModeratedObjectLogTile( + key: Key(log.id.toString()), + log: log, + ); + } + + Future _refreshLogs() async { + if (_refreshInProgress) return; + _setRefreshInProgress(true); + try { + _refreshLogsOperation = CancelableOperation.fromFuture(_userService + .getModeratedObjectLogs(widget.moderatedObject, count: 5)); + + ModeratedObjectLogsList moderationLogsList = + await _refreshLogsOperation.value; + _setLogs(moderationLogsList.moderatedObjectLogs); + } catch (error) { + _onError(error); + } finally { + _setRefreshInProgress(false); + } + } + + void _setRefreshInProgress(bool refreshInProgress) { + setState(() { + _refreshInProgress = refreshInProgress; + }); + } + + void _setLogs(List logs) { + setState(() { + _logs = logs; + }); + } + + void _onError(error) async { + if (error is HttpieConnectionRefusedError) { + _toastService.error( + message: error.toHumanReadableMessage(), context: context); + } else if (error is HttpieRequestError) { + String errorMessage = await error.toHumanReadableMessage(); + _toastService.error(message: errorMessage, context: context); + } else { + _toastService.error(message: 'Unknown error', context: context); + throw error; + } + } +} + +class OBModeratedObjectLogsController { + OBModeratedObjectLogsState _state; + + void attach(state) { + _state = state; + } + + Future refreshLogs() { + return _state._refreshLogs(); + } +} diff --git a/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/moderated_object_log_tile.dart b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/moderated_object_log_tile.dart new file mode 100644 index 000000000..8f9f12650 --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/moderated_object_log_tile.dart @@ -0,0 +1,97 @@ +import 'package:Openbook/models/moderation/moderated_object_log.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_category_changed_log_tile.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_description_changed_log_tile.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_log_actor.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_status_changed_log_tile.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_verified_changed_log_tile.dart'; +import 'package:Openbook/widgets/theming/divider.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:flutter/material.dart'; + +class OBModeratedObjectLogTile extends StatelessWidget { + final ModeratedObjectLog log; + final ValueChanged onModeratedObjectLogTileDeleted; + final ValueChanged onPressed; + + const OBModeratedObjectLogTile( + {Key key, + @required this.log, + this.onModeratedObjectLogTileDeleted, + this.onPressed}) + : super(key: key); + + @override + Widget build(BuildContext context) { + Widget logTile; + + switch (log.contentObject.runtimeType) { + case ModeratedObjectDescriptionChangedLog: + logTile = OBModeratedObjectDescriptionChangedLogTile( + moderatedObjectDescriptionChangedLog: log.contentObject, + log: log, + ); + break; + case ModeratedObjectVerifiedChangedLog: + logTile = OBModeratedObjectVerifiedChangedLogTile( + moderatedObjectVerifiedChangedLog: log.contentObject, + log: log, + ); + break; + case ModeratedObjectStatusChangedLog: + logTile = OBModeratedObjectStatusChangedLogTile( + moderatedObjectStatusChangedLog: log.contentObject, + log: log, + ); + break; + case ModeratedObjectCategoryChangedLog: + logTile = OBModeratedObjectCategoryChangedLogTile( + moderatedObjectCategoryChangedLog: log.contentObject, + log: log, + ); + break; + default: + logTile = ListTile( + title: OBText( + 'Unsupported log type', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + logTile, + const SizedBox( + height: 5, + ), + ListTile( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + OBText( + 'By:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + OBModeratedObjectLogActor( + actor: log.actor, + ), + SizedBox( + height: 5, + ), + OBText( + 'On:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + OBText(log.created.toString()), + ], + ), + ), + OBDivider() + ], + ); + } +} diff --git a/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_category_changed_log_tile.dart b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_category_changed_log_tile.dart new file mode 100644 index 000000000..e0034e479 --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_category_changed_log_tile.dart @@ -0,0 +1,45 @@ +import 'package:Openbook/models/moderation/moderated_object_log.dart'; +import 'package:Openbook/widgets/theming/secondary_text.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:Openbook/widgets/tiles/moderation_category_tile.dart'; +import 'package:flutter/material.dart'; + +import 'moderated_object_log_actor.dart'; + +class OBModeratedObjectCategoryChangedLogTile extends StatelessWidget { + final ModeratedObjectLog log; + final ModeratedObjectCategoryChangedLog moderatedObjectCategoryChangedLog; + final VoidCallback onPressed; + + const OBModeratedObjectCategoryChangedLogTile( + {Key key, + @required this.log, + @required this.moderatedObjectCategoryChangedLog, + this.onPressed}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return ListTile( + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OBText( + 'Category changed from:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + OBModerationCategoryTile( + contentPadding: const EdgeInsets.all(0), + category: moderatedObjectCategoryChangedLog.changedFrom), + OBText( + 'To:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + OBModerationCategoryTile( + contentPadding: const EdgeInsets.all(0), + category: moderatedObjectCategoryChangedLog.changedTo), + ], + ), + ); + } +} diff --git a/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_description_changed_log_tile.dart b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_description_changed_log_tile.dart new file mode 100644 index 000000000..e7c3bbc30 --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_description_changed_log_tile.dart @@ -0,0 +1,53 @@ +import 'package:Openbook/models/moderation/moderated_object_log.dart'; +import 'package:Openbook/widgets/theming/secondary_text.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:flutter/material.dart'; + +import 'moderated_object_log_actor.dart'; + +class OBModeratedObjectDescriptionChangedLogTile extends StatelessWidget { + final ModeratedObjectLog log; + final ModeratedObjectDescriptionChangedLog + moderatedObjectDescriptionChangedLog; + final VoidCallback onPressed; + + const OBModeratedObjectDescriptionChangedLogTile( + {Key key, + @required this.log, + @required this.moderatedObjectDescriptionChangedLog, + this.onPressed}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return ListTile( + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OBText( + 'Description changed from:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + OBSecondaryText( + moderatedObjectDescriptionChangedLog.changedFrom != null + ? moderatedObjectDescriptionChangedLog.changedFrom + : 'No description', + style: TextStyle( + fontStyle: + moderatedObjectDescriptionChangedLog.changedFrom != null + ? FontStyle.normal + : FontStyle.italic), + ), + const SizedBox( + height: 10, + ), + OBText( + 'To:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + OBSecondaryText(moderatedObjectDescriptionChangedLog.changedTo), + ], + ), + ); + } +} diff --git a/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_log_actor.dart b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_log_actor.dart new file mode 100644 index 000000000..af77ea5fa --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_log_actor.dart @@ -0,0 +1,43 @@ +import 'package:Openbook/models/user.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/navigation_service.dart'; +import 'package:Openbook/widgets/avatars/avatar.dart'; +import 'package:Openbook/widgets/theming/secondary_text.dart'; +import 'package:flutter/material.dart'; + +class OBModeratedObjectLogActor extends StatelessWidget { + final User actor; + + const OBModeratedObjectLogActor({Key key, @required this.actor}) + : super(key: key); + + Widget build(BuildContext context) { + OpenbookProviderState openbookProvider = OpenbookProvider.of(context); + NavigationService navigationService = openbookProvider.navigationService; + + return GestureDetector( + onTap: () { + navigationService.navigateToUserProfile(user: actor, context: context); + }, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 3), + child: Row( + children: [ + OBAvatar( + borderRadius: 4, + customSize: 16, + avatarUrl: actor.getProfileAvatar(), + ), + const SizedBox( + width: 6, + ), + OBSecondaryText( + '@' + actor.username, + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_status_changed_log_tile.dart b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_status_changed_log_tile.dart new file mode 100644 index 000000000..128e8f31a --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_status_changed_log_tile.dart @@ -0,0 +1,50 @@ +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/moderation/moderated_object_log.dart'; +import 'package:Openbook/widgets/theming/secondary_text.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:flutter/material.dart'; + +import 'moderated_object_log_actor.dart'; + +class OBModeratedObjectStatusChangedLogTile extends StatelessWidget { + final ModeratedObjectLog log; + final ModeratedObjectStatusChangedLog moderatedObjectStatusChangedLog; + final VoidCallback onPressed; + + const OBModeratedObjectStatusChangedLogTile( + {Key key, + @required this.log, + @required this.moderatedObjectStatusChangedLog, + this.onPressed}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return ListTile( + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OBText( + 'Status changed from:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + OBSecondaryText(ModeratedObject.factory + .convertStatusToHumanReadableString( + moderatedObjectStatusChangedLog.changedFrom, + capitalize: true)), + const SizedBox( + height: 10, + ), + OBText( + 'To:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + OBSecondaryText(ModeratedObject.factory + .convertStatusToHumanReadableString( + moderatedObjectStatusChangedLog.changedTo, + capitalize: true)), + ], + ), + ); + } +} diff --git a/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_verified_changed_log_tile.dart b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_verified_changed_log_tile.dart new file mode 100644 index 000000000..834433a02 --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_logs/widgets/moderated_object_log_tile/widgets/moderated_object_verified_changed_log_tile.dart @@ -0,0 +1,45 @@ +import 'package:Openbook/models/moderation/moderated_object_log.dart'; +import 'package:Openbook/widgets/theming/secondary_text.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:flutter/material.dart'; + +import 'moderated_object_log_actor.dart'; + +class OBModeratedObjectVerifiedChangedLogTile extends StatelessWidget { + final ModeratedObjectLog log; + final ModeratedObjectVerifiedChangedLog moderatedObjectVerifiedChangedLog; + final VoidCallback onPressed; + + const OBModeratedObjectVerifiedChangedLogTile( + {Key key, + @required this.log, + @required this.moderatedObjectVerifiedChangedLog, + this.onPressed}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return ListTile( + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OBText( + 'Verified changed from:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + OBSecondaryText( + moderatedObjectVerifiedChangedLog.changedFrom.toString()), + const SizedBox( + height: 10, + ), + OBText( + 'To:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + OBSecondaryText( + moderatedObjectVerifiedChangedLog.changedTo.toString()), + ], + ), + ); + } +} diff --git a/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_reports_preview/moderated_object_reports_preview.dart b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_reports_preview/moderated_object_reports_preview.dart new file mode 100644 index 000000000..871bb9e52 --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_reports_preview/moderated_object_reports_preview.dart @@ -0,0 +1,160 @@ +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/moderation/moderation_report.dart'; +import 'package:Openbook/models/moderation/moderation_report_list.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/navigation_service.dart'; +import 'package:Openbook/services/toast.dart'; +import 'package:Openbook/services/user.dart'; +import 'package:Openbook/widgets/avatars/avatar.dart'; +import 'package:Openbook/widgets/buttons/see_all_button.dart'; +import 'package:Openbook/widgets/progress_indicator.dart'; +import 'package:Openbook/widgets/theming/divider.dart'; +import 'package:Openbook/widgets/theming/secondary_text.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:Openbook/widgets/tile_group_title.dart'; +import 'package:Openbook/widgets/tiles/user_tile.dart'; +import 'package:async/async.dart'; +import 'package:flutter/material.dart'; + +import 'moderation_report_tile.dart'; + +class OBModeratedObjectReportsPreview extends StatefulWidget { + final bool isEditable; + final ModeratedObject moderatedObject; + + const OBModeratedObjectReportsPreview( + {Key key, @required this.moderatedObject, @required this.isEditable}) + : super(key: key); + + @override + OBModeratedObjectReportsPreviewState createState() { + return OBModeratedObjectReportsPreviewState(); + } +} + +class OBModeratedObjectReportsPreviewState + extends State { + static int reportsPreviewsCount = 5; + + bool _needsBootstrap; + UserService _userService; + ToastService _toastService; + NavigationService _navigationService; + + CancelableOperation _refreshReportsOperation; + bool _refreshInProgress; + List _reports; + + @override + void initState() { + super.initState(); + _needsBootstrap = true; + _refreshInProgress = false; + _reports = []; + } + + @override + void dispose() { + super.dispose(); + if (_refreshReportsOperation != null) _refreshReportsOperation.cancel(); + } + + @override + Widget build(BuildContext context) { + if (_needsBootstrap) { + OpenbookProviderState openbookProvider = OpenbookProvider.of(context); + _userService = openbookProvider.userService; + _toastService = openbookProvider.toastService; + _navigationService = openbookProvider.navigationService; + _refreshReports(); + _needsBootstrap = false; + _refreshInProgress = true; + } + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OBTileGroupTitle( + title: 'Reports', + ), + OBDivider(), + _refreshInProgress + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.all(20), + child: OBProgressIndicator(), + ) + ], + ) + : ListView.separated( + separatorBuilder: (BuildContext context, int index) { + return OBDivider(); + }, + itemBuilder: _buildModerationReport, + itemCount: _reports.length, + shrinkWrap: true, + ), + OBDivider(), + OBSeeAllButton( + previewedResourcesCount: _reports.length, + resourcesCount: widget.moderatedObject.reportsCount, + resourceName: 'reports', + onPressed: _onWantsToSeeAllReports, + ) + ], + ); + } + + Widget _buildModerationReport(BuildContext contenxt, int index) { + ModerationReport report = _reports[index]; + + return OBModerationReportTile(report: report); + } + + Future _refreshReports() async { + _setRefreshInProgress(true); + try { + _refreshReportsOperation = CancelableOperation.fromFuture(_userService + .getModeratedObjectReports(widget.moderatedObject, count: 5)); + + ModerationReportsList moderationReportsList = + await _refreshReportsOperation.value; + _setReports(moderationReportsList.moderationReports); + } catch (error) { + _onError(error); + } + _setRefreshInProgress(false); + } + + void _onWantsToSeeAllReports() { + _navigationService.navigateToModeratedObjectReports( + context: context, moderatedObject: widget.moderatedObject); + } + + void _setRefreshInProgress(bool refreshInProgress) { + setState(() { + _refreshInProgress = refreshInProgress; + }); + } + + void _setReports(List reports) { + setState(() { + _reports = reports; + }); + } + + void _onError(error) async { + if (error is HttpieConnectionRefusedError) { + _toastService.error( + message: error.toHumanReadableMessage(), context: context); + } else if (error is HttpieRequestError) { + String errorMessage = await error.toHumanReadableMessage(); + _toastService.error(message: errorMessage, context: context); + } else { + _toastService.error(message: 'Unknown error', context: context); + throw error; + } + } +} diff --git a/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_reports_preview/moderation_report_tile.dart b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_reports_preview/moderation_report_tile.dart new file mode 100644 index 000000000..94ac6b719 --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_reports_preview/moderation_report_tile.dart @@ -0,0 +1,114 @@ +import 'package:Openbook/models/moderation/moderation_report.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/widgets/avatars/avatar.dart'; +import 'package:Openbook/widgets/theming/secondary_text.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:flutter/material.dart'; + +class OBModerationReportTile extends StatelessWidget { + final ModerationReport report; + + const OBModerationReportTile({Key key, @required this.report}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildReportCategory(report), + const SizedBox( + height: 5, + ), + _buildReportDescription(report), + const SizedBox( + height: 5, + ), + _buildReportReporter(report: report, context: context), + ], + ), + ), + ], + ); + } + + Widget _buildReportReporter( + {@required ModerationReport report, @required BuildContext context}) { + return GestureDetector( + onTap: () { + OpenbookProviderState openbookProvider = OpenbookProvider.of(context); + openbookProvider.navigationService + .navigateToUserProfile(user: report.reporter, context: context); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OBText( + 'Reporter', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 5), + child: Row( + children: [ + OBAvatar( + borderRadius: 4, + customSize: 16, + avatarUrl: report.reporter.getProfileAvatar(), + ), + const SizedBox( + width: 6, + ), + OBSecondaryText( + '@' + report.reporter.username, + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ) + ], + )); + } + + Widget _buildReportDescription(ModerationReport report) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + OBText( + 'Description', + style: TextStyle(fontWeight: FontWeight.bold), + ), + OBSecondaryText( + report.description != null ? report.description : 'No description', + style: TextStyle( + fontStyle: report.description == null + ? FontStyle.italic + : FontStyle.normal), + ), + ], + ); + } + + Widget _buildReportCategory(ModerationReport report) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + OBText( + 'Category', + style: TextStyle(fontWeight: FontWeight.bold), + ), + OBSecondaryText( + report.category.title, + ), + ], + ); + } +} diff --git a/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_reports_preview/pages/moderated_object_reports.dart b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_reports_preview/pages/moderated_object_reports.dart new file mode 100644 index 000000000..261b2d548 --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_reports_preview/pages/moderated_object_reports.dart @@ -0,0 +1,120 @@ +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/moderation/moderation_report.dart'; +import 'package:Openbook/models/moderation/moderation_report_list.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/toast.dart'; +import 'package:Openbook/services/user.dart'; +import 'package:Openbook/widgets/progress_indicator.dart'; +import 'package:async/async.dart'; +import 'package:flutter/material.dart'; + +import '../moderation_report_tile.dart'; + +class OBModeratedObjectReportsPage extends StatefulWidget { + final ModeratedObject moderatedObject; + + const OBModeratedObjectReportsPage({Key key, @required this.moderatedObject}) + : super(key: key); + + @override + OBModeratedObjectReportsPageState createState() { + return OBModeratedObjectReportsPageState(); + } +} + +class OBModeratedObjectReportsPageState + extends State { + bool _needsBootstrap; + UserService _userService; + ToastService _toastService; + + CancelableOperation _refreshReportsOperation; + bool _refreshInProgress; + List _reports; + + @override + void initState() { + super.initState(); + _needsBootstrap = true; + _refreshInProgress = false; + _reports = []; + } + + @override + void dispose() { + super.dispose(); + if (_refreshReportsOperation != null) _refreshReportsOperation.cancel(); + } + + @override + Widget build(BuildContext context) { + if (_needsBootstrap) { + OpenbookProviderState openbookProvider = OpenbookProvider.of(context); + _userService = openbookProvider.userService; + _toastService = openbookProvider.toastService; + _refreshReports(); + _needsBootstrap = false; + _refreshInProgress = true; + } + return _refreshInProgress + ? Row( + children: [ + Padding( + padding: EdgeInsets.all(20), + child: OBProgressIndicator(), + ) + ], + ) + : ListView.builder( + itemBuilder: _buildModerationReport, + itemCount: _reports.length, + ); + } + + Widget _buildModerationReport(BuildContext contenxt, int index) { + ModerationReport report = _reports[index]; + return OBModerationReportTile( + report: report, + ); + } + + Future _refreshReports() async { + _setRefreshInProgress(true); + try { + _refreshReportsOperation = CancelableOperation.fromFuture(_userService + .getModeratedObjectReports(widget.moderatedObject, count: 5)); + + ModerationReportsList moderationReportsList = + await _refreshReportsOperation.value; + _setReports(moderationReportsList.moderationReports); + } catch (error) { + _onError(error); + } + _setRefreshInProgress(false); + } + + void _setRefreshInProgress(bool refreshInProgress) { + setState(() { + _refreshInProgress = refreshInProgress; + }); + } + + void _setReports(List reports) { + setState(() { + _reports = reports; + }); + } + + void _onError(error) async { + if (error is HttpieConnectionRefusedError) { + _toastService.error( + message: error.toHumanReadableMessage(), context: context); + } else if (error is HttpieRequestError) { + String errorMessage = await error.toHumanReadableMessage(); + _toastService.error(message: errorMessage, context: context); + } else { + _toastService.error(message: 'Unknown error', context: context); + throw error; + } + } +} diff --git a/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_status/modals/moderated_object_update_status.dart b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_status/modals/moderated_object_update_status.dart new file mode 100644 index 000000000..038173588 --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_status/modals/moderated_object_update_status.dart @@ -0,0 +1,211 @@ +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/services/toast.dart'; +import 'package:Openbook/services/user.dart'; +import 'package:Openbook/widgets/buttons/button.dart'; +import 'package:Openbook/widgets/checkbox.dart'; +import 'package:Openbook/widgets/moderated_object_status_circle.dart'; +import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/widgets/page_scaffold.dart'; +import 'package:Openbook/widgets/progress_indicator.dart'; +import 'package:Openbook/widgets/theming/primary_color_container.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:async/async.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class OBModeratedObjectUpdateStatusModal extends StatefulWidget { + final ModeratedObject moderatedObject; + + const OBModeratedObjectUpdateStatusModal( + {Key key, @required this.moderatedObject}) + : super(key: key); + + @override + OBModeratedObjectUpdateStatusModalState createState() { + return OBModeratedObjectUpdateStatusModalState(); + } +} + +class OBModeratedObjectUpdateStatusModalState + extends State { + UserService _userService; + ToastService _toastService; + List _moderationStatuses = [ + ModeratedObjectStatus.rejected, + ModeratedObjectStatus.approved, + ]; + ModeratedObjectStatus _selectedModerationStatus; + bool _needsBootstrap; + bool _requestInProgress; + + CancelableOperation _updateStatusOperation; + + @override + void initState() { + super.initState(); + _needsBootstrap = true; + _requestInProgress = false; + _selectedModerationStatus = widget.moderatedObject.status; + } + + @override + Widget build(BuildContext context) { + if (_needsBootstrap) { + var openbookProvider = OpenbookProvider.of(context); + _toastService = openbookProvider.toastService; + _userService = openbookProvider.userService; + _needsBootstrap = false; + } + + return OBCupertinoPageScaffold( + navigationBar: _buildNavigationBar(), + child: OBPrimaryColorContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _moderationStatuses.isEmpty + ? _buildProgressIndicator() + : _buildModerationStatuses(), + ], + ), + )); + } + + @override + void dispose() { + super.dispose(); + if (_updateStatusOperation != null) _updateStatusOperation.cancel(); + } + + Widget _buildProgressIndicator() { + return Expanded( + child: Center( + child: OBProgressIndicator(), + ), + ); + } + + Widget _buildModerationStatuses() { + return Expanded( + child: ListView.separated( + itemBuilder: _buildModerationStatusTile, + padding: EdgeInsets.symmetric(vertical: 20, horizontal: 10), + separatorBuilder: (context, index) { + return const Divider(); + }, + itemCount: _moderationStatuses.length, + ), + ); + } + + Widget _buildModerationStatusTile(context, index) { + ModeratedObjectStatus status = _moderationStatuses[index]; + String statusString = ModeratedObject.factory + .convertStatusToHumanReadableString(status, capitalize: true); + + return GestureDetector( + key: Key(statusString), + onTap: () => _setSelectedModerationStatus(status), + child: Row( + children: [ + Expanded( + child: ListTile( + title: Row( + children: [ + OBModeratedObjectStatusCircle( + status: status, + ), + const SizedBox( + width: 10, + ), + OBText( + statusString, + style: TextStyle(fontWeight: FontWeight.bold), + ) + ], + ), + //trailing: OBIcon(OBIcons.chevronRight), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 20), + child: OBCheckbox( + value: _selectedModerationStatus == status, + ), + ) + ], + ), + ); + } + + void _setSelectedModerationStatus(ModeratedObjectStatus status) { + setState(() { + _selectedModerationStatus = status; + }); + } + + void _saveModerationStatus() async { + _setRequestInProgress(true); + try { + if (_selectedModerationStatus == widget.moderatedObject.status) { + Navigator.of(context).pop(); + return; + } + + switch (_selectedModerationStatus) { + case ModeratedObjectStatus.approved: + _updateStatusOperation = CancelableOperation.fromFuture( + _userService.approveModeratedObject(widget.moderatedObject)); + break; + case ModeratedObjectStatus.rejected: + _updateStatusOperation = CancelableOperation.fromFuture( + _userService.rejectModeratedObject(widget.moderatedObject)); + break; + default: + throw 'Unsuppported update type'; + } + await _updateStatusOperation.value; + Navigator.of(context).pop(_selectedModerationStatus); + widget.moderatedObject.setStatus(_selectedModerationStatus); + } catch (error) { + _onError(error); + } finally { + _updateStatusOperation = null; + _setRequestInProgress(false); + } + } + + void _onError(error) async { + if (error is HttpieConnectionRefusedError) { + _toastService.error( + message: error.toHumanReadableMessage(), context: context); + } else if (error is HttpieRequestError) { + String errorMessage = await error.toHumanReadableMessage(); + _toastService.error(message: errorMessage, context: context); + } else { + _toastService.error(message: 'Unknown error', context: context); + throw error; + } + } + + Widget _buildNavigationBar() { + return OBThemedNavigationBar( + title: 'Update status', + trailing: OBButton( + isLoading: _requestInProgress, + size: OBButtonSize.small, + onPressed: _saveModerationStatus, + child: Text('Save'), + ), + ); + } + + _setRequestInProgress(bool requestInProgress) { + setState(() { + _requestInProgress = requestInProgress; + }); + } +} + +typedef OnObjectReported(dynamic object); diff --git a/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_status/moderated_object_status.dart b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_status/moderated_object_status.dart new file mode 100644 index 000000000..6e6b6f506 --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/pages/widgets/moderated_object_status/moderated_object_status.dart @@ -0,0 +1,60 @@ +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:Openbook/widgets/tile_group_title.dart'; +import 'package:Openbook/widgets/tiles/moderated_object_status_tile.dart'; +import 'package:flutter/material.dart'; + +class OBModeratedObjectStatus extends StatelessWidget { + final bool isEditable; + final ModeratedObject moderatedObject; + final ValueChanged onStatusChanged; + + const OBModeratedObjectStatus( + {Key key, + @required this.moderatedObject, + @required this.isEditable, + this.onStatusChanged}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + OBTileGroupTitle( + title: 'Status', + ), + StreamBuilder( + initialData: moderatedObject, + stream: moderatedObject.updateSubject, + builder: + (BuildContext context, AsyncSnapshot snapshot) { + return OBModeratedObjectStatusTile( + moderatedObject: moderatedObject, + trailing: isEditable + ? const OBIcon( + OBIcons.edit, + themeColor: OBIconThemeColor.secondaryText, + ) + : null, + onPressed: (moderatedObject) async { + if (!isEditable) return; + OpenbookProviderState openbookProvider = + OpenbookProvider.of(context); + ModeratedObjectStatus newModerationStatus = + await openbookProvider.modalService + .openModeratedObjectUpdateStatus( + context: context, moderatedObject: moderatedObject); + if (newModerationStatus != null && onStatusChanged != null) + onStatusChanged(newModerationStatus); + }, + ); + }, + ), + ], + ); + } +} diff --git a/lib/pages/home/pages/moderated_objects/widgets/moderated_object/moderated_object.dart b/lib/pages/home/pages/moderated_objects/widgets/moderated_object/moderated_object.dart new file mode 100644 index 000000000..29e90993a --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/widgets/moderated_object/moderated_object.dart @@ -0,0 +1,112 @@ +import 'package:Openbook/models/community.dart'; +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_category/moderated_object_category.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/widgets/moderated_object/widgets/moderated_object_actions.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/widgets/moderated_object/widgets/moderated_object_preview.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/widgets/theming/divider.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:Openbook/widgets/tile_group_title.dart'; +import 'package:Openbook/widgets/tiles/moderated_object_status_tile.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class OBModeratedObject extends StatelessWidget { + final ModeratedObject moderatedObject; + final Community community; + + const OBModeratedObject( + {Key key, @required this.moderatedObject, this.community}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + OBTileGroupTitle( + title: 'Object', + ), + OBModeratedObjectPreview( + moderatedObject: moderatedObject, + ), + const SizedBox( + height: 10, + ), + OBModeratedObjectCategory( + moderatedObject: moderatedObject, + isEditable: false, + ), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + OBTileGroupTitle( + title: 'Status', + ), + OBModeratedObjectStatusTile( + moderatedObject: moderatedObject, + ), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + OBTileGroupTitle( + title: 'Reports count', + ), + ListTile( + title: OBText(moderatedObject.reportsCount.toString())), + ], + ), + ), + ], + ), + OBTileGroupTitle( + title: community != null ? 'Verified by Openbook staff' : 'Verified', + ), + StreamBuilder( + stream: moderatedObject.updateSubject, + initialData: moderatedObject, + builder: + (BuildContext context, AsyncSnapshot snapshot) { + return Padding( + padding: EdgeInsets.all(15), + child: Row( + children: [ + OBIcon( + moderatedObject.verified + ? OBIcons.verify + : OBIcons.unverify, + size: OBIconSize.small, + ), + const SizedBox( + width: 10, + ), + OBText( + moderatedObject.verified ? 'True' : 'False', + ) + ], + ), + ); + }, + ), + OBModeratedObjectActions( + moderatedObject: moderatedObject, + community: community, + ), + const SizedBox( + height: 10, + ), + const OBDivider() + ], + ); + } +} diff --git a/lib/pages/home/pages/moderated_objects/widgets/moderated_object/widgets/moderated_object_actions.dart b/lib/pages/home/pages/moderated_objects/widgets/moderated_object/widgets/moderated_object_actions.dart new file mode 100644 index 000000000..53713969a --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/widgets/moderated_object/widgets/moderated_object_actions.dart @@ -0,0 +1,63 @@ +import 'package:Openbook/models/community.dart'; +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/widgets/buttons/button.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:flutter/material.dart'; + +class OBModeratedObjectActions extends StatelessWidget { + final Community community; + final ModeratedObject moderatedObject; + + OBModeratedObjectActions( + {@required this.community, @required this.moderatedObject}); + + @override + Widget build(BuildContext context) { + List moderatedObjectActions = [ + Expanded( + child: OBButton( + type: OBButtonType.highlight, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const OBIcon( + OBIcons.reviewModeratedObject, + customSize: 20.0, + ), + const SizedBox( + width: 10.0, + ), + const OBText('Review'), + ], + ), + onPressed: () { + OpenbookProviderState openbookProvider = + OpenbookProvider.of(context); + if (community != null) { + openbookProvider.navigationService + .navigateToModeratedObjectCommunityReview( + moderatedObject: moderatedObject, + community: community, + context: context); + } else { + openbookProvider.navigationService + .navigateToModeratedObjectGlobalReview( + moderatedObject: moderatedObject, context: context); + } + })), + ]; + + return Padding( + padding: EdgeInsets.only(left: 20.0, top: 10.0, right: 20.0), + child: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.max, + children: moderatedObjectActions, + ) + ], + )); + } +} diff --git a/lib/pages/home/pages/moderated_objects/widgets/moderated_object/widgets/moderated_object_preview.dart b/lib/pages/home/pages/moderated_objects/widgets/moderated_object/widgets/moderated_object_preview.dart new file mode 100644 index 000000000..f32d3ac4d --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/widgets/moderated_object/widgets/moderated_object_preview.dart @@ -0,0 +1,72 @@ +import 'package:Openbook/models/community.dart'; +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/post_comment.dart'; +import 'package:Openbook/pages/home/pages/post_comments/widgets/post_comment/post_comment.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/widgets/post/widgets/post-body/post_body.dart'; +import 'package:Openbook/widgets/post/widgets/post_header/post_header.dart'; +import 'package:Openbook/widgets/tiles/community_tile.dart'; +import 'package:Openbook/widgets/tiles/user_tile.dart'; +import 'package:flutter/material.dart'; + +class OBModeratedObjectPreview extends StatelessWidget { + final ModeratedObject moderatedObject; + + const OBModeratedObjectPreview({Key key, @required this.moderatedObject}) + : super(key: key); + + @override + Widget build(BuildContext context) { + Widget widget; + + switch (moderatedObject.type) { + case ModeratedObjectType.post: + widget = Column( + mainAxisSize: MainAxisSize.min, + children: [ + OBPostHeader( + post: moderatedObject.contentObject, + hasActions: false, + ), + OBPostBody(moderatedObject.contentObject), + ], + ); + break; + case ModeratedObjectType.community: + widget = Padding( + padding: EdgeInsets.all(10), + child: OBCommunityTile( + moderatedObject.contentObject, + onCommunityTilePressed: (Community community) { + OpenbookProviderState openbookProvider = + OpenbookProvider.of(context); + openbookProvider.navigationService + .navigateToCommunity(community: community, context: context); + }, + ), + ); + break; + case ModeratedObjectType.postComment: + PostComment postComment = moderatedObject.contentObject; + widget = Column( + children: [ + OBPostComment( + post: postComment.post, + postComment: moderatedObject.contentObject, + ), + ], + ); + break; + case ModeratedObjectType.user: + widget = Column( + children: [ + OBUserTile(moderatedObject.contentObject), + ], + ); + break; + default: + widget = const SizedBox(); + } + return widget; + } +} diff --git a/lib/pages/home/pages/moderated_objects/widgets/no_moderated_objects.dart b/lib/pages/home/pages/moderated_objects/widgets/no_moderated_objects.dart new file mode 100644 index 000000000..a6377cc49 --- /dev/null +++ b/lib/pages/home/pages/moderated_objects/widgets/no_moderated_objects.dart @@ -0,0 +1,20 @@ +import 'package:Openbook/widgets/alerts/button_alert.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:flutter/material.dart'; + +class OBNoModeratedObjects extends StatelessWidget { + final VoidCallback onWantsToRefreshModeratedObjects; + + OBNoModeratedObjects({@required this.onWantsToRefreshModeratedObjects}); + + @override + Widget build(BuildContext context) { + return OBButtonAlert( + text: 'No moderation items', + onPressed: onWantsToRefreshModeratedObjects, + buttonText: 'Refresh', + buttonIcon: OBIcons.refresh, + assetImage: 'assets/images/stickers/perplexed-owl.png', + ); + } +} diff --git a/lib/pages/home/pages/post_comments/post.dart b/lib/pages/home/pages/post_comments/post.dart deleted file mode 100644 index 2b2708b43..000000000 --- a/lib/pages/home/pages/post_comments/post.dart +++ /dev/null @@ -1,444 +0,0 @@ -import 'package:Openbook/models/community.dart'; -import 'package:Openbook/models/post.dart'; -import 'package:Openbook/models/post_comment.dart'; -import 'package:Openbook/models/user.dart'; -import 'package:Openbook/pages/home/pages/post_comments/widgets/post-commenter.dart'; -import 'package:Openbook/pages/home/pages/post_comments/widgets/post_comment/post_comment.dart'; -import 'package:Openbook/services/theme.dart'; -import 'package:Openbook/services/theme_value_parser.dart'; -import 'package:Openbook/services/user_preferences.dart'; -import 'package:Openbook/widgets/alerts/alert.dart'; -import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart'; -import 'package:Openbook/widgets/page_scaffold.dart'; -import 'package:Openbook/provider.dart'; -import 'package:Openbook/services/toast.dart'; -import 'package:Openbook/services/user.dart'; -import 'package:Openbook/widgets/theming/primary_color_container.dart'; -import 'package:Openbook/widgets/theming/secondary_text.dart'; -import 'package:Openbook/widgets/theming/text.dart'; -import 'package:async/async.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:Openbook/widgets/load_more.dart'; -import 'package:Openbook/services/httpie.dart'; - -class OBPostCommentsPage extends StatefulWidget { - final Post post; - final bool autofocusCommentInput; - - OBPostCommentsPage( - this.post, { - this.autofocusCommentInput: false, - }); - - @override - State createState() { - return OBPostCommentsPageState(); - } -} - -class OBPostCommentsPageState extends State { - UserService _userService; - ToastService _toastService; - ThemeService _themeService; - UserPreferencesService _userPreferencesService; - ThemeValueParserService _themeValueParserService; - GlobalKey _refreshIndicatorKey; - - ScrollController _postCommentsScrollController; - List _postComments = []; - bool _noMoreItemsToLoad; - bool _needsBootstrap; - FocusNode _commentInputFocusNode; - PostCommentsSortType _currentSort; - - CancelableOperation _refreshCommentsOperation; - CancelableOperation _refreshPostOperation; - CancelableOperation _loadMoreBottomCommentsOperation; - CancelableOperation _toggleSortCommentsOperation; - - @override - void initState() { - super.initState(); - _postCommentsScrollController = ScrollController(); - _refreshIndicatorKey = GlobalKey(); - _needsBootstrap = true; - _postComments = []; - _currentSort = PostCommentsSortType.dec; - _noMoreItemsToLoad = true; - _commentInputFocusNode = FocusNode(); - } - - @override - void dispose() { - super.dispose(); - if (_refreshCommentsOperation != null) _refreshCommentsOperation.cancel(); - if (_loadMoreBottomCommentsOperation != null) - _loadMoreBottomCommentsOperation.cancel(); - if (_refreshPostOperation != null) _refreshPostOperation.cancel(); - if (_toggleSortCommentsOperation != null) - _toggleSortCommentsOperation.cancel(); - } - - @override - Widget build(BuildContext context) { - if (_needsBootstrap) { - var provider = OpenbookProvider.of(context); - _userService = provider.userService; - _toastService = provider.toastService; - _themeValueParserService = provider.themeValueParserService; - _themeService = provider.themeService; - _userPreferencesService = provider.userPreferencesService; - _bootstrap(); - _needsBootstrap = false; - } - - return OBCupertinoPageScaffold( - backgroundColor: Color.fromARGB(0, 0, 0, 0), - navigationBar: OBThemedNavigationBar( - title: 'Post comments', - ), - child: OBPrimaryColorContainer( - child: Column( - children: [ - Expanded( - child: RefreshIndicator( - key: _refreshIndicatorKey, - child: GestureDetector( - onTap: _unfocusCommentInput, - child: LoadMore( - whenEmptyLoad: false, - isFinish: _noMoreItemsToLoad, - delegate: OBInfinitePostCommentsLoadMoreDelegate(), - child: ListView.builder( - physics: const AlwaysScrollableScrollPhysics(), - controller: _postCommentsScrollController, - padding: EdgeInsets.all(0), - itemCount: _postComments.length + 1, - itemBuilder: (context, index) { - if (index == 0) { - return Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - _buildCommentsHeader(), - ], - ); - } - - int commentIndex = index - 1; - - var postComment = _postComments[commentIndex]; - - var onPostCommentDeletedCallback = () { - _removePostCommentAtIndex(commentIndex); - }; - - return OBPostComment( - key: Key('postComment#${postComment.id}'), - postComment: postComment, - post: widget.post, - onPostCommentDeletedCallback: - onPostCommentDeletedCallback); - }), - onLoadMore: _loadMoreBottomComments), - ), - onRefresh: _refreshComments), - ), - _buildPostCommenterSection() - ], - ), - )); - } - - void _bootstrap() async { - await _setPostCommentsSortTypeFromPreferences(); - await _refreshPost(); - await _refreshComments(); - } - - Future _setPostCommentsSortTypeFromPreferences() async { - PostCommentsSortType sortType = - await _userPreferencesService.getPostCommentsSortType(); - _currentSort = sortType; - } - - Widget _buildCommentsHeader() { - var theme = _themeService.getActiveTheme(); - return Container( - padding: EdgeInsets.symmetric(horizontal: 0.0, vertical: 10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 0.0), - child: OBSecondaryText( - _postComments.length > 0 - ? _currentSort == PostCommentsSortType.dec - ? 'Newest comments' - : 'Oldest comments' - : 'Be the first to comment!', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0), - ), - ), - FlatButton( - child: Row( - children: [ - OBText( - _postComments.length > 0 - ? _currentSort == PostCommentsSortType.dec - ? 'See oldest comments' - : 'See newest comments' - : '', - style: TextStyle( - color: _themeValueParserService - .parseGradient(theme.primaryAccentColor) - .colors[1], - fontWeight: FontWeight.bold), - ), - ], - ), - onPressed: _onWantsToToggleSortComments), - ], - ), - ); - } - - Widget _buildPostCommenterSection() { - User loggedInUser = _userService.getLoggedInUser(); - if (widget.post.areCommentsEnabled || loggedInUser.canCommentOnPostWithDisabledComments(widget.post)) { - return OBPostCommenter( - widget.post, - autofocus: widget.autofocusCommentInput, - commentTextFieldFocusNode: _commentInputFocusNode, - onPostCommentCreated: _onPostCommentCreated, - onPostCommentWillBeCreated: _onPostCommentWillBeCreated, - ); - } else { - return Container( - padding: EdgeInsets.all(10.0), - child: OBAlert( - padding: EdgeInsets.all(10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: OBText('Comments have been disabled for this post', textAlign: TextAlign.center,), - ), - ], - ) - ), - ); - } - } - - void _onWantsToToggleSortComments() async { - if (_toggleSortCommentsOperation != null) - _toggleSortCommentsOperation.cancel(); - PostCommentsSortType newSortType; - - if (_currentSort == PostCommentsSortType.asc) { - newSortType = PostCommentsSortType.dec; - } else { - newSortType = PostCommentsSortType.asc; - } - - try { - _toggleSortCommentsOperation = CancelableOperation.fromFuture( - _userService.getCommentsForPost(widget.post, sort: newSortType)); - - _postComments = (await _toggleSortCommentsOperation.value).comments; - _setCurrentSortValue(newSortType); - _userPreferencesService.setPostCommentsSortType(newSortType); - _setPostComments(_postComments); - _scrollToTop(); - _setNoMoreItemsToLoad(false); - } catch (error) { - _onError(error); - } finally { - _toggleSortCommentsOperation = null; - } - } - - Future _refreshComments() async { - if (_refreshCommentsOperation != null) _refreshCommentsOperation.cancel(); - try { - _refreshCommentsOperation = CancelableOperation.fromFuture( - _userService.getCommentsForPost(widget.post, sort: _currentSort)); - _postComments = (await _refreshCommentsOperation.value).comments; - _setPostComments(_postComments); - _scrollToTop(); - _setNoMoreItemsToLoad(false); - } catch (error) { - _onError(error); - } finally { - _refreshCommentsOperation = null; - } - } - - Future _refreshPost() async { - if (_refreshPostOperation != null) _refreshPostOperation.cancel(); - try { - // This will trigger the updateSubject of the post - _refreshPostOperation = CancelableOperation.fromFuture( - _userService.getPostWithUuid(widget.post.uuid)); - await _refreshPostOperation.value; - } catch (error) { - _onError(error); - } finally { - _refreshPostOperation = null; - } - } - - Future _loadMoreBottomComments() async { - if (_loadMoreBottomCommentsOperation != null) - _loadMoreBottomCommentsOperation.cancel(); - if (_postComments.length == 0) return true; - - var lastPost = _postComments.last; - var lastPostId = lastPost.id; - - try { - var moreComments; - - if (_currentSort == PostCommentsSortType.dec) { - _loadMoreBottomCommentsOperation = CancelableOperation.fromFuture( - _userService.getCommentsForPost(widget.post, maxId: lastPostId)); - } else { - _loadMoreBottomCommentsOperation = CancelableOperation.fromFuture( - _userService.getCommentsForPost(widget.post, - minId: lastPostId + 1, sort: _currentSort)); - } - - moreComments = (await _loadMoreBottomCommentsOperation.value).comments; - - if (moreComments.length == 0) { - _setNoMoreItemsToLoad(true); - } else { - _addPostComments(moreComments); - } - return true; - } catch (error) { - _onError(error); - } finally { - _loadMoreBottomCommentsOperation = null; - } - - return false; - } - - void _removePostCommentAtIndex(int index) { - setState(() { - _postComments.removeAt(index); - }); - } - - void _onPostCommentCreated(PostComment createdPostComment) { - _unfocusCommentInput(); - setState(() { - this._postComments.insert(0, createdPostComment); - }); - } - - Future _onPostCommentWillBeCreated() { - _setCurrentSortValue(PostCommentsSortType.dec); - return _refreshComments(); - } - - void _setCurrentSortValue(PostCommentsSortType newSortType) { - setState(() { - _currentSort = newSortType; - }); - } - - void _scrollToTop() { - _postCommentsScrollController.animateTo( - 0.0, - curve: Curves.easeOut, - duration: const Duration(milliseconds: 300), - ); - } - - void _unfocusCommentInput() { - FocusScope.of(context).requestFocus(new FocusNode()); - } - - void _addPostComments(List postComments) { - setState(() { - this._postComments.addAll(postComments); - }); - } - - void _setPostComments(List postComments) { - setState(() { - this._postComments = postComments; - }); - } - - void _setNoMoreItemsToLoad(bool noMoreItemsToLoad) { - setState(() { - _noMoreItemsToLoad = noMoreItemsToLoad; - }); - } - - void _onError(error) async { - if (error is HttpieConnectionRefusedError) { - _toastService.error( - message: error.toHumanReadableMessage(), context: context); - } else if (error is HttpieRequestError) { - String errorMessage = await error.toHumanReadableMessage(); - _toastService.error(message: errorMessage, context: context); - } else { - _toastService.error(message: 'Unknown error', context: context); - throw error; - } - } -} - -class OBInfinitePostCommentsLoadMoreDelegate extends LoadMoreDelegate { - const OBInfinitePostCommentsLoadMoreDelegate(); - - @override - Widget buildChild(LoadMoreStatus status, {LoadMoreTextBuilder builder}) { - String text = builder(status); - - if (status == LoadMoreStatus.fail) { - return SizedBox( - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.refresh), - const SizedBox( - width: 10.0, - ), - Text('Tap to retry loading comments.') - ], - ), - ); - } - if (status == LoadMoreStatus.idle) { - // No clue why is this even a state. - return const SizedBox(); - } - if (status == LoadMoreStatus.loading) { - return SizedBox( - child: Center( - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: 20.0, - maxWidth: 20.0, - ), - child: CircularProgressIndicator( - strokeWidth: 2.0, - ), - ), - )); - } - if (status == LoadMoreStatus.nomore) { - return const SizedBox(); - } - - return Text(text); - } -} diff --git a/lib/pages/home/pages/post_comments/post_comments_linked.dart b/lib/pages/home/pages/post_comments/post_comments_linked.dart deleted file mode 100644 index 5beadaf90..000000000 --- a/lib/pages/home/pages/post_comments/post_comments_linked.dart +++ /dev/null @@ -1,816 +0,0 @@ -import 'package:Openbook/models/post.dart'; -import 'package:Openbook/models/post_comment.dart'; -import 'package:Openbook/pages/home/pages/post_comments/widgets/post-commenter.dart'; -import 'package:Openbook/pages/home/pages/post_comments/widgets/post_comment/post_comment.dart'; -import 'package:Openbook/services/theme.dart'; -import 'package:Openbook/services/theme_value_parser.dart'; -import 'package:Openbook/services/user_preferences.dart'; -import 'package:Openbook/widgets/icon.dart'; -import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart'; -import 'package:Openbook/widgets/page_scaffold.dart'; -import 'package:Openbook/provider.dart'; -import 'package:Openbook/services/toast.dart'; -import 'package:Openbook/services/user.dart'; -import 'package:Openbook/widgets/post/widgets/post-actions/post_actions.dart'; -import 'package:Openbook/widgets/post/widgets/post-body/post_body.dart'; -import 'package:Openbook/widgets/post/widgets/post_circles.dart'; -import 'package:Openbook/widgets/post/widgets/post_header/post_header.dart'; -import 'package:Openbook/widgets/post/widgets/post_reactions/post_reactions.dart'; -import 'package:Openbook/widgets/theming/post_divider.dart'; -import 'package:Openbook/widgets/theming/primary_color_container.dart'; -import 'package:Openbook/widgets/theming/secondary_text.dart'; -import 'package:Openbook/widgets/theming/text.dart'; -import 'package:async/async.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:Openbook/widgets/load_more.dart'; -import 'package:Openbook/services/httpie.dart'; - -class OBPostCommentsLinkedPage extends StatefulWidget { - final PostComment postComment; - final bool autofocusCommentInput; - - OBPostCommentsLinkedPage( - this.postComment, { - this.autofocusCommentInput: false, - }); - - @override - State createState() { - return OBPostCommentsLinkedPageState(); - } -} - -class OBPostCommentsLinkedPageState extends State - with SingleTickerProviderStateMixin { - UserService _userService; - UserPreferencesService _userPreferencesService; - ToastService _toastService; - ThemeService _themeService; - ThemeValueParserService _themeValueParserService; - Post _post; - AnimationController _animationController; - Animation _animation; - - GlobalKey _keyPostBody = GlobalKey(); - double _positionTopCommentSection; - ScrollController _postCommentsScrollController; - List _postComments = []; - bool _noMoreBottomItemsToLoad; - bool _noMoreTopItemsToLoad; - bool _needsBootstrap; - bool _shouldHideStackedLoadingScreen; - bool _startScrollWasInitialised; - PostCommentsSortType _currentSort; - FocusNode _commentInputFocusNode; - GlobalKey _refreshIndicatorKey; - static const OFFSET_TOP_HEADER = 64.0; - static const HEIGHT_POST_HEADER = 72.0; - static const HEIGHT_POST_REACTIONS = 35.0; - static const HEIGHT_POST_CIRCLES = 26.0; - static const HEIGHT_POST_ACTIONS = 46.0; - static const TOTAL_PADDING_POST_TEXT = 40.0; - static const HEIGHT_POST_DIVIDER = 5.5; - static const HEIGHT_SIZED_BOX = 16.0; - static const TOTAL_FIXED_OFFSET_Y = OFFSET_TOP_HEADER + - HEIGHT_POST_HEADER + - HEIGHT_POST_REACTIONS + - HEIGHT_POST_CIRCLES + - HEIGHT_POST_ACTIONS + - HEIGHT_SIZED_BOX + - HEIGHT_POST_DIVIDER; - static const LOAD_MORE_COMMENTS_COUNT = 5; - static const COUNT_MIN_INCLUDING_LINKED_COMMENT = 3; - static const COUNT_MAX_AFTER_LINKED_COMMENT = 2; - static const TOTAL_COMMENTS_IN_SLICE = - COUNT_MIN_INCLUDING_LINKED_COMMENT + COUNT_MAX_AFTER_LINKED_COMMENT; - - CancelableOperation _refreshCommentsOperation; - CancelableOperation _refreshCommentsSliceOperation; - CancelableOperation _refreshCommentsWithCreatedPostCommentVisibleOperation; - CancelableOperation _refreshPostOperation; - CancelableOperation _loadMoreBottomCommentsOperation; - CancelableOperation _loadMoreTopCommentsOperation; - CancelableOperation _toggleSortCommentsOperation; - - @override - void initState() { - super.initState(); - _post = widget.postComment.post; - _needsBootstrap = true; - _postComments = []; - _noMoreBottomItemsToLoad = true; - _currentSort = PostCommentsSortType.dec; - _noMoreTopItemsToLoad = false; - _startScrollWasInitialised = false; - _shouldHideStackedLoadingScreen = false; - _commentInputFocusNode = FocusNode(); - _refreshIndicatorKey = GlobalKey(); - _animationController = AnimationController( - duration: const Duration(milliseconds: 300), vsync: this); - _animation = new Tween(begin: 1.0, end: 0.0).animate(_animationController); - _animation.addStatusListener(_onAnimationStatusChanged); - } - - @override - Widget build(BuildContext context) { - if (_needsBootstrap) { - var provider = OpenbookProvider.of(context); - _userService = provider.userService; - _userPreferencesService = provider.userPreferencesService; - _toastService = provider.toastService; - _themeValueParserService = provider.themeValueParserService; - _themeService = provider.themeService; - _bootstrap(); - _needsBootstrap = false; - } - - return OBCupertinoPageScaffold( - backgroundColor: Color.fromARGB(0, 0, 0, 0), - navigationBar: OBThemedNavigationBar( - title: 'Post comments', - ), - child: OBPrimaryColorContainer( - child: Stack( - children: _getStackChildren(), - ), - )); - } - - void _bootstrap() async { - await _setPostCommentsSortTypeFromPreferences(); - await _refreshPost(); - await _refreshCommentsSlice(); - } - - Future _setPostCommentsSortTypeFromPreferences() async { - PostCommentsSortType sortType = - await _userPreferencesService.getPostCommentsSortType(); - _currentSort = sortType; - } - - void dispose() { - super.dispose(); - _animation.removeStatusListener(_onAnimationStatusChanged); - if (_refreshCommentsOperation != null) _refreshCommentsOperation.cancel(); - if (_refreshCommentsSliceOperation != null) - _refreshCommentsSliceOperation.cancel(); - if (_loadMoreBottomCommentsOperation != null) - _loadMoreBottomCommentsOperation.cancel(); - if (_refreshPostOperation != null) _refreshPostOperation.cancel(); - if (_toggleSortCommentsOperation != null) - _toggleSortCommentsOperation.cancel(); - if (_loadMoreTopCommentsOperation != null) - _loadMoreTopCommentsOperation.cancel(); - if (_refreshCommentsWithCreatedPostCommentVisibleOperation != null) - _refreshCommentsWithCreatedPostCommentVisibleOperation.cancel(); - } - - void _onAnimationStatusChanged(status) { - if (status == AnimationStatus.completed) { - setState(() { - _shouldHideStackedLoadingScreen = true; - }); - } - } - - List _getStackChildren() { - var theme = _themeService.getActiveTheme(); - var primaryColor = _themeValueParserService.parseColor(theme.primaryColor); - - List _stackChildren = []; - - if (_shouldHideStackedLoadingScreen) { - _stackChildren.add(Column( - children: _getColumnChildren(), - )); - } else { - _stackChildren.addAll([ - Column( - children: _getColumnChildren(), - ), - Positioned( - top: 0.0, - left: 0.0, - right: 0.0, - bottom: 0, - child: IgnorePointer( - ignoring: true, - child: FadeTransition( - opacity: _animation, - child: DecoratedBox( - decoration: BoxDecoration(color: primaryColor), - child: Center( - child: CircularProgressIndicator( - strokeWidth: 2.0, - ), - ), - ), - )), - ) - ]); - } - - return _stackChildren; - } - - List _getColumnChildren() { - List _columnChildren = []; - _postCommentsScrollController = ScrollController( - initialScrollOffset: _calculatePositionTopCommentSection()); - _columnChildren.addAll([ - Expanded( - child: RefreshIndicator( - child: GestureDetector( - key: _refreshIndicatorKey, - onTap: _unfocusCommentInput, - child: LoadMore( - whenEmptyLoad: false, - isFinish: _noMoreBottomItemsToLoad, - delegate: OBInfinitePostCommentsLoadMoreDelegate(), - child: ListView.builder( - physics: const ClampingScrollPhysics(), - controller: _postCommentsScrollController, - padding: EdgeInsets.all(0), - itemCount: _postComments.length + 1, - itemBuilder: (context, index) { - if (index == 0) { - return _getPostPreview(); - } else { - return _getCommentTile(index); - } - }), - onLoadMore: _loadMoreBottomComments), - ), - onRefresh: _onWantsToRefreshComments), - ), - OBPostCommenter( - _post, - autofocus: widget.autofocusCommentInput, - commentTextFieldFocusNode: _commentInputFocusNode, - onPostCommentCreated: _refreshCommentsWithCreatedPostCommentVisible, - ) - ]); - - return _columnChildren; - } - - Widget _getCommentTile(int index) { - int commentIndex = index - 1; - var postComment = _postComments[commentIndex]; - var onPostCommentDeletedCallback = () { - _removePostCommentAtIndex(commentIndex); - }; - - if (_animationController.status != AnimationStatus.completed && - !_startScrollWasInitialised) { - Future.delayed(Duration(milliseconds: 0), () { - _postCommentsScrollController.animateTo( - _positionTopCommentSection - 100.0, - duration: Duration(milliseconds: 5), - curve: Curves.easeIn); - }); - } - - if (commentIndex == 0) { - _animationController.forward(); - Future.delayed(Duration(milliseconds: 0), () { - if (!_startScrollWasInitialised) { - setState(() { - _startScrollWasInitialised = true; - }); - } - }); - } - - if (postComment.id == widget.postComment.id) { - var theme = _themeService.getActiveTheme(); - var primaryColor = - _themeValueParserService.parseColor(theme.primaryColor); - final bool isDarkPrimaryColor = primaryColor.computeLuminance() < 0.179; - return DecoratedBox( - decoration: BoxDecoration( - color: isDarkPrimaryColor - ? Color.fromARGB(20, 255, 255, 255) - : Color.fromARGB(10, 0, 0, 0), - ), - child: OBPostComment( - key: Key('postComment#${postComment.id}'), - postComment: postComment, - post: _post, - onPostCommentDeletedCallback: onPostCommentDeletedCallback, - ), - ); - } else { - return OBPostComment( - key: Key('postComment#${postComment.id}'), - postComment: postComment, - post: _post, - onPostCommentDeletedCallback: onPostCommentDeletedCallback, - ); - } - } - - Widget _getPostPreview() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - OBPostHeader( - post: _post, - onPostDeleted: _onPostDeleted, - ), - Container( - key: _keyPostBody, - child: OBPostBody(_post), - ), - OBPostReactions(_post), - OBPostCircles(_post), - OBPostActions( - _post, - onWantsToCommentPost: _focusCommentInput, - ), - const SizedBox( - height: 16, - ), - OBPostDivider(), - _buildLoadTopCommentsBar(), - ], - ); - } - - Future _refreshCommentsSlice() async { - if (_refreshCommentsSliceOperation != null) - _refreshCommentsSliceOperation.cancel(); - try { - _refreshCommentsSliceOperation = CancelableOperation.fromFuture( - _userService.getCommentsForPost(_post, - minId: widget.postComment.id, - maxId: widget.postComment.id, - countMax: COUNT_MAX_AFTER_LINKED_COMMENT, - countMin: COUNT_MIN_INCLUDING_LINKED_COMMENT, - sort: _currentSort)); - - _postComments = (await _refreshCommentsSliceOperation.value).comments; - _setPostComments(_postComments); - _checkIfMoreTopItemsToLoad(); - _setNoMoreBottomItemsToLoad(false); - } catch (error) { - _onError(error); - } finally { - _refreshCommentsSliceOperation = null; - } - } - - void _setPositionTopCommentSection() { - setState(() { - _positionTopCommentSection = _calculatePositionTopCommentSection(); - }); - } - - Future _refreshPost() async { - if (_refreshPostOperation != null) _refreshPostOperation.cancel(); - try { - // This will trigger the updateSubject of the post - _refreshPostOperation = CancelableOperation.fromFuture( - _userService.getPostWithUuid(_post.uuid)); - - await _refreshPostOperation.value; - _setPositionTopCommentSection(); - } catch (error) { - _onError(error); - } finally { - _refreshPostOperation = null; - } - } - - Future _loadMoreBottomComments() async { - if (_loadMoreBottomCommentsOperation != null) - _loadMoreBottomCommentsOperation.cancel(); - if (_postComments.length == 0) return true; - - PostComment lastPost = _postComments.last; - int lastPostId = lastPost.id; - List moreComments; - try { - if (_currentSort == PostCommentsSortType.dec) { - _loadMoreBottomCommentsOperation = CancelableOperation.fromFuture( - _userService.getCommentsForPost(_post, - countMax: LOAD_MORE_COMMENTS_COUNT, - maxId: lastPostId, - sort: _currentSort)); - } else { - _loadMoreBottomCommentsOperation = CancelableOperation.fromFuture( - _userService.getCommentsForPost(_post, - countMin: LOAD_MORE_COMMENTS_COUNT, - minId: lastPostId + 1, - sort: _currentSort)); - } - - moreComments = (await _loadMoreBottomCommentsOperation.value).comments; - - if (moreComments.length == 0) { - _setNoMoreBottomItemsToLoad(true); - } else { - _addPostComments(moreComments); - } - return true; - } catch (error) { - _onError(error); - } finally { - _loadMoreBottomCommentsOperation = null; - } - - return false; - } - - Future _loadMoreTopComments() async { - if (_loadMoreTopCommentsOperation != null) - _loadMoreTopCommentsOperation.cancel(); - if (_postComments.length == 0) return true; - - List topComments; - PostComment firstPost = _postComments.first; - int firstPostId = firstPost.id; - try { - if (_currentSort == PostCommentsSortType.dec) { - _loadMoreTopCommentsOperation = CancelableOperation.fromFuture( - _userService.getCommentsForPost(_post, - sort: PostCommentsSortType.dec, - countMin: LOAD_MORE_COMMENTS_COUNT, - minId: firstPostId + 1)); - } else if (_currentSort == PostCommentsSortType.asc) { - _loadMoreTopCommentsOperation = CancelableOperation.fromFuture( - _userService.getCommentsForPost(_post, - sort: PostCommentsSortType.asc, - countMax: LOAD_MORE_COMMENTS_COUNT, - maxId: firstPostId)); - } - - topComments = (await _loadMoreTopCommentsOperation.value).comments; - - if (topComments.length < LOAD_MORE_COMMENTS_COUNT && - topComments.length != 0) { - _setNoMoreTopItemsToLoad(true); - _addToStartPostComments(topComments); - } else if (topComments.length == LOAD_MORE_COMMENTS_COUNT) { - _addToStartPostComments(topComments); - } else { - _setNoMoreTopItemsToLoad(true); - _showNoMoreTopItemsToLoadToast(); - } - return true; - } catch (error) { - _onError(error); - } finally { - _loadMoreTopCommentsOperation = null; - } - - return false; - } - - void _removePostCommentAtIndex(int index) { - setState(() { - _postComments.removeAt(index); - }); - } - - void _refreshCommentsWithCreatedPostCommentVisible( - PostComment createdPostComment) async { - if (_refreshCommentsWithCreatedPostCommentVisibleOperation != null) - _refreshCommentsWithCreatedPostCommentVisibleOperation.cancel(); - _unfocusCommentInput(); - List comments; - int createdCommentId = createdPostComment.id; - try { - if (_currentSort == PostCommentsSortType.dec) { - _refreshCommentsWithCreatedPostCommentVisibleOperation = - CancelableOperation.fromFuture(_userService.getCommentsForPost( - _post, - sort: PostCommentsSortType.dec, - countMin: LOAD_MORE_COMMENTS_COUNT, - minId: createdCommentId)); - } else if (_currentSort == PostCommentsSortType.asc) { - _refreshCommentsWithCreatedPostCommentVisibleOperation = - CancelableOperation.fromFuture(_userService.getCommentsForPost( - _post, - sort: PostCommentsSortType.asc, - countMax: LOAD_MORE_COMMENTS_COUNT, - maxId: createdCommentId + 1)); - } - - comments = - (await _refreshCommentsWithCreatedPostCommentVisibleOperation.value) - .comments; - - _setPostComments(comments); - _setNoMoreTopItemsToLoad(false); - _setNoMoreBottomItemsToLoad(false); - _scrollToNewComment(); - } catch (error) { - _onError(error); - } finally { - _refreshCommentsWithCreatedPostCommentVisibleOperation = null; - } - } - - void _onPostDeleted(Post post) { - Navigator.of(context).pop(); - } - - void _checkIfMoreTopItemsToLoad() { - int linkedCommentId = widget.postComment.id; - Iterable listBeforeLinkedComment = []; - if (_currentSort == PostCommentsSortType.dec) { - listBeforeLinkedComment = - _postComments.where((comment) => comment.id > linkedCommentId); - } else if (_currentSort == PostCommentsSortType.asc) { - listBeforeLinkedComment = - _postComments.where((comment) => comment.id < linkedCommentId); - } - if (listBeforeLinkedComment.length < 2) { - _setNoMoreTopItemsToLoad(true); - } - } - - Future _onWantsToRefreshComments() async { - if (_refreshCommentsOperation != null) _refreshCommentsOperation.cancel(); - try { - _refreshCommentsOperation = CancelableOperation.fromFuture( - _userService.getCommentsForPost(_post, sort: _currentSort)); - _postComments = (await _refreshCommentsOperation.value).comments; - _setPostComments(_postComments); - _setNoMoreBottomItemsToLoad(false); - _setNoMoreTopItemsToLoad(true); - } catch (error) { - _onError(error); - } finally { - _refreshCommentsOperation = null; - } - } - - void _focusCommentInput() { - FocusScope.of(context).requestFocus(_commentInputFocusNode); - } - - void _unfocusCommentInput() { - FocusScope.of(context).requestFocus(new FocusNode()); - } - - void _addPostComments(List postComments) { - setState(() { - this._postComments.addAll(postComments); - }); - } - - void _addToStartPostComments(List postComments) { - postComments.reversed.forEach((comment) { - setState(() { - this._postComments.insert(0, comment); - }); - }); - } - - void _setPostComments(List postComments) { - setState(() { - this._postComments = postComments; - }); - } - - void _setNoMoreBottomItemsToLoad(bool noMoreItemsToLoad) { - setState(() { - _noMoreBottomItemsToLoad = noMoreItemsToLoad; - }); - } - - void _setNoMoreTopItemsToLoad(bool noMoreItemsToLoad) { - setState(() { - _noMoreTopItemsToLoad = noMoreItemsToLoad; - }); - } - - void _showNoMoreTopItemsToLoadToast() { - _toastService.info(context: context, message: 'No more comments to load'); - } - - void _setCurrentSortValue(PostCommentsSortType newSortValue) { - setState(() { - _currentSort = newSortValue; - }); - } - - void _scrollToNewComment() { - if (_currentSort == PostCommentsSortType.asc) { - _postCommentsScrollController.animateTo(10000, - duration: Duration(milliseconds: 5), curve: Curves.easeIn); - } else if (_currentSort == PostCommentsSortType.dec) { - _postCommentsScrollController.animateTo( - _positionTopCommentSection - 200.0, - duration: Duration(milliseconds: 5), - curve: Curves.easeIn); - } - } - - void _onWantsToToggleSortComments() async { - PostCommentsSortType newSortType; - - if (_currentSort == PostCommentsSortType.asc) { - newSortType = PostCommentsSortType.dec; - } else { - newSortType = PostCommentsSortType.asc; - } - _userPreferencesService.setPostCommentsSortType(newSortType); - _setCurrentSortValue(newSortType); - _onWantsToRefreshComments(); - } - - void _onError(error) async { - if (error is HttpieConnectionRefusedError) { - _toastService.error( - message: error.toHumanReadableMessage(), context: context); - } else if (error is HttpieRequestError) { - String errorMessage = await error.toHumanReadableMessage(); - _toastService.error(message: errorMessage, context: context); - } else { - _toastService.error(message: 'Unknown error', context: context); - throw error; - } - } - - double _calculatePositionTopCommentSection() { - double aspectRatio; - double finalMediaScreenHeight = 0.0; - double finalTextHeight = 0.0; - double totalOffsetY = 0.0; - double screenWidth = MediaQuery.of(context).size.width; - if (_post.hasImage()) { - aspectRatio = _post.getImageWidth() / _post.getImageHeight(); - finalMediaScreenHeight = screenWidth / aspectRatio; - } - if (_post.hasVideo()) { - aspectRatio = _post.getVideoWidth() / _post.getVideoHeight(); - finalMediaScreenHeight = screenWidth / aspectRatio; - } - - if (_post.hasText()) { - TextStyle style = TextStyle(fontSize: 16.0); - TextSpan text = new TextSpan(text: _post.text, style: style); - - TextPainter textPainter = new TextPainter( - text: text, - textDirection: TextDirection.ltr, - textAlign: TextAlign.left, - ); - textPainter.layout( - maxWidth: screenWidth - 40.0); //padding is 20 in OBPostBodyText - finalTextHeight = textPainter.size.height + TOTAL_PADDING_POST_TEXT; - } - - if (_post.hasCircles() || - (_post.isEncircled != null && _post.isEncircled)) { - totalOffsetY = totalOffsetY + HEIGHT_POST_CIRCLES; - } - - totalOffsetY = totalOffsetY + - finalMediaScreenHeight + - finalTextHeight + - TOTAL_FIXED_OFFSET_Y; - - return totalOffsetY; - } - - Widget _buildLoadTopCommentsBar() { - var theme = _themeService.getActiveTheme(); - - if (_noMoreTopItemsToLoad) { - return Container( - padding: EdgeInsets.symmetric(horizontal: 0.0, vertical: 10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Padding( - padding: EdgeInsets.fromLTRB(10.0, 0.0, 0.0, 0.0), - child: OBSecondaryText( - _postComments.length > 0 - ? _currentSort == PostCommentsSortType.dec - ? 'Newest comments' - : 'Oldest comments' - : 'Be the first to comment!', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0), - ), - ), - ), - Expanded( - child: FlatButton( - child: OBText( - _postComments.length > 0 - ? _currentSort == PostCommentsSortType.dec - ? 'See oldest comments' - : 'See newest comments' - : '', - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: _themeValueParserService - .parseGradient(theme.primaryAccentColor) - .colors[1], - fontWeight: FontWeight.bold), - ), - onPressed: _onWantsToToggleSortComments), - ), - ], - ), - ); - } else { - return Container( - padding: EdgeInsets.symmetric(horizontal: 0.0, vertical: 10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - flex: 4, - child: FlatButton( - child: Row( - children: [ - OBIcon(OBIcons.arrowUp), - const SizedBox(width: 10.0), - OBText( - _currentSort == PostCommentsSortType.dec - ? 'Newer' - : 'Older', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), - onPressed: _loadMoreTopComments), - ), - Expanded( - flex: 6, - child: FlatButton( - child: OBText( - _currentSort == PostCommentsSortType.dec - ? 'View newest comments' - : 'View oldest comments', - style: TextStyle( - color: _themeValueParserService - .parseGradient(theme.primaryAccentColor) - .colors[1], - fontWeight: FontWeight.bold), - overflow: TextOverflow.ellipsis, - ), - onPressed: _onWantsToRefreshComments), - ), - ], - ), - ); - } - } -} - -class OBInfinitePostCommentsLoadMoreDelegate extends LoadMoreDelegate { - const OBInfinitePostCommentsLoadMoreDelegate(); - - @override - Widget buildChild(LoadMoreStatus status, - {LoadMoreTextBuilder builder = DefaultLoadMoreTextBuilder.english}) { - String text = builder(status); - - if (status == LoadMoreStatus.fail) { - return SizedBox( - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.refresh), - const SizedBox( - width: 10.0, - ), - Text('Tap to retry loading comments.') - ], - ), - ); - } - if (status == LoadMoreStatus.idle) { - // No clue why is this even a state. - return const SizedBox(); - } - if (status == LoadMoreStatus.loading) { - return SizedBox( - child: Center( - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: 20.0, - maxWidth: 20.0, - ), - child: CircularProgressIndicator( - strokeWidth: 2.0, - ), - ), - )); - } - if (status == LoadMoreStatus.nomore) { - return const SizedBox(); - } - - return Text(text); - } -} diff --git a/lib/pages/home/pages/post_comments/post_comments_page.dart b/lib/pages/home/pages/post_comments/post_comments_page.dart new file mode 100644 index 000000000..69bc13799 --- /dev/null +++ b/lib/pages/home/pages/post_comments/post_comments_page.dart @@ -0,0 +1,627 @@ +import 'package:Openbook/models/post.dart'; +import 'package:Openbook/models/post_comment.dart'; +import 'package:Openbook/pages/home/pages/post_comments/post_comments_page_controller.dart'; +import 'package:Openbook/pages/home/pages/post_comments/widgets/post-commenter.dart'; +import 'package:Openbook/pages/home/pages/post_comments/widgets/post_comment/post_comment.dart'; +import 'package:Openbook/pages/home/pages/post_comments/widgets/post_comment/widgets/post_comment_tile.dart'; +import 'package:Openbook/pages/home/pages/post_comments/widgets/post_comments_header_bar.dart'; +import 'package:Openbook/pages/home/pages/post_comments/widgets/post_preview.dart'; +import 'package:Openbook/services/theme.dart'; +import 'package:Openbook/services/theme_value_parser.dart'; +import 'package:Openbook/services/user_preferences.dart'; +import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart'; +import 'package:Openbook/widgets/page_scaffold.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/toast.dart'; +import 'package:Openbook/services/user.dart'; +import 'package:Openbook/widgets/theming/post_divider.dart'; +import 'package:Openbook/widgets/theming/primary_color_container.dart'; +import 'package:async/async.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:Openbook/widgets/load_more.dart'; +import 'package:Openbook/services/httpie.dart'; + +class OBPostCommentsPage extends StatefulWidget { + final PostComment linkedPostComment; + final Post post; + final PostCommentsPageType pageType; + final bool autofocusCommentInput; + final bool showPostPreview; + final PostComment postComment; + Function(PostComment) onCommentDeleted; + Function(PostComment) onCommentAdded; + + OBPostCommentsPage({ + @required this.pageType, + this.post, + this.linkedPostComment, + this.postComment, + this.onCommentDeleted, + this.onCommentAdded, + this.showPostPreview, + this.autofocusCommentInput: false, + }); + + @override + State createState() { + return OBPostCommentsPageState(); + } +} + +class OBPostCommentsPageState extends State + with SingleTickerProviderStateMixin { + UserService _userService; + UserPreferencesService _userPreferencesService; + ToastService _toastService; + ThemeService _themeService; + ThemeValueParserService _themeValueParserService; + Post _post; + AnimationController _animationController; + Animation _animation; + + double _positionTopCommentSection; + ScrollController _postCommentsScrollController; + List _postComments = []; + bool _noMoreBottomItemsToLoad; + bool _noMoreTopItemsToLoad; + bool _needsBootstrap; + bool _shouldHideStackedLoadingScreen; + bool _startScrollWasInitialised; + PostCommentsSortType _currentSort; + FocusNode _commentInputFocusNode; + GlobalKey _refreshIndicatorKey; + OBPostCommentsPageController _commentsPageController; + Map _pageTextMap; + + static const OFFSET_TOP_HEADER = 64.0; + static const HEIGHT_POST_HEADER = 72.0; + static const HEIGHT_POST_REACTIONS = 35.0; + static const HEIGHT_POST_CIRCLES = 26.0; + static const HEIGHT_POST_ACTIONS = 46.0; + static const TOTAL_PADDING_POST_TEXT = 40.0; + static const HEIGHT_POST_DIVIDER = 5.5; + static const HEIGHT_SIZED_BOX = 16.0; + static const TOTAL_FIXED_OFFSET_Y = OFFSET_TOP_HEADER + + HEIGHT_POST_HEADER + + HEIGHT_POST_REACTIONS + + HEIGHT_POST_CIRCLES + + HEIGHT_POST_ACTIONS + + HEIGHT_SIZED_BOX + + HEIGHT_POST_DIVIDER; + + static const PAGE_COMMENTS_TEXT_MAP = { + 'TITLE': 'Post comments', + 'NO_MORE_TO_LOAD': 'No more comments to load', + 'TAP_TO_RETRY': 'Tap to retry loading comments.', + }; + + static const PAGE_REPLIES_TEXT_MAP = { + 'TITLE': 'Post replies', + 'NO_MORE_TO_LOAD': 'No more replies to load', + 'TAP_TO_RETRY': 'Tap to retry loading replies.', + }; + + CancelableOperation _refreshPostOperation; + + @override + void initState() { + super.initState(); + if (widget.linkedPostComment != null) _post = widget.linkedPostComment.post; + if (widget.post != null) _post = widget.post; + if (widget.pageType == PostCommentsPageType.comments) { + _pageTextMap = PAGE_COMMENTS_TEXT_MAP; + } else { + _pageTextMap = PAGE_REPLIES_TEXT_MAP; + } + _needsBootstrap = true; + _postComments = []; + _noMoreBottomItemsToLoad = true; + _currentSort = PostCommentsSortType.dec; + _noMoreTopItemsToLoad = false; + _startScrollWasInitialised = false; + _shouldHideStackedLoadingScreen = false; + _commentInputFocusNode = FocusNode(); + _refreshIndicatorKey = GlobalKey(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), vsync: this); + _animation = new Tween(begin: 1.0, end: 0.0).animate(_animationController); + _animation.addStatusListener(_onAnimationStatusChanged); + } + + @override + Widget build(BuildContext context) { + if (_needsBootstrap) { + var provider = OpenbookProvider.of(context); + _userService = provider.userService; + _userPreferencesService = provider.userPreferencesService; + _toastService = provider.toastService; + _themeValueParserService = provider.themeValueParserService; + _themeService = provider.themeService; + _bootstrap(); + _needsBootstrap = false; + } + + return OBCupertinoPageScaffold( + backgroundColor: Color.fromARGB(0, 0, 0, 0), + navigationBar: OBThemedNavigationBar( + title: _pageTextMap['TITLE'], + ), + child: OBPrimaryColorContainer( + child: Stack( + children: _getStackChildren(), + ), + )); + } + + void _bootstrap() async { + await _setPostCommentsSortTypeFromPreferences(); + _initialiseCommentsPageController(); + if (widget.post != null) await _refreshPost(); + } + + Future _setPostCommentsSortTypeFromPreferences() async { + PostCommentsSortType sortType = + await _userPreferencesService.getPostCommentsSortType(); + _currentSort = sortType; + } + + void _initialiseCommentsPageController() { + _commentsPageController = OBPostCommentsPageController( + pageType: widget.pageType, + userService: _userService, + userPreferencesService: _userPreferencesService, + currentSort: _currentSort, + post: _post, + postComment: widget.postComment, + linkedPostComment: widget.linkedPostComment, + addPostComments: _addPostComments, + addToStartPostComments: _addToStartPostComments, + setPostComments: _setPostComments, + setCurrentSortValue: _setCurrentSortValue, + setNoMoreBottomItemsToLoad: _setNoMoreBottomItemsToLoad, + setNoMoreTopItemsToLoad: _setNoMoreTopItemsToLoad, + showNoMoreTopItemsToLoadToast: _showNoMoreTopItemsToLoadToast, + scrollToNewComment: _scrollToNewComment, + scrollToTop: _scrollToTop, + unfocusCommentInput: _unfocusCommentInput, + onError: _onError); + } + + void dispose() { + super.dispose(); + _animation.removeStatusListener(_onAnimationStatusChanged); + _commentsPageController.dispose(); + } + + void _onAnimationStatusChanged(status) { + if (status == AnimationStatus.completed) { + setState(() { + _shouldHideStackedLoadingScreen = true; + }); + } + } + + List _getStackChildren() { + var theme = _themeService.getActiveTheme(); + var primaryColor = _themeValueParserService.parseColor(theme.primaryColor); + + List _stackChildren = []; + + if (_shouldHideStackedLoadingScreen) { + _stackChildren.add(Column( + children: _getColumnChildren(), + )); + } else { + _stackChildren.addAll([ + Column( + children: _getColumnChildren(), + ), + Positioned( + top: 0.0, + left: 0.0, + right: 0.0, + bottom: 0, + child: IgnorePointer( + ignoring: true, + child: FadeTransition( + opacity: _animation, + child: DecoratedBox( + decoration: BoxDecoration(color: primaryColor), + child: Center( + child: CircularProgressIndicator( + strokeWidth: 2.0, + ), + ), + ), + )), + ) + ]); + } + + return _stackChildren; + } + + List _getColumnChildren() { + List _columnChildren = []; + _postCommentsScrollController = ScrollController( + initialScrollOffset: _calculatePositionTopCommentSection()); + _columnChildren.addAll([ + Expanded( + child: RefreshIndicator( + key: _refreshIndicatorKey, + child: GestureDetector( + onTap: _unfocusCommentInput, + child: LoadMore( + whenEmptyLoad: false, + isFinish: _noMoreBottomItemsToLoad, + delegate: + OBInfinitePostCommentsLoadMoreDelegate(_pageTextMap), + child: ListView.builder( + shrinkWrap: true, + physics: const AlwaysScrollableScrollPhysics(), + controller: _postCommentsScrollController, + padding: EdgeInsets.all(0), + itemCount: _postComments.length + 1, + itemBuilder: (context, index) { + if (index == 0) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _getPostPreview(), + _getCommentPreview(), + _getDivider(), + OBPostCommentsHeaderBar( + pageType: widget.pageType, + noMoreTopItemsToLoad: _noMoreTopItemsToLoad, + postComments: _postComments, + currentSort: _currentSort, + onWantsToToggleSortComments: () => + _commentsPageController + .onWantsToToggleSortComments(), + loadMoreTopComments: () => + _commentsPageController + .loadMoreTopComments(), + onWantsToRefreshComments: () => + _commentsPageController + .onWantsToRefreshComments()), + ], + ); + } else { + return _getCommentTile(index); + } + }), + onLoadMore: () => + _commentsPageController.loadMoreBottomComments()), + ), + onRefresh: () => + _commentsPageController.onWantsToRefreshComments()), + ), + OBPostCommenter( + _post, + postComment: widget.postComment, + autofocus: widget.autofocusCommentInput, + commentTextFieldFocusNode: _commentInputFocusNode, + onPostCommentCreated: (PostComment createdPostComment) { + _commentsPageController + .refreshCommentsWithCreatedPostCommentVisible(createdPostComment); + if (widget.onCommentAdded != null) { + widget.onCommentAdded(createdPostComment); + } + }, + ) + ]); + + return _columnChildren; + } + + Widget _getDivider() { + if (widget.postComment != null) { + return OBPostDivider(); + } + return SizedBox(); + } + + Widget _getCommentPreview() { + if (widget.postComment == null) { + return SizedBox(); + } + return OBPostCommentTile( + post: widget.post, postComment: widget.postComment); + } + + Widget _getCommentTile(int index) { + int commentIndex = index - 1; + var postComment = _postComments[commentIndex]; + var onPostCommentDeletedCallback = (PostComment comment) { + _removePostCommentAtIndex(commentIndex); + if (widget.onCommentDeleted != null) widget.onCommentDeleted(postComment); + }; + + if (_animationController.status != AnimationStatus.completed && + !_startScrollWasInitialised && + widget.linkedPostComment != null) { + Future.delayed(Duration(milliseconds: 0), () { + _postCommentsScrollController.animateTo( + _positionTopCommentSection - 100.0, + duration: Duration(milliseconds: 5), + curve: Curves.easeIn); + }); + } + + if (commentIndex == 0) { + _animationController.forward(); + Future.delayed(Duration(milliseconds: 0), () { + if (!_startScrollWasInitialised) { + setState(() { + _startScrollWasInitialised = true; + }); + } + }); + } + + if (widget.linkedPostComment != null && + postComment.id == widget.linkedPostComment.id) { + var theme = _themeService.getActiveTheme(); + var primaryColor = + _themeValueParserService.parseColor(theme.primaryColor); + final bool isDarkPrimaryColor = primaryColor.computeLuminance() < 0.179; + return DecoratedBox( + decoration: BoxDecoration( + color: isDarkPrimaryColor + ? Color.fromARGB(20, 255, 255, 255) + : Color.fromARGB(10, 0, 0, 0), + ), + child: OBPostComment( + key: Key('postComment#${widget.pageType}#${postComment.id}'), + postComment: postComment, + post: _post, + onPostCommentDeletedCallback: onPostCommentDeletedCallback, + onPostCommentReported: onPostCommentDeletedCallback, + ), + ); + } else { + return OBPostComment( + key: Key('postComment#${widget.pageType}#${postComment.id}'), + postComment: postComment, + post: _post, + onPostCommentDeletedCallback: onPostCommentDeletedCallback, + onPostCommentReported: onPostCommentDeletedCallback, + ); + } + } + + Widget _getPostPreview() { + if (widget.post == null || !widget.showPostPreview) { + return SizedBox(); + } + + return OBPostPreview( + post: _post, + onPostDeleted: _onPostDeleted, + focusCommentInput: _focusCommentInput, + ); + } + + void _scrollToTop() { + Future.delayed(Duration(milliseconds: 0), () { + _postCommentsScrollController.animateTo( + 0.0, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 300), + ); + }); + } + + void _setPositionTopCommentSection() { + setState(() { + _positionTopCommentSection = _calculatePositionTopCommentSection(); + }); + } + + Future _refreshPost() async { + if (_refreshPostOperation != null) _refreshPostOperation.cancel(); + try { + // This will trigger the updateSubject of the post + _refreshPostOperation = CancelableOperation.fromFuture( + _userService.getPostWithUuid(_post.uuid)); + + await _refreshPostOperation.value; + _setPositionTopCommentSection(); + } catch (error) { + _onError(error); + } finally { + _refreshPostOperation = null; + } + } + + void _removePostCommentAtIndex(int index) { + setState(() { + _postComments.removeAt(index); + }); + } + + void _onPostDeleted(Post post) { + Navigator.of(context).pop(); + } + + void _focusCommentInput() { + FocusScope.of(context).requestFocus(_commentInputFocusNode); + } + + void _unfocusCommentInput() { + FocusScope.of(context).requestFocus(new FocusNode()); + } + + void _addPostComments(List postComments) { + setState(() { + this._postComments.addAll(postComments); + }); + _commentsPageController.updateControllerPostComments(this._postComments); + } + + void _addToStartPostComments(List postComments) { + postComments.reversed.forEach((comment) { + setState(() { + this._postComments.insert(0, comment); + }); + }); + _commentsPageController.updateControllerPostComments(this._postComments); + } + + void _setPostComments(List postComments) { + setState(() { + this._postComments = postComments; + }); + _commentsPageController.updateControllerPostComments(this._postComments); + if (this._postComments.length == 0) { + _animationController.forward(); + } + } + + void _setNoMoreBottomItemsToLoad(bool noMoreItemsToLoad) { + setState(() { + _noMoreBottomItemsToLoad = noMoreItemsToLoad; + }); + } + + void _setNoMoreTopItemsToLoad(bool noMoreItemsToLoad) { + setState(() { + _noMoreTopItemsToLoad = noMoreItemsToLoad; + }); + } + + void _showNoMoreTopItemsToLoadToast() { + _toastService.info( + context: context, message: _pageTextMap['NO_MORE_TO_LOAD']); + } + + void _setCurrentSortValue(PostCommentsSortType newSortValue) { + setState(() { + _currentSort = newSortValue; + }); + } + + void _scrollToNewComment() { + if (_currentSort == PostCommentsSortType.asc) { + _postCommentsScrollController.animateTo( + _postCommentsScrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 50), + curve: Curves.easeIn); + } else if (_currentSort == PostCommentsSortType.dec) { + _postCommentsScrollController.animateTo( + _positionTopCommentSection - 200.0, + duration: Duration(milliseconds: 5), + curve: Curves.easeIn); + } + } + + void _onError(error) async { + if (error is HttpieConnectionRefusedError) { + _toastService.error( + message: error.toHumanReadableMessage(), context: context); + } else if (error is HttpieRequestError) { + String errorMessage = await error.toHumanReadableMessage(); + _toastService.error(message: errorMessage, context: context); + } else { + _toastService.error(message: 'Unknown error', context: context); + throw error; + } + } + + double _calculatePositionTopCommentSection() { + double aspectRatio; + double finalMediaScreenHeight = 0.0; + double finalTextHeight = 0.0; + double totalOffsetY = 0.0; + + if (widget.post == null) return totalOffsetY; + + double screenWidth = MediaQuery.of(context).size.width; + if (_post.hasImage()) { + aspectRatio = _post.getImageWidth() / _post.getImageHeight(); + finalMediaScreenHeight = screenWidth / aspectRatio; + } + if (_post.hasVideo()) { + aspectRatio = _post.getVideoWidth() / _post.getVideoHeight(); + finalMediaScreenHeight = screenWidth / aspectRatio; + } + + if (_post.hasText()) { + TextStyle style = TextStyle(fontSize: 16.0); + TextSpan text = new TextSpan(text: _post.text, style: style); + + TextPainter textPainter = new TextPainter( + text: text, + textDirection: TextDirection.ltr, + textAlign: TextAlign.left, + ); + textPainter.layout( + maxWidth: screenWidth - 40.0); //padding is 20 in OBPostBodyText + finalTextHeight = textPainter.size.height + TOTAL_PADDING_POST_TEXT; + } + + if (_post.hasCircles() || + (_post.isEncircled != null && _post.isEncircled)) { + totalOffsetY = totalOffsetY + HEIGHT_POST_CIRCLES; + } + + totalOffsetY = totalOffsetY + + finalMediaScreenHeight + + finalTextHeight + + TOTAL_FIXED_OFFSET_Y; + + return totalOffsetY; + } +} + +class OBInfinitePostCommentsLoadMoreDelegate extends LoadMoreDelegate { + Map pageTextMap; + + OBInfinitePostCommentsLoadMoreDelegate(Map pageTextMap); + + @override + Widget buildChild(LoadMoreStatus status, + {LoadMoreTextBuilder builder = DefaultLoadMoreTextBuilder.english}) { + String text = builder(status); + + if (status == LoadMoreStatus.fail) { + return SizedBox( + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.refresh), + const SizedBox( + width: 10.0, + ), + Text(pageTextMap['TAP_TO_RETRY']) + ], + ), + ); + } + if (status == LoadMoreStatus.idle) { + // No clue why is this even a state. + return const SizedBox(); + } + if (status == LoadMoreStatus.loading) { + return SizedBox( + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: 20.0, + maxWidth: 20.0, + ), + child: CircularProgressIndicator( + strokeWidth: 2.0, + ), + ), + )); + } + if (status == LoadMoreStatus.nomore) { + return const SizedBox(); + } + + return Text(text); + } +} diff --git a/lib/pages/home/pages/post_comments/post_comments_page_controller.dart b/lib/pages/home/pages/post_comments/post_comments_page_controller.dart new file mode 100644 index 000000000..fcb251fbe --- /dev/null +++ b/lib/pages/home/pages/post_comments/post_comments_page_controller.dart @@ -0,0 +1,311 @@ +import 'package:Openbook/models/post.dart'; +import 'package:Openbook/models/post_comment.dart'; +import 'package:Openbook/models/post_comment_list.dart'; +import 'package:Openbook/services/user_preferences.dart'; +import 'package:Openbook/services/user.dart'; +import 'package:async/async.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class OBPostCommentsPageController { + final Post post; + final PostCommentsPageType pageType; + PostCommentsSortType currentSort; + Function(List) setPostComments; + Function(List) addPostComments; + Function(List) addToStartPostComments; + Function(PostCommentsSortType) setCurrentSortValue; + Function(bool) setNoMoreBottomItemsToLoad; + Function(bool) setNoMoreTopItemsToLoad; + VoidCallback showNoMoreTopItemsToLoadToast; + VoidCallback scrollToTop; + VoidCallback scrollToNewComment; + VoidCallback unfocusCommentInput; + Function(dynamic) onError; + UserService userService; + UserPreferencesService userPreferencesService; + + List postComments = []; + PostComment linkedPostComment; + PostComment postComment; + + static const LOAD_MORE_COMMENTS_COUNT = 5; + static const COUNT_MIN_INCLUDING_LINKED_COMMENT = 3; + static const COUNT_MAX_AFTER_LINKED_COMMENT = 2; + static const TOTAL_COMMENTS_IN_SLICE = + COUNT_MIN_INCLUDING_LINKED_COMMENT + COUNT_MAX_AFTER_LINKED_COMMENT; + + CancelableOperation _refreshCommentsOperation; + CancelableOperation _refreshCommentsSliceOperation; + CancelableOperation _refreshCommentsWithCreatedPostCommentVisibleOperation; + CancelableOperation _refreshPostOperation; + CancelableOperation _loadMoreBottomCommentsOperation; + CancelableOperation _loadMoreTopCommentsOperation; + CancelableOperation _toggleSortCommentsOperation; + + OBPostCommentsPageController({ + @required this.post, + @required this.pageType, + @required this.currentSort, + @required this.userService, + @required this.userPreferencesService, + @required this.setPostComments, + @required this.setCurrentSortValue, + @required this.setNoMoreBottomItemsToLoad, + @required this.setNoMoreTopItemsToLoad, + @required this.addPostComments, + @required this.addToStartPostComments, + @required this.showNoMoreTopItemsToLoadToast, + @required this.scrollToTop, + @required this.scrollToNewComment, + @required this.unfocusCommentInput, + @required this.onError, + this.linkedPostComment, + this.postComment + }) { + this.bootstrapController(); + } + + Future bootstrapController() async { + if (this.linkedPostComment != null) { + await this.refreshCommentsSlice(); + } else { + await this.refreshComments(); + } + } + + CancelableOperation retrieveObjects({int minId, int maxId, int countMax, + int countMin, PostCommentsSortType sort}) { + + if (this.pageType == PostCommentsPageType.comments) { + return CancelableOperation.fromFuture( + this.userService.getCommentsForPost(this.post, + sort: sort, + minId: minId, + maxId: maxId, + countMax: countMax, + countMin: countMin)); + + } else { + return CancelableOperation.fromFuture( + this.userService.getCommentRepliesForPostComment(this.post, this.postComment, + sort: sort, + minId: minId, + maxId: maxId, + countMax: countMax, + countMin: countMin)); + } + } + + void onWantsToToggleSortComments() async { + PostCommentsSortType newSortType; + if (currentSort == PostCommentsSortType.asc) { + newSortType = PostCommentsSortType.dec; + } else { + newSortType = PostCommentsSortType.asc; + } + this.userPreferencesService.setPostCommentsSortType(newSortType); + this.setNewSortValue(newSortType); + this.onWantsToRefreshComments(); + } + + Future onWantsToRefreshComments() async { + if (_refreshCommentsOperation != null) _refreshCommentsOperation.cancel(); + try { + _refreshCommentsOperation = this.retrieveObjects(sort: this.currentSort); + this.postComments = (await _refreshCommentsOperation.value).comments; + this.setPostComments(this.postComments); + this.setNoMoreBottomItemsToLoad(false); + this.setNoMoreTopItemsToLoad(true); + } catch (error) { + this.onError(error); + } finally { + _refreshCommentsOperation = null; + } + } + + Future refreshComments() async { + await this.onWantsToRefreshComments(); + this.scrollToTop(); + } + + Future loadMoreTopComments() async { + if (_loadMoreTopCommentsOperation != null) + _loadMoreTopCommentsOperation.cancel(); + if (this.postComments.length == 0) return true; + + List topComments; + PostComment firstPost = this.postComments.first; + int firstPostId = firstPost.id; + try { + if (this.currentSort == PostCommentsSortType.dec) { + _loadMoreTopCommentsOperation = this.retrieveObjects( + sort: PostCommentsSortType.dec, + countMin: LOAD_MORE_COMMENTS_COUNT, + minId: firstPostId + 1); + } else if (this.currentSort == PostCommentsSortType.asc) { + _loadMoreTopCommentsOperation = this.retrieveObjects( + sort: PostCommentsSortType.asc, + countMax: LOAD_MORE_COMMENTS_COUNT, + maxId: firstPostId); + } + + topComments = (await _loadMoreTopCommentsOperation.value).comments; + + if (topComments.length < LOAD_MORE_COMMENTS_COUNT && + topComments.length != 0) { + this.setNoMoreTopItemsToLoad(true); + this.addToStartPostComments(topComments); + } else if (topComments.length == LOAD_MORE_COMMENTS_COUNT) { + this.addToStartPostComments(topComments); + } else { + this.setNoMoreTopItemsToLoad(true); + this.showNoMoreTopItemsToLoadToast(); + } + return true; + } catch (error) { + this.onError(error); + } finally { + _loadMoreTopCommentsOperation = null; + } + + return false; + } + + Future loadMoreBottomComments() async { + if (_loadMoreBottomCommentsOperation != null) + _loadMoreBottomCommentsOperation.cancel(); + if (this.postComments.length == 0 || _refreshCommentsWithCreatedPostCommentVisibleOperation != null) return true; + + PostComment lastPost = this.postComments.last; + int lastPostId = lastPost.id; + List moreComments; + try { + if (this.currentSort == PostCommentsSortType.dec) { + _loadMoreBottomCommentsOperation = this.retrieveObjects( + countMax: LOAD_MORE_COMMENTS_COUNT, + maxId: lastPostId, + sort: this.currentSort); + } else { + _loadMoreBottomCommentsOperation = this.retrieveObjects( + countMin: LOAD_MORE_COMMENTS_COUNT, + minId: lastPostId + 1, + sort: this.currentSort); + } + + moreComments = (await _loadMoreBottomCommentsOperation.value).comments; + + if (moreComments.length == 0) { + this.setNoMoreBottomItemsToLoad(true); + } else { + this.addPostComments(moreComments); + } + return true; + } catch (error) { + this.onError(error); + } finally { + _loadMoreBottomCommentsOperation = null; + } + + return false; + } + + Future refreshCommentsSlice() async { + if (_refreshCommentsSliceOperation != null) + _refreshCommentsSliceOperation.cancel(); + try { + _refreshCommentsSliceOperation = this.retrieveObjects( + minId: this.linkedPostComment.id, + maxId: this.linkedPostComment.id, + countMax: COUNT_MAX_AFTER_LINKED_COMMENT, + countMin: COUNT_MIN_INCLUDING_LINKED_COMMENT, + sort: this.currentSort); + + this.postComments = (await _refreshCommentsSliceOperation.value).comments; + this.setPostComments(this.postComments); + this.checkIfMoreTopItemsToLoad(); + this.setNoMoreBottomItemsToLoad(false); + } catch (error) { + this.onError(error); + } finally { + _refreshCommentsSliceOperation = null; + } + } + + void checkIfMoreTopItemsToLoad() { + int linkedCommentId = this.linkedPostComment.id; + Iterable listBeforeLinkedComment = []; + if (this.currentSort == PostCommentsSortType.dec) { + listBeforeLinkedComment = + postComments.where((comment) => comment.id > linkedCommentId); + } else if (this.currentSort == PostCommentsSortType.asc) { + listBeforeLinkedComment = + postComments.where((comment) => comment.id < linkedCommentId); + } + if (listBeforeLinkedComment.length < 2) { + this.setNoMoreTopItemsToLoad(true); + } + } + + void refreshCommentsWithCreatedPostCommentVisible( + PostComment createdPostComment) async { + if (_refreshCommentsWithCreatedPostCommentVisibleOperation != null) + _refreshCommentsWithCreatedPostCommentVisibleOperation.cancel(); + this.unfocusCommentInput(); + List comments; + int createdCommentId = createdPostComment.id; + try { + if (this.currentSort == PostCommentsSortType.dec) { + _refreshCommentsWithCreatedPostCommentVisibleOperation = this.retrieveObjects( + sort: PostCommentsSortType.dec, + countMax: LOAD_MORE_COMMENTS_COUNT, + maxId: createdCommentId + 1); + this.setNoMoreTopItemsToLoad(true); + this.setNoMoreBottomItemsToLoad(false); + } else if (this.currentSort == PostCommentsSortType.asc) { + _refreshCommentsWithCreatedPostCommentVisibleOperation = this.retrieveObjects( + sort: PostCommentsSortType.asc, + countMax: LOAD_MORE_COMMENTS_COUNT, + maxId: createdCommentId + 1); + this.setNoMoreTopItemsToLoad(false); + this.setNoMoreBottomItemsToLoad(false); + } + comments = + (await _refreshCommentsWithCreatedPostCommentVisibleOperation.value) + .comments; + this.postComments = comments; + this.setPostComments(this.postComments); + this.scrollToNewComment(); + } catch (error) { + this.onError(error); + } finally { + _refreshCommentsWithCreatedPostCommentVisibleOperation = null; + } + } + + void setNewSortValue(PostCommentsSortType newSortType) { + this.currentSort = newSortType; + this.setCurrentSortValue(newSortType); + } + + void updateControllerPostComments(List comments) { + this.postComments = comments; + } + + void dispose() { + if (_refreshCommentsOperation != null) _refreshCommentsOperation.cancel(); + if (_refreshCommentsSliceOperation != null) + _refreshCommentsSliceOperation.cancel(); + if (_loadMoreBottomCommentsOperation != null) + _loadMoreBottomCommentsOperation.cancel(); + if (_refreshPostOperation != null) _refreshPostOperation.cancel(); + if (_toggleSortCommentsOperation != null) + _toggleSortCommentsOperation.cancel(); + if (_loadMoreTopCommentsOperation != null) + _loadMoreTopCommentsOperation.cancel(); + if (_refreshCommentsWithCreatedPostCommentVisibleOperation != null) + _refreshCommentsWithCreatedPostCommentVisibleOperation.cancel(); + } +} + +enum PostCommentsPageType { replies, comments } \ No newline at end of file diff --git a/lib/pages/home/pages/post_comments/widgets/post-commenter.dart b/lib/pages/home/pages/post_comments/widgets/post-commenter.dart index f2e627c3c..440847c78 100644 --- a/lib/pages/home/pages/post_comments/widgets/post-commenter.dart +++ b/lib/pages/home/pages/post_comments/widgets/post-commenter.dart @@ -16,13 +16,15 @@ import 'package:Openbook/services/httpie.dart'; class OBPostCommenter extends StatefulWidget { final Post post; + final PostComment postComment; final bool autofocus; final FocusNode commentTextFieldFocusNode; final OnPostCommentCreatedCallback onPostCommentCreated; final OnPostCommentWillBeCreatedCallback onPostCommentWillBeCreated; OBPostCommenter(this.post, - {this.autofocus = false, + {this.postComment, + this.autofocus = false, this.commentTextFieldFocusNode, this.onPostCommentCreated, this.onPostCommentWillBeCreated}); @@ -195,10 +197,16 @@ class OBPostCommenterState extends State { ? widget.onPostCommentWillBeCreated() : Future.value()); String commentText = _textController.text; - _submitFormOperation = CancelableOperation.fromFuture( - _userService.commentPost(text: commentText, post: widget.post)); + if (widget.postComment != null) { + _submitFormOperation = CancelableOperation.fromFuture( + _userService.replyPostComment(text: commentText, post: widget.post, postComment: widget.postComment)); + } else { + _submitFormOperation = CancelableOperation.fromFuture( + _userService.commentPost(text: commentText, post: widget.post)); + } + PostComment createdPostComment = await _submitFormOperation.value; - widget.post.incrementCommentsCount(); + if (createdPostComment.parentComment == null) widget.post.incrementCommentsCount(); _textController.clear(); _setFormWasSubmitted(false); _validateForm(); diff --git a/lib/pages/home/pages/post_comments/widgets/post_comment/post_comment.dart b/lib/pages/home/pages/post_comments/widgets/post_comment/post_comment.dart index b708f1c35..836fb4363 100644 --- a/lib/pages/home/pages/post_comments/widgets/post_comment/post_comment.dart +++ b/lib/pages/home/pages/post_comments/widgets/post_comment/post_comment.dart @@ -1,15 +1,13 @@ -import 'package:Openbook/models/community.dart'; import 'package:Openbook/models/post.dart'; import 'package:Openbook/models/post_comment.dart'; import 'package:Openbook/models/user.dart'; -import 'package:Openbook/pages/home/pages/post_comments/widgets/post_comment/widgets/post_comment_text.dart'; +import 'package:Openbook/pages/home/pages/post_comments/widgets/post_comment/widgets/post_comment_tile.dart'; import 'package:Openbook/provider.dart'; import 'package:Openbook/services/modal_service.dart'; import 'package:Openbook/services/navigation_service.dart'; import 'package:Openbook/services/toast.dart'; import 'package:Openbook/services/user.dart'; -import 'package:Openbook/widgets/avatars/avatar.dart'; -import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/services/user_preferences.dart'; import 'package:Openbook/widgets/theming/secondary_text.dart'; import 'package:async/async.dart'; import 'package:flutter/material.dart'; @@ -18,12 +16,14 @@ import 'package:flutter_slidable/flutter_slidable.dart'; class OBPostComment extends StatefulWidget { final PostComment postComment; final Post post; - final VoidCallback onPostCommentDeletedCallback; + final Function(PostComment) onPostCommentDeletedCallback; + final ValueChanged onPostCommentReported; OBPostComment( {@required this.post, @required this.postComment, this.onPostCommentDeletedCallback, + this.onPostCommentReported, Key key}) : super(key: key); @@ -36,9 +36,12 @@ class OBPostComment extends StatefulWidget { class OBPostCommentState extends State { NavigationService _navigationService; UserService _userService; + UserPreferencesService _userPreferencesService; ToastService _toastService; ModalService _modalService; bool _requestInProgress; + int _repliesCount; + List _replies; CancelableOperation _requestOperation; @@ -46,6 +49,8 @@ class OBPostCommentState extends State { void initState() { super.initState(); _requestInProgress = false; + _repliesCount = widget.postComment.repliesCount; + _replies = widget.postComment.getPostCommentReplies(); } @override @@ -59,12 +64,13 @@ class OBPostCommentState extends State { var provider = OpenbookProvider.of(context); _navigationService = provider.navigationService; _userService = provider.userService; + _userPreferencesService = provider.userPreferencesService; _toastService = provider.toastService; _modalService = provider.modalService; - Widget postTile = _buildPostCommentTile(widget.postComment); + Widget commentTile = OBPostCommentTile(post:widget.post, postComment: widget.postComment); Widget postComment = _buildPostCommentActions( - child: postTile, + child: commentTile, ); if (_requestInProgress) { @@ -76,66 +82,47 @@ class OBPostCommentState extends State { ); } - return postComment; - } - - Widget _buildPostCommentTile(PostComment postComment) { - return StreamBuilder( - stream: widget.postComment.updateSubject, - initialData: widget.postComment, - builder: (BuildContext context, AsyncSnapshot snapshot) { - PostComment postComment = snapshot.data; - - return Padding( - padding: - const EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - OBAvatar( - onPressed: () { - _navigationService.navigateToUserProfile( - user: postComment.commenter, context: context); - }, - size: OBAvatarSize.small, - avatarUrl: postComment.getCommenterProfileAvatar(), - ), - const SizedBox( - width: 20.0, - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - OBPostCommentText( - postComment, - badge: _getCommunityBadge(postComment), - onUsernamePressed: () { - _navigationService.navigateToUserProfile( - user: postComment.commenter, context: context); - }, - ), - const SizedBox( - height: 5.0, - ), - OBSecondaryText( - postComment.getRelativeCreated(), - style: TextStyle(fontSize: 12.0), - ) - ], - )) - ], - ), - ); - }); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + postComment, + _buildPostCommentReplies() + ], + ); } Widget _buildPostCommentActions({@required Widget child}) { - List _editCommentActions = []; + List _commentActions = []; User loggedInUser = _userService.getLoggedInUser(); - if (loggedInUser.canEditPostComment(widget.postComment)) { - _editCommentActions.add( + if (loggedInUser.canDeletePostComment(widget.post, widget.postComment)) { + _commentActions.add( + new IconSlideAction( + caption: 'Delete', + color: Colors.red, + icon: Icons.delete, + onTap: _deletePostComment, + ), + ); + } + + if (loggedInUser.canReportPostComment(widget.postComment)) { + _commentActions.add( + Opacity( + opacity: widget.postComment.isReported ?? false ? 0.5 : 1, + child: IconSlideAction( + caption: + widget.postComment.isReported ?? false ? 'Reported' : 'Report', + color: Colors.black38, + icon: Icons.report, + onTap: _reportPostComment, + ), + ), + ); + } + + if (loggedInUser.canEditPostComment(widget.postComment, widget.post)) { + _commentActions.add( new IconSlideAction( caption: 'Edit', color: Colors.blueGrey, @@ -145,14 +132,14 @@ class OBPostCommentState extends State { ); } - if (loggedInUser.canDeletePostComment(widget.post, widget.postComment)) { - _editCommentActions.add( - new IconSlideAction( - caption: 'Delete', - color: Colors.red, - icon: Icons.delete, - onTap: _deletePostComment, - ), + if (loggedInUser.canReplyPostComment(widget.postComment)) { + _commentActions.add( + new IconSlideAction( + caption: 'Reply', + color: Colors.blue, + icon: Icons.reply, + onTap: _replyPostComment, + ) ); } @@ -160,15 +147,115 @@ class OBPostCommentState extends State { delegate: new SlidableDrawerDelegate(), actionExtentRatio: 0.2, child: child, - secondaryActions: _editCommentActions, + secondaryActions: _commentActions, + ); + } + + Widget _buildPostCommentReplies() { + if (_repliesCount == 0) return SizedBox(); + return Padding( + padding: EdgeInsets.only(left: 30.0, top: 0.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListView.builder( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + padding: EdgeInsets.all(0), + itemCount: widget.postComment.getPostCommentReplies().length, + itemBuilder: (context, index) { + PostComment reply = widget.postComment.getPostCommentReplies()[index]; + + return OBPostComment( + key: Key('postCommentReply#${reply.id}'), + postComment: reply, + post: widget.post, + onPostCommentDeletedCallback: _onReplyDeleted, + ); + } + ), + _buildViewAllReplies() + ], + ) + ); + } + + Widget _buildViewAllReplies() { + if (!widget.postComment.hasReplies() || (_repliesCount == _replies.length)) { + return SizedBox(); + } + + return FlatButton( + child: OBSecondaryText('View all $_repliesCount replies', + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold), + ), + onPressed: _onWantsToViewAllReplies); + } + + void _onWantsToViewAllReplies() { + _navigationService.navigateToPostCommentReplies( + post: widget.post, + postComment: widget.postComment, + context: context, + onReplyDeleted: _onReplyDeleted, + onReplyAdded: _onReplyAdded ); } + void _onReplyDeleted(PostComment postCommentReply) async { + setState(() { + _repliesCount -= 1; + _replies.removeWhere((reply) => reply.id == postCommentReply.id); + }); + } + + void _onReplyAdded(PostComment postCommentReply) async { + PostCommentsSortType sortType = await _userPreferencesService.getPostCommentsSortType(); + setState(() { + if (sortType == PostCommentsSortType.dec) { + _replies.insert(0, postCommentReply); + } else if (_repliesCount == _replies.length) { + _replies.add(postCommentReply); + } + _repliesCount += 1; + }); + } + void _editPostComment() async { await _modalService.openExpandedCommenter( context: context, post: widget.post, postComment: widget.postComment); } + void _reportPostComment() async { + await _navigationService.navigateToReportObject( + context: context, + object: widget.postComment, + extraData: {'post': widget.post}, + onObjectReported: (dynamic reportedObject) { + if (widget.onPostCommentReported != null && reportedObject != null) + widget.onPostCommentReported(reportedObject as PostComment); + }); + } + + void _replyPostComment() async { + PostComment comment = await _modalService.openExpandedReplyCommenter( + context: context, + post: widget.post, + postComment: widget.postComment, + onReplyDeleted: _onReplyDeleted, + onReplyAdded: _onReplyAdded); + if (comment != null) { + await _navigationService.navigateToPostCommentReplies( + post: widget.post, + postComment: widget.postComment, + onReplyAdded: _onReplyAdded, + onReplyDeleted: _onReplyDeleted, + context: context); + } + } + void _deletePostComment() async { if (_requestInProgress) return; _setRequestInProgress(true); @@ -178,10 +265,10 @@ class OBPostCommentState extends State { postComment: widget.postComment, post: widget.post)); await _requestOperation.value; - widget.post.decreaseCommentsCount(); + if (widget.postComment.parentComment == null) widget.post.decreaseCommentsCount(); _toastService.success(message: 'Comment deleted', context: context); if (widget.onPostCommentDeletedCallback != null) { - widget.onPostCommentDeletedCallback(); + widget.onPostCommentDeletedCallback(widget.postComment); } } catch (error) { _onError(error); @@ -208,47 +295,6 @@ class OBPostCommentState extends State { throw error; } } - - Widget _getCommunityBadge(PostComment postComment) { - Post post = widget.post; - User postCommenter = postComment.commenter; - - if (post.hasCommunity()) { - Community postCommunity = post.community; - - bool isCommunityAdministrator = - postCommenter.isAdministratorOfCommunity(postCommunity); - - if (isCommunityAdministrator) { - return _buildCommunityAdministratorBadge(); - } - - bool isCommunityModerator = - postCommenter.isModeratorOfCommunity(postCommunity); - - if (isCommunityModerator) { - return _buildCommunityModeratorBadge(); - } - } - - return const SizedBox(); - } - - Widget _buildCommunityAdministratorBadge() { - return const OBIcon( - OBIcons.communityAdministrators, - size: OBIconSize.small, - themeColor: OBIconThemeColor.primaryAccent, - ); - } - - Widget _buildCommunityModeratorBadge() { - return const OBIcon( - OBIcons.communityModerators, - size: OBIconSize.small, - themeColor: OBIconThemeColor.primaryAccent, - ); - } } typedef void OnWantsToSeeUserProfile(User user); diff --git a/lib/pages/home/pages/post_comments/widgets/post_comment/widgets/post_comment_tile.dart b/lib/pages/home/pages/post_comments/widgets/post_comment/widgets/post_comment_tile.dart new file mode 100644 index 000000000..5a917d991 --- /dev/null +++ b/lib/pages/home/pages/post_comments/widgets/post_comment/widgets/post_comment_tile.dart @@ -0,0 +1,118 @@ +import 'package:Openbook/models/community.dart'; +import 'package:Openbook/models/post.dart'; +import 'package:Openbook/models/post_comment.dart'; +import 'package:Openbook/models/user.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/navigation_service.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:flutter/material.dart'; +import 'package:Openbook/pages/home/pages/post_comments/widgets/post_comment/widgets/post_comment_text.dart'; +import 'package:Openbook/widgets/avatars/avatar.dart'; +import 'package:Openbook/widgets/theming/secondary_text.dart'; + + +class OBPostCommentTile extends StatelessWidget { + final PostComment postComment; + final Post post; + + OBPostCommentTile({ + @required this.post, + @required this.postComment}); + + @override + Widget build(BuildContext context) { + var provider = OpenbookProvider.of(context); + NavigationService _navigationService = provider.navigationService; + + return StreamBuilder( + key: Key('OBPostCommentTile#${this.postComment.id}'), + stream: this.postComment.updateSubject, + initialData: this.postComment, + builder: (BuildContext context, AsyncSnapshot snapshot) { + PostComment postComment = snapshot.data; + + return Padding( + padding: + const EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OBAvatar( + onPressed: () { + _navigationService.navigateToUserProfile( + user: postComment.commenter, context: context); + }, + size: OBAvatarSize.small, + avatarUrl: postComment.getCommenterProfileAvatar(), + ), + const SizedBox( + width: 20.0, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OBPostCommentText( + postComment, + badge: _getCommunityBadge(postComment), + onUsernamePressed: () { + _navigationService.navigateToUserProfile( + user: postComment.commenter, context: context); + }, + ), + const SizedBox( + height: 5.0, + ), + OBSecondaryText( + postComment.getRelativeCreated(), + style: TextStyle(fontSize: 12.0), + ) + ], + )) + ], + ), + ); + }); + } + + Widget _getCommunityBadge(PostComment postComment) { + Post post = this.post; + User postCommenter = postComment.commenter; + + if (post.hasCommunity()) { + Community postCommunity = post.community; + + bool isCommunityAdministrator = + postCommenter.isAdministratorOfCommunity(postCommunity); + + if (isCommunityAdministrator) { + return _buildCommunityAdministratorBadge(); + } + + bool isCommunityModerator = + postCommenter.isModeratorOfCommunity(postCommunity); + + if (isCommunityModerator) { + return _buildCommunityModeratorBadge(); + } + } + + return const SizedBox(); + } + + Widget _buildCommunityAdministratorBadge() { + return const OBIcon( + OBIcons.communityAdministrators, + size: OBIconSize.small, + themeColor: OBIconThemeColor.primaryAccent, + ); + } + + Widget _buildCommunityModeratorBadge() { + return const OBIcon( + OBIcons.communityModerators, + size: OBIconSize.small, + themeColor: OBIconThemeColor.primaryAccent, + ); + } +} \ No newline at end of file diff --git a/lib/pages/home/pages/post_comments/widgets/post_comments_header_bar.dart b/lib/pages/home/pages/post_comments/widgets/post_comments_header_bar.dart new file mode 100644 index 000000000..d13ab23e9 --- /dev/null +++ b/lib/pages/home/pages/post_comments/widgets/post_comments_header_bar.dart @@ -0,0 +1,152 @@ +import 'package:Openbook/models/post_comment.dart'; +import 'package:Openbook/pages/home/pages/post_comments/post_comments_page_controller.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/theme.dart'; +import 'package:Openbook/services/theme_value_parser.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:flutter/material.dart'; +import 'package:Openbook/widgets/theming/secondary_text.dart'; + + +class OBPostCommentsHeaderBar extends StatelessWidget { + PostCommentsPageType pageType; + bool noMoreTopItemsToLoad; + List postComments; + PostCommentsSortType currentSort; + VoidCallback onWantsToToggleSortComments; + VoidCallback loadMoreTopComments; + VoidCallback onWantsToRefreshComments; + + static const PAGE_COMMENTS_TEXT_MAP = { + 'NEWEST': 'Newest comments', + 'NEWER': 'Newer', + 'VIEW_NEWEST': 'View newest comments', + 'SEE_NEWEST': 'See newest comments', + 'OLDEST': 'Oldest comments', + 'OLDER': 'Older', + 'VIEW_OLDEST': 'View oldest comments', + 'SEE_OLDEST': 'See oldest comments', + 'BE_THE_FIRST': 'Be the first to comment', + }; + + static const PAGE_REPLIES_TEXT_MAP = { + 'NEWEST': 'Newest replies', + 'NEWER': 'Newer', + 'VIEW_NEWEST': 'View newest replies', + 'SEE_NEWEST': 'See newest replies', + 'OLDEST': 'Oldest replies', + 'OLDER': 'Older', + 'VIEW_OLDEST': 'View oldest replies', + 'SEE_OLDEST': 'See oldest replies', + 'BE_THE_FIRST': 'Be the first to reply', + }; + + OBPostCommentsHeaderBar({ + @required this.pageType, + @required this.noMoreTopItemsToLoad, + @required this.postComments, + @required this.currentSort, + @required this.onWantsToToggleSortComments, + @required this.loadMoreTopComments, + @required this.onWantsToRefreshComments, + }); + + @override + Widget build(BuildContext context) { + var provider = OpenbookProvider.of(context); + ThemeService _themeService = provider.themeService; + ThemeValueParserService _themeValueParserService = provider.themeValueParserService; + var theme = _themeService.getActiveTheme(); + Map _pageTextMap; + if (this.pageType == PostCommentsPageType.comments) { + _pageTextMap = PAGE_COMMENTS_TEXT_MAP; + } else { + _pageTextMap = PAGE_REPLIES_TEXT_MAP; + } + + + if (this.noMoreTopItemsToLoad) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 0.0, vertical: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Padding( + padding: EdgeInsets.fromLTRB(10.0, 0.0, 0.0, 0.0), + child: OBSecondaryText( + this.postComments.length > 0 + ? this.currentSort == PostCommentsSortType.dec + ? _pageTextMap['NEWEST'] + : _pageTextMap['OLDEST'] + : _pageTextMap['BE_THE_FIRST'], + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0), + ), + ), + ), + Expanded( + child: FlatButton( + child: OBText( + this.postComments.length > 0 + ? this.currentSort == PostCommentsSortType.dec + ? _pageTextMap['SEE_OLDEST'] + : _pageTextMap['SEE_NEWEST'] + : '', + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: _themeValueParserService + .parseGradient(theme.primaryAccentColor) + .colors[1], + fontWeight: FontWeight.bold), + ), + onPressed: this.onWantsToToggleSortComments), + ), + ], + ), + ); + } else { + return Container( + padding: EdgeInsets.symmetric(horizontal: 0.0, vertical: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 4, + child: FlatButton( + child: Row( + children: [ + OBIcon(OBIcons.arrowUp), + const SizedBox(width: 10.0), + OBText( + this.currentSort == PostCommentsSortType.dec + ? _pageTextMap['NEWER'] + : _pageTextMap['OLDER'], + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + onPressed: this.loadMoreTopComments), + ), + Expanded( + flex: 6, + child: FlatButton( + child: OBText( + this.currentSort == PostCommentsSortType.dec + ? _pageTextMap['VIEW_NEWEST'] + : _pageTextMap['VIEW_OLDEST'], + style: TextStyle( + color: _themeValueParserService + .parseGradient(theme.primaryAccentColor) + .colors[1], + fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + onPressed: this.onWantsToRefreshComments), + ), + ], + ), + ); + } + } +} diff --git a/lib/pages/home/pages/post_comments/widgets/post_preview.dart b/lib/pages/home/pages/post_comments/widgets/post_preview.dart new file mode 100644 index 000000000..73d6ff5bd --- /dev/null +++ b/lib/pages/home/pages/post_comments/widgets/post_preview.dart @@ -0,0 +1,51 @@ +import 'package:Openbook/models/post.dart'; +import 'package:Openbook/models/post_comment.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/theme.dart'; +import 'package:Openbook/services/theme_value_parser.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/widgets/post/widgets/post-actions/post_actions.dart'; +import 'package:Openbook/widgets/post/widgets/post-body/post_body.dart'; +import 'package:Openbook/widgets/post/widgets/post_circles.dart'; +import 'package:Openbook/widgets/post/widgets/post_header/post_header.dart'; +import 'package:Openbook/widgets/post/widgets/post_reactions/post_reactions.dart'; +import 'package:Openbook/widgets/theming/post_divider.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:flutter/material.dart'; +import 'package:Openbook/widgets/theming/secondary_text.dart'; + + +class OBPostPreview extends StatelessWidget { + final Post post; + final Function(Post) onPostDeleted; + final VoidCallback focusCommentInput; + GlobalKey _keyPostBody = GlobalKey(); + + OBPostPreview({this.post, this.onPostDeleted, this.focusCommentInput}); + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OBPostHeader( + post: this.post, + onPostDeleted: this.onPostDeleted, + ), + Container( + key: _keyPostBody, + child: OBPostBody(this.post), + ), + OBPostReactions(this.post), + OBPostCircles(this.post), + OBPostActions( + this.post, + onWantsToCommentPost: this.focusCommentInput, + ), + const SizedBox( + height: 16, + ), + OBPostDivider() + ], + ); + } +} \ No newline at end of file diff --git a/lib/pages/home/pages/profile/widgets/profile_card/widgets/profile_actions/widgets/profile_action_more/profile_action_more.dart b/lib/pages/home/pages/profile/widgets/profile_card/widgets/profile_actions/widgets/profile_action_more/profile_action_more.dart index 5ffb95d28..1487c7159 100644 --- a/lib/pages/home/pages/profile/widgets/profile_card/widgets/profile_actions/widgets/profile_action_more/profile_action_more.dart +++ b/lib/pages/home/pages/profile/widgets/profile_card/widgets/profile_actions/widgets/profile_action_more/profile_action_more.dart @@ -10,6 +10,7 @@ import 'package:Openbook/widgets/icon.dart'; import 'package:Openbook/widgets/theming/primary_color_container.dart'; import 'package:Openbook/widgets/theming/text.dart'; import 'package:Openbook/widgets/tiles/actions/block_user_tile.dart'; +import 'package:Openbook/widgets/tiles/actions/report_user_tile.dart'; import 'package:flutter/material.dart'; class OBProfileActionMore extends StatelessWidget { @@ -87,19 +88,28 @@ class OBProfileActionMore extends StatelessWidget { if (loggedInUser.canBlockOrUnblockUser(user)) { moreTiles.add(OBBlockUserTile( user: user, - onBlockedUser: (){ + onBlockedUser: () { // Bottom sheet Navigator.pop(context); - openbookProvider.toastService.success(message: 'User blocked', context:context); + openbookProvider.toastService + .success(message: 'User blocked', context: context); }, - onUnblockedUser: (){ + onUnblockedUser: () { // Bottom sheet Navigator.pop(context); - openbookProvider.toastService.success(message: 'User unblocked', context:context); + openbookProvider.toastService + .success(message: 'User unblocked', context: context); }, )); } + moreTiles.add(OBReportUserTile( + user: user, + onWantsToReportUser: () { + Navigator.of(context).pop(); + }, + )); + showModalBottomSheet( context: context, builder: (BuildContext context) { diff --git a/lib/pages/home/pages/report_object/pages/confirm_report_object.dart b/lib/pages/home/pages/report_object/pages/confirm_report_object.dart new file mode 100644 index 000000000..ad410291d --- /dev/null +++ b/lib/pages/home/pages/report_object/pages/confirm_report_object.dart @@ -0,0 +1,222 @@ +import 'package:Openbook/libs/type_to_str.dart'; +import 'package:Openbook/models/community.dart'; +import 'package:Openbook/models/moderation/moderation_category.dart'; +import 'package:Openbook/models/post.dart'; +import 'package:Openbook/models/post_comment.dart'; +import 'package:Openbook/models/user.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/toast.dart'; +import 'package:Openbook/services/user.dart'; +import 'package:Openbook/widgets/alerts/alert.dart'; +import 'package:Openbook/widgets/buttons/button.dart'; +import 'package:Openbook/widgets/fields/text_form_field.dart'; +import 'package:Openbook/widgets/markdown.dart'; +import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart'; +import 'package:Openbook/widgets/page_scaffold.dart'; +import 'package:Openbook/widgets/theming/primary_color_container.dart'; +import 'package:Openbook/widgets/theming/secondary_text.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:async/async.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class OBConfirmReportObject extends StatefulWidget { + final dynamic object; + final Map extraData; + final ModerationCategory category; + + const OBConfirmReportObject( + {Key key, @required this.object, @required this.category, this.extraData}) + : super(key: key); + + @override + OBConfirmReportObjectState createState() { + return OBConfirmReportObjectState(); + } +} + +class OBConfirmReportObjectState extends State { + bool _confirmationInProgress; + UserService _userService; + ToastService _toastService; + bool _needsBootstrap; + TextEditingController _descriptionController; + + String description; + + CancelableOperation _submitReportOperation; + + @override + void initState() { + super.initState(); + _needsBootstrap = true; + _confirmationInProgress = false; + _descriptionController = TextEditingController(); + } + + @override + void dispose() { + super.dispose(); + if (_submitReportOperation != null) _submitReportOperation.cancel(); + } + + @override + Widget build(BuildContext context) { + if (_needsBootstrap) { + OpenbookProviderState openbookProvider = OpenbookProvider.of(context); + _userService = openbookProvider.userService; + _toastService = openbookProvider.toastService; + _needsBootstrap = false; + } + + return OBCupertinoPageScaffold( + navigationBar: OBThemedNavigationBar(title: 'Submit report'), + child: OBPrimaryColorContainer( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 40, vertical: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + OBText( + 'Can you provide extra details that might be relevant to the report?', + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox( + height: 10, + ), + OBSecondaryText( + '(Optional)', + ), + const SizedBox( + height: 10, + ), + OBAlert( + padding: const EdgeInsets.all(10), + child: OBTextFormField( + controller: _descriptionController, + maxLines: 3, + hasBorder: false, + decoration: const InputDecoration( + hintText: 'Type here...', + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, horizontal: 10), + ), + ), + ), + const SizedBox( + height: 40, + ), + OBText( + 'Here\'s what will happen next:', + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox( + height: 20, + ), + OBMarkdown( + onlyBody: true, + data: '- Your report will be submitted anonymously. \n ' + '- If you are reporting a post or comment, the report will be sent to the Openbook staff and the community moderators if applicable and the post will be hidden from your feed \n' + '- If you are reporting an account or community, it will be sent to the Openbook staff. \n' + '- We\'ll review it, if approved, content will be deleted and penalties delivered to the people involved ranging from deletion of account to hours of suspension depending on the severity of the report. \n' + '- If the report is found to be made in an attempt to damage another member or community in the platform with no infringement of the stated reason, penalties will be applied to you. \n') + ], + ), + ), + )), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 20), + child: Row( + children: [ + Expanded( + child: OBButton( + size: OBButtonSize.large, + child: Text('I understand, submit.'), + onPressed: _onConfirm, + isLoading: _confirmationInProgress, + ), + ) + ], + ), + ) + ], + ))); + } + + void _onConfirm() async { + _setConfirmationInProgress(true); + try { + if (widget.object is Post) { + _submitReportOperation = CancelableOperation.fromFuture( + _userService.reportPost( + description: _descriptionController.text, + post: widget.object, + moderationCategory: widget.category)); + } else if (widget.object is PostComment) { + _submitReportOperation = CancelableOperation.fromFuture( + _userService.reportPostComment( + description: _descriptionController.text, + post: widget.extraData['post'], + postComment: widget.object, + moderationCategory: widget.category)); + } else if (widget.object is Community) { + _submitReportOperation = CancelableOperation.fromFuture( + _userService.reportCommunity( + description: _descriptionController.text, + community: widget.object, + moderationCategory: widget.category)); + } else if (widget.object is User) { + _submitReportOperation = CancelableOperation.fromFuture( + _userService.reportUser( + description: _descriptionController.text, + user: widget.object, + moderationCategory: widget.category)); + } else { + throw 'Object type not supported'; + } + await _submitReportOperation.value; + if (widget.object is User || + widget.object is Community || + widget.object is Post || + widget.object is PostComment) { + widget.object.setIsReported(true); + } + _toastService.success( + message: + modelTypeToString(widget.object, capitalize: true) + ' reported', + context: context); + Navigator.of(context).pop(true); + } catch (error) { + _onError(error); + } finally { + _setConfirmationInProgress(false); + } + } + + void _onError(error) async { + if (error is HttpieConnectionRefusedError) { + _toastService.error( + message: error.toHumanReadableMessage(), context: context); + } else if (error is HttpieRequestError) { + String errorMessage = await error.toHumanReadableMessage(); + _toastService.error(message: errorMessage, context: context); + } else { + _toastService.error(message: 'Unknown error', context: context); + throw error; + } + } + + void _setConfirmationInProgress(confirmationInProgress) { + setState(() { + _confirmationInProgress = confirmationInProgress; + }); + } +} diff --git a/lib/pages/home/pages/report_object/report_object.dart b/lib/pages/home/pages/report_object/report_object.dart new file mode 100644 index 000000000..85af0a651 --- /dev/null +++ b/lib/pages/home/pages/report_object/report_object.dart @@ -0,0 +1,144 @@ +import 'package:Openbook/libs/type_to_str.dart'; +import 'package:Openbook/models/moderation/moderation_category.dart'; +import 'package:Openbook/models/moderation/moderation_category_list.dart'; +import 'package:Openbook/services/navigation_service.dart'; +import 'package:Openbook/services/user.dart'; +import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/widgets/page_scaffold.dart'; +import 'package:Openbook/widgets/progress_indicator.dart'; +import 'package:Openbook/widgets/theming/primary_color_container.dart'; +import 'package:Openbook/widgets/theming/secondary_text.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class OBReportObjectPage extends StatefulWidget { + final dynamic object; + final OnObjectReported onObjectReported; + final Map extraData; + + const OBReportObjectPage({ + Key key, + this.object, + this.onObjectReported, + this.extraData, + }) : super(key: key); + + @override + OBReportObjectPageState createState() { + return OBReportObjectPageState(); + } +} + +class OBReportObjectPageState extends State { + NavigationService _navigationService; + UserService _userService; + List _moderationCategories = []; + bool _needsBootstrap; + + @override + void initState() { + super.initState(); + _needsBootstrap = true; + } + + @override + Widget build(BuildContext context) { + if (_needsBootstrap) { + var openbookProvider = OpenbookProvider.of(context); + _userService = openbookProvider.userService; + _navigationService = openbookProvider.navigationService; + _bootstrap(); + _needsBootstrap = false; + } + + return OBCupertinoPageScaffold( + navigationBar: _buildNavigationBar(), + child: OBPrimaryColorContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: + EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 20), + child: OBText( + 'Why are you reporting this ' + + modelTypeToString(widget.object) + + '?', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), + ), + ), + _moderationCategories.isEmpty + ? _buildProgressIndicator() + : _buildModerationCategories(), + ], + ), + )); + } + + Widget _buildProgressIndicator() { + return Expanded( + child: Center( + child: OBProgressIndicator(), + ), + ); + } + + Widget _buildModerationCategories() { + return Expanded( + child: ListView.separated( + padding: EdgeInsets.all(0.0), + itemBuilder: _buildModerationCategoryTile, + separatorBuilder: (context, index) { + return const Divider(); + }, + itemCount: _moderationCategories.length, + ), + ); + } + + Widget _buildModerationCategoryTile(context, index) { + ModerationCategory category = _moderationCategories[index]; + + return ListTile( + onTap: () async { + var result = await _navigationService.navigateToConfirmReportObject( + extraData: widget.extraData, + object: widget.object, + category: category, + context: context); + if (result != null && result) { + if (widget.onObjectReported != null) + widget.onObjectReported(widget.object); + Navigator.pop(context); + } + }, + title: OBText( + category.title, + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: OBSecondaryText(category.description), + //trailing: OBIcon(OBIcons.chevronRight), + ); + } + + Widget _buildNavigationBar() { + return OBThemedNavigationBar( + title: 'Report ' + modelTypeToString(widget.object), + ); + } + + void _bootstrap() async { + var moderationCategories = await _userService.getModerationCategories(); + _setModerationCategories(moderationCategories); + } + + _setModerationCategories(ModerationCategoriesList moderationCategoriesList) { + setState(() { + _moderationCategories = moderationCategoriesList.moderationCategories; + }); + } +} + +typedef OnObjectReported(dynamic object); diff --git a/lib/pages/waitlist/subscribe_done_step.dart b/lib/pages/waitlist/subscribe_done_step.dart new file mode 100644 index 000000000..0296a9f3d --- /dev/null +++ b/lib/pages/waitlist/subscribe_done_step.dart @@ -0,0 +1,118 @@ +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/localization.dart'; +import 'package:Openbook/widgets/buttons/button.dart'; +import 'package:Openbook/widgets/buttons/success_button.dart'; +import 'package:flutter/material.dart'; + +class WaitlistSubscribeArguments { + int count; + + WaitlistSubscribeArguments({this.count}); + +} + +class OBWaitlistSubscribeDoneStep extends StatefulWidget { + final int count; + + OBWaitlistSubscribeDoneStep({@required this.count}); + + @override + State createState() { + return OBWaitlistSubscribeDoneStepState(); + } +} + +class OBWaitlistSubscribeDoneStepState extends State { + LocalizationService localizationService; + + @override + Widget build(BuildContext context) { + var openbookProvider = OpenbookProvider.of(context); + localizationService = openbookProvider.localizationService; + + return Scaffold( + body: Container( + child: Center(child: SingleChildScrollView(child: _buildAllSet())), + ), + bottomNavigationBar: _buildBottomBar(), + ); + } + + Widget _buildBottomBar() { + return BottomAppBar( + color: Colors.transparent, + elevation: 0.0, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 20.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: _buildNextButton(context: context), + ), + ], + ), + ), + ); + } + + Widget _buildAllSet() { + String congratulationsText = localizationService.trans('AUTH.CREATE_ACC.CONGRATULATIONS'); + String countText = localizationService.trans('AUTH.CREATE_ACC.YOUR_SUBSCRIBED'); + + return Column( + children: [ + Text( + '👍‍', + style: TextStyle(fontSize: 45.0, color: Colors.white), + ), + const SizedBox( + height: 20.0, + ), + Text(congratulationsText, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold, + //color: Colors.white + )), + const SizedBox( + height: 20.0, + ), + Text(countText.replaceFirst('{0}', widget.count.toString()), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20.0 + ), + ), + const SizedBox( + height: 20.0, + ), + ] + ); + } + + Widget _buildNextButton({@required BuildContext context}) { + + return OBSuccessButton( + minWidth: double.infinity, + size: OBButtonSize.large, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Done', + style: TextStyle(fontSize: 18.0), + ) + ], + ), + onPressed: () { + Navigator.popUntil(context, (route){ + return route.isFirst; + }); + Navigator.pushReplacementNamed(context, '/auth'); + }, + ); + } +} diff --git a/lib/pages/waitlist/subscribe_email_step.dart b/lib/pages/waitlist/subscribe_email_step.dart new file mode 100644 index 000000000..dda22cbcb --- /dev/null +++ b/lib/pages/waitlist/subscribe_email_step.dart @@ -0,0 +1,218 @@ +import 'package:Openbook/pages/waitlist/subscribe_done_step.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/localization.dart'; +import 'package:Openbook/services/toast.dart'; +import 'package:Openbook/services/user.dart'; +import 'package:Openbook/services/validation.dart'; +import 'package:Openbook/widgets/buttons/button.dart'; +import 'package:Openbook/widgets/buttons/success_button.dart'; +import 'package:Openbook/widgets/buttons/secondary_button.dart'; +import 'package:Openbook/pages/auth/create_account/widgets/auth_text_field.dart'; +import 'package:flutter/material.dart'; + +class OBWaitlistSubscribePage extends StatefulWidget { + @override + State createState() { + return OBWaitlistSubscribePageState(); + } +} + +class OBWaitlistSubscribePageState extends State { + bool _subscribeInProgress; + final GlobalKey _formKey = GlobalKey(); + + bool _isSubmitted; + UserService _userService; + LocalizationService _localizationService; + ValidationService _validationService; + ToastService _toastService; + + TextEditingController _emailController = TextEditingController(); + + @override + void initState() { + super.initState(); + _isSubmitted = false; + _subscribeInProgress = false; + _emailController.addListener(_validateForm); + } + + @override + Widget build(BuildContext context) { + var openbookProvider = OpenbookProvider.of(context); + _localizationService = openbookProvider.localizationService; + _validationService = openbookProvider.validationService; + _userService = openbookProvider.userService; + _toastService = openbookProvider.toastService; + + return Scaffold( + body: Center( + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 40.0), + child: Column( + children: [ + _buildSubscribeEmailText(context: context), + const SizedBox( + height: 20.0, + ), + _buildEmailForm() + ], + ))), + ), + backgroundColor: Color(0xFFFFB649), + bottomNavigationBar: BottomAppBar( + color: Colors.transparent, + elevation: 0.0, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 20.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: _buildPreviousButton(context: context), + ), + Expanded(child: _buildNextButton(context)), + ], + ), + ), + ), + ); + } + + bool _validateForm() { + if (!_isSubmitted) return null; + return _formKey.currentState.validate(); + } + + void onPressedNextStep(BuildContext context) async { + if (_subscribeInProgress) return; + _isSubmitted = true; + bool isEmailValid = _validateForm(); + + if (!isEmailValid) return; + + _setSubscribeInProgress(true); + try { + int count = await _userService.subscribeToBetaWaitlist( + email: _emailController.text); + WaitlistSubscribeArguments args = + new WaitlistSubscribeArguments(count: count); + Navigator.pushNamed(context, '/waitlist/subscribe_done_step', + arguments: args); + } catch (error) { + _onError(error); + } finally { + _setSubscribeInProgress(false); + } + } + + Widget _buildNextButton(BuildContext context) { + String buttonText = _localizationService.trans('AUTH.CREATE_ACC.SUBSCRIBE'); + + return OBSuccessButton( + minWidth: double.infinity, + size: OBButtonSize.large, + isLoading: _subscribeInProgress, + child: Text(buttonText, style: TextStyle(fontSize: 18.0)), + onPressed: () { + onPressedNextStep(context); + }, + ); + } + + Widget _buildPreviousButton({@required BuildContext context}) { + String buttonText = _localizationService.trans('AUTH.CREATE_ACC.PREVIOUS'); + + return OBSecondaryButton( + isFullWidth: true, + isLarge: true, + child: Row( + children: [ + Icon( + Icons.arrow_back_ios, + color: Colors.white, + ), + const SizedBox( + width: 10.0, + ), + Text( + buttonText, + style: TextStyle(fontSize: 18.0, color: Colors.white), + ) + ], + ), + onPressed: () { + Navigator.pop(context); + }, + ); + } + + Widget _buildSubscribeEmailText({@required BuildContext context}) { + String subscribeEmailText = _localizationService + .trans('AUTH.CREATE_ACC.SUBSCRIBE_TO_WAITLIST_TEXT'); + + return Column( + children: [ + Text( + '💌', + style: TextStyle(fontSize: 45.0, color: Colors.white), + ), + const SizedBox( + height: 20.0, + ), + Text(subscribeEmailText, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold, + color: Colors.white)), + ], + ); + } + + Widget _buildEmailForm() { + String emailInputPlaceholder = + _localizationService.trans('AUTH.CREATE_ACC.EMAIL_PLACEHOLDER'); + + return Form( + key: _formKey, + child: Row(children: [ + new Expanded( + child: Container( + color: Colors.transparent, + child: OBAuthTextField( + autocorrect: false, + hintText: emailInputPlaceholder, + validator: (String email) { + String validateEMail = + _validationService.validateUserEmail(email); + if (validateEMail != null) return validateEMail; + }, + controller: _emailController, + )), + ), + ]), + ); + } + + void _onError(error) async { + if (error is HttpieConnectionRefusedError) { + _toastService.error( + message: error.toHumanReadableMessage(), context: context); + } else if (error is HttpieRequestError) { + String errorMessage = await error.toHumanReadableMessage(); + _toastService.error(message: errorMessage, context: context); + } else { + _toastService.error(message: 'Unknown error', context: context); + throw error; + } + } + + void _setSubscribeInProgress(subscribeInProgress) { + setState(() { + _subscribeInProgress = subscribeInProgress; + }); + } +} diff --git a/lib/provider.dart b/lib/provider.dart index cff9b6374..6a1ff4f79 100644 --- a/lib/provider.dart +++ b/lib/provider.dart @@ -10,6 +10,7 @@ import 'package:Openbook/services/devices_api.dart'; import 'package:Openbook/services/dialog.dart'; import 'package:Openbook/services/documents.dart'; import 'package:Openbook/services/intercom.dart'; +import 'package:Openbook/services/moderation_api.dart'; import 'package:Openbook/services/notifications_api.dart'; import 'package:Openbook/services/push_notifications/push_notifications.dart'; import 'package:Openbook/services/universal_links/universal_links.dart'; @@ -35,6 +36,7 @@ import 'package:Openbook/services/user_invites_api.dart'; import 'package:Openbook/services/user_preferences.dart'; import 'package:Openbook/services/utils_service.dart'; import 'package:Openbook/services/validation.dart'; +import 'package:Openbook/services/waitlist_service.dart'; import 'package:flutter/material.dart'; import 'package:sentry/sentry.dart'; @@ -67,6 +69,7 @@ class OpenbookProviderState extends State { PostsApiService postsApiService = PostsApiService(); StorageService storageService = StorageService(); UserService userService = UserService(); + ModerationApiService moderationApiService = ModerationApiService(); ToastService toastService = ToastService(); StringTemplateService stringTemplateService = StringTemplateService(); EmojisApiService emojisApiService = EmojisApiService(); @@ -87,6 +90,7 @@ class OpenbookProviderState extends State { ThemeValueParserService themeValueParserService = ThemeValueParserService(); ModalService modalService = ModalService(); NavigationService navigationService = NavigationService(); + WaitlistApiService waitlistApiService = WaitlistApiService(); LocalizationService localizationService; UniversalLinksService universalLinksService = UniversalLinksService(); @@ -138,6 +142,9 @@ class OpenbookProviderState extends State { userService.setNotificationsApiService(notificationsApiService); userService.setDevicesApiService(devicesApiService); userService.setCreateAccountBlocService(createAccountBloc); + userService.setWaitlistApiService(waitlistApiService); + waitlistApiService.setHttpService(httpService); + userService.setModerationApiService(moderationApiService); emojisApiService.setHttpService(httpService); categoriesApiService.setHttpService(httpService); postsApiService.setHttpieService(httpService); @@ -158,6 +165,8 @@ class OpenbookProviderState extends State { dialogService.setThemeValueParserService(themeValueParserService); imagePickerService.setValidationService(validationService); documentsService.setHttpService(httpService); + moderationApiService.setStringTemplateService(stringTemplateService); + moderationApiService.setHttpieService(httpService); } void initAsyncState() async { @@ -170,6 +179,7 @@ class OpenbookProviderState extends State { emojisApiService.setApiURL(environment.apiUrl); userInvitesApiService.setApiURL(environment.apiUrl); followsApiService.setApiURL(environment.apiUrl); + moderationApiService.setApiURL(environment.apiUrl); connectionsApiService.setApiURL(environment.apiUrl); connectionsCirclesApiService.setApiURL(environment.apiUrl); followsListsApiService.setApiURL(environment.apiUrl); @@ -177,6 +187,7 @@ class OpenbookProviderState extends State { categoriesApiService.setApiURL(environment.apiUrl); notificationsApiService.setApiURL(environment.apiUrl); devicesApiService.setApiURL(environment.apiUrl); + waitlistApiService.setOpenbookSocialApiURL(environment.openbookSocialApiUrl); intercomService.bootstrap( iosApiKey: environment.intercomIosKey, androidApiKey: environment.intercomAndroidKey, diff --git a/lib/services/auth_api.dart b/lib/services/auth_api.dart index 4f7bed739..5064d1bb0 100644 --- a/lib/services/auth_api.dart +++ b/lib/services/auth_api.dart @@ -21,6 +21,7 @@ class AuthApiService { static const GET_AUTHENTICATED_USER_PATH = 'api/auth/user/'; static const UPDATE_AUTHENTICATED_USER_PATH = 'api/auth/user/'; static const GET_USERS_PATH = 'api/auth/users/'; + static const REPORT_USER_PATH = 'api/auth/users/{userUsername}/report/'; static const GET_LINKED_USERS_PATH = 'api/auth/linked-users/'; static const SEARCH_LINKED_USERS_PATH = 'api/auth/linked-users/search/'; static const GET_BLOCKED_USERS_PATH = 'api/auth/blocked-users/'; @@ -130,13 +131,14 @@ class AuthApiService { body: body, appendAuthorizationToken: true); } - Future createUser({@required String email, - @required String token, - @required String name, - @required bool isOfLegalAge, - @required bool areGuidelinesAccepted, - @required String password, - File avatar}) { + Future createUser( + {@required String email, + @required String token, + @required String name, + @required bool isOfLegalAge, + @required bool areGuidelinesAccepted, + @required String password, + File avatar}) { Map body = { 'email': email, 'token': token, @@ -186,10 +188,11 @@ class AuthApiService { queryParameters: queryParams, appendAuthorizationToken: true); } - Future getLinkedUsers({bool authenticatedRequest = true, - int maxId, - int count, - String withCommunity}) { + Future getLinkedUsers( + {bool authenticatedRequest = true, + int maxId, + int count, + String withCommunity}) { Map queryParams = {}; if (count != null) queryParams['count'] = count; @@ -213,9 +216,11 @@ class AuthApiService { queryParameters: queryParams, appendAuthorizationToken: true); } - Future getBlockedUsers({bool authenticatedRequest = true, + Future getBlockedUsers({ + bool authenticatedRequest = true, int maxId, - int count,}) { + int count, + }) { Map queryParams = {}; if (count != null) queryParams['count'] = count; @@ -351,8 +356,25 @@ class AuthApiService { } Future acceptGuidelines() { - return this._httpService.post( - '$apiURL$ACCEPT_GUIDELINES', appendAuthorizationToken: true); + return this + ._httpService + .post('$apiURL$ACCEPT_GUIDELINES', appendAuthorizationToken: true); + } + + Future reportUserWithUsername( + {@required String userUsername, + @required int moderationCategoryId, + String description}) { + String path = _makeReportUserPath(username: userUsername); + + Map body = {'category_id': moderationCategoryId.toString()}; + + if (description != null && description.isNotEmpty) { + body['description'] = description; + } + + return _httpService.post(_makeApiUrl(path), + body: body, appendAuthorizationToken: true); } String _makeBlockUserWithUsernamePath(String username) { @@ -365,6 +387,11 @@ class AuthApiService { .parse(UNBLOCK_USER_PATH, {'userUsername': username}); } + String _makeReportUserPath({@required username}) { + return _stringTemplateService + .parse(REPORT_USER_PATH, {'userUsername': username}); + } + String _makeApiUrl(String string) { return '$apiURL$string'; } diff --git a/lib/services/bottom_sheet.dart b/lib/services/bottom_sheet.dart index 3b98f6d25..b8b1d9b85 100644 --- a/lib/services/bottom_sheet.dart +++ b/lib/services/bottom_sheet.dart @@ -11,11 +11,9 @@ import 'package:Openbook/pages/home/bottom_sheets/community_type_picker.dart'; import 'package:Openbook/pages/home/bottom_sheets/connection_circles_picker.dart'; import 'package:Openbook/pages/home/bottom_sheets/comment_more_actions.dart'; import 'package:Openbook/pages/home/bottom_sheets/follows_lists_picker.dart'; -import 'package:Openbook/pages/home/bottom_sheets/photo_picker.dart'; import 'package:Openbook/pages/home/bottom_sheets/post_actions.dart'; import 'package:Openbook/pages/home/bottom_sheets/video_picker.dart'; import 'package:Openbook/pages/home/modals/react_to_post/react_to_post.dart'; -import 'package:Openbook/services/image_picker.dart'; import 'package:flutter/material.dart'; import 'dart:async'; import 'package:meta/meta.dart'; @@ -82,7 +80,7 @@ class BottomSheetService { {@required BuildContext context, @required Post post, @required OnPostDeleted onPostDeleted, - @required OnPostReported onPostReported, + @required ValueChanged onPostReported, List initialPickedFollowsLists}) { return showModalBottomSheetApp( context: context, @@ -111,27 +109,13 @@ class BottomSheetService { Future showMoreCommentActions( {@required BuildContext context, - @required Post post, - @required PostComment postComment}) { + @required Post post, + @required PostComment postComment}) { return showModalBottomSheetApp( context: context, builder: (BuildContext context) { return OBCommentMoreActionsBottomSheet( - post: post, - postComment: postComment - ); - }); - } - - Future showPhotoPicker( - {@required BuildContext context, - OBImageType imageType = OBImageType.post}) { - return showModalBottomSheetApp( - context: context, - builder: (BuildContext context) { - return OBPhotoPickerBottomSheet( - imageType: imageType, - ); + post: post, postComment: postComment); }); } diff --git a/lib/services/communities_api.dart b/lib/services/communities_api.dart index 23e8a112c..219925e71 100644 --- a/lib/services/communities_api.dart +++ b/lib/services/communities_api.dart @@ -20,6 +20,8 @@ class CommunitiesApiService { static const DELETE_COMMUNITY_PATH = 'api/communities/{communityName}/'; static const UPDATE_COMMUNITY_PATH = 'api/communities/{communityName}/'; static const GET_COMMUNITY_PATH = 'api/communities/{communityName}/'; + static const REPORT_COMMUNITY_PATH = + 'api/communities/{communityName}/report/'; static const JOIN_COMMUNITY_PATH = 'api/communities/{communityName}/members/join/'; static const LEAVE_COMMUNITY_PATH = @@ -75,6 +77,8 @@ class CommunitiesApiService { 'api/communities/{communityName}/moderators/{username}/'; static const CREATE_COMMUNITY_POSTS_PATH = 'api/communities/{communityName}/posts/'; + static const GET_COMMUNITY_MODERATED_OBJECTS_PATH = + 'api/communities/{communityName}/moderated-objects/'; void setHttpieService(HttpieService httpService) { _httpService = httpService; @@ -144,8 +148,11 @@ class CommunitiesApiService { appendAuthorizationToken: authenticatedRequest); } - Future getClosedPostsForCommunityWithName(String communityName, - {int maxId, int count, bool authenticatedRequest = true}) { + Future getClosedPostsForCommunityWithName( + String communityName, + {int maxId, + int count, + bool authenticatedRequest = true}) { Map queryParams = {}; if (count != null) queryParams['count'] = count; @@ -159,7 +166,6 @@ class CommunitiesApiService { appendAuthorizationToken: authenticatedRequest); } - Future getCommunitiesWithQuery( {bool authenticatedRequest = true, @required String query}) { Map queryParams = {'query': query}; @@ -566,11 +572,61 @@ class CommunitiesApiService { queryParameters: {'offset': offset}); } + Future reportCommunity( + {@required String communityName, + @required int moderationCategoryId, + String description}) { + String path = _makeReportCommunityPath(communityName); + + Map body = {'category_id': moderationCategoryId.toString()}; + + if (description != null && description.isNotEmpty) { + body['description'] = description; + } + + return _httpService.post(_makeApiUrl(path), body: body, appendAuthorizationToken: true); + } + + Future getModeratedObjects({ + @required String communityName, + int count, + int maxId, + String type, + bool verified, + List statuses, + List types, + }) { + Map queryParams = {}; + if (count != null) queryParams['count'] = count; + + if (maxId != null) queryParams['max_id'] = maxId; + + if (statuses != null) queryParams['statuses'] = statuses; + if (types != null) queryParams['types'] = types; + + if (verified != null) queryParams['verified'] = verified; + + String path = _makeGetCommunityModeratedObjectsPath(communityName); + + return _httpService.get(_makeApiUrl(path), + queryParameters: queryParams, appendAuthorizationToken: true); + } + + String _makeGetCommunityModeratedObjectsPath(String communityName) { + return _stringTemplateService.parse( + GET_COMMUNITY_MODERATED_OBJECTS_PATH, {'communityName': communityName}); + } + String _makeCreateCommunityPost(String communityName) { return _stringTemplateService .parse(CREATE_COMMUNITY_POST_PATH, {'communityName': communityName}); } + String _makeReportCommunityPath(String communityName) { + return _stringTemplateService + .parse(REPORT_COMMUNITY_PATH, {'communityName': communityName}); + } + String _makeClosedCommunityPostsPath(String communityName) { return _stringTemplateService .parse(CLOSED_COMMUNITY_POSTS_PATH, {'communityName': communityName}); diff --git a/lib/services/environment_loader.dart b/lib/services/environment_loader.dart index e114fa1b7..995c8ecc0 100644 --- a/lib/services/environment_loader.dart +++ b/lib/services/environment_loader.dart @@ -24,9 +24,11 @@ class Environment { final String intercomAndroidKey; final String intercomAppId; final String sentryDsn; + final String openbookSocialApiUrl; const Environment( {this.sentryDsn = '', + this.openbookSocialApiUrl = '', this.apiUrl = '', this.magicHeaderName = '', this.magicHeaderValue = '', @@ -43,6 +45,7 @@ class Environment { intercomIosKey: jsonMap["INTERCOM_IOS_KEY"], intercomAndroidKey: jsonMap["INTERCOM_ANDROID_KEY"], sentryDsn: jsonMap["SENTRY_DSN"], + openbookSocialApiUrl: jsonMap["OPENBOOK_SOCIAL_API_URL"] ); } } diff --git a/lib/services/image_picker.dart b/lib/services/image_picker.dart index cf247d704..dfc0f247b 100644 --- a/lib/services/image_picker.dart +++ b/lib/services/image_picker.dart @@ -1,12 +1,19 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:flutter_exif_rotation/flutter_exif_rotation.dart'; import 'package:image_cropper/image_cropper.dart'; import 'package:image_picker/image_picker.dart'; import 'package:meta/meta.dart'; import 'package:Openbook/services/validation.dart'; +import 'package:multi_image_picker/multi_image_picker.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:uuid/uuid.dart'; export 'package:image_picker/image_picker.dart'; class ImagePickerService { + static Uuid uuid = new Uuid(); + static const Map IMAGE_RATIOS = { OBImageType.avatar: {'x': 1.0, 'y': 1.0}, OBImageType.cover: {'x': 16.0, 'y': 9.0} @@ -19,18 +26,30 @@ class ImagePickerService { } Future pickImage( - {@required OBImageType imageType, - ImageSource source = ImageSource.gallery}) async { - var image = await ImagePicker.pickImage(source: source); + {@required OBImageType imageType}) async { + List pickedAssets = + await MultiImagePicker.pickImages(maxImages: 1, enableCamera: true); - if (image == null) { + if (pickedAssets.isEmpty) { return null; } - if (!await _validationService.isImageAllowedSize(image, imageType)) { - throw ImageTooLargeException(_validationService.getAllowedImageSize(imageType)); + Asset pickedAsset = pickedAssets.first; + + String tmpImageName = uuid.v4() + '.jpg'; + final path = await _getTempPath(); + final file = File('$path/$tmpImageName'); + ByteData byteData = await pickedAsset.requestOriginal(); + List imageData = byteData.buffer.asUint8List(); + file.writeAsBytesSync(imageData); + + if (!await _validationService.isImageAllowedSize(file, imageType)) { + throw ImageTooLargeException( + _validationService.getAllowedImageSize(imageType)); } + File processedImage = await _processImage(file); + double ratioX = imageType != OBImageType.post ? IMAGE_RATIOS[imageType]['x'] : null; double ratioY = @@ -41,7 +60,7 @@ class ImagePickerService { toolbarColor: Colors.black, statusBarColor: Colors.black, toolbarWidgetColor: Colors.white, - sourcePath: image.path, + sourcePath: processedImage.path, ratioX: ratioX, ratioY: ratioY, ); @@ -54,6 +73,19 @@ class ImagePickerService { return video; } + + Future _processImage(File image) async { + /// Fix rotation issue on android + if (Platform.isAndroid) + return FlutterExifRotation.rotateImage(path: image.path); + return image; + } + + Future _getTempPath() async { + final directory = await getTemporaryDirectory(); + + return directory.path; + } } class ImageTooLargeException implements Exception { diff --git a/lib/services/modal_service.dart b/lib/services/modal_service.dart index 81c06f85b..4a2c2ffab 100644 --- a/lib/services/modal_service.dart +++ b/lib/services/modal_service.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'package:Openbook/models/circle.dart'; import 'package:Openbook/models/community.dart'; import 'package:Openbook/models/follows_list.dart'; +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/moderation/moderation_category.dart'; import 'package:Openbook/models/post.dart'; import 'package:Openbook/models/post_comment.dart'; import 'package:Openbook/models/post_reaction.dart'; @@ -11,6 +13,7 @@ import 'package:Openbook/models/user_invite.dart'; import 'package:Openbook/pages/home/modals/accept_guidelines/accept_guidelines.dart'; import 'package:Openbook/pages/home/modals/edit_post/edit_post.dart'; import 'package:Openbook/pages/home/modals/invite_to_community.dart'; +import 'package:Openbook/pages/home/modals/post_comment/post-comment-reply-expanded.dart'; import 'package:Openbook/pages/home/modals/post_comment/post-commenter-expanded.dart'; import 'package:Openbook/pages/home/pages/community/pages/manage_community/pages/community_administrators/modals/add_community_administrator/add_community_administrator.dart'; import 'package:Openbook/pages/home/modals/create_post/create_post.dart'; @@ -23,6 +26,11 @@ import 'package:Openbook/pages/home/pages/community/pages/manage_community/pages import 'package:Openbook/pages/home/pages/community/pages/manage_community/pages/community_moderators/modals/add_community_moderator/add_community_moderator.dart'; import 'package:Openbook/pages/home/modals/user_invites/create_user_invite.dart'; import 'package:Openbook/pages/home/modals/user_invites/send_invite_email.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/modals/moderated_objects_filters/moderated_objects_filters.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/moderated_objects.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_category/modals/moderated_object_update_category.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_description/modals/moderated_object_update_description.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_status/modals/moderated_object_update_status.dart'; import 'package:Openbook/pages/home/pages/timeline/timeline.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -83,6 +91,27 @@ class ModalService { return editedComment; } + Future openExpandedReplyCommenter( + {@required BuildContext context, + @required PostComment postComment, + @required Post post, + @required Function(PostComment) onReplyAdded, + @required Function(PostComment) onReplyDeleted}) async { + PostComment replyComment = await Navigator.of(context, rootNavigator: true) + .push(CupertinoPageRoute( + fullscreenDialog: true, + builder: (BuildContext context) { + return Material( + child: OBPostCommentReplyExpandedModal( + post: post, + postComment: postComment, + onReplyAdded: onReplyAdded, + onReplyDeleted: onReplyDeleted), + ); + })); + return replyComment; + } + Future openEditUserProfile( {@required User user, @required BuildContext context}) async { Navigator.of(context, rootNavigator: true) @@ -307,6 +336,21 @@ class ModalService { })); } + Future openModeratedObjectsFilters( + {@required + BuildContext context, + @required + OBModeratedObjectsPageController + moderatedObjectsPageController}) async { + await Navigator.of(context).push(CupertinoPageRoute( + fullscreenDialog: true, + builder: (BuildContext context) { + return OBModeratedObjectsFiltersModal( + moderatedObjectsPageController: moderatedObjectsPageController, + ); + })); + } + Future openAcceptGuidelines({@required BuildContext context}) async { await Navigator.of(context).push(CupertinoPageRoute( fullscreenDialog: true, @@ -316,4 +360,40 @@ class ModalService { ); })); } + + Future openModeratedObjectUpdateDescription( + {@required BuildContext context, + @required ModeratedObject moderatedObject}) async { + return Navigator.of(context).push(CupertinoPageRoute( + fullscreenDialog: true, + builder: (BuildContext context) { + return OBModeratedObjectUpdateDescriptionModal( + moderatedObject: moderatedObject, + ); + })); + } + + Future openModeratedObjectUpdateCategory( + {@required BuildContext context, + @required ModeratedObject moderatedObject}) async { + return Navigator.of(context).push(CupertinoPageRoute( + fullscreenDialog: true, + builder: (BuildContext context) { + return OBModeratedObjectUpdateCategoryModal( + moderatedObject: moderatedObject, + ); + })); + } + + Future openModeratedObjectUpdateStatus( + {@required BuildContext context, + @required ModeratedObject moderatedObject}) async { + return Navigator.of(context).push(CupertinoPageRoute( + fullscreenDialog: true, + builder: (BuildContext context) { + return OBModeratedObjectUpdateStatusModal( + moderatedObject: moderatedObject, + ); + })); + } } diff --git a/lib/services/moderation_api.dart b/lib/services/moderation_api.dart new file mode 100644 index 000000000..03935869a --- /dev/null +++ b/lib/services/moderation_api.dart @@ -0,0 +1,208 @@ +import 'package:Openbook/services/httpie.dart'; +import 'package:Openbook/services/string_template.dart'; + +class ModerationApiService { + HttpieService _httpService; + StringTemplateService _stringTemplateService; + + String apiURL; + + static const GET_GLOBAL_MODERATED_OBJECTS_PATH = + 'api/moderation/moderated-objects/global/'; + static const USER_MODERATION_PENALTIES_PATH = + 'api/moderation/user/penalties/'; + static const USER_PENDING_MODERATED_OBJECTS_COMMUNITIES_PATH = + 'api/moderation/user/pending-moderated-objects-communities/'; + static const GET_MODERATION_CATEGORIES_PATH = 'api/moderation/categories/'; + static const MODERATED_OBJECT_PATH = + 'api/moderation/moderated-objects/{moderatedObjectId}/'; + static const APPROVE_MODERATED_OBJECT_PATH = + 'api/moderation/moderated-objects/{moderatedObjectId}/approve/'; + static const REJECT_MODERATED_OBJECT_PATH = + 'api/moderation/moderated-objects/{moderatedObjectId}/reject/'; + static const VERIFY_MODERATED_OBJECT_PATH = + 'api/moderation/moderated-objects/{moderatedObjectId}/verify/'; + static const UNVERIFY_MODERATED_OBJECT_PATH = + 'api/moderation/moderated-objects/{moderatedObjectId}/unverify/'; + static const MODERATED_OBJECT_LOGS_PATH = + 'api/moderation/moderated-objects/{moderatedObjectId}/logs/'; + static const MODERATED_OBJECT_REPORTS_PATH = + 'api/moderation/moderated-objects/{moderatedObjectId}/reports/'; + + void setHttpieService(HttpieService httpService) { + _httpService = httpService; + } + + void setStringTemplateService(StringTemplateService stringTemplateService) { + _stringTemplateService = stringTemplateService; + } + + void setApiURL(String newApiURL) { + apiURL = newApiURL; + } + + Future getGlobalModeratedObjects({ + int count, + int maxId, + String type, + bool verified, + List statuses, + List types, + }) { + Map queryParams = {}; + if (count != null) queryParams['count'] = count; + + if (maxId != null) queryParams['max_id'] = maxId; + + if (statuses != null) queryParams['statuses'] = statuses; + + if (types != null) queryParams['types'] = types; + + if (verified != null) queryParams['verified'] = verified; + + String path = GET_GLOBAL_MODERATED_OBJECTS_PATH; + + return _httpService.get(_makeApiUrl(path), + queryParameters: queryParams, appendAuthorizationToken: true); + } + + Future getModeratedObjectLogs( + int moderatedObjectId, { + int count, + int maxId, + }) { + Map queryParams = {}; + if (count != null) queryParams['count'] = count; + + if (maxId != null) queryParams['max_id'] = maxId; + + String path = _makeModeratedObjectLogsPath(moderatedObjectId); + + return _httpService.get(_makeApiUrl(path), + queryParameters: queryParams, appendAuthorizationToken: true); + } + + Future getModeratedObjectReports( + int moderatedObjectId, { + int count, + int maxId, + }) { + Map queryParams = {}; + if (count != null) queryParams['count'] = count; + + if (maxId != null) queryParams['max_id'] = maxId; + + String path = _makeModeratedObjectReportsPath(moderatedObjectId); + + return _httpService.get(_makeApiUrl(path), + queryParameters: queryParams, appendAuthorizationToken: true); + } + + Future getModerationCategories() { + String path = GET_MODERATION_CATEGORIES_PATH; + + return _httpService.get(_makeApiUrl(path), appendAuthorizationToken: true); + } + + Future getUserModerationPenalties({int maxId, int count}) { + Map queryParams = {}; + if (count != null) queryParams['count'] = count; + + if (maxId != null) queryParams['max_id'] = maxId; + + String path = USER_MODERATION_PENALTIES_PATH; + + return _httpService.get(_makeApiUrl(path), + queryParameters: queryParams, appendAuthorizationToken: true); + } + + Future getUserPendingModeratedObjectsCommunities( + {int maxId, int count}) { + Map queryParams = {}; + if (count != null) queryParams['count'] = count; + + if (maxId != null) queryParams['max_id'] = maxId; + + String path = USER_PENDING_MODERATED_OBJECTS_COMMUNITIES_PATH; + + return _httpService.get(_makeApiUrl(path), + queryParameters: queryParams, appendAuthorizationToken: true); + } + + Future verifyModeratedObjectWithId(int moderatedObjectId) { + String path = _makeVerifyModeratedObjectsPath(moderatedObjectId); + + return _httpService.post(_makeApiUrl(path), appendAuthorizationToken: true); + } + + Future unverifyModeratedObjectWithId(int moderatedObjectId) { + String path = _makeUnverifyModeratedObjectsPath(moderatedObjectId); + + return _httpService.post(_makeApiUrl(path), appendAuthorizationToken: true); + } + + Future approveModeratedObjectWithId(int moderatedObjectId) { + String path = _makeApproveModeratedObjectsPath(moderatedObjectId); + + return _httpService.post(_makeApiUrl(path), appendAuthorizationToken: true); + } + + Future rejectModeratedObjectWithId(int moderatedObjectId) { + String path = _makeRejectModeratedObjectsPath(moderatedObjectId); + + return _httpService.post(_makeApiUrl(path), appendAuthorizationToken: true); + } + + Future updateModeratedObjectWithId(int moderatedObjectId, + {String description, int categoryId}) { + Map body = {}; + + if (description != null) body['description'] = description; + + if (categoryId != null) body['category_id'] = categoryId.toString(); + + String path = _makeModeratedObjectsPath(moderatedObjectId); + + return _httpService.patchJSON(_makeApiUrl(path), + body: body, appendAuthorizationToken: true); + } + + String _makeModeratedObjectsPath(int moderatedObjectId) { + return _stringTemplateService + .parse(MODERATED_OBJECT_PATH, {'moderatedObjectId': moderatedObjectId}); + } + + String _makeVerifyModeratedObjectsPath(int moderatedObjectId) { + return _stringTemplateService.parse( + VERIFY_MODERATED_OBJECT_PATH, {'moderatedObjectId': moderatedObjectId}); + } + + String _makeUnverifyModeratedObjectsPath(int moderatedObjectId) { + return _stringTemplateService.parse(UNVERIFY_MODERATED_OBJECT_PATH, + {'moderatedObjectId': moderatedObjectId}); + } + + String _makeApproveModeratedObjectsPath(int moderatedObjectId) { + return _stringTemplateService.parse(APPROVE_MODERATED_OBJECT_PATH, + {'moderatedObjectId': moderatedObjectId}); + } + + String _makeModeratedObjectLogsPath(int moderatedObjectId) { + return _stringTemplateService.parse( + MODERATED_OBJECT_LOGS_PATH, {'moderatedObjectId': moderatedObjectId}); + } + + String _makeModeratedObjectReportsPath(int moderatedObjectId) { + return _stringTemplateService.parse(MODERATED_OBJECT_REPORTS_PATH, + {'moderatedObjectId': moderatedObjectId}); + } + + String _makeRejectModeratedObjectsPath(int moderatedObjectId) { + return _stringTemplateService.parse( + REJECT_MODERATED_OBJECT_PATH, {'moderatedObjectId': moderatedObjectId}); + } + + String _makeApiUrl(String string) { + return '$apiURL$string'; + } +} diff --git a/lib/services/navigation_service.dart b/lib/services/navigation_service.dart index 65b33a262..0548eb847 100644 --- a/lib/services/navigation_service.dart +++ b/lib/services/navigation_service.dart @@ -2,6 +2,8 @@ import 'package:Openbook/models/circle.dart'; import 'package:Openbook/models/community.dart'; import 'package:Openbook/models/emoji.dart'; import 'package:Openbook/models/follows_list.dart'; +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/moderation/moderation_category.dart'; import 'package:Openbook/models/post.dart'; import 'package:Openbook/models/post_comment.dart'; import 'package:Openbook/models/post_reactions_emoji_count.dart'; @@ -34,6 +36,8 @@ import 'package:Openbook/pages/home/pages/menu/pages/followers.dart'; import 'package:Openbook/pages/home/pages/menu/pages/following.dart'; import 'package:Openbook/pages/home/pages/menu/pages/follows_list/follows_list.dart'; import 'package:Openbook/pages/home/pages/menu/pages/follows_lists/follows_lists.dart'; +import 'package:Openbook/pages/home/pages/menu/pages/my_moderation_penalties/my_moderation_penalties.dart'; +import 'package:Openbook/pages/home/pages/menu/pages/my_moderation_tasks/my_moderation_tasks.dart'; import 'package:Openbook/pages/home/pages/menu/pages/settings/pages/account_settings/account_settings.dart'; import 'package:Openbook/pages/home/pages/menu/pages/settings/pages/account_settings/pages/blocked_users.dart'; import 'package:Openbook/pages/home/pages/menu/pages/settings/pages/application_settings.dart'; @@ -42,11 +46,17 @@ import 'package:Openbook/pages/home/pages/menu/pages/useful_links.dart'; import 'package:Openbook/pages/home/pages/menu/pages/user_invites/pages/user_invite_detail.dart'; import 'package:Openbook/pages/home/pages/menu/pages/user_invites/user_invites.dart'; import 'package:Openbook/pages/home/pages/menu/pages/themes/themes.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/moderated_objects.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/moderated_object_community_review.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/moderated_object_global_review.dart'; +import 'package:Openbook/pages/home/pages/moderated_objects/pages/widgets/moderated_object_reports_preview/pages/moderated_object_reports.dart'; import 'package:Openbook/pages/home/pages/notifications/pages/notifications_settings.dart'; import 'package:Openbook/pages/home/pages/post/post.dart'; -import 'package:Openbook/pages/home/pages/post_comments/post.dart'; -import 'package:Openbook/pages/home/pages/post_comments/post_comments_linked.dart'; +import 'package:Openbook/pages/home/pages/post_comments/post_comments_page.dart'; +import 'package:Openbook/pages/home/pages/post_comments/post_comments_page_controller.dart'; import 'package:Openbook/pages/home/pages/profile/profile.dart'; +import 'package:Openbook/pages/home/pages/report_object/pages/confirm_report_object.dart'; +import 'package:Openbook/pages/home/pages/report_object/report_object.dart'; import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart'; import 'package:Openbook/widgets/routes/slide_right_route.dart'; import 'package:Openbook/widgets/theming/primary_color_container.dart'; @@ -230,25 +240,74 @@ class NavigationService { context, OBSlideRightRoute( key: Key('obSlidePostComments'), - widget: OBPostCommentsPage(post, autofocusCommentInput: true))); + widget: OBPostCommentsPage( + pageType: PostCommentsPageType.comments, + post: post, + showPostPreview: false, + autofocusCommentInput: true))); } - Future navigateToPostComments( + Future navigateToPostComments( {@required Post post, @required BuildContext context}) { return Navigator.push( context, OBSlideRightRoute( key: Key('obSlideViewComments'), - widget: OBPostCommentsPage(post, autofocusCommentInput: false))); + widget: OBPostCommentsPage( + post: post, + showPostPreview: false, + pageType: PostCommentsPageType.comments, + autofocusCommentInput: false))); + } + + Future navigateToPostCommentReplies( + {@required Post post, + @required PostComment postComment, + @required BuildContext context, + Function(PostComment) onReplyDeleted, + Function(PostComment) onReplyAdded}) { + return Navigator.push( + context, + OBSlideRightRoute( + key: Key('obSlideViewComments'), + widget: OBPostCommentsPage( + pageType: PostCommentsPageType.replies, + post: post, + showPostPreview: false, + postComment: postComment, + onCommentDeleted: onReplyDeleted, + onCommentAdded: onReplyAdded, + autofocusCommentInput: false))); } - Future navigateToPostCommentsLinked( + Future navigateToPostCommentsLinked( {@required PostComment postComment, @required BuildContext context}) { return Navigator.push( context, OBSlideRightRoute( key: Key('obSlideViewCommentsLinked'), - widget: OBPostCommentsLinkedPage(postComment, + widget: OBPostCommentsPage( + post: postComment.post, + showPostPreview: true, + pageType: PostCommentsPageType.comments, + linkedPostComment: postComment, + autofocusCommentInput: false))); + } + + Future navigateToPostCommentRepliesLinked( + {@required PostComment postComment, + @required PostComment parentComment, + @required BuildContext context}) { + return Navigator.push( + context, + OBSlideRightRoute( + key: Key('obSlideViewCommentsLinked'), + widget: OBPostCommentsPage( + post: postComment.post, + postComment: parentComment, + showPostPreview: true, + pageType: PostCommentsPageType.replies, + linkedPostComment: postComment, autofocusCommentInput: false))); } @@ -464,6 +523,114 @@ class NavigationService { ))); } + Future navigateToConfirmReportObject( + {@required BuildContext context, + @required dynamic object, + Map extraData, + @required ModerationCategory category}) { + return Navigator.push( + context, + OBSlideRightRoute( + key: Key('obConfirmReportObject'), + widget: OBConfirmReportObject( + extraData: extraData, + object: object, + category: category, + ))); + } + + Future navigateToReportObject( + {@required BuildContext context, + @required dynamic object, + Map extraData, + ValueChanged onObjectReported}) async { + return Navigator.push( + context, + OBSlideRightRoute( + key: Key('obReportObject'), + widget: OBReportObjectPage( + object: object, + extraData: extraData, + onObjectReported: onObjectReported, + ))); + } + + Future navigateToCommunityModeratedObjects( + {@required BuildContext context, @required Community community}) async { + return Navigator.push( + context, + OBSlideRightRoute( + key: Key('obCommunityModeratedObjects'), + widget: OBModeratedObjectsPage( + community: community, + ))); + } + + Future navigateToGlobalModeratedObjects( + {@required BuildContext context}) async { + return Navigator.push( + context, + OBSlideRightRoute( + key: Key('obGlobalModeratedObjects'), + widget: OBModeratedObjectsPage())); + } + + Future navigateToModeratedObjectReports( + {@required BuildContext context, + @required ModeratedObject moderatedObject}) async { + return Navigator.push( + context, + OBSlideRightRoute( + key: Key('obModeratedObjectReportsPage'), + widget: OBModeratedObjectReportsPage( + moderatedObject: moderatedObject, + ))); + } + + Future navigateToModeratedObjectGlobalReview( + {@required BuildContext context, + @required ModeratedObject moderatedObject}) async { + return Navigator.push( + context, + OBSlideRightRoute( + key: Key('obModeratedObjectGlobalReviewPage'), + widget: OBModeratedObjectGlobalReviewPage( + moderatedObject: moderatedObject, + ))); + } + + Future navigateToModeratedObjectCommunityReview( + {@required BuildContext context, + @required Community community, + @required ModeratedObject moderatedObject}) async { + return Navigator.push( + context, + OBSlideRightRoute( + key: Key('obModeratedObjectCommunityReviewPage'), + widget: OBModeratedObjectCommunityReviewPage( + community: community, + moderatedObject: moderatedObject, + ))); + } + + Future navigateToMyModerationTasksPage( + {@required BuildContext context}) async { + return Navigator.push( + context, + OBSlideRightRoute( + key: Key('obMyModerationTasksPage'), + widget: OBMyModerationTasksPage())); + } + + Future navigateToMyModerationPenaltiesPage( + {@required BuildContext context}) async { + return Navigator.push( + context, + OBSlideRightRoute( + key: Key('obMyModerationPenaltiesPage'), + widget: OBMyModerationPenaltiesPage())); + } + Future navigateToBlankPageWithWidget( {@required BuildContext context, @required String navBarTitle, diff --git a/lib/services/posts_api.dart b/lib/services/posts_api.dart index f6515cf3a..1cc201870 100644 --- a/lib/services/posts_api.dart +++ b/lib/services/posts_api.dart @@ -18,14 +18,22 @@ class PostsApiService { static const OPEN_POST_PATH = 'api/posts/{postUuid}/open/'; static const CLOSE_POST_PATH = 'api/posts/{postUuid}/close/'; static const COMMENT_POST_PATH = 'api/posts/{postUuid}/comments/'; - static const EDIT_COMMENT_POST_PATH = 'api/posts/{postUuid}/comments/{postCommentId}/'; + static const EDIT_COMMENT_POST_PATH = + 'api/posts/{postUuid}/comments/{postCommentId}/'; + static const REPLY_COMMENT_POST_PATH = + 'api/posts/{postUuid}/comments/{postCommentId}/replies/'; static const MUTE_POST_PATH = 'api/posts/{postUuid}/notifications/mute/'; static const UNMUTE_POST_PATH = 'api/posts/{postUuid}/notifications/unmute/'; + static const REPORT_POST_PATH = 'api/posts/{postUuid}/report/'; static const DELETE_POST_COMMENT_PATH = 'api/posts/{postUuid}/comments/{postCommentId}/'; + static const REPORT_POST_COMMENT_PATH = + 'api/posts/{postUuid}/comments/{postCommentId}/report/'; static const GET_POST_COMMENTS_PATH = 'api/posts/{postUuid}/comments/'; - static const DISABLE_POST_COMMENTS_PATH = 'api/posts/{postUuid}/comments/disable/'; - static const ENABLE_POST_COMMENTS_PATH = 'api/posts/{postUuid}/comments/enable/'; + static const DISABLE_POST_COMMENTS_PATH = + 'api/posts/{postUuid}/comments/disable/'; + static const ENABLE_POST_COMMENTS_PATH = + 'api/posts/{postUuid}/comments/enable/'; static const REACT_TO_POST_PATH = 'api/posts/{postUuid}/reactions/'; static const DELETE_POST_REACTION_PATH = 'api/posts/{postUuid}/reactions/{postReactionId}/'; @@ -145,6 +153,23 @@ class PostsApiService { queryParameters: queryParams, appendAuthorizationToken: true); } + Future getRepliesForCommentWithIdForPostWithUuid( + String postUuid, int postCommentId, + {int countMax, int maxId, int countMin, int minId, String sort}) { + Map queryParams = {}; + if (countMax != null) queryParams['count_max'] = countMax; + if (countMin != null) queryParams['count_min'] = countMin; + + if (maxId != null) queryParams['max_id'] = maxId; + if (minId != null) queryParams['min_id'] = minId; + if (sort != null) queryParams['sort'] = sort; + + String path = _makeGetReplyCommentsPostPath(postUuid, postCommentId); + + return _httpService.get(_makeApiUrl(path), + queryParameters: queryParams, appendAuthorizationToken: true); + } + Future commentPost( {@required String postUuid, @required String text}) { Map body = {'text': text}; @@ -155,7 +180,9 @@ class PostsApiService { } Future editPostComment( - {@required String postUuid, @required int postCommentId, @required String text}) { + {@required String postUuid, + @required int postCommentId, + @required String text}) { Map body = {'text': text}; String path = _makeEditCommentPostPath(postUuid, postCommentId); @@ -163,6 +190,17 @@ class PostsApiService { body: body, appendAuthorizationToken: true); } + Future replyPostComment( + {@required String postUuid, + @required int postCommentId, + @required String text}) { + Map body = {'text': text}; + + String path = _makeReplyCommentPostPath(postUuid, postCommentId); + return _httpService.putJSON(_makeApiUrl(path), + body: body, appendAuthorizationToken: true); + } + Future deletePostComment( {@required postCommentId, @required postUuid}) { String path = _makeDeletePostCommentPath( @@ -249,6 +287,44 @@ class PostsApiService { return _httpService.get(url, appendAuthorizationToken: true); } + Future reportPostComment( + {@required int postCommentId, + @required String postUuid, + @required int moderationCategoryId, + String description}) { + String path = _makeReportPostCommentPath( + postCommentId: postCommentId, postUuid: postUuid); + + Map body = { + 'category_id': moderationCategoryId.toString() + }; + + if (description != null && description.isNotEmpty) { + body['description'] = description; + } + + return _httpService.post(_makeApiUrl(path), + body: body, appendAuthorizationToken: true); + } + + Future reportPost( + {@required String postUuid, + @required int moderationCategoryId, + String description}) { + String path = _makeReportPostPath(postUuid: postUuid); + + Map body = { + 'category_id': moderationCategoryId.toString() + }; + + if (description != null && description.isNotEmpty) { + body['description'] = description; + } + + return _httpService.post(_makeApiUrl(path), + body: body, appendAuthorizationToken: true); + } + String _makePostPath(String postUuid) { return _stringTemplateService.parse(POST_PATH, {'postUuid': postUuid}); } @@ -273,8 +349,7 @@ class PostsApiService { } String _makeOpenPostPath(String postUuid) { - return _stringTemplateService - .parse(OPEN_POST_PATH, {'postUuid': postUuid}); + return _stringTemplateService.parse(OPEN_POST_PATH, {'postUuid': postUuid}); } String _makeClosePostPath(String postUuid) { @@ -288,8 +363,18 @@ class PostsApiService { } String _makeEditCommentPostPath(String postUuid, int postCommentId) { - return _stringTemplateService - .parse(EDIT_COMMENT_POST_PATH, {'postUuid': postUuid, 'postCommentId': postCommentId}); + return _stringTemplateService.parse(EDIT_COMMENT_POST_PATH, + {'postUuid': postUuid, 'postCommentId': postCommentId}); + } + + String _makeReplyCommentPostPath(String postUuid, int postCommentId) { + return _stringTemplateService.parse(REPLY_COMMENT_POST_PATH, + {'postUuid': postUuid, 'postCommentId': postCommentId}); + } + + String _makeGetReplyCommentsPostPath(String postUuid, int postCommentId) { + return _stringTemplateService.parse(REPLY_COMMENT_POST_PATH, + {'postUuid': postUuid, 'postCommentId': postCommentId}); } String _makeGetPostCommentsPath(String postUuid) { @@ -324,6 +409,17 @@ class PostsApiService { .parse(GET_POST_REACTIONS_EMOJI_COUNT_PATH, {'postUuid': postUuid}); } + String _makeReportPostCommentPath( + {@required int postCommentId, @required String postUuid}) { + return _stringTemplateService.parse(REPORT_POST_COMMENT_PATH, + {'postCommentId': postCommentId, 'postUuid': postUuid}); + } + + String _makeReportPostPath({@required postUuid}) { + return _stringTemplateService + .parse(REPORT_POST_PATH, {'postUuid': postUuid}); + } + String _makeApiUrl(String string) { return '$apiURL$string'; } diff --git a/lib/services/push_notifications/push_notifications.dart b/lib/services/push_notifications/push_notifications.dart index c4786f36e..619ecadcf 100644 --- a/lib/services/push_notifications/push_notifications.dart +++ b/lib/services/push_notifications/push_notifications.dart @@ -5,7 +5,7 @@ import 'package:Openbook/models/push_notification.dart'; import 'package:Openbook/models/user.dart'; import 'package:Openbook/services/user.dart'; import 'package:crypto/crypto.dart'; -import 'package:onesignal/onesignal.dart'; +import 'package:onesignalflutter/onesignalflutter.dart'; import 'package:rxdart/rxdart.dart'; class PushNotificationsService { diff --git a/lib/services/user.dart b/lib/services/user.dart index 1f87a83ea..ba1881980 100644 --- a/lib/services/user.dart +++ b/lib/services/user.dart @@ -17,6 +17,13 @@ import 'package:Openbook/models/emoji.dart'; import 'package:Openbook/models/emoji_group_list.dart'; import 'package:Openbook/models/follow.dart'; import 'package:Openbook/models/follows_list.dart'; +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/moderation/moderated_object_list.dart'; +import 'package:Openbook/models/moderation/moderated_object_log_list.dart'; +import 'package:Openbook/models/moderation/moderation_category.dart'; +import 'package:Openbook/models/moderation/moderation_category_list.dart'; +import 'package:Openbook/models/moderation/moderation_penalty_list.dart'; +import 'package:Openbook/models/moderation/moderation_report_list.dart'; import 'package:Openbook/models/notifications/notification.dart'; import 'package:Openbook/models/notifications/notifications_list.dart'; import 'package:Openbook/models/post.dart'; @@ -42,10 +49,12 @@ import 'package:Openbook/services/emojis_api.dart'; import 'package:Openbook/services/follows_api.dart'; import 'package:Openbook/services/httpie.dart'; import 'package:Openbook/services/follows_lists_api.dart'; +import 'package:Openbook/services/moderation_api.dart'; import 'package:Openbook/services/notifications_api.dart'; import 'package:Openbook/services/posts_api.dart'; import 'package:Openbook/services/storage.dart'; import 'package:Openbook/services/user_invites_api.dart'; +import 'package:Openbook/services/waitlist_service.dart'; import 'package:crypto/crypto.dart'; import 'package:device_info/device_info.dart'; import 'package:flutter_advanced_networkimage/provider.dart'; @@ -63,6 +72,7 @@ class UserService { AuthApiService _authApiService; HttpieService _httpieService; PostsApiService _postsApiService; + ModerationApiService _moderationApiService; CommunitiesApiService _communitiesApiService; CategoriesApiService _categoriesApiService; EmojisApiService _emojisApiService; @@ -74,6 +84,7 @@ class UserService { NotificationsApiService _notificationsApiService; DevicesApiService _devicesApiService; CreateAccountBloc _createAccountBlocService; + WaitlistApiService _waitlistApiService; // If this is null, means user logged out. Stream get loggedInUserChange => _loggedInUserChangeSubject.stream; @@ -90,6 +101,10 @@ class UserService { _authApiService = authApiService; } + void setModerationApiService(ModerationApiService moderationApiService) { + _moderationApiService = moderationApiService; + } + void setPostsApiService(PostsApiService postsApiService) { _postsApiService = postsApiService; } @@ -149,6 +164,10 @@ class UserService { _createAccountBlocService = createAccountBloc; } + void setWaitlistApiService(WaitlistApiService waitlistApiService) { + _waitlistApiService = waitlistApiService; + } + Future deleteAccountWithPassword(String password) async { HttpieResponse response = await _authApiService.deleteUser(password: password); @@ -179,7 +198,6 @@ class UserService { {@required String username, @required String password}) async { HttpieResponse response = await _authApiService.loginWithCredentials( username: username, password: password); - if (response.isOk()) { var parsedResponse = response.parseJsonBody(); var authToken = parsedResponse['token']; @@ -210,6 +228,13 @@ class UserService { _checkResponseIsOk(response); } + Future subscribeToBetaWaitlist({String email}) async { + HttpieResponse response = await _waitlistApiService.subscribeToBetaWaitlist(email: email); + _checkResponseIsOk(response); + Map parsedJson = json.decode(response.body); + return parsedJson['count']; + } + Future loginWithAuthToken(String authToken) async { await _setAuthToken(authToken); await refreshUser(); @@ -403,14 +428,14 @@ class UserService { Future closePost(Post post) async { HttpieResponse response = - await _postsApiService.closePostWithUuid(post.uuid); + await _postsApiService.closePostWithUuid(post.uuid); _checkResponseIsOk(response); return Post.fromJson(json.decode(response.body)); } Future openPost(Post post) async { HttpieResponse response = - await _postsApiService.openPostWithUuid(post.uuid); + await _postsApiService.openPostWithUuid(post.uuid); _checkResponseIsOk(response); return Post.fromJson(json.decode(response.body)); } @@ -477,6 +502,16 @@ class UserService { return PostComment.fromJSON(json.decode(response.body)); } + Future replyPostComment( + {@required Post post, + @required PostComment postComment, + @required String text}) async { + HttpieResponse response = await _postsApiService.replyPostComment( + postUuid: post.uuid, postCommentId: postComment.id, text: text); + _checkResponseIsCreated(response); + return PostComment.fromJSON(json.decode(response.body)); + } + Future deletePostComment( {@required PostComment postComment, @required Post post}) async { HttpieResponse response = await _postsApiService.deletePostComment( @@ -518,6 +553,27 @@ class UserService { return PostCommentList.fromJson(json.decode(response.body)); } + Future getCommentRepliesForPostComment( + Post post, PostComment postComment, + {int maxId, + int countMax, + int minId, + int countMin, + PostCommentsSortType sort}) async { + HttpieResponse response = await _postsApiService + .getRepliesForCommentWithIdForPostWithUuid(post.uuid, postComment.id, + countMax: countMax, + maxId: maxId, + countMin: countMin, + minId: minId, + sort: sort != null + ? PostComment.convertPostCommentSortTypeToString(sort) + : null); + + _checkResponseIsOk(response); + return PostCommentList.fromJson(json.decode(response.body)); + } + Future getEmojiGroups() async { HttpieResponse response = await this._emojisApiService.getEmojiGroups(); @@ -857,10 +913,9 @@ class UserService { Future getClosedPostsForCommunity(Community community, {int maxId, int count}) async { - HttpieResponse response = - await _communitiesApiService.getClosedPostsForCommunityWithName(community.name, - count: count, maxId: maxId - ); + HttpieResponse response = await _communitiesApiService + .getClosedPostsForCommunityWithName(community.name, + count: count, maxId: maxId); _checkResponseIsOk(response); return PostsList.fromJson(json.decode(response.body)); } @@ -1411,6 +1466,198 @@ class UserService { return UserNotificationsSettings.fromJSON(json.decode(response.body)); } + Future reportUser( + {@required User user, + String description, + @required ModerationCategory moderationCategory}) async { + HttpieResponse response = await _authApiService.reportUserWithUsername( + description: description, + userUsername: user.username, + moderationCategoryId: moderationCategory.id); + _checkResponseIsCreated(response); + } + + Future reportCommunity( + {@required Community community, + String description, + @required ModerationCategory moderationCategory}) async { + HttpieResponse response = await _communitiesApiService.reportCommunity( + communityName: community.name, + description: description, + moderationCategoryId: moderationCategory.id); + _checkResponseIsCreated(response); + } + + Future reportPost( + {@required Post post, + String description, + @required ModerationCategory moderationCategory}) async { + HttpieResponse response = await _postsApiService.reportPost( + description: description, + postUuid: post.uuid, + moderationCategoryId: moderationCategory.id); + _checkResponseIsCreated(response); + } + + Future reportPostComment( + {@required PostComment postComment, + @required Post post, + String description, + @required ModerationCategory moderationCategory}) async { + HttpieResponse response = await _postsApiService.reportPostComment( + postCommentId: postComment.id, + postUuid: post.uuid, + description: description, + moderationCategoryId: moderationCategory.id); + _checkResponseIsCreated(response); + } + + Future getGlobalModeratedObjects( + {List statuses, + List types, + int count, + int maxId, + bool verified}) async { + HttpieResponse response = + await _moderationApiService + .getGlobalModeratedObjects( + maxId: maxId, + verified: verified, + types: types != null + ? types + .map((ModeratedObjectType type) => + ModeratedObject.factory.convertTypeToString(type)) + .toList() + : null, + statuses: + statuses != null + ? statuses + .map( + (ModeratedObjectStatus status) => + ModeratedObject.factory + .convertStatusToString(status)) + .toList() + : null, + count: count); + + _checkResponseIsOk(response); + + return ModeratedObjectsList.fromJson(json.decode(response.body)); + } + + Future getCommunityModeratedObjects( + {@required Community community, + List statuses, + List types, + int count, + int maxId, + bool verified}) async { + HttpieResponse response = await _communitiesApiService.getModeratedObjects( + communityName: community.name, + maxId: maxId, + verified: verified, + types: types != null + ? types + .map((status) => + ModeratedObject.factory.convertTypeToString(status)) + .toList() + : null, + statuses: statuses != null + ? statuses + .map((status) => + ModeratedObject.factory.convertStatusToString(status)) + .toList() + : null, + count: count); + + _checkResponseIsOk(response); + + return ModeratedObjectsList.fromJson(json.decode(response.body)); + } + + Future updateModeratedObject(ModeratedObject moderatedObject, + {String description, ModerationCategory category}) async { + HttpieResponse response = await _moderationApiService + .updateModeratedObjectWithId(moderatedObject.id, + description: description, categoryId: category?.id); + _checkResponseIsOk(response); + return ModeratedObject.fromJSON(json.decode(response.body)); + } + + Future verifyModeratedObject(ModeratedObject moderatedObject) async { + HttpieResponse response = await _moderationApiService + .verifyModeratedObjectWithId(moderatedObject.id); + _checkResponseIsOk(response); + } + + Future getModeratedObjectLogs( + ModeratedObject moderatedObject, + {int maxId, + int count}) async { + HttpieResponse response = await _moderationApiService + .getModeratedObjectLogs(moderatedObject.id, maxId: maxId, count: count); + _checkResponseIsOk(response); + + return ModeratedObjectLogsList.fromJson(json.decode(response.body)); + } + + Future getModeratedObjectReports( + ModeratedObject moderatedObject, + {int maxId, + int count}) async { + HttpieResponse response = await _moderationApiService + .getModeratedObjectReports(moderatedObject.id, + maxId: maxId, count: count); + _checkResponseIsOk(response); + + return ModerationReportsList.fromJson(json.decode(response.body)); + } + + Future getModerationPenalties( + {int maxId, int count}) async { + HttpieResponse response = await _moderationApiService + .getUserModerationPenalties(maxId: maxId, count: count); + _checkResponseIsOk(response); + + return ModerationPenaltiesList.fromJson(json.decode(response.body)); + } + + Future getPendingModeratedObjectsCommunities( + {int maxId, int count}) async { + HttpieResponse response = await _moderationApiService + .getUserPendingModeratedObjectsCommunities(maxId: maxId, count: count); + _checkResponseIsOk(response); + + return CommunitiesList.fromJson(json.decode(response.body)); + } + + Future unverifyModeratedObject(ModeratedObject moderatedObject) async { + HttpieResponse response = await _moderationApiService + .unverifyModeratedObjectWithId(moderatedObject.id); + _checkResponseIsOk(response); + } + + Future approveModeratedObject(ModeratedObject moderatedObject) async { + HttpieResponse response = await _moderationApiService + .approveModeratedObjectWithId(moderatedObject.id); + _checkResponseIsOk(response); + } + + Future rejectModeratedObject(ModeratedObject moderatedObject) async { + HttpieResponse response = await _moderationApiService + .rejectModeratedObjectWithId(moderatedObject.id); + _checkResponseIsOk(response); + } + + Future getModerationCategories() async { + HttpieResponse response = + await _moderationApiService.getModerationCategories(); + + _checkResponseIsOk(response); + + return ModerationCategoriesList.fromJson(json.decode(response.body)); + } + Future _getDeviceName() async { DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); diff --git a/lib/services/validation.dart b/lib/services/validation.dart index 7975719e4..da52ef787 100644 --- a/lib/services/validation.dart +++ b/lib/services/validation.dart @@ -27,6 +27,7 @@ class ValidationService { static const int COLOR_ATTR_MAX_LENGTH = 7; static const int LIST_MAX_LENGTH = 100; static const int PROFILE_NAME_MAX_LENGTH = 192; + static const int MODERATED_OBJECT_DESCRIPTION_MAX_LENGTH = 1000; static const int PROFILE_NAME_MIN_LENGTH = 1; static const int PROFILE_LOCATION_MAX_LENGTH = 64; static const int PROFILE_BIO_MAX_LENGTH = 150; @@ -87,55 +88,56 @@ class ValidationService { } bool isPostTextAllowedLength(String postText) { - return postText.length < POST_MAX_LENGTH; + return postText.length <= POST_MAX_LENGTH; } bool isBioAllowedLength(String bio) { - return bio.length > 0 && bio.length < PROFILE_BIO_MAX_LENGTH; + return bio.length > 0 && bio.length <= PROFILE_BIO_MAX_LENGTH; } bool isLocationAllowedLength(String location) { - return location.length > 0 && location.length < PROFILE_LOCATION_MAX_LENGTH; + return location.length > 0 && + location.length <= PROFILE_LOCATION_MAX_LENGTH; } bool isUsernameAllowedLength(String username) { - return username.length > 0 && username.length < USERNAME_MAX_LENGTH; + return username.length > 0 && username.length <= USERNAME_MAX_LENGTH; } bool isPostCommentAllowedLength(String postComment) { return postComment.length > 0 && - postComment.length < POST_COMMENT_MAX_LENGTH; + postComment.length <= POST_COMMENT_MAX_LENGTH; } bool isCommunityNameAllowedLength(String name) { - return name.length > 0 && name.length < COMMUNITY_NAME_MAX_LENGTH; + return name.length > 0 && name.length <= COMMUNITY_NAME_MAX_LENGTH; } bool isCommunityDescriptionAllowedLength(String description) { return description.length > 0 && - description.length < COMMUNITY_DESCRIPTION_MAX_LENGTH; + description.length <= COMMUNITY_DESCRIPTION_MAX_LENGTH; } bool isCommunityTitleAllowedLength(String title) { - return title.length > 0 && title.length < COMMUNITY_TITLE_MAX_LENGTH; + return title.length > 0 && title.length <= COMMUNITY_TITLE_MAX_LENGTH; } bool isCommunityRulesAllowedLength(String rules) { - return rules.length > 0 && rules.length < COMMUNITY_RULES_MAX_LENGTH; + return rules.length > 0 && rules.length <= COMMUNITY_RULES_MAX_LENGTH; } bool isCommunityUserAdjectiveAllowedLength(String userAdjective) { return userAdjective.length > 0 && - userAdjective.length < COMMUNITY_USER_ADJECTIVE_MAX_LENGTH; + userAdjective.length <= COMMUNITY_USER_ADJECTIVE_MAX_LENGTH; } bool isFollowsListNameAllowedLength(String followsList) { - return followsList.length > 0 && followsList.length < LIST_MAX_LENGTH; + return followsList.length > 0 && followsList.length <= LIST_MAX_LENGTH; } bool isConnectionsCircleNameAllowedLength(String connectionsCircle) { return connectionsCircle.length > 0 && - connectionsCircle.length < CIRCLE_MAX_LENGTH; + connectionsCircle.length <= CIRCLE_MAX_LENGTH; } bool isUsernameAllowedCharacters(String username) { @@ -219,6 +221,11 @@ class ValidationService { name.length <= PROFILE_NAME_MAX_LENGTH; } + bool isModeratedObjectDescriptionAllowedLength( + String moderatedObjectDescription) { + return moderatedObjectDescription.length <= PROFILE_NAME_MAX_LENGTH; + } + Future isImageAllowedSize(File image, OBImageType type) async { int size = await image.length(); return size <= getAllowedImageSize(type); @@ -324,6 +331,20 @@ class ValidationService { return errorMsg; } + String validateModeratedObjectDescription(String description) { + assert(description != null); + + String errorMsg; + + if (description.isEmpty) { + errorMsg = 'Description can\'t be empty.'; + } else if (!isModeratedObjectDescriptionAllowedLength(description)) { + errorMsg = + 'Description must be between $PROFILE_NAME_MIN_LENGTH and $PROFILE_NAME_MAX_LENGTH characters.'; + } + return errorMsg; + } + String validateUserProfileUrl(String url) { assert(url != null); diff --git a/lib/services/waitlist_service.dart b/lib/services/waitlist_service.dart new file mode 100644 index 000000000..504e69979 --- /dev/null +++ b/lib/services/waitlist_service.dart @@ -0,0 +1,27 @@ +import 'package:Openbook/services/httpie.dart'; + +class WaitlistApiService { + HttpieService _httpService; + String openbookSocialApiURL; + + static const MAILCHIMP_SUBSCRIBE_PATH = 'waitlist/subscribe/'; + static const HEALTH_PATH = 'health/'; + + void setOpenbookSocialApiURL(String newApiURL) { + openbookSocialApiURL = newApiURL; + } + + void setHttpService(HttpieService httpService) { + _httpService = httpService; + } + + Future subscribeToBetaWaitlist({String email}) { + var body = {}; + if (email != null && email != '') { + body['email'] = email; + } + return this + ._httpService + .postJSON('$openbookSocialApiURL$MAILCHIMP_SUBSCRIBE_PATH', body: body); + } +} \ No newline at end of file diff --git a/lib/widgets/buttons/see_all_button.dart b/lib/widgets/buttons/see_all_button.dart new file mode 100644 index 000000000..01bc78de8 --- /dev/null +++ b/lib/widgets/buttons/see_all_button.dart @@ -0,0 +1,46 @@ +import 'package:Openbook/widgets/theming/secondary_text.dart'; +import 'package:flutter/material.dart'; + +import '../icon.dart'; + +class OBSeeAllButton extends StatelessWidget { + final VoidCallback onPressed; + final String resourceName; + final int previewedResourcesCount; + final int resourcesCount; + + const OBSeeAllButton( + {Key key, + @required this.onPressed, + @required this.resourceName, + @required this.previewedResourcesCount, + @required this.resourcesCount}) + : super(key: key); + + @override + Widget build(BuildContext context) { + int remainingResourcesToDisplay = resourcesCount - previewedResourcesCount; + + if (previewedResourcesCount == 0 || remainingResourcesToDisplay <= 0) return const SizedBox(); + + return GestureDetector( + onTap: onPressed, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + OBSecondaryText( + 'See all $resourcesCount $resourceName', + style: TextStyle(fontSize: 16), + ), + const SizedBox( + width: 5, + ), + OBIcon(OBIcons.seeMore, themeColor: OBIconThemeColor.secondaryText) + ], + ), + ), + ); + } +} diff --git a/lib/widgets/checkbox.dart b/lib/widgets/checkbox.dart index e67c628bb..53ef7a931 100644 --- a/lib/widgets/checkbox.dart +++ b/lib/widgets/checkbox.dart @@ -16,7 +16,7 @@ class OBCheckbox extends StatelessWidget { return GestureDetector( child: DecoratedBox( decoration: BoxDecoration( - color: Colors.white, borderRadius: BorderRadius.circular(50)), + borderRadius: BorderRadius.circular(50)), child: Center( child: OBIcon( value ? OBIcons.checkCircleSelected : OBIcons.checkCircle, diff --git a/lib/widgets/fields/checkbox_field.dart b/lib/widgets/fields/checkbox_field.dart index b43df3cab..32eb0afc6 100644 --- a/lib/widgets/fields/checkbox_field.dart +++ b/lib/widgets/fields/checkbox_field.dart @@ -10,6 +10,7 @@ class OBCheckboxField extends StatelessWidget { final String title; final String subtitle; final bool isDisabled; + final TextStyle titleStyle; OBCheckboxField( {@required this.value, @@ -17,17 +18,21 @@ class OBCheckboxField extends StatelessWidget { this.onTap, this.leading, @required this.title, - this.isDisabled = false}); + this.isDisabled = false, + this.titleStyle}); @override Widget build(BuildContext context) { + TextStyle finalTitleStyle = TextStyle(fontWeight: FontWeight.bold); + if (titleStyle != null) finalTitleStyle = finalTitleStyle.merge(titleStyle); + Widget field = MergeSemantics( child: ListTile( selected: value, leading: leading, title: OBText( title, - style: TextStyle(fontWeight: FontWeight.bold), + style: finalTitleStyle, ), subtitle: subtitle != null ? OBText(subtitle) : null, trailing: Row( @@ -39,7 +44,7 @@ class OBCheckboxField extends StatelessWidget { ], ), onTap: () { - if (!isDisabled) onTap(); + if (!isDisabled && onTap != null) onTap(); }), ); diff --git a/lib/widgets/fields/text_form_field.dart b/lib/widgets/fields/text_form_field.dart index 5f922c91e..019cbe56d 100644 --- a/lib/widgets/fields/text_form_field.dart +++ b/lib/widgets/fields/text_form_field.dart @@ -88,7 +88,7 @@ class OBTextFormField extends StatelessWidget { obscureText: obscureText, style: finalStyle, decoration: InputDecoration( - hintText: decoration.hintText, + hintText: decoration?.hintText, labelStyle: TextStyle( height: labelHeight, fontWeight: FontWeight.bold, @@ -98,13 +98,13 @@ class OBTextFormField extends StatelessWidget { hintStyle: TextStyle( color: themeValueParserService .parseColor(theme.primaryTextColor)), - contentPadding: decoration.contentPadding ?? + contentPadding: decoration?.contentPadding ?? EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0), border: InputBorder.none, - labelText: decoration.labelText, - prefixIcon: decoration.prefixIcon, - prefixText: decoration.prefixText, - errorMaxLines: decoration.errorMaxLines ?? 3 + labelText: decoration?.labelText, + prefixIcon: decoration?.prefixIcon, + prefixText: decoration?.prefixText, + errorMaxLines: decoration?.errorMaxLines ?? 3 ), ), hasBorder ? const OBDivider() : const SizedBox() diff --git a/lib/widgets/icon.dart b/lib/widgets/icon.dart index e673a4621..5617e1ba7 100644 --- a/lib/widgets/icon.dart +++ b/lib/widgets/icon.dart @@ -167,7 +167,7 @@ class OBIcons { static const disconnect = OBIconData(nativeIcon: Icons.remove_circle_outline); static const deletePost = OBIconData(nativeIcon: Icons.delete); static const clear = OBIconData(nativeIcon: Icons.delete); - static const reportPost = OBIconData(nativeIcon: Icons.report); + static const report = OBIconData(nativeIcon: Icons.flag); static const filter = OBIconData(nativeIcon: Icons.tune); static const gallery = OBIconData(nativeIcon: Icons.apps); static const camera = OBIconData(nativeIcon: Icons.camera_alt); @@ -188,7 +188,7 @@ class OBIcons { static const deleteCommunity = OBIconData(nativeIcon: Icons.delete_forever); static const seeMore = OBIconData(nativeIcon: Icons.arrow_right); static const leaveCommunity = OBIconData(nativeIcon: Icons.exit_to_app); - static const reportCommunity = OBIconData(nativeIcon: Icons.report); + static const reportCommunity = OBIconData(nativeIcon: Icons.flag); static const communityInvites = OBIconData(nativeIcon: Icons.email); static const favoriteCommunity = OBIconData(nativeIcon: Icons.favorite); static const unfavoriteCommunity = @@ -196,6 +196,8 @@ class OBIcons { static const expand = OBIconData(filename: 'expand-icon.png'); static const mutePost = OBIconData(nativeIcon: Icons.notifications_active); static const editPost = OBIconData(nativeIcon: Icons.edit); + static const edit = OBIconData(nativeIcon: Icons.edit); + static const reviewModeratedObject = OBIconData(nativeIcon: Icons.gavel); static const unmutePost = OBIconData(nativeIcon: Icons.notifications_off); static const deleteAccount = OBIconData(nativeIcon: Icons.delete_forever); static const account = OBIconData(nativeIcon: Icons.account_circle); @@ -210,10 +212,16 @@ class OBIcons { static const themes = OBIconData(nativeIcon: Icons.format_paint); static const invite = OBIconData(nativeIcon: Icons.card_giftcard); static const disableComments = OBIconData(nativeIcon: Icons.chat_bubble); - static const enableComments = OBIconData(nativeIcon: Icons.chat_bubble_outline); + static const enableComments = + OBIconData(nativeIcon: Icons.chat_bubble_outline); static const closePost = OBIconData(nativeIcon: Icons.lock_outline); static const openPost = OBIconData(nativeIcon: Icons.lock_open); static const block = OBIconData(nativeIcon: Icons.block); + static const chevronRight = OBIconData(nativeIcon: Icons.chevron_right); + static const verify = OBIconData(nativeIcon: Icons.check); + static const unverify = OBIconData(nativeIcon: Icons.close); + static const globalModerator = OBIconData(nativeIcon: Icons.account_balance); + static const moderationPenalties = OBIconData(nativeIcon: Icons.flag); static const success = OBIconData(filename: 'success-icon.png'); static const error = OBIconData(filename: 'error-icon.png'); static const warning = OBIconData(filename: 'warning-icon.png'); diff --git a/lib/widgets/moderated_object_status_circle.dart b/lib/widgets/moderated_object_status_circle.dart new file mode 100644 index 000000000..cad19d433 --- /dev/null +++ b/lib/widgets/moderated_object_status_circle.dart @@ -0,0 +1,43 @@ +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/theme.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:flutter/material.dart'; +import 'package:pigment/pigment.dart'; + +class OBModeratedObjectStatusCircle extends StatelessWidget { + final ModeratedObjectStatus status; + + static double statusCircleSize = 10; + static String pendingColor = '#f48c42'; + + const OBModeratedObjectStatusCircle({Key key, @required this.status}) + : super(key: key); + + @override + Widget build(BuildContext context) { + OpenbookProviderState openbookProvider = OpenbookProvider.of(context); + OBTheme currentTheme = openbookProvider.themeService.getActiveTheme(); + + String circleColor; + switch (status) { + case ModeratedObjectStatus.rejected: + circleColor = currentTheme.dangerColor; + break; + case ModeratedObjectStatus.approved: + circleColor = currentTheme.successColor; + break; + case ModeratedObjectStatus.pending: + circleColor = pendingColor; + break; + default: + } + + return Container( + height: statusCircleSize, + width: statusCircleSize, + decoration: BoxDecoration( + color: Pigment.fromString(circleColor), + borderRadius: BorderRadius.circular(50))); + } +} diff --git a/lib/widgets/post/post.dart b/lib/widgets/post/post.dart index 11ac7c4e9..69ddffce9 100644 --- a/lib/widgets/post/post.dart +++ b/lib/widgets/post/post.dart @@ -12,7 +12,7 @@ import 'package:flutter/material.dart'; class OBPost extends StatelessWidget { final Post post; - final OnPostDeleted onPostDeleted; + final ValueChanged onPostDeleted; const OBPost(this.post, {Key key, @required this.onPostDeleted}) : super(key: key); @@ -27,6 +27,7 @@ class OBPost extends StatelessWidget { OBPostHeader( post: post, onPostDeleted: onPostDeleted, + onPostReported: onPostDeleted, ), OBPostBody(post), OBPostReactions(post), diff --git a/lib/widgets/post/widgets/post_header/post_header.dart b/lib/widgets/post/widgets/post_header/post_header.dart index 9352bfb5f..f0bcf1be7 100644 --- a/lib/widgets/post/widgets/post_header/post_header.dart +++ b/lib/widgets/post/widgets/post_header/post_header.dart @@ -7,20 +7,27 @@ import 'package:flutter/material.dart'; class OBPostHeader extends StatelessWidget { final Post post; final OnPostDeleted onPostDeleted; + final ValueChanged onPostReported; + final bool hasActions; - const OBPostHeader({Key key, this.onPostDeleted, this.post}) + const OBPostHeader( + {Key key, + this.onPostDeleted, + this.post, + this.onPostReported, + this.hasActions = true}) : super(key: key); @override Widget build(BuildContext context) { return post.isCommunityPost() - ? OBCommunityPostHeader( - post, + ? OBCommunityPostHeader(post, onPostDeleted: onPostDeleted, - ) - : OBUserPostHeader( - post, + onPostReported: onPostReported, + hasActions: hasActions) + : OBUserPostHeader(post, onPostDeleted: onPostDeleted, - ); + onPostReported: onPostReported, + hasActions: hasActions); } } diff --git a/lib/widgets/post/widgets/post_header/widgets/community_post_header.dart b/lib/widgets/post/widgets/post_header/widgets/community_post_header.dart index fbadd6b43..698c0d66c 100644 --- a/lib/widgets/post/widgets/post_header/widgets/community_post_header.dart +++ b/lib/widgets/post/widgets/post_header/widgets/community_post_header.dart @@ -14,9 +14,14 @@ import 'package:flutter/material.dart'; class OBCommunityPostHeader extends StatelessWidget { final Post _post; final OnPostDeleted onPostDeleted; + final ValueChanged onPostReported; + final bool hasActions; const OBCommunityPostHeader(this._post, - {Key key, @required this.onPostDeleted}) + {Key key, + @required this.onPostDeleted, + this.onPostReported, + this.hasActions = true}) : super(key: key); @override @@ -40,15 +45,17 @@ class OBCommunityPostHeader extends StatelessWidget { user: _post.creator, context: context); }, ), - trailing: IconButton( - icon: const OBIcon(OBIcons.moreVertical), - onPressed: () { - bottomSheetService.showPostActions( - context: context, - post: _post, - onPostDeleted: onPostDeleted, - onPostReported: null); - }), + trailing: hasActions + ? IconButton( + icon: const OBIcon(OBIcons.moreVertical), + onPressed: () { + bottomSheetService.showPostActions( + context: context, + post: _post, + onPostDeleted: onPostDeleted, + onPostReported: onPostReported); + }) + : null, title: GestureDetector( onTap: () { navigationService.navigateToCommunity( diff --git a/lib/widgets/post/widgets/post_header/widgets/user_post_header.dart b/lib/widgets/post/widgets/post_header/widgets/user_post_header.dart index a42ba6992..f1934fa30 100644 --- a/lib/widgets/post/widgets/post_header/widgets/user_post_header.dart +++ b/lib/widgets/post/widgets/post_header/widgets/user_post_header.dart @@ -14,8 +14,14 @@ import 'package:flutter/material.dart'; class OBUserPostHeader extends StatelessWidget { final Post _post; final OnPostDeleted onPostDeleted; + final ValueChanged onPostReported; + final bool hasActions; - const OBUserPostHeader(this._post, {Key key, @required this.onPostDeleted}) + const OBUserPostHeader(this._post, + {Key key, + @required this.onPostDeleted, + this.onPostReported, + this.hasActions = true}) : super(key: key); @override @@ -44,15 +50,17 @@ class OBUserPostHeader extends StatelessWidget { avatarUrl: postCreator.getProfileAvatar(), ); }), - trailing: IconButton( - icon: const OBIcon(OBIcons.moreVertical), - onPressed: () { - bottomSheetService.showPostActions( - context: context, - post: _post, - onPostDeleted: onPostDeleted, - onPostReported: null); - }), + trailing: hasActions + ? IconButton( + icon: const OBIcon(OBIcons.moreVertical), + onPressed: () { + bottomSheetService.showPostActions( + context: context, + post: _post, + onPostDeleted: onPostDeleted, + onPostReported: onPostReported); + }) + : null, title: GestureDetector( onTap: () { navigationService.navigateToUserProfile( diff --git a/lib/widgets/tile_group_title.dart b/lib/widgets/tile_group_title.dart index ff87ccb00..c8fc6874c 100644 --- a/lib/widgets/tile_group_title.dart +++ b/lib/widgets/tile_group_title.dart @@ -3,17 +3,21 @@ import 'package:flutter/material.dart'; class OBTileGroupTitle extends StatelessWidget { final String title; + final TextStyle style; - const OBTileGroupTitle({Key key, this.title}) : super(key: key); + const OBTileGroupTitle({Key key, this.title, this.style}) : super(key: key); @override Widget build(BuildContext context) { + var finalStyle = TextStyle(fontWeight: FontWeight.bold); + if (style != null) finalStyle = finalStyle.merge(style); + return Padding( - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 5), + padding: EdgeInsets.symmetric(horizontal: 15, vertical: 5), child: OBText( title, size: OBTextSize.large, - style: TextStyle(fontWeight: FontWeight.bold), + style: finalStyle, ), ); } diff --git a/lib/widgets/tiles/actions/report_community_tile.dart b/lib/widgets/tiles/actions/report_community_tile.dart new file mode 100644 index 000000000..8d09d2908 --- /dev/null +++ b/lib/widgets/tiles/actions/report_community_tile.dart @@ -0,0 +1,70 @@ +import 'package:Openbook/models/community.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/navigation_service.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:Openbook/widgets/tiles/loading_tile.dart'; +import 'package:flutter/material.dart'; + +class OBReportCommunityTile extends StatefulWidget { + final Community community; + final ValueChanged onCommunityReported; + final VoidCallback onWantsToReportCommunity; + + const OBReportCommunityTile({ + Key key, + this.onCommunityReported, + @required this.community, + this.onWantsToReportCommunity, + }) : super(key: key); + + @override + OBReportCommunityTileState createState() { + return OBReportCommunityTileState(); + } +} + +class OBReportCommunityTileState extends State { + NavigationService _navigationService; + bool _requestInProgress; + + @override + void initState() { + super.initState(); + _requestInProgress = false; + } + + @override + Widget build(BuildContext context) { + var openbookProvider = OpenbookProvider.of(context); + _navigationService = openbookProvider.navigationService; + + return StreamBuilder( + stream: widget.community.updateSubject, + initialData: widget.community, + builder: (BuildContext context, AsyncSnapshot snapshot) { + var community = snapshot.data; + + bool isReported = community.isReported ?? false; + + return OBLoadingTile( + isLoading: _requestInProgress || isReported, + leading: OBIcon(OBIcons.report), + title: OBText(isReported + ? 'You have reported this community' + : 'Report community'), + onTap: isReported ? () {} : _reportCommunity, + ); + }, + ); + } + + void _reportCommunity() { + if (widget.onWantsToReportCommunity != null) + widget.onWantsToReportCommunity(); + _navigationService.navigateToReportObject( + context: context, + object: widget.community, + onObjectReported: widget.onCommunityReported); + } +} diff --git a/lib/widgets/tiles/actions/report_post_comment_tile.dart b/lib/widgets/tiles/actions/report_post_comment_tile.dart new file mode 100644 index 000000000..3b66511e1 --- /dev/null +++ b/lib/widgets/tiles/actions/report_post_comment_tile.dart @@ -0,0 +1,69 @@ +import 'package:Openbook/models/post_comment.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/navigation_service.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:Openbook/widgets/tiles/loading_tile.dart'; +import 'package:flutter/material.dart'; + +class OBReportPostCommentTile extends StatefulWidget { + final PostComment postComment; + final ValueChanged onPostCommentReported; + final VoidCallback onWantsToReportPostComment; + + const OBReportPostCommentTile({ + Key key, + this.onPostCommentReported, + @required this.postComment, + this.onWantsToReportPostComment, + }) : super(key: key); + + @override + OBReportPostCommentTileState createState() { + return OBReportPostCommentTileState(); + } +} + +class OBReportPostCommentTileState extends State { + NavigationService _navigationService; + bool _requestInProgress; + + @override + void initState() { + super.initState(); + _requestInProgress = false; + } + + @override + Widget build(BuildContext context) { + var openbookProvider = OpenbookProvider.of(context); + _navigationService = openbookProvider.navigationService; + + return StreamBuilder( + stream: widget.postComment.updateSubject, + initialData: widget.postComment, + builder: (BuildContext context, AsyncSnapshot snapshot) { + var postComment = snapshot.data; + + bool isReported = postComment.isReported ?? false; + + return OBLoadingTile( + isLoading: _requestInProgress || isReported, + leading: OBIcon(OBIcons.report), + title: OBText( + isReported ? 'You have reported this comment' : 'Report comment'), + onTap: isReported ? () {} : _reportPostComment, + ); + }, + ); + } + + void _reportPostComment() { + if (widget.onWantsToReportPostComment != null) + widget.onWantsToReportPostComment(); + _navigationService.navigateToReportObject( + context: context, + object: widget.postComment, + onObjectReported: widget.onPostCommentReported); + } +} diff --git a/lib/widgets/tiles/actions/report_post_tile.dart b/lib/widgets/tiles/actions/report_post_tile.dart new file mode 100644 index 000000000..41f5a397d --- /dev/null +++ b/lib/widgets/tiles/actions/report_post_tile.dart @@ -0,0 +1,71 @@ +import 'package:Openbook/models/post.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/navigation_service.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:Openbook/widgets/tiles/loading_tile.dart'; +import 'package:flutter/material.dart'; + +class OBReportPostTile extends StatefulWidget { + final Post post; + final ValueChanged onPostReported; + final VoidCallback onWantsToReportPost; + + const OBReportPostTile({ + Key key, + this.onPostReported, + @required this.post, + this.onWantsToReportPost, + }) : super(key: key); + + @override + OBReportPostTileState createState() { + return OBReportPostTileState(); + } +} + +class OBReportPostTileState extends State { + NavigationService _navigationService; + bool _requestInProgress; + + @override + void initState() { + super.initState(); + _requestInProgress = false; + } + + @override + Widget build(BuildContext context) { + var openbookProvider = OpenbookProvider.of(context); + _navigationService = openbookProvider.navigationService; + + return StreamBuilder( + stream: widget.post.updateSubject, + initialData: widget.post, + builder: (BuildContext context, AsyncSnapshot snapshot) { + var post = snapshot.data; + + bool isReported = post.isReported ?? false; + + return OBLoadingTile( + isLoading: _requestInProgress || isReported, + leading: OBIcon(OBIcons.report), + title: OBText( + isReported ? 'You have reported this post' : 'Report post'), + onTap: isReported ? () {} : _reportPost, + ); + }, + ); + } + + void _reportPost() { + if (widget.onWantsToReportPost != null) widget.onWantsToReportPost(); + _navigationService.navigateToReportObject( + context: context, + object: widget.post, + onObjectReported: (dynamic reportedObject) { + if (reportedObject != null && widget.onPostReported != null) + widget.onPostReported(reportedObject as Post); + }); + } +} diff --git a/lib/widgets/tiles/actions/report_user_tile.dart b/lib/widgets/tiles/actions/report_user_tile.dart new file mode 100644 index 000000000..89fe1d9d8 --- /dev/null +++ b/lib/widgets/tiles/actions/report_user_tile.dart @@ -0,0 +1,68 @@ +import 'package:Openbook/models/user.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/services/navigation_service.dart'; +import 'package:Openbook/widgets/icon.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:Openbook/widgets/tiles/loading_tile.dart'; +import 'package:flutter/material.dart'; + +class OBReportUserTile extends StatefulWidget { + final User user; + final ValueChanged onUserReported; + final VoidCallback onWantsToReportUser; + + const OBReportUserTile({ + Key key, + this.onUserReported, + @required this.user, + this.onWantsToReportUser, + }) : super(key: key); + + @override + OBReportUserTileState createState() { + return OBReportUserTileState(); + } +} + +class OBReportUserTileState extends State { + NavigationService _navigationService; + bool _requestInProgress; + + @override + void initState() { + super.initState(); + _requestInProgress = false; + } + + @override + Widget build(BuildContext context) { + var openbookProvider = OpenbookProvider.of(context); + _navigationService = openbookProvider.navigationService; + + return StreamBuilder( + stream: widget.user.updateSubject, + initialData: widget.user, + builder: (BuildContext context, AsyncSnapshot snapshot) { + var user = snapshot.data; + + bool isReported = user.isReported ?? false; + + return OBLoadingTile( + isLoading: _requestInProgress || isReported, + leading: OBIcon(OBIcons.report), + title: OBText( + isReported ? 'You have reported this account' : 'Report account'), + onTap: isReported ? () {} : _reportUser, + ); + }, + ); + } + + void _reportUser() { + if (widget.onWantsToReportUser != null) widget.onWantsToReportUser(); + _navigationService.navigateToReportObject( + context: context, + object: widget.user, + onObjectReported: widget.onUserReported); + } +} diff --git a/lib/widgets/tiles/moderated_object_status_tile.dart b/lib/widgets/tiles/moderated_object_status_tile.dart new file mode 100644 index 000000000..90b2d7b7a --- /dev/null +++ b/lib/widgets/tiles/moderated_object_status_tile.dart @@ -0,0 +1,52 @@ +import 'package:Openbook/models/moderation/moderated_object.dart'; +import 'package:Openbook/models/theme.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:flutter/material.dart'; +import 'package:pigment/pigment.dart'; + +import '../moderated_object_status_circle.dart'; + +class OBModeratedObjectStatusTile extends StatelessWidget { + final ModeratedObject moderatedObject; + final ValueChanged onPressed; + final Widget trailing; + + static double statusCircleSize = 10; + static String pendingColor = '#f48c42'; + + const OBModeratedObjectStatusTile( + {Key key, @required this.moderatedObject, this.onPressed, this.trailing}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: moderatedObject.updateSubject, + initialData: moderatedObject, + builder: (BuildContext context, AsyncSnapshot snapshot) { + ModeratedObject currentModeratedObject = snapshot.data; + + return ListTile( + title: Row( + children: [ + OBModeratedObjectStatusCircle( + status: moderatedObject.status, + ), + const SizedBox( + width: 10, + ), + OBText(ModeratedObject.factory.convertStatusToHumanReadableString( + currentModeratedObject.status, + capitalize: true)) + ], + ), + onTap: () async { + if (onPressed != null) onPressed(moderatedObject); + }, + trailing: trailing, + ); + }, + ); + } +} diff --git a/lib/widgets/tiles/moderation_category_tile.dart b/lib/widgets/tiles/moderation_category_tile.dart new file mode 100644 index 000000000..27e08d559 --- /dev/null +++ b/lib/widgets/tiles/moderation_category_tile.dart @@ -0,0 +1,39 @@ +import 'package:Openbook/models/moderation/moderation_category.dart'; +import 'package:Openbook/widgets/theming/secondary_text.dart'; +import 'package:Openbook/widgets/theming/text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class OBModerationCategoryTile extends StatelessWidget { + final ModerationCategory category; + final Widget trailing; + final ValueChanged onPressed; + final EdgeInsets contentPadding; + + const OBModerationCategoryTile( + {Key key, + this.trailing, + @required this.category, + this.onPressed, + this.contentPadding}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + key: Key(category.id.toString()), + onTap: () { + if (onPressed != null) onPressed(category); + }, + child: ListTile( + contentPadding: contentPadding, + title: OBText( + category.title, + ), + subtitle: OBSecondaryText(category.description), + trailing: trailing, + //trailing: OBIcon(OBIcons.chevronRight), + ), + ); + } +} diff --git a/lib/widgets/tiles/notification_tile/notification_tile.dart b/lib/widgets/tiles/notification_tile/notification_tile.dart index adeb99c78..b57692da8 100644 --- a/lib/widgets/tiles/notification_tile/notification_tile.dart +++ b/lib/widgets/tiles/notification_tile/notification_tile.dart @@ -4,6 +4,7 @@ import 'package:Openbook/models/notifications/connection_request_notification.da import 'package:Openbook/models/notifications/follow_notification.dart'; import 'package:Openbook/models/notifications/notification.dart'; import 'package:Openbook/models/notifications/post_comment_notification.dart'; +import 'package:Openbook/models/notifications/post_comment_reply_notification.dart'; import 'package:Openbook/models/notifications/post_reaction_notification.dart'; import 'package:Openbook/models/theme.dart'; import 'package:Openbook/provider.dart'; @@ -14,6 +15,7 @@ import 'package:Openbook/widgets/tiles/notification_tile/widgets/connection_conf import 'package:Openbook/widgets/tiles/notification_tile/widgets/connection_request_notification_tile.dart'; import 'package:Openbook/widgets/tiles/notification_tile/widgets/follow_notification_tile.dart'; import 'package:Openbook/widgets/tiles/notification_tile/widgets/post_comment_notification_tile.dart'; +import 'package:Openbook/widgets/tiles/notification_tile/widgets/post_comment_reply_notification_tile.dart'; import 'package:Openbook/widgets/tiles/notification_tile/widgets/post_reaction_notification_tile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -78,6 +80,13 @@ class OBNotificationTile extends StatelessWidget { onPressed: finalOnPressed, ); break; + case PostCommentReplyNotification: + notificationTile = OBPostCommentReplyNotificationTile( + notification: notification, + postCommentNotification: notificationContentObject, + onPressed: finalOnPressed, + ); + break; case PostReactionNotification: notificationTile = OBPostReactionNotificationTile( notification: notification, diff --git a/lib/widgets/tiles/notification_tile/widgets/post_comment_reply_notification_tile.dart b/lib/widgets/tiles/notification_tile/widgets/post_comment_reply_notification_tile.dart new file mode 100644 index 000000000..dfd7ef7b4 --- /dev/null +++ b/lib/widgets/tiles/notification_tile/widgets/post_comment_reply_notification_tile.dart @@ -0,0 +1,81 @@ +import 'package:Openbook/models/notifications/notification.dart'; +import 'package:Openbook/models/notifications/post_comment_notification.dart'; +import 'package:Openbook/models/notifications/post_comment_reply_notification.dart'; +import 'package:Openbook/models/post.dart'; +import 'package:Openbook/models/post_comment.dart'; +import 'package:Openbook/provider.dart'; +import 'package:Openbook/widgets/avatars/avatar.dart'; +import 'package:Openbook/widgets/theming/actionable_smart_text.dart'; +import 'package:Openbook/widgets/theming/secondary_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_advanced_networkimage/provider.dart'; + +class OBPostCommentReplyNotificationTile extends StatelessWidget { + final OBNotification notification; + final PostCommentReplyNotification postCommentNotification; + static final double postImagePreviewSize = 40; + final VoidCallback onPressed; + + const OBPostCommentReplyNotificationTile( + {Key key, + @required this.notification, + @required this.postCommentNotification, + this.onPressed}) + : super(key: key); + + @override + Widget build(BuildContext context) { + PostComment postComment = postCommentNotification.postComment; + PostComment parentComment = postCommentNotification.parentComment; + Post post = postComment.post; + String postCommenterUsername = postComment.getCommenterUsername(); + String postCommentText = postComment.text; + + int postCreatorId = postCommentNotification.getPostCreatorId(); + OpenbookProviderState openbookProvider = OpenbookProvider.of(context); + bool isOwnPostNotification = + openbookProvider.userService.getLoggedInUser().id == postCreatorId; + + Widget postImagePreview; + if (post.hasImage()) { + postImagePreview = ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: Image( + image: AdvancedNetworkImage(post.getImage(), useDiskCache: true), + height: postImagePreviewSize, + width: postImagePreviewSize, + fit: BoxFit.cover, + ), + ); + } + + Function navigateToCommenterProfile = () { + OpenbookProviderState openbookProvider = OpenbookProvider.of(context); + + openbookProvider.navigationService + .navigateToUserProfile(user: postComment.commenter, context: context); + }; + + return ListTile( + onTap: () { + if (onPressed != null) onPressed(); + OpenbookProviderState openbookProvider = OpenbookProvider.of(context); + + openbookProvider.navigationService.navigateToPostCommentRepliesLinked( + postComment: postComment, context: context, parentComment: parentComment); + }, + leading: OBAvatar( + onPressed: navigateToCommenterProfile, + size: OBAvatarSize.medium, + avatarUrl: postComment.commenter.getProfileAvatar(), + ), + title: OBActionableSmartText( + text: isOwnPostNotification + ? '@$postCommenterUsername replied: $postCommentText' + : '@$postCommenterUsername also replied: $postCommentText', + ), + trailing: postImagePreview, + subtitle: OBSecondaryText(notification.getRelativeCreated()), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 9b86ad845..8fc7490c7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,35 +7,35 @@ packages: name: after_layout url: "https://pub.dartlang.org" source: hosted - version: "1.0.7" + version: "1.0.7+1" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.35.4" + version: "0.36.3" archive: dependency: transitive description: name: archive url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.0.9" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.5.1" + version: "1.5.2" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.1.0" back_button_interceptor: dependency: "direct main" description: @@ -85,6 +85,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.6" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.0" cupertino_icons: dependency: "direct main" description: @@ -92,15 +99,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.2" - dart_config: - dependency: transitive - description: - path: "." - ref: HEAD - resolved-ref: a7ed88a4793e094a4d5d5c2d88a89e55510accde - url: "https://github.com/MarkOSullivan94/dart_config.git" - source: git - version: "0.5.0" dcache: dependency: "direct main" description: @@ -114,7 +112,7 @@ packages: name: device_info url: "https://pub.dartlang.org" source: hosted - version: "0.4.0+1" + version: "0.4.0+2" flutter: dependency: "direct main" description: flutter @@ -140,21 +138,21 @@ packages: name: flutter_colorpicker url: "https://pub.dartlang.org" source: hosted - version: "0.2.3" + version: "0.2.5" flutter_exif_rotation: dependency: "direct main" description: name: flutter_exif_rotation url: "https://pub.dartlang.org" source: hosted - version: "0.2.1+1" + version: "0.2.2" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons url: "https://pub.dartlang.org" source: hosted - version: "0.7.0" + version: "0.7.2" flutter_localizations: dependency: "direct main" description: flutter @@ -194,7 +192,7 @@ packages: name: flutter_svg url: "https://pub.dartlang.org" source: hosted - version: "0.12.4" + version: "0.12.4+2" flutter_test: dependency: "direct dev" description: flutter @@ -206,7 +204,7 @@ packages: name: front_end url: "https://pub.dartlang.org" source: hosted - version: "0.1.14" + version: "0.1.18" glob: dependency: transitive description: @@ -214,6 +212,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.7" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+2" http: dependency: "direct main" description: @@ -227,7 +232,7 @@ packages: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.1.0" http_parser: dependency: transitive description: @@ -241,14 +246,14 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "2.0.7" + version: "2.1.4" image_cropper: dependency: "direct main" description: name: image_cropper url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" image_picker: dependency: "direct main" description: @@ -269,7 +274,7 @@ packages: name: intl url: "https://pub.dartlang.org" source: hosted - version: "0.15.7" + version: "0.15.8" io: dependency: transitive description: @@ -297,7 +302,7 @@ packages: name: kernel url: "https://pub.dartlang.org" source: hosted - version: "0.3.14" + version: "0.3.18" markdown: dependency: transitive description: @@ -311,7 +316,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.3+1" + version: "0.12.5" meta: dependency: transitive description: @@ -325,7 +330,14 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "0.9.6+2" + version: "0.9.6+3" + multi_image_picker: + dependency: "direct main" + description: + name: multi_image_picker + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.3" multi_server_socket: dependency: transitive description: @@ -340,12 +352,14 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.4.4" - onesignal: + onesignalflutter: dependency: "direct main" description: - name: onesignal - url: "https://pub.dartlang.org" - source: hosted + path: "." + ref: HEAD + resolved-ref: fb4bd2da344db58f2073d314ff1e363556804673 + url: "git://github.com/jmrobles/OneSignal-Flutter-SDK.git" + source: git version: "1.1.0" package_config: dependency: transitive @@ -374,14 +388,14 @@ packages: name: path_drawing url: "https://pub.dartlang.org" source: hosted - version: "0.4.0" + version: "0.4.1" path_parsing: dependency: transitive description: name: path_parsing url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" path_provider: dependency: transitive description: @@ -395,14 +409,14 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.5.0" petitparser: dependency: transitive description: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.2.1" photo_view: dependency: "direct main" description: @@ -437,7 +451,7 @@ packages: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" rxdart: dependency: "direct main" description: @@ -465,7 +479,7 @@ packages: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "0.5.2" + version: "0.5.3+1" shelf: dependency: transitive description: @@ -526,7 +540,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.4" + version: "1.5.5" sprintf: dependency: "direct main" description: @@ -554,7 +568,7 @@ packages: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "1.6.8" + version: "2.0.0" string_scanner: dependency: transitive description: @@ -582,28 +596,28 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.5.3" + version: "1.6.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.2" + version: "0.2.4" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.2.1+1" + version: "0.2.3" timeago: dependency: "direct main" description: name: timeago url: "https://pub.dartlang.org" source: hosted - version: "2.0.14" + version: "2.0.16" tinycolor: dependency: "direct main" description: @@ -640,7 +654,7 @@ packages: source: hosted version: "3.4.1" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid url: "https://pub.dartlang.org" @@ -666,7 +680,7 @@ packages: name: video_player url: "https://pub.dartlang.org" source: hosted - version: "0.10.0+8" + version: "0.10.1+3" vm_service_client: dependency: transitive description: @@ -687,14 +701,14 @@ packages: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "1.0.12" + version: "1.0.13" xml: dependency: transitive description: name: xml url: "https://pub.dartlang.org" source: hosted - version: "3.3.1" + version: "3.4.1" yaml: dependency: transitive description: @@ -703,5 +717,5 @@ packages: source: hosted version: "2.1.15" sdks: - dart: ">=2.1.0 <3.0.0" - flutter: ">=1.2.1 <2.0.0" + dart: ">=2.2.0 <3.0.0" + flutter: ">=1.5.0 <1.5.9" diff --git a/pubspec.yaml b/pubspec.yaml index 8bd421023..c8a8f952e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,17 +13,20 @@ environment: sdk: ">=2.1.0 <3.0.0" dependencies: + uuid: ^2.0.1 + multi_image_picker: ^4.3.3 + flutter_exif_rotation: ^0.2.2 shared_preferences: ^0.5.2 flutter_markdown: ^0.2.0 sentry: ^2.2.0 - flutter_exif_rotation: ^0.2.0 back_button_interceptor: ^4.0.1 flutter_colorpicker: any intercom_flutter: ^1.0.11 device_info: ^0.4.0+1 flutter_pagewise: ^1.2.2 tinycolor: ^1.0.2 - onesignal: ^1.0.5 + onesignalflutter: + git: git://github.com/jmrobles/OneSignal-Flutter-SDK.git flutter_advanced_networkimage: ^0.4.13 dcache: ^0.1.0 validators: ^1.0.0+1