diff --git a/.buildkite/cache-builder.yml b/.buildkite/cache-builder.yml index 0b44a369a583..3e11fb963796 100644 --- a/.buildkite/cache-builder.yml +++ b/.buildkite/cache-builder.yml @@ -14,7 +14,7 @@ common_params: # Common environment values to use with the `env` key. - &common_env # Be sure to also update the `.xcode-version` file when updating the Xcode image/version here - IMAGE_ID: xcode-14.2 + IMAGE_ID: xcode-14.3.1 steps: diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 0cf2db6a8a2e..2db9e6d19145 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -9,7 +9,7 @@ common_params: # Common environment values to use with the `env` key. - &common_env # Be sure to also update the `.xcode-version` file when updating the Xcode image/version here - IMAGE_ID: xcode-14.2 + IMAGE_ID: xcode-14.3.1 # This is the default pipeline – it will build and test the app steps: diff --git a/.buildkite/release-builds.yml b/.buildkite/release-builds.yml index 120d158d6dc8..1003df4508be 100644 --- a/.buildkite/release-builds.yml +++ b/.buildkite/release-builds.yml @@ -11,7 +11,7 @@ common_params: # Common environment values to use with the `env` key. - &common_env # Be sure to also update the `.xcode-version` file when updating the Xcode image/version here - IMAGE_ID: xcode-14.2 + IMAGE_ID: xcode-14.3.1 steps: diff --git a/.xcode-version b/.xcode-version index 6b5bab0678ab..6dfe8b1298c0 100644 --- a/.xcode-version +++ b/.xcode-version @@ -1 +1 @@ -14.2 +14.3.1 diff --git a/Gutenberg/version.rb b/Gutenberg/version.rb index 4722006d7961..567633e7b64b 100644 --- a/Gutenberg/version.rb +++ b/Gutenberg/version.rb @@ -12,7 +12,7 @@ # LOCAL_GUTENBERG=../my-gutenberg-fork bundle exec pod install GUTENBERG_CONFIG = { # commit: '' - tag: 'v1.98.1' + tag: 'v1.99.0' } GITHUB_ORG = 'wordpress-mobile' diff --git a/MIGRATIONS.md b/MIGRATIONS.md index 78bedcc79b39..59922a1b271b 100644 --- a/MIGRATIONS.md +++ b/MIGRATIONS.md @@ -3,6 +3,26 @@ This file documents changes in the data model. Please explain any changes to the data model as well as any custom migrations. +## WordPress 151 + +@dvdchr 2023-06-28 + +- `Blog`: added `planActiveFeatures` (optional, no default, `Transformable` with type `[String]`) + +@dvdchr 2023-06-23 + +- Created a new entity `PublicizeInfo` with: + - `sharedPostsCount` (required, default `0`, `Int 64`) + - `sharesRemaining` (required, default `0`, `Int 64`) + - `shareLimit` (required, default `0`, `Int 64`) + - `toBePublicizedCount` (required, default `0`, `Int 64`) + +- Created one-to-many relationship between `PublicizeInfo` and `Blog` + - `PublicizeInfo` + - `blog` (optional, to-one, nullify on delete) + - `Blog` + - `publicizeInfo` (optional, to-one, cascade on delete) + ## WordPress 150 @momozw 2023-06-20 diff --git a/Podfile b/Podfile index 8d5a7f5bc272..edb23713a037 100644 --- a/Podfile +++ b/Podfile @@ -23,7 +23,7 @@ workspace 'WordPress.xcworkspace' ## =================================== ## def wordpress_shared - pod 'WordPressShared', '~> 2.2-beta' + pod 'WordPressShared', '~> 2.2' # pod 'WordPressShared', git: 'https://github.com/wordpress-mobile/WordPress-iOS-Shared.git', tag: '' # pod 'WordPressShared', git: 'https://github.com/wordpress-mobile/WordPress-iOS-Shared.git', branch: 'trunk' # pod 'WordPressShared', git: 'https://github.com/wordpress-mobile/WordPress-iOS-Shared.git', commit: '' @@ -142,14 +142,14 @@ abstract_target 'Apps' do pod 'NSURL+IDN', '~> 0.4' - pod 'WPMediaPicker', '~> 1.8-beta' + pod 'WPMediaPicker', '~> 1.8.8' ## while PR is in review: # pod 'WPMediaPicker', git: 'https://github.com/wordpress-mobile/MediaPicker-iOS.git', branch: '' # pod 'WPMediaPicker', path: '../MediaPicker-iOS' pod 'Gridicons', '~> 1.1.0' - pod 'WordPressAuthenticator', '~> 6.1-beta' + pod 'WordPressAuthenticator', '~> 6.2.0' # pod 'WordPressAuthenticator', git: 'https://github.com/wordpress-mobile/WordPressAuthenticator-iOS.git', branch: '' # pod 'WordPressAuthenticator', git: 'https://github.com/wordpress-mobile/WordPressAuthenticator-iOS.git', commit: '' # pod 'WordPressAuthenticator', path: '../WordPressAuthenticator-iOS' diff --git a/Podfile.lock b/Podfile.lock index 089906197b23..03c54408dff3 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -54,7 +54,7 @@ PODS: - AppAuth/Core (~> 1.6) - GTMSessionFetcher/Core (< 3.0, >= 1.5) - GTMSessionFetcher/Core (1.7.2) - - Gutenberg (1.98.1): + - Gutenberg (1.99.0): - React (= 0.69.4) - React-CoreModules (= 0.69.4) - React-RCTImage (= 0.69.4) @@ -481,7 +481,7 @@ PODS: - React-Core - RNSVG (9.13.6): - React-Core - - RNTAztecView (1.98.1): + - RNTAztecView (1.99.0): - React-Core - WordPress-Aztec-iOS (~> 1.19.8) - SDWebImage (5.11.1): @@ -545,18 +545,18 @@ DEPENDENCIES: - AppCenter (~> 4.1) - AppCenter/Distribute (~> 4.1) - Automattic-Tracks-iOS (~> 2.2) - - boost (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/boost.podspec.json`) - - BVLinearGradient (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/BVLinearGradient.podspec.json`) + - boost (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/boost.podspec.json`) + - BVLinearGradient (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/BVLinearGradient.podspec.json`) - CocoaLumberjack/Swift (~> 3.0) - CropViewController (= 2.5.3) - Down (~> 0.6.6) - - FBLazyVector (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/FBLazyVector.podspec.json`) - - FBReactNativeSpec (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/FBReactNativeSpec/FBReactNativeSpec.podspec.json`) + - FBLazyVector (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/FBLazyVector.podspec.json`) + - FBReactNativeSpec (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/FBReactNativeSpec/FBReactNativeSpec.podspec.json`) - FSInteractiveMap (from `https://github.com/wordpress-mobile/FSInteractiveMap.git`, tag `0.2.0`) - Gifu (= 3.2.0) - - glog (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/glog.podspec.json`) + - glog (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/glog.podspec.json`) - Gridicons (~> 1.1.0) - - Gutenberg (from `https://github.com/wordpress-mobile/gutenberg-mobile.git`, tag `v1.98.1`) + - Gutenberg (from `https://github.com/wordpress-mobile/gutenberg-mobile.git`, tag `v1.99.0`) - JTAppleCalendar (~> 8.0.2) - Kanvas (~> 1.4.4) - MediaEditor (>= 1.2.2, ~> 1.2) @@ -565,58 +565,58 @@ DEPENDENCIES: - "NSURL+IDN (~> 0.4)" - OCMock (~> 3.4.3) - OHHTTPStubs/Swift (~> 9.1.0) - - RCT-Folly (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RCT-Folly.podspec.json`) - - RCTRequired (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RCTRequired.podspec.json`) - - RCTTypeSafety (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RCTTypeSafety.podspec.json`) + - RCT-Folly (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RCT-Folly.podspec.json`) + - RCTRequired (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RCTRequired.podspec.json`) + - RCTTypeSafety (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RCTTypeSafety.podspec.json`) - Reachability (= 3.2) - - React (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React.podspec.json`) - - React-bridging (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-bridging.podspec.json`) - - React-callinvoker (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-callinvoker.podspec.json`) - - React-Codegen (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-Codegen.podspec.json`) - - React-Core (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-Core.podspec.json`) - - React-CoreModules (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-CoreModules.podspec.json`) - - React-cxxreact (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-cxxreact.podspec.json`) - - React-jsi (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-jsi.podspec.json`) - - React-jsiexecutor (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-jsiexecutor.podspec.json`) - - React-jsinspector (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-jsinspector.podspec.json`) - - React-logger (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-logger.podspec.json`) - - react-native-blur (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/react-native-blur.podspec.json`) - - react-native-get-random-values (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/react-native-get-random-values.podspec.json`) - - react-native-safe-area (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/react-native-safe-area.podspec.json`) - - react-native-safe-area-context (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/react-native-safe-area-context.podspec.json`) - - react-native-slider (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/react-native-slider.podspec.json`) - - react-native-video (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/react-native-video.podspec.json`) - - react-native-webview (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/react-native-webview.podspec.json`) - - React-perflogger (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-perflogger.podspec.json`) - - React-RCTActionSheet (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-RCTActionSheet.podspec.json`) - - React-RCTAnimation (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-RCTAnimation.podspec.json`) - - React-RCTBlob (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-RCTBlob.podspec.json`) - - React-RCTImage (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-RCTImage.podspec.json`) - - React-RCTLinking (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-RCTLinking.podspec.json`) - - React-RCTNetwork (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-RCTNetwork.podspec.json`) - - React-RCTSettings (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-RCTSettings.podspec.json`) - - React-RCTText (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-RCTText.podspec.json`) - - React-RCTVibration (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-RCTVibration.podspec.json`) - - React-runtimeexecutor (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-runtimeexecutor.podspec.json`) - - ReactCommon (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/ReactCommon.podspec.json`) - - RNCClipboard (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RNCClipboard.podspec.json`) - - RNCMaskedView (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RNCMaskedView.podspec.json`) - - RNFastImage (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RNFastImage.podspec.json`) - - RNGestureHandler (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RNGestureHandler.podspec.json`) - - RNReanimated (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RNReanimated.podspec.json`) - - RNScreens (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RNScreens.podspec.json`) - - RNSVG (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RNSVG.podspec.json`) - - RNTAztecView (from `https://github.com/wordpress-mobile/gutenberg-mobile.git`, tag `v1.98.1`) + - React (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React.podspec.json`) + - React-bridging (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-bridging.podspec.json`) + - React-callinvoker (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-callinvoker.podspec.json`) + - React-Codegen (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-Codegen.podspec.json`) + - React-Core (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-Core.podspec.json`) + - React-CoreModules (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-CoreModules.podspec.json`) + - React-cxxreact (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-cxxreact.podspec.json`) + - React-jsi (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-jsi.podspec.json`) + - React-jsiexecutor (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-jsiexecutor.podspec.json`) + - React-jsinspector (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-jsinspector.podspec.json`) + - React-logger (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-logger.podspec.json`) + - react-native-blur (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/react-native-blur.podspec.json`) + - react-native-get-random-values (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/react-native-get-random-values.podspec.json`) + - react-native-safe-area (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/react-native-safe-area.podspec.json`) + - react-native-safe-area-context (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/react-native-safe-area-context.podspec.json`) + - react-native-slider (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/react-native-slider.podspec.json`) + - react-native-video (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/react-native-video.podspec.json`) + - react-native-webview (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/react-native-webview.podspec.json`) + - React-perflogger (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-perflogger.podspec.json`) + - React-RCTActionSheet (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-RCTActionSheet.podspec.json`) + - React-RCTAnimation (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-RCTAnimation.podspec.json`) + - React-RCTBlob (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-RCTBlob.podspec.json`) + - React-RCTImage (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-RCTImage.podspec.json`) + - React-RCTLinking (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-RCTLinking.podspec.json`) + - React-RCTNetwork (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-RCTNetwork.podspec.json`) + - React-RCTSettings (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-RCTSettings.podspec.json`) + - React-RCTText (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-RCTText.podspec.json`) + - React-RCTVibration (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-RCTVibration.podspec.json`) + - React-runtimeexecutor (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-runtimeexecutor.podspec.json`) + - ReactCommon (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/ReactCommon.podspec.json`) + - RNCClipboard (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RNCClipboard.podspec.json`) + - RNCMaskedView (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RNCMaskedView.podspec.json`) + - RNFastImage (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RNFastImage.podspec.json`) + - RNGestureHandler (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RNGestureHandler.podspec.json`) + - RNReanimated (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RNReanimated.podspec.json`) + - RNScreens (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RNScreens.podspec.json`) + - RNSVG (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RNSVG.podspec.json`) + - RNTAztecView (from `https://github.com/wordpress-mobile/gutenberg-mobile.git`, tag `v1.99.0`) - Starscream (= 3.0.6) - SVProgressHUD (= 2.2.5) - SwiftLint (~> 0.50) - WordPress-Editor-iOS (~> 1.19.8) - - WordPressAuthenticator (~> 6.1-beta) + - WordPressAuthenticator (~> 6.2.0) - WordPressKit (~> 8.5) - - WordPressShared (~> 2.2-beta) + - WordPressShared (~> 2.2) - WordPressUI (~> 1.12.5) - - WPMediaPicker (~> 1.8-beta) - - Yoga (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/Yoga.podspec.json`) + - WPMediaPicker (~> 1.8.8) + - Yoga (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/Yoga.podspec.json`) - ZendeskSupportSDK (= 5.3.0) - ZIPFoundation (~> 0.9.8) @@ -677,108 +677,108 @@ SPEC REPOS: EXTERNAL SOURCES: boost: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/boost.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/boost.podspec.json BVLinearGradient: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/BVLinearGradient.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/BVLinearGradient.podspec.json FBLazyVector: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/FBLazyVector.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/FBLazyVector.podspec.json FBReactNativeSpec: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/FBReactNativeSpec/FBReactNativeSpec.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/FBReactNativeSpec/FBReactNativeSpec.podspec.json FSInteractiveMap: :git: https://github.com/wordpress-mobile/FSInteractiveMap.git :tag: 0.2.0 glog: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/glog.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/glog.podspec.json Gutenberg: :git: https://github.com/wordpress-mobile/gutenberg-mobile.git :submodules: true - :tag: v1.98.1 + :tag: v1.99.0 RCT-Folly: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RCT-Folly.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RCT-Folly.podspec.json RCTRequired: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RCTRequired.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RCTRequired.podspec.json RCTTypeSafety: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RCTTypeSafety.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RCTTypeSafety.podspec.json React: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React.podspec.json React-bridging: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-bridging.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-bridging.podspec.json React-callinvoker: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-callinvoker.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-callinvoker.podspec.json React-Codegen: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-Codegen.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-Codegen.podspec.json React-Core: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-Core.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-Core.podspec.json React-CoreModules: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-CoreModules.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-CoreModules.podspec.json React-cxxreact: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-cxxreact.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-cxxreact.podspec.json React-jsi: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-jsi.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-jsi.podspec.json React-jsiexecutor: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-jsiexecutor.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-jsiexecutor.podspec.json React-jsinspector: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-jsinspector.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-jsinspector.podspec.json React-logger: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-logger.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-logger.podspec.json react-native-blur: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/react-native-blur.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/react-native-blur.podspec.json react-native-get-random-values: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/react-native-get-random-values.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/react-native-get-random-values.podspec.json react-native-safe-area: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/react-native-safe-area.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/react-native-safe-area.podspec.json react-native-safe-area-context: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/react-native-safe-area-context.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/react-native-safe-area-context.podspec.json react-native-slider: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/react-native-slider.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/react-native-slider.podspec.json react-native-video: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/react-native-video.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/react-native-video.podspec.json react-native-webview: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/react-native-webview.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/react-native-webview.podspec.json React-perflogger: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-perflogger.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-perflogger.podspec.json React-RCTActionSheet: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-RCTActionSheet.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-RCTActionSheet.podspec.json React-RCTAnimation: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-RCTAnimation.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-RCTAnimation.podspec.json React-RCTBlob: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-RCTBlob.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-RCTBlob.podspec.json React-RCTImage: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-RCTImage.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-RCTImage.podspec.json React-RCTLinking: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-RCTLinking.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-RCTLinking.podspec.json React-RCTNetwork: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-RCTNetwork.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-RCTNetwork.podspec.json React-RCTSettings: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-RCTSettings.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-RCTSettings.podspec.json React-RCTText: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-RCTText.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-RCTText.podspec.json React-RCTVibration: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-RCTVibration.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-RCTVibration.podspec.json React-runtimeexecutor: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/React-runtimeexecutor.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/React-runtimeexecutor.podspec.json ReactCommon: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/ReactCommon.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/ReactCommon.podspec.json RNCClipboard: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RNCClipboard.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RNCClipboard.podspec.json RNCMaskedView: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RNCMaskedView.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RNCMaskedView.podspec.json RNFastImage: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RNFastImage.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RNFastImage.podspec.json RNGestureHandler: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RNGestureHandler.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RNGestureHandler.podspec.json RNReanimated: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RNReanimated.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RNReanimated.podspec.json RNScreens: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RNScreens.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RNScreens.podspec.json RNSVG: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/RNSVG.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/RNSVG.podspec.json RNTAztecView: :git: https://github.com/wordpress-mobile/gutenberg-mobile.git :submodules: true - :tag: v1.98.1 + :tag: v1.99.0 Yoga: - :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.98.1/third-party-podspecs/Yoga.podspec.json + :podspec: https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.99.0/third-party-podspecs/Yoga.podspec.json CHECKOUT OPTIONS: FSInteractiveMap: @@ -787,11 +787,11 @@ CHECKOUT OPTIONS: Gutenberg: :git: https://github.com/wordpress-mobile/gutenberg-mobile.git :submodules: true - :tag: v1.98.1 + :tag: v1.99.0 RNTAztecView: :git: https://github.com/wordpress-mobile/gutenberg-mobile.git :submodules: true - :tag: v1.98.1 + :tag: v1.99.0 SPEC CHECKSUMS: Alamofire: 3ec537f71edc9804815215393ae2b1a8ea33a844 @@ -816,7 +816,7 @@ SPEC CHECKSUMS: Gridicons: 17d660b97ce4231d582101b02f8280628b141c9a GTMAppAuth: 0ff230db599948a9ad7470ca667337803b3fc4dd GTMSessionFetcher: 5595ec75acf5be50814f81e9189490412bad82ba - Gutenberg: fd4cb66c253b00ccae222d01ed3824521ed50b76 + Gutenberg: 0b6b57b7c48f79212d23c947fd6c9decb6c196c6 JTAppleCalendar: 932cadea40b1051beab10f67843451d48ba16c99 Kanvas: f932eaed3d3f47aae8aafb6c2d27c968bdd49030 libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef @@ -867,7 +867,7 @@ SPEC CHECKSUMS: RNReanimated: b5730b32243a35f955202d807ecb43755133ac62 RNScreens: bd1f43d7dfcd435bc11d4ee5c60086717c45a113 RNSVG: 259ef12cbec2591a45fc7c5f09d7aa09e6692533 - RNTAztecView: 0cf287757e0879ea9e87e6628851c24f798ba809 + RNTAztecView: d96d1e9b317e7bfe153bcb9e82f9287862893579 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d Sentry: 927dfb29d18a14d924229a59cc2ad149f43349f2 @@ -895,6 +895,6 @@ SPEC CHECKSUMS: ZendeskSupportSDK: 3a8e508ab1d9dd22dc038df6c694466414e037ba ZIPFoundation: ae5b4b813d216d3bf0a148773267fff14bd51d37 -PODFILE CHECKSUM: 05c81b021c29ea99daf4ef573cb2dcf6ecaf1e4c +PODFILE CHECKSUM: 463ed7d39926c127d8197fe925fd7d05125f647b COCOAPODS: 1.12.1 diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 811221163b1e..bf2d6f7fc5d0 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,7 +1,17 @@ -22.8 +22.9 ----- +22.8 +----- +* [*] Blogging Reminders: Disabled prompt for self-hosted sites not connected to Jetpack. [#20970] +* [**] [internal] Do not save synced blogs if the app has signed out. [#20959] +* [**] [internal] Make sure synced posts are saved before calling completion block. [#20960] +* [**] [internal] Fix observing Quick Start notifications. [#20997] +* [**] [internal] Fixed an issue that was causing a memory leak in the domain selection flow. [#20813] +* [*] [Jetpack-only] Block editor: Rename "Reusable blocks" to "Synced patterns", aligning with the web editor. [https://github.com/wordpress-mobile/gutenberg-mobile/pull/5885] +* [**] [internal] Block editor: Fix a crash related to Reanimated when closing the editor [https://github.com/wordpress-mobile/gutenberg-mobile/pull/5938] + 22.7 ----- * [**] [internal] Blaze: Switch to using new canBlaze property to determine Blaze eligiblity. [#20916] diff --git a/WordPress/Classes/Extensions/Colors and Styles/WPStyleGuide+ApplicationStyles.swift b/WordPress/Classes/Extensions/Colors and Styles/WPStyleGuide+ApplicationStyles.swift index b580087879c3..ca0456d81a5d 100644 --- a/WordPress/Classes/Extensions/Colors and Styles/WPStyleGuide+ApplicationStyles.swift +++ b/WordPress/Classes/Extensions/Colors and Styles/WPStyleGuide+ApplicationStyles.swift @@ -57,9 +57,7 @@ extension WPStyleGuide { /// Style `UITableView` in the app class func configureTableViewAppearance() { - if #available(iOS 15.0, *) { - UITableView.appearance().sectionHeaderTopPadding = 0 - } + UITableView.appearance().sectionHeaderTopPadding = 0 } /// Style the tab bar using Muriel colors @@ -67,14 +65,12 @@ extension WPStyleGuide { UITabBar.appearance().tintColor = .tabSelected UITabBar.appearance().unselectedItemTintColor = .tabUnselected - if #available(iOS 15.0, *) { - let appearance = UITabBarAppearance() - appearance.configureWithOpaqueBackground() - appearance.backgroundColor = .systemBackground + let appearance = UITabBarAppearance() + appearance.configureWithOpaqueBackground() + appearance.backgroundColor = .systemBackground - UITabBar.appearance().standardAppearance = appearance - UITabBar.appearance().scrollEdgeAppearance = appearance - } + UITabBar.appearance().standardAppearance = appearance + UITabBar.appearance().scrollEdgeAppearance = appearance } /// Style the `LightNavigationController` UINavigationBar and BarButtonItems @@ -114,10 +110,7 @@ extension WPStyleGuide { appearance.configureWithDefaultBackground() UIToolbar.appearance().standardAppearance = appearance - - if #available(iOS 15.0, *) { - UIToolbar.appearance().scrollEdgeAppearance = appearance - } + UIToolbar.appearance().scrollEdgeAppearance = appearance } } diff --git a/WordPress/Classes/Extensions/NSAttributedString+Helpers.swift b/WordPress/Classes/Extensions/NSAttributedString+Helpers.swift index b57f17fd0728..721518d8acaa 100644 --- a/WordPress/Classes/Extensions/NSAttributedString+Helpers.swift +++ b/WordPress/Classes/Extensions/NSAttributedString+Helpers.swift @@ -26,12 +26,7 @@ public extension NSAttributedString { for (value, image) in unwrappedEmbeds { let imageAttachment = NSTextAttachment() let gifType = UTType.gif.identifier - var displayAnimatedGifs = false - - // Check to see if the animated gif view provider is registered - if #available(iOS 15.0, *) { - displayAnimatedGifs = NSTextAttachment.textAttachmentViewProviderClass(forFileType: gifType) == AnimatedGifAttachmentViewProvider.self - } + let displayAnimatedGifs = NSTextAttachment.textAttachmentViewProviderClass(forFileType: gifType) == AnimatedGifAttachmentViewProvider.self // When displaying an animated gif pass the gif data instead of the image if diff --git a/WordPress/Classes/Extensions/UIApplication+mainWindow.swift b/WordPress/Classes/Extensions/UIApplication+mainWindow.swift index 59d59af81a0a..4d5068ae895e 100644 --- a/WordPress/Classes/Extensions/UIApplication+mainWindow.swift +++ b/WordPress/Classes/Extensions/UIApplication+mainWindow.swift @@ -2,13 +2,9 @@ import UIKit extension UIApplication { @objc var mainWindow: UIWindow? { - if #available(iOS 15, *) { - return connectedScenes - .compactMap { ($0 as? UIWindowScene)?.keyWindow } - .first - } else { - return windows.filter { $0.isKeyWindow }.first - } + connectedScenes + .compactMap { ($0 as? UIWindowScene)?.keyWindow } + .first } @objc var currentStatusBarFrame: CGRect { diff --git a/WordPress/Classes/Extensions/UITextField+WorkaroundContinueIssue.swift b/WordPress/Classes/Extensions/UITextField+WorkaroundContinueIssue.swift deleted file mode 100644 index c26ef1d79428..000000000000 --- a/WordPress/Classes/Extensions/UITextField+WorkaroundContinueIssue.swift +++ /dev/null @@ -1,50 +0,0 @@ -import Foundation - -@objc -extension UITextField { - - /// This method takes care of resolving whether the iOS version is vulnerable to the Bulgarian / Icelandic keyboard crash issue - /// by Apple. Once the issue is resolved by Apple we should consider setting an upper iOS version to limit this workaround. - /// - /// Once we drop support for iOS 14, we could remove this extension entirely. - /// - public class func shouldActivateWorkaroundForBulgarianKeyboardCrash() -> Bool { - return true - } - - /// We're swizzling `UITextField.becomeFirstResponder()` so that we can fix an issue with - /// Bulgarian and Icelandic keyboards when appropriate. - /// - /// Ref: https://github.com/wordpress-mobile/WordPress-iOS/issues/15187 - /// - @objc - class func activateWorkaroundForBulgarianKeyboardCrash() { - guard let original = class_getInstanceMethod( - UITextField.self, - #selector(UITextField.becomeFirstResponder)), - let new = class_getInstanceMethod( - UITextField.self, - #selector(UITextField.swizzledBecomeFirstResponder)) else { - - DDLogError("Could not activate workaround for Bulgarian keyboard crash.") - - return - } - - method_exchangeImplementations(original, new) - } - - /// This method simply replaces the `returnKeyType == .continue` with - /// `returnKeyType == .next`when the Bulgarian Keyboard crash workaround is needed. - /// - public func swizzledBecomeFirstResponder() { - if UITextField.shouldActivateWorkaroundForBulgarianKeyboardCrash(), - returnKeyType == .continue { - returnKeyType = .next - } - - // This can look confusing - it's basically calling the original method to - // make sure we don't disrupt anything. - swizzledBecomeFirstResponder() - } -} diff --git a/WordPress/Classes/Models/Blog+JetpackSocial.swift b/WordPress/Classes/Models/Blog+JetpackSocial.swift new file mode 100644 index 000000000000..18cb9ffb0a20 --- /dev/null +++ b/WordPress/Classes/Models/Blog+JetpackSocial.swift @@ -0,0 +1,31 @@ +import Foundation + +/// Blog extension for methods related to Jetpack Social. +extension Blog { + // MARK: - Publicize + + /// Whether the blog has Social auto-sharing limited. + /// Note that sites hosted at WP.com has no Social sharing limitations. + var isSocialSharingLimited: Bool { + let features = planActiveFeatures ?? [] + return !isHostedAtWPcom && !features.contains(Constants.socialSharingFeature) + } + + /// The auto-sharing limit information for the blog. + var sharingLimit: PublicizeInfo.SharingLimit? { + // For blogs with unlimited shares, return nil early. + // This is because the endpoint will still return sharing limits as if the blog doesn't have unlimited sharing. + guard isSocialSharingLimited else { + return nil + } + return publicizeInfo?.sharingLimit + } + + // MARK: - Private constants + + private enum Constants { + /// The feature key listed in the blog's plan's features. At the moment, `social-shares-1000` means unlimited + /// sharing, but in the future we might introduce a proper differentiation between 1000 and unlimited. + static let socialSharingFeature = "social-shares-1000" + } +} diff --git a/WordPress/Classes/Models/Blog.h b/WordPress/Classes/Models/Blog.h index 5d59bd92d52f..5e0a202659b9 100644 --- a/WordPress/Classes/Models/Blog.h +++ b/WordPress/Classes/Models/Blog.h @@ -16,6 +16,7 @@ NS_ASSUME_NONNULL_BEGIN @class SiteSuggestion; @class PageTemplateCategory; @class JetpackFeaturesRemovalCoordinator; +@class PublicizeInfo; extern NSString * const BlogEntityName; extern NSString * const PostFormatStandard; @@ -163,6 +164,7 @@ typedef NS_ENUM(NSInteger, SiteVisibility) { @property (nonatomic, assign, readwrite) SiteVisibility siteVisibility; @property (nonatomic, strong, readwrite, nullable) NSNumber *planID; @property (nonatomic, strong, readwrite, nullable) NSString *planTitle; +@property (nonatomic, strong, readwrite, nullable) NSArray *planActiveFeatures; @property (nonatomic, assign, readwrite) BOOL hasPaidPlan; @property (nonatomic, strong, readwrite, nullable) NSSet *sharingButtons; @property (nonatomic, strong, readwrite, nullable) NSDictionary *capabilities; @@ -181,6 +183,11 @@ typedef NS_ENUM(NSInteger, SiteVisibility) { */ @property (nonatomic, strong, readwrite, nullable) BlogSettings *settings; +/** + * @details Maps to a PublicizeInfo instance, which contains Jetpack Social auto-sharing information. + */ +@property (nonatomic, strong, readwrite, nullable) PublicizeInfo *publicizeInfo; + /** * @details Flags whether the current user is an admin on the blog. */ diff --git a/WordPress/Classes/Models/Blog.m b/WordPress/Classes/Models/Blog.m index 454c81022ebe..187a21f3b3dd 100644 --- a/WordPress/Classes/Models/Blog.m +++ b/WordPress/Classes/Models/Blog.m @@ -4,7 +4,6 @@ #import "NSURL+IDN.h" #import "CoreDataStack.h" #import "Constants.h" -#import "WordPress-Swift.h" #import "WPUserAgent.h" #import "WordPress-Swift.h" @@ -82,6 +81,7 @@ @implementation Blog @dynamic settings; @dynamic planID; @dynamic planTitle; +@dynamic planActiveFeatures; @dynamic hasPaidPlan; @dynamic sharingButtons; @dynamic capabilities; @@ -91,6 +91,7 @@ @implementation Blog @dynamic quotaSpaceAllowed; @dynamic quotaSpaceUsed; @dynamic pageTemplateCategories; +@dynamic publicizeInfo; @synthesize isSyncingPosts; @synthesize isSyncingPages; diff --git a/WordPress/Classes/Models/PublicizeInfo+CoreDataClass.swift b/WordPress/Classes/Models/PublicizeInfo+CoreDataClass.swift new file mode 100644 index 000000000000..2cb7b29f3a48 --- /dev/null +++ b/WordPress/Classes/Models/PublicizeInfo+CoreDataClass.swift @@ -0,0 +1,40 @@ +import Foundation +import CoreData +import WordPressKit + +/// `PublicizeInfo` encapsulates the information related to Jetpack Social auto-sharing. +/// +/// WP.com sites will not have a `PublicizeInfo`, and currently doesn't have auto-sharing limitations. +/// Furthermore, sites eligible for unlimited sharing will still return a `PublicizeInfo` along with its sharing +/// limitations, but the numbers should be ignored (at least for now). +/// +@objc public class PublicizeInfo: NSManagedObject { + + var sharingLimit: SharingLimit { + SharingLimit(remaining: Int(sharesRemaining), limit: Int(shareLimit)) + } + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "PublicizeInfo") + } + + @nonobjc public class func newObject(in context: NSManagedObjectContext) -> PublicizeInfo? { + return NSEntityDescription.insertNewObject(forEntityName: Self.classNameWithoutNamespaces(), into: context) as? PublicizeInfo + } + + func configure(with remote: RemotePublicizeInfo) { + self.shareLimit = Int64(remote.shareLimit) + self.toBePublicizedCount = Int64(remote.toBePublicizedCount) + self.sharedPostsCount = Int64(remote.sharedPostsCount) + self.sharesRemaining = Int64(remote.sharesRemaining) + } + + /// A value-type representation for Publicize auto-sharing usage. + struct SharingLimit { + /// The remaining shares available for use. + let remaining: Int + + /// Maximum number of shares allowed for the site. + let limit: Int + } +} diff --git a/WordPress/Classes/Models/PublicizeInfo+CoreDataProperties.swift b/WordPress/Classes/Models/PublicizeInfo+CoreDataProperties.swift new file mode 100644 index 000000000000..ac8f223f7b06 --- /dev/null +++ b/WordPress/Classes/Models/PublicizeInfo+CoreDataProperties.swift @@ -0,0 +1,19 @@ +import Foundation +import CoreData + +extension PublicizeInfo { + /// The maximum number of Social shares for the associated `blog`. + @NSManaged public var shareLimit: Int64 + + /// The number of Social sharing to be published in the future. + @NSManaged public var toBePublicizedCount: Int64 + + /// The number of posts that have been auto-shared. + @NSManaged public var sharedPostsCount: Int64 + + /// The remaining Social shares available for the associated `blog`. + @NSManaged public var sharesRemaining: Int64 + + /// The associated Blog instance. + @NSManaged public var blog: Blog? +} diff --git a/WordPress/Classes/Services/BlazeService.swift b/WordPress/Classes/Services/BlazeService.swift index 60215938b754..6694aff6bc0e 100644 --- a/WordPress/Classes/Services/BlazeService.swift +++ b/WordPress/Classes/Services/BlazeService.swift @@ -1,21 +1,20 @@ import Foundation import WordPressKit -@objc final class BlazeService: NSObject { +protocol BlazeServiceProtocol { + func getRecentCampaigns(for blog: Blog, page: Int, completion: @escaping (Result) -> Void) +} +@objc final class BlazeService: NSObject, BlazeServiceProtocol { private let contextManager: CoreDataStackSwift private let remote: BlazeServiceRemote // MARK: - Init - required init?(contextManager: CoreDataStackSwift = ContextManager.shared, + required init(contextManager: CoreDataStackSwift = ContextManager.shared, remote: BlazeServiceRemote? = nil) { - guard let account = try? WPAccount.lookupDefaultWordPressComAccount(in: contextManager.mainContext) else { - return nil - } - self.contextManager = contextManager - self.remote = remote ?? .init(wordPressComRestApi: account.wordPressComRestV2Api) + self.remote = remote ?? BlazeServiceRemote(wordPressComRestApi: WordPressComRestApi.defaultApi(in: contextManager.mainContext, localeKey: WordPressComRestApi.LocaleKeyV2)) } @objc class func createService() -> BlazeService? { @@ -25,16 +24,22 @@ import WordPressKit // MARK: - Methods func getRecentCampaigns(for blog: Blog, + page: Int = 1, completion: @escaping (Result) -> Void) { + guard blog.canBlaze else { + completion(.failure(BlazeServiceError.notEligibleForBlaze)) + return + } guard let siteId = blog.dotComID?.intValue else { DDLogError("Invalid site ID for Blaze") completion(.failure(BlazeServiceError.missingBlogId)) return } - remote.searchCampaigns(forSiteId: siteId, callback: completion) + remote.searchCampaigns(forSiteId: siteId, page: page, callback: completion) } } enum BlazeServiceError: Error { + case notEligibleForBlaze case missingBlogId } diff --git a/WordPress/Classes/Services/BlogService.m b/WordPress/Classes/Services/BlogService.m index 5065924ee101..45c8e38bb8ab 100644 --- a/WordPress/Classes/Services/BlogService.m +++ b/WordPress/Classes/Services/BlogService.m @@ -423,7 +423,13 @@ - (void)associateSyncedBlogsToJetpackAccount:(WPAccount *)account - (void)mergeBlogs:(NSArray *)blogs withAccountID:(NSManagedObjectID *)accountID inContext:(NSManagedObjectContext *)context { // Nuke dead blogs - WPAccount *account = [context existingObjectWithID:accountID error:nil]; + NSError *error = nil; + WPAccount *account = [context existingObjectWithID:accountID error:&error]; + if (account == nil) { + DDLogInfo(@"Can't find the account. User may have signed out. Error: %@", error); + return; + } + NSSet *remoteSet = [NSSet setWithArray:[blogs valueForKey:@"blogID"]]; NSSet *localSet = [account.blogs valueForKey:@"dotComID"]; NSMutableSet *toDelete = [localSet mutableCopy]; @@ -499,6 +505,7 @@ - (void)updateBlog:(Blog *)blog withRemoteBlog:(RemoteBlog *)remoteBlog blog.options = remoteBlog.options; blog.planID = remoteBlog.planID; blog.planTitle = remoteBlog.planTitle; + blog.planActiveFeatures = remoteBlog.planActiveFeatures; blog.hasPaidPlan = remoteBlog.hasPaidPlan; blog.quotaSpaceAllowed = remoteBlog.quotaSpaceAllowed; blog.quotaSpaceUsed = remoteBlog.quotaSpaceUsed; diff --git a/WordPress/Classes/Services/CommentService+Likes.swift b/WordPress/Classes/Services/CommentService+Likes.swift index fa2d761440c2..188a623805ea 100644 --- a/WordPress/Classes/Services/CommentService+Likes.swift +++ b/WordPress/Classes/Services/CommentService+Likes.swift @@ -40,9 +40,10 @@ extension CommentService { commentID: commentID, siteID: siteID, purgeExisting: purgeExisting) { - let users = self.likeUsersFor(commentID: commentID, siteID: siteID) + assert(Thread.isMainThread) + + let users = LikeUserHelper.likeUsersFor(commentID: commentID, siteID: siteID, in: self.coreDataStack.mainContext) success(users, totalLikes.intValue, count) - LikeUserHelper.purgeStaleLikes() } }, failure: { error in DDLogError(String(describing: error)) @@ -50,36 +51,6 @@ extension CommentService { }) } - /** - Fetches a list of users from Core Data that liked the comment with the given IDs. - - @param commentID The ID of the comment to fetch likes for. - @param siteID The ID of the site that contains the post. - @param after Filter results to likes after this Date. Optional. - */ - func likeUsersFor(commentID: NSNumber, siteID: NSNumber, after: Date? = nil) -> [LikeUser] { - self.coreDataStack.performQuery { context in - let request = LikeUser.fetchRequest() as NSFetchRequest - - request.predicate = { - if let after = after { - // The date comparison is 'less than' because Likes are in descending order. - return NSPredicate(format: "likedSiteID = %@ AND likedCommentID = %@ AND dateLiked < %@", siteID, commentID, after as CVarArg) - } - - return NSPredicate(format: "likedSiteID = %@ AND likedCommentID = %@", siteID, commentID) - }() - - request.sortDescriptors = [NSSortDescriptor(key: "dateLiked", ascending: false)] - - if let users = try? context.fetch(request) { - return users - } - - return [LikeUser]() - } - } - } private extension CommentService { @@ -106,6 +77,8 @@ private extension CommentService { if purgeExisting { self.deleteExistingUsersFor(commentID: commentID, siteID: siteID, from: derivedContext, likesToKeep: likers) } + + LikeUserHelper.purgeStaleLikes(fromContext: derivedContext) }, completion: onComplete, on: .main) } diff --git a/WordPress/Classes/Services/JetpackSocialService.swift b/WordPress/Classes/Services/JetpackSocialService.swift new file mode 100644 index 000000000000..6f2a6fd1a9f4 --- /dev/null +++ b/WordPress/Classes/Services/JetpackSocialService.swift @@ -0,0 +1,77 @@ +import WordPressKit +import CoreData + +class JetpackSocialService { + + // MARK: Properties + + private let coreDataStack: CoreDataStackSwift + + private lazy var remote: JetpackSocialServiceRemote = { + let api = coreDataStack.performQuery { context in + return WordPressComRestApi.defaultV2Api(in: context) + } + return .init(wordPressComRestApi: api) + }() + + // MARK: Methods + + init(coreDataStack: CoreDataStackSwift = ContextManager.shared) { + self.coreDataStack = coreDataStack + } + + /// Fetches and updates the Publicize information for the site associated with the `blogID`. + /// The method returns a value type that contains the remaining usage of Social auto-sharing and the maximum limit for the associated site. + /// + /// - Note: If the returned result is a success with nil sharing limit, it's likely that the blog is hosted on WP.com, and has no Social sharing limitations. + /// + /// Furthermore, even if the sharing limit exists, it may not be applicable for the blog since the user might have purchased a product that ignores this limitation. + /// + /// - Parameters: + /// - blogID: The ID of the blog. + /// - completion: Closure that's called after the sync process completes. + func syncSharingLimit(for blogID: Int, completion: @escaping (Result) -> Void) { + remote.fetchPublicizeInfo(for: blogID) { [weak self] result in + switch result { + case .success(let remotePublicizeInfo): + self?.coreDataStack.performAndSave({ context -> PublicizeInfo.SharingLimit? in + guard let blog = try Blog.lookup(withID: blogID, in: context) else { + // unexpected to fall into this case, since the API should return an error response. + throw ServiceError.blogNotFound(id: blogID) + } + + if let remotePublicizeInfo, + let newOrExistingInfo = blog.publicizeInfo ?? PublicizeInfo.newObject(in: context) { + // add or update the publicizeInfo for the blog. + newOrExistingInfo.configure(with: remotePublicizeInfo) + blog.publicizeInfo = newOrExistingInfo + + } else if let existingPublicizeInfo = blog.publicizeInfo { + // if the remote object is nil, delete the blog's publicizeInfo if it exists. + context.delete(existingPublicizeInfo) + blog.publicizeInfo = nil + } + + return blog.publicizeInfo?.sharingLimit + + }, completion: { completion($0) }, on: .main) + + case .failure(let error): + completion(.failure(error)) + } + } + } + + // MARK: Errors + + enum ServiceError: LocalizedError { + case blogNotFound(id: Int) + + var errorDescription: String? { + switch self { + case .blogNotFound(let id): + return "Blog with id: \(id) was unexpectedly not found." + } + } + } +} diff --git a/WordPress/Classes/Services/LikeUserHelpers.swift b/WordPress/Classes/Services/LikeUserHelpers.swift index b96f0df1c2fc..8c3f0b21ba92 100644 --- a/WordPress/Classes/Services/LikeUserHelpers.swift +++ b/WordPress/Classes/Services/LikeUserHelpers.swift @@ -39,6 +39,34 @@ import CoreData return try? context.fetch(request).first } + /** + Fetches a list of users from Core Data that liked the comment with the given IDs. + + @param commentID The ID of the comment to fetch likes for. + @param siteID The ID of the site that contains the post. + @param after Filter results to likes after this Date. Optional. + */ + class func likeUsersFor(commentID: NSNumber, siteID: NSNumber, after: Date? = nil, in context: NSManagedObjectContext) -> [LikeUser] { + let request = LikeUser.fetchRequest() as NSFetchRequest + + request.predicate = { + if let after = after { + // The date comparison is 'less than' because Likes are in descending order. + return NSPredicate(format: "likedSiteID = %@ AND likedCommentID = %@ AND dateLiked < %@", siteID, commentID, after as CVarArg) + } + + return NSPredicate(format: "likedSiteID = %@ AND likedCommentID = %@", siteID, commentID) + }() + + request.sortDescriptors = [NSSortDescriptor(key: "dateLiked", ascending: false)] + + if let users = try? context.fetch(request) { + return users + } + + return [LikeUser]() + } + private class func updatePreferredBlog(for user: LikeUser, with remoteUser: RemoteLikeUser, context: NSManagedObjectContext) { guard let remotePreferredBlog = remoteUser.preferredBlog else { if let existingPreferredBlog = user.preferredBlog { @@ -58,14 +86,8 @@ import CoreData preferredBlog.user = user } - class func purgeStaleLikes() { - ContextManager.shared.performAndSave { - purgeStaleLikes(fromContext: $0) - } - } - // Delete all LikeUsers that were last fetched at least 7 days ago. - private class func purgeStaleLikes(fromContext context: NSManagedObjectContext) { + class func purgeStaleLikes(fromContext context: NSManagedObjectContext) { guard let staleDate = Calendar.current.date(byAdding: .day, value: -7, to: Date()) else { DDLogError("Error creating date to purge stale Likes.") return diff --git a/WordPress/Classes/Services/PostService+Likes.swift b/WordPress/Classes/Services/PostService+Likes.swift index fccfda6d5cbd..97cb37375d40 100644 --- a/WordPress/Classes/Services/PostService+Likes.swift +++ b/WordPress/Classes/Services/PostService+Likes.swift @@ -42,7 +42,6 @@ extension PostService { purgeExisting: purgeExisting) { let users = self.likeUsersFor(postID: postID, siteID: siteID) success(users, totalLikes.intValue, count) - LikeUserHelper.purgeStaleLikes() } }, failure: { error in DDLogError(String(describing: error)) @@ -104,6 +103,8 @@ private extension PostService { if purgeExisting { self.deleteExistingUsersFor(postID: postID, siteID: siteID, from: derivedContext, likesToKeep: likers) } + + LikeUserHelper.purgeStaleLikes(fromContext: derivedContext) }, completion: onComplete, on: .main) } diff --git a/WordPress/Classes/Services/PostService.m b/WordPress/Classes/Services/PostService.m index eabb2e04dc5d..4310d8555168 100644 --- a/WordPress/Classes/Services/PostService.m +++ b/WordPress/Classes/Services/PostService.m @@ -701,10 +701,15 @@ - (void)mergePosts:(NSArray *)remotePosts } } - [[ContextManager sharedInstance] saveContext:self.managedObjectContext]; - if (completion) { - completion(posts); - } + [[ContextManager sharedInstance] saveContext:self.managedObjectContext withCompletionBlock:^{ + // Call the completion block after context is saved. The callback is called on the context queue because `posts` + // contains models that are bound to the `self.managedObjectContext` object. + if (completion) { + [self.managedObjectContext performBlock:^{ + completion(posts); + }]; + } + } onQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; } - (NSDictionary *)remoteSyncParametersDictionaryForRemote:(nonnull id )remote diff --git a/WordPress/Classes/Stores/StatsWidgetsStore.swift b/WordPress/Classes/Stores/StatsWidgetsStore.swift index 2647d0de02a8..d9532becf851 100644 --- a/WordPress/Classes/Stores/StatsWidgetsStore.swift +++ b/WordPress/Classes/Stores/StatsWidgetsStore.swift @@ -258,23 +258,24 @@ private extension StatsWidgetsStore { /// Observes WPAccountDefaultWordPressComAccountChanged notification and reloads widget data based on the state of account. /// The site data is not yet loaded after this notification and widget data cannot be cached for newly signed in account. func observeAccountChangesForWidgets() { - NotificationCenter.default.addObserver(forName: .WPAccountDefaultWordPressComAccountChanged, - object: nil, - queue: nil) { notification in + NotificationCenter.default.addObserver(self, selector: #selector(handleAccountChangedNotification), name: .WPAccountDefaultWordPressComAccountChanged, object: nil) + } - UserDefaults(suiteName: WPAppGroupName)?.setValue(AccountHelper.isLoggedIn, forKey: AppConfiguration.Widget.Stats.userDefaultsLoggedInKey) + @objc func handleAccountChangedNotification() { + let isLoggedIn = AccountHelper.isLoggedIn + let userDefaults = UserDefaults(suiteName: WPAppGroupName) + userDefaults?.setValue(isLoggedIn, forKey: AppConfiguration.Widget.Stats.userDefaultsLoggedInKey) - if !AccountHelper.isLoggedIn { - HomeWidgetTodayData.delete() - HomeWidgetThisWeekData.delete() - HomeWidgetAllTimeData.delete() + guard !isLoggedIn else { return } - UserDefaults(suiteName: WPAppGroupName)?.setValue(nil, forKey: AppConfiguration.Widget.Stats.userDefaultsSiteIdKey) - WidgetCenter.shared.reloadTimelines(ofKind: AppConfiguration.Widget.Stats.todayKind) - WidgetCenter.shared.reloadTimelines(ofKind: AppConfiguration.Widget.Stats.thisWeekKind) - WidgetCenter.shared.reloadTimelines(ofKind: AppConfiguration.Widget.Stats.allTimeKind) - } - } + HomeWidgetTodayData.delete() + HomeWidgetThisWeekData.delete() + HomeWidgetAllTimeData.delete() + + userDefaults?.setValue(nil, forKey: AppConfiguration.Widget.Stats.userDefaultsSiteIdKey) + WidgetCenter.shared.reloadTimelines(ofKind: AppConfiguration.Widget.Stats.todayKind) + WidgetCenter.shared.reloadTimelines(ofKind: AppConfiguration.Widget.Stats.thisWeekKind) + WidgetCenter.shared.reloadTimelines(ofKind: AppConfiguration.Widget.Stats.allTimeKind) } /// Observes WPSigninDidFinishNotification and wordpressLoginFinishedJetpackLogin notifications and initializes the widget. @@ -286,11 +287,11 @@ private extension StatsWidgetsStore { /// Observes applicationLaunchCompleted notification and runs migration. func observeApplicationLaunched() { - NotificationCenter.default.addObserver(forName: NSNotification.Name.applicationLaunchCompleted, - object: nil, - queue: nil) { [weak self] _ in - self?.handleJetpackWidgetsMigration() - } + NotificationCenter.default.addObserver(self, selector: #selector(handleApplicationLaunchCompleted), name: NSNotification.Name.applicationLaunchCompleted, object: nil) + } + + @objc private func handleApplicationLaunchCompleted() { + handleJetpackWidgetsMigration() } func observeJetpackFeaturesState() { diff --git a/WordPress/Classes/System/WordPressAppDelegate.swift b/WordPress/Classes/System/WordPressAppDelegate.swift index a975ba64aab4..3726179dc8c2 100644 --- a/WordPress/Classes/System/WordPressAppDelegate.swift +++ b/WordPress/Classes/System/WordPressAppDelegate.swift @@ -128,13 +128,6 @@ class WordPressAppDelegate: UIResponder, UIApplicationDelegate { ABTest.start() - if UITextField.shouldActivateWorkaroundForBulgarianKeyboardCrash() { - // WORKAROUND: this is a workaround for an issue with UITextField in iOS 14. - // Please refer to the documentation of the called method to learn the details and know - // how to tell if this call can be removed. - UITextField.activateWorkaroundForBulgarianKeyboardCrash() - } - InteractiveNotificationsManager.shared.registerForUserNotifications() setupPingHub() setupBackgroundRefresh(application) diff --git a/WordPress/Classes/Utility/CoreDataHelper.swift b/WordPress/Classes/Utility/CoreDataHelper.swift index 9c05910ff15a..fd54853ac6a4 100644 --- a/WordPress/Classes/Utility/CoreDataHelper.swift +++ b/WordPress/Classes/Utility/CoreDataHelper.swift @@ -196,12 +196,12 @@ extension ContextManager.ContextManagerError: LocalizedError, CustomDebugStringC extension CoreDataStack { /// Perform a query using the `mainContext` and return the result. + /// + /// - Warning: Do not return `NSManagedObject` instances from the closure. func performQuery(_ block: @escaping (NSManagedObjectContext) -> T) -> T { - var value: T! = nil - self.mainContext.performAndWait { - value = block(self.mainContext) + mainContext.performAndWait { [mainContext] in + block(mainContext) } - return value } // MARK: - Database Migration @@ -211,21 +211,29 @@ extension CoreDataStack { func createStoreCopy(to backupLocation: URL) throws { try? removeBackupData(from: backupLocation) guard let storeCoordinator = mainContext.persistentStoreCoordinator, - let store = storeCoordinator.persistentStores.first else { + let store = storeCoordinator.persistentStores.first, + let currentDatabaseLocation = store.url else { throw ContextManager.ContextManagerError.missingCoordinatorOrStore } - let model = storeCoordinator.managedObjectModel - let storeCoordinatorCopy = NSPersistentStoreCoordinator(managedObjectModel: model) - var storeOptions = store.options - storeOptions?[NSReadOnlyPersistentStoreOption] = true - let storeCopy = try storeCoordinatorCopy.addPersistentStore(ofType: store.type, - configurationName: store.configurationName, - at: store.url, - options: storeOptions) - try storeCoordinatorCopy.migratePersistentStore(storeCopy, - to: backupLocation, - withType: storeCopy.type) + do { + try storeCoordinator.replacePersistentStore(at: backupLocation, + withPersistentStoreFrom: currentDatabaseLocation, + ofType: store.type) + } catch { + // Fallback to the previous migration method + let model = storeCoordinator.managedObjectModel + let storeCoordinatorCopy = NSPersistentStoreCoordinator(managedObjectModel: model) + var storeOptions = store.options + storeOptions?[NSReadOnlyPersistentStoreOption] = true + let storeCopy = try storeCoordinatorCopy.addPersistentStore(ofType: store.type, + configurationName: store.configurationName, + at: store.url, + options: storeOptions) + try storeCoordinatorCopy.migratePersistentStore(storeCopy, + to: backupLocation, + withType: storeCopy.type) + } } /// Removes any copy of the store from the backup location. @@ -257,9 +265,7 @@ extension CoreDataStack { let (databaseLocation, shmLocation, walLocation) = databaseFiles(for: databaseLocation) guard let currentDatabaseLocation = store.url, - FileManager.default.fileExists(atPath: databaseLocation.path), - FileManager.default.fileExists(atPath: shmLocation.path), - FileManager.default.fileExists(atPath: walLocation.path) else { + FileManager.default.fileExists(atPath: databaseLocation.path) else { throw ContextManager.ContextManagerError.missingDatabase } @@ -354,3 +360,69 @@ extension CoreDataStack { try ContextManager.migrateDataModelsIfNecessary(storeURL: databaseLocation, objectModel: objectModel) } } + +/// This extension declares many `performQuery` usages that may introduce Core Data concurrency issues. +/// +/// The context object used by the `performQuery` function is opaque to the caller. The caller should not assume what +/// the context object is, nor the context queue type (the main queue or a background queue). That means the caller +/// does not have enough information to guarantee safe access to the returned `NSManagedObject` instances. +/// +/// The closure passed to the `performQuery` function should use the context to query objects and return non- Core Data +/// types. Here is an example of how it should be used. +/// +/// ``` +/// // Wrong: +/// let account = coreDataStack.performQuery { context in +/// return Account.lookUp(in: context) +/// } +/// let name = account.username +/// +/// // Right: +/// let name = coreDataStack.performQuery { context in +/// let account = Account.lookUp(in: context) +/// return account.username +/// } +/// ``` +extension CoreDataStack { + @available(*, deprecated, message: "Returning `NSManagedObject` instances may introduce Core Data concurrency issues.") + func performQuery(_ block: @escaping (NSManagedObjectContext) -> T) -> T where T: NSManagedObject { + mainContext.performAndWait { [mainContext] in + block(mainContext) + } + } + + @available(*, deprecated, message: "Returning `NSManagedObject` instances may introduce Core Data concurrency issues.") + func performQuery(_ block: @escaping (NSManagedObjectContext) -> T?) -> T? where T: NSManagedObject { + mainContext.performAndWait { [mainContext] in + block(mainContext) + } + } + + @available(*, deprecated, message: "Returning `NSManagedObject` instances may introduce Core Data concurrency issues.") + func performQuery(_ block: @escaping (NSManagedObjectContext) -> T) -> T where T: Sequence, T.Element: NSManagedObject { + mainContext.performAndWait { [mainContext] in + block(mainContext) + } + } + + @available(*, deprecated, message: "Returning `NSManagedObject` instances may introduce Core Data concurrency issues.") + func performQuery(_ block: @escaping (NSManagedObjectContext) -> T?) -> T? where T: Sequence, T.Element: NSManagedObject { + mainContext.performAndWait { [mainContext] in + block(mainContext) + } + } + + @available(*, deprecated, message: "Returning `NSManagedObject` instances may introduce Core Data concurrency issues.") + func performQuery(_ block: @escaping (NSManagedObjectContext) -> Result) -> Result where T: NSManagedObject, E: Error { + mainContext.performAndWait { [mainContext] in + block(mainContext) + } + } + + @available(*, deprecated, message: "Returning `NSManagedObject` instances may introduce Core Data concurrency issues.") + func performQuery(_ block: @escaping (NSManagedObjectContext) -> Result?) -> Result? where T: NSManagedObject, E: Error { + mainContext.performAndWait { [mainContext] in + block(mainContext) + } + } +} diff --git a/WordPress/Classes/Utility/Editor/GutenbergSettings.swift b/WordPress/Classes/Utility/Editor/GutenbergSettings.swift index c15048e33866..603c2fee212b 100644 --- a/WordPress/Classes/Utility/Editor/GutenbergSettings.swift +++ b/WordPress/Classes/Utility/Editor/GutenbergSettings.swift @@ -4,19 +4,19 @@ class GutenbergSettings { // MARK: - Enabled Editors Keys enum Key { static let appWideEnabled = "kUserDefaultsGutenbergEditorEnabled" - static func enabledOnce(for blog: Blog) -> String { - let url = urlStringFrom(blog) + static func enabledOnce(forBlogURL url: String?) -> String { + let url = urlString(fromBlogURL: url) return "com.wordpress.gutenberg-autoenabled-" + url } - static func showPhase2Dialog(for blog: Blog) -> String { - let url = urlStringFrom(blog) + static func showPhase2Dialog(forBlogURL url: String?) -> String { + let url = urlString(fromBlogURL: url) return "kShowGutenbergPhase2Dialog-" + url } static let focalPointPickerTooltipShown = "kGutenbergFocalPointPickerTooltipShown" static let blockTypeImpressions = "kBlockTypeImpressions" - private static func urlStringFrom(_ blog: Blog) -> String { - return (blog.url ?? "") + private static func urlString(fromBlogURL url: String?) -> String { + return (url ?? "") // New sites will add a slash at the end of URL. // This is removed when the URL is refreshed from remote. // Removing trailing '/' in case there is one for consistency. @@ -61,7 +61,7 @@ class GutenbergSettings { softSetGutenbergEnabled(isEnabled, for: blog, source: source) if isEnabled { - database.set(true, forKey: Key.enabledOnce(for: blog)) + database.set(true, forKey: Key.enabledOnce(forBlogURL: blog.url)) } } @@ -82,12 +82,16 @@ class GutenbergSettings { } private func setGutenbergEnabledForAllSites() { - let allBlogs = coreDataStack.performQuery({ (try? BlogQuery().blogs(in: $0)) ?? [] }) - allBlogs.forEach { blog in - if blog.editor == .aztec { - setShowPhase2Dialog(true, for: blog) - database.set(true, forKey: Key.enabledOnce(for: blog)) - } + let blogURLs: [String?] = coreDataStack.performQuery { context in + guard let blogs = try? BlogQuery().blogs(in: context) else { return [] } + + return blogs + .filter { $0.editor == .aztec } + .map { $0.url } + } + blogURLs.forEach { blogURL in + setShowPhase2Dialog(true, forBlogURL: blogURL) + database.set(true, forKey: Key.enabledOnce(forBlogURL: blogURL)) } let editorSettingsService = EditorSettingsService(coreDataStack: coreDataStack) editorSettingsService.migrateGlobalSettingToRemote(isGutenbergEnabled: true, overrideRemote: true, onSuccess: { @@ -96,11 +100,15 @@ class GutenbergSettings { } func shouldPresentInformativeDialog(for blog: Blog) -> Bool { - return database.bool(forKey: Key.showPhase2Dialog(for: blog)) + return database.bool(forKey: Key.showPhase2Dialog(forBlogURL: blog.url)) } func setShowPhase2Dialog(_ showDialog: Bool, for blog: Blog) { - database.set(showDialog, forKey: Key.showPhase2Dialog(for: blog)) + setShowPhase2Dialog(showDialog, forBlogURL: blog.url) + } + + func setShowPhase2Dialog(_ showDialog: Bool, forBlogURL url: String?) { + database.set(showDialog, forKey: Key.showPhase2Dialog(forBlogURL: url)) } /// Sets gutenberg enabled without registering the enabled action ("enabledOnce") @@ -153,7 +161,7 @@ class GutenbergSettings { /// True if gutenberg editor has been enabled at least once on the given blog func wasGutenbergEnabledOnce(for blog: Blog) -> Bool { - return database.object(forKey: Key.enabledOnce(for: blog)) != nil + return database.object(forKey: Key.enabledOnce(forBlogURL: blog.url)) != nil } /// True if gutenberg should be autoenabled for the blog hosting the given post. @@ -162,7 +170,7 @@ class GutenbergSettings { } func willShowDialog(for blog: Blog) { - database.set(true, forKey: Key.enabledOnce(for: blog)) + database.set(true, forKey: Key.enabledOnce(forBlogURL: blog.url)) } /// True if it should show the tooltip for the focal point picker @@ -208,7 +216,7 @@ class GutenbergSettings { } func getDefaultEditor(for blog: Blog) -> MobileEditor { - database.set(true, forKey: Key.enabledOnce(for: blog)) + database.set(true, forKey: Key.enabledOnce(forBlogURL: blog.url)) return .gutenberg } } diff --git a/WordPress/Classes/Utility/Logging/WPCrashLoggingProvider.swift b/WordPress/Classes/Utility/Logging/WPCrashLoggingProvider.swift index 43770b7f486a..17582673230c 100644 --- a/WordPress/Classes/Utility/Logging/WPCrashLoggingProvider.swift +++ b/WordPress/Classes/Utility/Logging/WPCrashLoggingProvider.swift @@ -1,4 +1,5 @@ import UIKit +import Combine import AutomatticTracks /// A wrapper around the logging stack – provides shared initialization and configuration for Tracks Crash and Event Logging @@ -12,6 +13,8 @@ struct WPLoggingStack { private let eventLoggingDataProvider = EventLoggingDataProvider.fromDDFileLogger(WPLogger.shared().fileLogger) private let eventLoggingDelegate = EventLoggingDelegate() + private let enterForegroundObserver: AnyCancellable + init() { let eventLogging = EventLogging(dataSource: eventLoggingDataProvider, delegate: eventLoggingDelegate) @@ -20,18 +23,16 @@ struct WPLoggingStack { self.crashLogging = CrashLogging(dataProvider: WPCrashLoggingDataProvider(), eventLogging: eventLogging) /// Upload any remaining files any time the app becomes active - let willEnterForeground = UIApplication.willEnterForegroundNotification - NotificationCenter.default.addObserver(forName: willEnterForeground, object: nil, queue: nil, using: self.willEnterForeground) + enterForegroundObserver = NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification) + .sink(receiveValue: { [eventLogging] _ in + eventLogging.uploadNextLogFileIfNeeded() + DDLogDebug("📜 Resumed encrypted log upload queue due to app entering foreground") + }) } func start() throws { _ = try crashLogging.start() } - - private func willEnterForeground(note: Foundation.Notification) { - self.eventLogging.uploadNextLogFileIfNeeded() - DDLogDebug("📜 Resumed encrypted log upload queue due to app entering foreground") - } } struct WPCrashLoggingDataProvider: CrashLoggingDataProvider { diff --git a/WordPress/Classes/Utility/Migration/ContentMigrationCoordinator.swift b/WordPress/Classes/Utility/Migration/ContentMigrationCoordinator.swift index 033162e5d7d5..4430acd0312d 100644 --- a/WordPress/Classes/Utility/Migration/ContentMigrationCoordinator.swift +++ b/WordPress/Classes/Utility/Migration/ContentMigrationCoordinator.swift @@ -141,15 +141,16 @@ private extension ContentMigrationCoordinator { return } - notificationCenter.addObserver(forName: .WPAccountDefaultWordPressComAccountChanged, object: nil, queue: nil) { [weak self] notification in - // nil notification object means it's a logout event. - guard let self, - notification.object == nil else { - return - } + notificationCenter.addObserver(self, selector: #selector(handleAccountChangedNotification(_:)), name: .WPAccountDefaultWordPressComAccountChanged, object: nil) + } - self.cleanupExportedDataIfNeeded() + @objc private func handleAccountChangedNotification(_ notification: Foundation.Notification) { + // nil notification object means it's a logout event. + guard notification.object == nil else { + return } + + self.cleanupExportedDataIfNeeded() } /// A "middleware" logic that attempts to record (or clear) any migration error to the App Group space diff --git a/WordPress/Classes/Utility/WebKitViewController.swift b/WordPress/Classes/Utility/WebKitViewController.swift index 78d0649b598e..37c6bfeb895e 100644 --- a/WordPress/Classes/Utility/WebKitViewController.swift +++ b/WordPress/Classes/Utility/WebKitViewController.swift @@ -349,10 +349,7 @@ class WebKitViewController: UIViewController, WebKitAuthenticatable { appearance.backgroundColor = UIColor(light: .white, dark: .appBarBackground) toolBar.standardAppearance = appearance - - if #available(iOS 15.0, *) { - toolBar.scrollEdgeAppearance = appearance - } + toolBar.scrollEdgeAppearance = appearance fixBarButtonsColorForBoldText(on: toolBar) } diff --git a/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift b/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift index 452f76fc5181..b80ec2e8635a 100644 --- a/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift +++ b/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift @@ -1,3 +1,5 @@ +import UIKit + final class BlazeCampaignTableViewCell: UITableViewCell, Reusable { // MARK: - Views diff --git a/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignsStream.swift b/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignsStream.swift new file mode 100644 index 000000000000..664f9109b4fc --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignsStream.swift @@ -0,0 +1,62 @@ +import Foundation +import SwiftUI +import WordPressKit + +protocol BlazeCampaignsStreamDelegate: AnyObject { + func stream(_ stream: BlazeCampaignsStream, didAppendItemsAt indexPaths: [IndexPath]) + func streamDidRefreshState(_ stream: BlazeCampaignsStream) +} + +final class BlazeCampaignsStream { + weak var delegate: BlazeCampaignsStreamDelegate? + + private(set) var campaigns: [BlazeCampaign] = [] + private(set) var isLoading = false + private(set) var error: Error? + + private var pages: [BlazeCampaignsSearchResponse] = [] + private var campaignIDs: Set = [] + private var hasMore = true + private let service: BlazeServiceProtocol + private let blog: Blog + + init(blog: Blog, service: BlazeServiceProtocol = BlazeService()) { + self.blog = blog + self.service = service + } + + /// Loads the next page. Does nothing if it's already loading or has no more items to load. + func load(_ completion: ((Result) -> Void)? = nil) { + guard !isLoading && hasMore else { + return + } + isLoading = true + error = nil + delegate?.streamDidRefreshState(self) + + service.getRecentCampaigns(for: blog, page: pages.count + 1) { [weak self] in + self?.didLoad(with: $0) + completion?($0) + } + } + + private func didLoad(with result: Result) { + switch result { + case .success(let response): + let newCampaigns = (response.campaigns ?? []) + .filter { !campaignIDs.contains($0.campaignID) } + pages.append(response) + hasMore = (response.totalPages ?? 0) > pages.count && !newCampaigns.isEmpty + + campaigns += newCampaigns + campaignIDs.formUnion(newCampaigns.map(\.campaignID)) + let indexPaths = campaigns.indices.suffix(newCampaigns.count) + .map { IndexPath(row: $0, section: 0) } + delegate?.stream(self, didAppendItemsAt: indexPaths) + case .failure(let error): + self.error = error + } + isLoading = false + delegate?.streamDidRefreshState(self) + } +} diff --git a/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignsViewController.swift b/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignsViewController.swift index ccb5b44f4d8e..f442473c0d46 100644 --- a/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignsViewController.swift +++ b/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignsViewController.swift @@ -1,51 +1,44 @@ import UIKit +import WordPressKit +import WordPressFlux -final class BlazeCampaignsViewController: UIViewController, NoResultsViewHost { - +final class BlazeCampaignsViewController: UIViewController, NoResultsViewHost, BlazeCampaignsStreamDelegate { // MARK: - Views - private lazy var plusButton: UIBarButtonItem = { - let button = UIBarButtonItem(image: UIImage(systemName: "plus"), - style: .plain, - target: self, - action: #selector(plusButtonTapped)) - return button - }() + private lazy var plusButton = UIBarButtonItem( + image: UIImage(systemName: "plus"), + style: .plain, + target: self, + action: #selector(buttonCreateCampaignTapped) + ) private lazy var tableView: UITableView = { - let tableView = UITableView() + let tableView = UITableView(frame: .zero, style: .plain) tableView.translatesAutoresizingMaskIntoConstraints = false tableView.rowHeight = UITableView.automaticDimension + tableView.estimatedRowHeight = 128 tableView.separatorStyle = .none + tableView.refreshControl = refreshControl + refreshControl.addTarget(self, action: #selector(setNeedsToRefreshCampaigns), for: .valueChanged) tableView.register(BlazeCampaignTableViewCell.self, forCellReuseIdentifier: BlazeCampaignTableViewCell.defaultReuseID) tableView.dataSource = self tableView.delegate = self return tableView }() + private let refreshControl = UIRefreshControl() + // MARK: - Properties + private var stream: BlazeCampaignsStream + private var pendingStream: AnyObject? private let blog: Blog - private var campaigns: [BlazeCampaign] = [] { - didSet { - tableView.reloadData() - updateNoResultsView() - } - } - - private var isLoading: Bool = false { - didSet { - if isLoading != oldValue { - updateNoResultsView() - } - } - } - // MARK: - Initializers init(blog: Blog) { self.blog = blog + self.stream = BlazeCampaignsStream(blog: blog) super.init(nibName: nil, bundle: nil) } @@ -58,17 +51,111 @@ final class BlazeCampaignsViewController: UIViewController, NoResultsViewHost { override func viewDidLoad() { super.viewDidLoad() + setupView() setupNavBar() setupNoResults() + + stream.delegate = self + stream.load() + + // Refresh data automatically when new campaign is created + NotificationCenter.default.addObserver(self, selector: #selector(setNeedsToRefreshCampaigns), name: .blazeCampaignCreated, object: nil) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + tableView.sizeToFitFooterView() + } + + // MARK: - Stream + + func stream(_ stream: BlazeCampaignsStream, didAppendItemsAt indexPaths: [IndexPath]) { + // Make sure the existing cells are not reloaded to avoid interfering with image loading + UIView.performWithoutAnimation { + tableView.insertRows(at: indexPaths, with: .none) + } + } + + func streamDidRefreshState(_ stream: BlazeCampaignsStream) { + reloadView() + } + + private func reloadView() { + reloadStateView() + reloadFooterView() + tableView.sizeToFitFooterView() + } + + private func reloadStateView() { + hideNoResults() + noResultsViewController.hideImageView(true) + if stream.campaigns.isEmpty { + if stream.isLoading { + noResultsViewController.hideImageView(false) + showLoadingView() + } else if stream.error != nil { + showErrorView() + } else { + showNoResultsView() + } + } + } + + private func reloadFooterView() { + guard !stream.campaigns.isEmpty else { + tableView.tableFooterView = nil + return + } + if stream.isLoading { + tableView.tableFooterView = PagingFooterView(state: .loading) + } else if stream.error != nil { + let footerView = PagingFooterView(state: .error) + footerView.buttonRetry.addTarget(self, action: #selector(buttonRetryTapped), for: .touchUpInside) + tableView.tableFooterView = footerView + } else { + tableView.tableFooterView = nil + } + } + + // MARK: - Actions + + @objc private func buttonRetryTapped() { + stream.load() + } + + @objc private func setNeedsToRefreshCampaigns() { + guard pendingStream == nil else { return } + + let stream = BlazeCampaignsStream(blog: blog) + stream.load { [weak self] in + guard let self else { return } + switch $0 { + case .success: + self.stream = stream + self.stream.delegate = self + self.tableView.reloadData() + self.reloadView() + case .failure(let error): + if self.refreshControl.isRefreshing { + ActionDispatcher.dispatch(NoticeAction.post(Notice(title: error.localizedDescription, feedbackType: .error))) + } + } + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) { + self.pendingStream = nil + self.refreshControl.endRefreshing() + } + } + pendingStream = stream } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - fetchCampaigns() + @objc private func buttonCreateCampaignTapped() { + BlazeEventsTracker.trackBlazeFlowStarted(for: .campaignsList) + BlazeFlowCoordinator.presentBlaze(in: self, source: .campaignsList, blog: blog) } - // MARK: - Private helpers + // MARK: - Private private func setupView() { view.backgroundColor = .DS.Background.primary @@ -84,22 +171,6 @@ final class BlazeCampaignsViewController: UIViewController, NoResultsViewHost { private func setupNoResults() { noResultsViewController.delegate = self } - - private func fetchCampaigns() { - isLoading = true - - // FIXME: Fetch campaigns via BlazeService - - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in - self?.isLoading = false - self?.campaigns = mockResponse.campaigns ?? [] - } - } - - @objc private func plusButtonTapped() { - // TODO: Track event - BlazeFlowCoordinator.presentBlaze(in: self, source: .campaignsList, blog: blog) - } } // MARK: - Table methods @@ -107,42 +178,39 @@ final class BlazeCampaignsViewController: UIViewController, NoResultsViewHost { extension BlazeCampaignsViewController: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return campaigns.count + stream.campaigns.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: BlazeCampaignTableViewCell.defaultReuseID) as? BlazeCampaignTableViewCell, - let campaign = campaigns[safe: indexPath.row] else { - return UITableViewCell() - } - + let cell = tableView.dequeueReusableCell(withIdentifier: BlazeCampaignTableViewCell.defaultReuseID) as! BlazeCampaignTableViewCell + let campaign = stream.campaigns[indexPath.row] let viewModel = BlazeCampaignViewModel(campaign: campaign) cell.configure(with: viewModel, blog: blog) return cell } -} -// MARK: - No results - -extension BlazeCampaignsViewController: NoResultsViewControllerDelegate { - - private func updateNoResultsView() { - guard !isLoading else { - showLoadingView() - return + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if scrollView.contentOffset.y + scrollView.frame.size.height > scrollView.contentSize.height - 500 { + if stream.error == nil { + stream.load() + } } + } - if campaigns.isEmpty { - showNoResultsView() + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + guard let campaign = stream.campaigns[safe: indexPath.row] else { return } - - hideNoResults() + BlazeFlowCoordinator.presentBlazeCampaignDetails(in: self, source: .campaignsList, blog: blog, campaignID: campaign.campaignID) } +} + +// MARK: - No results + +extension BlazeCampaignsViewController: NoResultsViewControllerDelegate { private func showNoResultsView() { - hideNoResults() - noResultsViewController.hideImageView(true) configureAndDisplayNoResults(on: view, title: Strings.NoResults.emptyTitle, subtitle: Strings.NoResults.emptySubtitle, @@ -150,16 +218,12 @@ extension BlazeCampaignsViewController: NoResultsViewControllerDelegate { } private func showErrorView() { - hideNoResults() - noResultsViewController.hideImageView(true) configureAndDisplayNoResults(on: view, title: Strings.NoResults.errorTitle, subtitle: Strings.NoResults.errorSubtitle) } private func showLoadingView() { - hideNoResults() - noResultsViewController.hideImageView(false) configureAndDisplayNoResults(on: view, title: Strings.NoResults.loadingTitle, accessoryView: NoResultsViewController.loadingAccessoryView()) @@ -182,81 +246,8 @@ private extension BlazeCampaignsViewController { static let loadingTitle = NSLocalizedString("blaze.campaigns.loading.title", value: "Loading campaigns...", comment: "Displayed while Blaze campaigns are being loaded.") static let emptyTitle = NSLocalizedString("blaze.campaigns.empty.title", value: "You have no campaigns", comment: "Title displayed when there are no Blaze campaigns to display.") static let emptySubtitle = NSLocalizedString("blaze.campaigns.empty.subtitle", value: "You have not created any campaigns yet. Click promote to get started.", comment: "Text displayed when there are no Blaze campaigns to display.") - static let errorTitle = NSLocalizedString("Oops", comment: "Title for the view when there's an error loading Blaze campiagns.") - static let errorSubtitle = NSLocalizedString("There was an error loading campaigns.", comment: "Text displayed when there is a failure loading Blaze campaigns.") + static let errorTitle = NSLocalizedString("blaze.campaigns.errorTitle", value: "Oops", comment: "Title for the view when there's an error loading Blaze campiagns.") + static let errorSubtitle = NSLocalizedString("blaze.campaigns.errorMessage", value: "There was an error loading campaigns.", comment: "Text displayed when there is a failure loading Blaze campaigns.") } } } - -private let mockResponse: BlazeCampaignsSearchResponse = { - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - decoder.dateDecodingStrategy = .iso8601 - return try! decoder.decode(BlazeCampaignsSearchResponse.self, from: """ - { - "totalItems": 3, - "campaigns": [ - { - "campaign_id": 26916, - "name": "Test Post - don't approve Test Post - don't approve", - "start_date": "2023-06-13T00:00:00Z", - "end_date": "2023-06-01T19:15:45Z", - "status": "finished", - "avatar_url": "https://0.gravatar.com/avatar/614d27bcc21db12e7c49b516b4750387?s=96&d=identicon&r=G", - "budget_cents": 500, - "target_url": "https://alextest9123.wordpress.com/2023/06/01/test-post/", - "content_config": { - "title": "Test Post - don't approve", - "snippet": "Test Post Empty Empty", - "clickUrl": "https://alextest9123.wordpress.com/2023/06/01/test-post/", - "imageUrl": "https://i0.wp.com/public-api.wordpress.com/wpcom/v2/wordads/dsp/api/v1/dsp/creatives/56259/image?w=600&zoom=2" - }, - "campaign_stats": { - "impressions_total": 1000, - "clicks_total": 235 - } - }, - { - "campaign_id": 1, - "name": "Test Post - don't approve", - "start_date": "2023-06-13T00:00:00Z", - "end_date": "2023-06-01T19:15:45Z", - "status": "rejected", - "avatar_url": "https://0.gravatar.com/avatar/614d27bcc21db12e7c49b516b4750387?s=96&d=identicon&r=G", - "budget_cents": 5000, - "target_url": "https://alextest9123.wordpress.com/2023/06/01/test-post/", - "content_config": { - "title": "Test Post - don't approve", - "snippet": "Test Post Empty Empty", - "clickUrl": "https://alextest9123.wordpress.com/2023/06/01/test-post/", - "imageUrl": "https://i0.wp.com/public-api.wordpress.com/wpcom/v2/wordads/dsp/api/v1/dsp/creatives/56259/image?w=600&zoom=2" - }, - "campaign_stats": { - "impressions_total": 1000, - "clicks_total": 235 - } - }, - { - "campaign_id": 2, - "name": "Test Post - don't approve", - "start_date": "2023-06-13T00:00:00Z", - "end_date": "2023-06-01T19:15:45Z", - "status": "active", - "avatar_url": "https://0.gravatar.com/avatar/614d27bcc21db12e7c49b516b4750387?s=96&d=identicon&r=G", - "budget_cents": 1000, - "target_url": "https://alextest9123.wordpress.com/2023/06/01/test-post/", - "content_config": { - "title": "Test Post - don't approve", - "snippet": "Test Post Empty Empty", - "clickUrl": "https://alextest9123.wordpress.com/2023/06/01/test-post/", - "imageUrl": "https://i0.wp.com/public-api.wordpress.com/wpcom/v2/wordads/dsp/api/v1/dsp/creatives/56259/image?w=600&zoom=2" - }, - "campaign_stats": { - "impressions_total": 5000, - "clicks_total": 1035 - } - } - ] - } - """.data(using: .utf8)!) -}() diff --git a/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazeOverlayViewController.swift b/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazeOverlayViewController.swift index ef2b16b5eae2..c2b9b3fce1b1 100644 --- a/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazeOverlayViewController.swift +++ b/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazeOverlayViewController.swift @@ -156,9 +156,7 @@ final class BlazeOverlayViewController: UIViewController { navigationItem.standardAppearance = appearance navigationItem.compactAppearance = appearance navigationItem.scrollEdgeAppearance = appearance - if #available(iOS 15.0, *) { - navigationItem.compactScrollEdgeAppearance = appearance - } + navigationItem.compactScrollEdgeAppearance = appearance } private func setupView() { diff --git a/WordPress/Classes/ViewRelated/Blaze/Webview/BlazeCampaignDetailsWebViewModel.swift b/WordPress/Classes/ViewRelated/Blaze/Webview/BlazeCampaignDetailsWebViewModel.swift new file mode 100644 index 000000000000..c3cc14a177f6 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blaze/Webview/BlazeCampaignDetailsWebViewModel.swift @@ -0,0 +1,116 @@ +import Foundation + +class BlazeCampaignDetailsWebViewModel: BlazeWebViewModel { + + // MARK: Private Variables + + private let source: BlazeSource + private let blog: Blog + private let campaignID: Int + private weak var view: BlazeWebView? + private let externalURLHandler: ExternalURLHandler + private var linkBehavior: LinkBehavior = .all + + // MARK: Initializer + + init(source: BlazeSource, + blog: Blog, + campaignID: Int, + view: BlazeWebView, + externalURLHandler: ExternalURLHandler = UIApplication.shared) { + self.source = source + self.blog = blog + self.campaignID = campaignID + self.view = view + self.externalURLHandler = externalURLHandler + setLinkBehavior() + } + + // MARK: Computed Variables + + private var initialURL: URL? { + guard let siteURL = blog.displayURL else { + return nil + } + let urlString = String(format: Constants.campaignDetailsURLFormat, siteURL, campaignID, source.description) + return URL(string: urlString) + } + + private var baseURLString: String? { + guard let siteURL = blog.displayURL else { + return nil + } + return String(format: Constants.baseURLFormat, siteURL) + } + + // MARK: Public Functions + + var isFlowCompleted: Bool { + return true + } + + var navigationTitle: String { + return Strings.navigationTitle + } + + func startBlazeFlow() { + guard let initialURL, + let cookieJar = view?.cookieJar else { + // TODO: Track Analytics Error Event + view?.dismissView() + return + } + authenticatedRequest(for: initialURL, with: cookieJar) { [weak self] (request) in + guard let weakSelf = self else { + return + } + weakSelf.view?.load(request: request) + // TODO: Track Analytics Event + } + } + + func dismissTapped() { + view?.dismissView() + // TODO: Track Analytics Event + } + + func shouldNavigate(to request: URLRequest, with type: WKNavigationType) -> WKNavigationActionPolicy { + return linkBehavior.handle(request: request, with: type, externalURLHandler: externalURLHandler) + } + + func isCurrentStepDismissible() -> Bool { + return true + } + + func webViewDidFail(with error: Error) { + // TODO: Track Analytics Error Event + } + + // MARK: Helpers + + private func setLinkBehavior() { + guard let baseURLString else { + return + } + self.linkBehavior = .withBaseURLOnly(baseURLString) + } +} + +extension BlazeCampaignDetailsWebViewModel: WebKitAuthenticatable { + var authenticator: RequestAuthenticator? { + RequestAuthenticator(blog: blog) + } +} + +private extension BlazeCampaignDetailsWebViewModel { + enum Strings { + static let navigationTitle = NSLocalizedString("feature.blaze.campaignDetails.title", + value: "Campaign Details", + comment: "Title of screen the displays the details of an advertisement campaign.") + } + enum Constants { + // TODO: Replace these constants with remote config params + static let baseURLFormat = "https://wordpress.com/advertising/%@" + static let campaignDetailsURLFormat = "https://wordpress.com/advertising/%@/campaigns/%d?source=%@" + } +} diff --git a/WordPress/Classes/ViewRelated/Blaze/Webview/BlazeWebViewModel.swift b/WordPress/Classes/ViewRelated/Blaze/Webview/BlazeCreateCampaignWebViewModel.swift similarity index 81% rename from WordPress/Classes/ViewRelated/Blaze/Webview/BlazeWebViewModel.swift rename to WordPress/Classes/ViewRelated/Blaze/Webview/BlazeCreateCampaignWebViewModel.swift index 445643f26040..3fca62ae7eaa 100644 --- a/WordPress/Classes/ViewRelated/Blaze/Webview/BlazeWebViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blaze/Webview/BlazeCreateCampaignWebViewModel.swift @@ -1,13 +1,6 @@ import Foundation -protocol BlazeWebView { - func load(request: URLRequest) - func reloadNavBar() - func dismissView() - var cookieJar: CookieJar { get } -} - -class BlazeWebViewModel { +class BlazeCreateCampaignWebViewModel: BlazeWebViewModel { // MARK: Public Variables @@ -19,7 +12,7 @@ class BlazeWebViewModel { private let source: BlazeSource private let blog: Blog private let postID: NSNumber? - private let view: BlazeWebView + private weak var view: BlazeWebView? private let remoteConfigStore: RemoteConfigStore private let externalURLHandler: ExternalURLHandler private var linkBehavior: LinkBehavior = .all @@ -66,24 +59,30 @@ class BlazeWebViewModel { // MARK: Public Functions + var navigationTitle: String { + return Strings.navigationTitle + } + func startBlazeFlow() { - guard let initialURL else { + guard let initialURL, + let cookieJar = view?.cookieJar else { BlazeEventsTracker.trackBlazeFlowError(for: source, currentStep: currentStep) - view.dismissView() + view?.dismissView() return } - authenticatedRequest(for: initialURL, with: view.cookieJar) { [weak self] (request) in + authenticatedRequest(for: initialURL, with: cookieJar) { [weak self] (request) in guard let weakSelf = self else { return } - weakSelf.view.load(request: request) + weakSelf.view?.load(request: request) BlazeEventsTracker.trackBlazeFlowStarted(for: weakSelf.source) } } func dismissTapped() { - view.dismissView() + view?.dismissView() if isFlowCompleted { + NotificationCenter.default.post(name: .blazeCampaignCreated, object: nil) BlazeEventsTracker.trackBlazeFlowCompleted(for: source, currentStep: currentStep) } else { BlazeEventsTracker.trackBlazeFlowCanceled(for: source, currentStep: currentStep) @@ -93,7 +92,7 @@ class BlazeWebViewModel { func shouldNavigate(to request: URLRequest, with type: WKNavigationType) -> WKNavigationActionPolicy { currentStep = extractCurrentStep(from: request) ?? currentStep updateIsFlowCompleted() - view.reloadNavBar() + view?.reloadNavBar() return linkBehavior.handle(request: request, with: type, externalURLHandler: externalURLHandler) } @@ -148,13 +147,22 @@ class BlazeWebViewModel { } } -extension BlazeWebViewModel: WebKitAuthenticatable { +extension Foundation.Notification.Name { + static let blazeCampaignCreated = Foundation.Notification.Name("BlazeWebFlowBlazeCampaignCreated") +} + +extension BlazeCreateCampaignWebViewModel: WebKitAuthenticatable { var authenticator: RequestAuthenticator? { RequestAuthenticator(blog: blog) } } -private extension BlazeWebViewModel { +private extension BlazeCreateCampaignWebViewModel { + enum Strings { + static let navigationTitle = NSLocalizedString("feature.blaze.title", + value: "Blaze", + comment: "Name of a feature that allows the user to promote their posts.") + } enum Constants { static let baseURLFormat = "https://wordpress.com/advertising/%@" static let blazeSiteURLFormat = "https://wordpress.com/advertising/%@?source=%@" diff --git a/WordPress/Classes/ViewRelated/Blaze/Webview/BlazeFlowCoordinator.swift b/WordPress/Classes/ViewRelated/Blaze/Webview/BlazeFlowCoordinator.swift index 8f6e0619f43a..6fdcfa1a71bb 100644 --- a/WordPress/Classes/ViewRelated/Blaze/Webview/BlazeFlowCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Blaze/Webview/BlazeFlowCoordinator.swift @@ -73,7 +73,12 @@ import UIKit blog: Blog, postID: NSNumber? = nil, delegate: BlazeWebViewControllerDelegate? = nil) { - let blazeViewController = BlazeWebViewController(source: source, blog: blog, postID: postID, delegate: delegate) + let blazeViewController = BlazeWebViewController(delegate: delegate) + let viewModel = BlazeCreateCampaignWebViewModel(source: source, + blog: blog, + postID: postID, + view: blazeViewController) + blazeViewController.viewModel = viewModel let navigationViewController = UINavigationController(rootViewController: blazeViewController) navigationViewController.overrideUserInterfaceStyle = .light navigationViewController.modalPresentationStyle = .formSheet @@ -108,4 +113,27 @@ import UIKit let campaignsViewController = BlazeCampaignsViewController(blog: blog) viewController.navigationController?.pushViewController(campaignsViewController, animated: true) } + + /// Used to display the blaze campaign details web view. + /// - Parameters: + /// - viewController: The view controller where the web view should be presented in. + /// - source: The source that triggers the display of the blaze web view. + /// - blog: `Blog` object representing the site that is being blazed + /// - campaignID: `Int` representing the ID of the campaign whose details is being accessed. + @objc(presentBlazeCampaignDetailsInViewController:source:blog:campaignID:) + static func presentBlazeCampaignDetails(in viewController: UIViewController, + source: BlazeSource, + blog: Blog, + campaignID: Int) { + let blazeViewController = BlazeWebViewController(delegate: nil) + let viewModel = BlazeCampaignDetailsWebViewModel(source: source, + blog: blog, + campaignID: campaignID, + view: blazeViewController) + blazeViewController.viewModel = viewModel + let navigationViewController = UINavigationController(rootViewController: blazeViewController) + navigationViewController.overrideUserInterfaceStyle = .light + navigationViewController.modalPresentationStyle = .formSheet + viewController.present(navigationViewController, animated: true) + } } diff --git a/WordPress/Classes/ViewRelated/Blaze/Webview/BlazeWebViewController.swift b/WordPress/Classes/ViewRelated/Blaze/Webview/BlazeWebViewController.swift index e4352647d868..9481c4d729fe 100644 --- a/WordPress/Classes/ViewRelated/Blaze/Webview/BlazeWebViewController.swift +++ b/WordPress/Classes/ViewRelated/Blaze/Webview/BlazeWebViewController.swift @@ -5,14 +5,34 @@ import WebKit func dismissBlazeWebViewController(_ controller: BlazeWebViewController) } +protocol BlazeWebViewModel { + func startBlazeFlow() + func dismissTapped() + func webViewDidFail(with error: Error) + func isCurrentStepDismissible() -> Bool + func shouldNavigate(to request: URLRequest, with type: WKNavigationType) -> WKNavigationActionPolicy + var isFlowCompleted: Bool { get } + var navigationTitle: String { get } +} + +protocol BlazeWebView: NSObjectProtocol { + func load(request: URLRequest) + func reloadNavBar() + func dismissView() + var cookieJar: CookieJar { get } +} + class BlazeWebViewController: UIViewController, BlazeWebView { + // MARK: Public Variables + + var viewModel: BlazeWebViewModel? + // MARK: Private Variables private weak var delegate: BlazeWebViewControllerDelegate? private let webView: WKWebView - private var viewModel: BlazeWebViewModel? private let progressView = WebProgressView() private var reachabilityObserver: Any? private var currentRequestURL: URL? @@ -32,11 +52,10 @@ class BlazeWebViewController: UIViewController, BlazeWebView { // MARK: Initializers - init(source: BlazeSource, blog: Blog, postID: NSNumber?, delegate: BlazeWebViewControllerDelegate?) { + init(delegate: BlazeWebViewControllerDelegate?) { self.delegate = delegate self.webView = WKWebView(frame: .zero) super.init(nibName: nil, bundle: nil) - viewModel = BlazeWebViewModel(source: source, blog: blog, postID: postID, view: self) } required init?(coder: NSCoder) { @@ -77,7 +96,7 @@ class BlazeWebViewController: UIViewController, BlazeWebView { } private func configureNavBar() { - title = Strings.navigationTitle + title = viewModel?.navigationTitle navigationItem.rightBarButtonItem = dismissButton configureNavBarAppearance() reloadNavBar() @@ -90,9 +109,7 @@ class BlazeWebViewController: UIViewController, BlazeWebView { navigationItem.standardAppearance = appearance navigationItem.compactAppearance = appearance navigationItem.scrollEdgeAppearance = appearance - if #available(iOS 15.0, *) { - navigationItem.compactScrollEdgeAppearance = appearance - } + navigationItem.compactScrollEdgeAppearance = appearance } // MARK: Reachability Helpers @@ -201,9 +218,6 @@ extension BlazeWebViewController: WKNavigationDelegate { private extension BlazeWebViewController { enum Strings { - static let navigationTitle = NSLocalizedString("feature.blaze.title", - value: "Blaze", - comment: "Name of a feature that allows the user to promote their posts.") static let cancelButtonTitle = NSLocalizedString("Cancel", comment: "Cancel. Action.") static let doneButtonTitle = NSLocalizedString("Done", comment: "Done. Action.") } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/BlogDashboardViewController.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/BlogDashboardViewController.swift index 54037f32961e..c5cf0c00a7a5 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/BlogDashboardViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/BlogDashboardViewController.swift @@ -164,30 +164,27 @@ final class BlogDashboardViewController: UIViewController { } private func addQuickStartObserver() { - NotificationCenter.default.addObserver(forName: .QuickStartTourElementChangedNotification, object: nil, queue: nil) { [weak self] notification in - - guard let self = self else { - return - } - - if let info = notification.userInfo, - let element = info[QuickStartTourGuide.notificationElementKey] as? QuickStartTourElement { - - switch element { - case .setupQuickStart: - self.loadCardsFromCache() - self.displayQuickStart() - case .updateQuickStart: - self.loadCardsFromCache() - case .stats, .mediaScreen: - if self.embeddedInScrollView { - self.mySiteScrollView?.scrollToTop(animated: true) - } else { - self.collectionView.scrollToTop(animated: true) - } - default: - break + NotificationCenter.default.addObserver(self, selector: #selector(handleQuickStartTourElementChangedNotification(_:)), name: .QuickStartTourElementChangedNotification, object: nil) + } + + @objc private func handleQuickStartTourElementChangedNotification(_ notification: Foundation.Notification) { + if let info = notification.userInfo, + let element = info[QuickStartTourGuide.notificationElementKey] as? QuickStartTourElement { + + switch element { + case .setupQuickStart: + self.loadCardsFromCache() + self.displayQuickStart() + case .updateQuickStart: + self.loadCardsFromCache() + case .stats, .mediaScreen: + if self.embeddedInScrollView { + self.mySiteScrollView?.scrollToTop(animated: true) + } else { + self.collectionView.scrollToTop(animated: true) } + default: + break } } } @@ -245,7 +242,7 @@ extension BlogDashboardViewController { let isQuickActionSection = viewModel.isQuickActionsSection(sectionIndex) let isMigrationSuccessCardSection = viewModel.isMigrationSuccessCardSection(sectionIndex) let horizontalInset = isQuickActionSection ? 0 : Constants.horizontalSectionInset - let bottomInset = isQuickActionSection || isMigrationSuccessCardSection ? 0 : Constants.verticalSectionInset + let bottomInset = isQuickActionSection || isMigrationSuccessCardSection ? 0 : Constants.bottomSectionInset section.contentInsets = NSDirectionalEdgeInsets(top: Constants.verticalSectionInset, leading: horizontalInset, bottom: bottomInset, @@ -328,24 +325,14 @@ extension BlogDashboardViewController { static let estimatedHeight: CGFloat = 44 static let horizontalSectionInset: CGFloat = 20 static let verticalSectionInset: CGFloat = 20 + static var bottomSectionInset: CGFloat { + // Make room for FAB on iPhone + WPDeviceIdentification.isiPad() ? verticalSectionInset : 86 + } static let cellSpacing: CGFloat = 20 } } -// MARK: - UI Popover Delegate - -/// This view controller may host a `DashboardPromptsCardCell` that requires presenting a `MenuSheetViewController`, -/// a fallback implementation of `UIMenu` for iOS 13. For more details, see the docs on `MenuSheetViewController`. -/// -/// NOTE: This should be removed once we drop support for iOS 13. -/// -extension BlogDashboardViewController: UIPopoverPresentationControllerDelegate { - // Force popover views to be presented as a popover (instead of being presented as a form sheet on iPhones). - public func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { - return .none - } -} - // MARK: - Helper functions private extension Collection where Element == DashboardCardModel { diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/BlazeCampaignStatusView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/BlazeCampaignStatusView.swift index ff564b0c0f3d..54e79092b649 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/BlazeCampaignStatusView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/BlazeCampaignStatusView.swift @@ -43,6 +43,10 @@ struct BlazeCampaignStatusViewModel { let textColor: UIColor let backgroundColor: UIColor + init(campaign: BlazeCampaign) { + self.init(status: campaign.uiStatus) + } + init(status: BlazeCampaign.Status) { self.isHidden = status == .unknown self.title = status.localizedTitle @@ -94,14 +98,17 @@ struct BlazeCampaignStatusViewModel { extension BlazeCampaign.Status { var localizedTitle: String { switch self { + case .created: + // There is no dedicated status for `In Moderation` on the backend. + // The app assumes that the campaign goes into moderation after creation. + return NSLocalizedString("blazeCampaign.status.inmoderation", value: "In Moderation", comment: "Short status description") case .scheduled: return NSLocalizedString("blazeCampaign.status.scheduled", value: "Scheduled", comment: "Short status description") - case .created: - return NSLocalizedString("blazeCampaign.status.created", value: "Created", comment: "Short status description") case .approved: return NSLocalizedString("blazeCampaign.status.approved", value: "Approved", comment: "Short status description") case .processing: - return NSLocalizedString("blazeCampaign.status.inmoderation", value: "In Moderation", comment: "Short status description") + // Should never be returned by `ui_status`. + return NSLocalizedString("blazeCampaign.status.processing", value: "Processing", comment: "Short status description") case .rejected: return NSLocalizedString("blazeCampaign.status.rejected", value: "Rejected", comment: "Short status description") case .active: diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignView.swift index a43fbb422277..db9ba0ac416c 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignView.swift @@ -69,10 +69,12 @@ final class DashboardBlazeCampaignView: UIView { } statsView.isHidden = !viewModel.isShowingStats - if viewModel.isShowingStats { statsView.arrangedSubviews.forEach { $0.removeFromSuperview() } + if viewModel.isShowingStats { makeStatsViews(for: viewModel).forEach(statsView.addArrangedSubview) } + + enableVoiceOver(with: viewModel) } private func makeStatsViews(for viewModel: BlazeCampaignViewModel) -> [UIView] { @@ -84,6 +86,16 @@ final class DashboardBlazeCampaignView: UIView { return [impressionsView, clicksView] } + + private func enableVoiceOver(with viewModel: BlazeCampaignViewModel) { + var accessibilityLabel = "\(viewModel.title), \(viewModel.status.title)" + if viewModel.isShowingStats { + accessibilityLabel += ", \(Strings.impressions) \(viewModel.impressions), \(Strings.clicks) \(viewModel.clicks)" + } + self.isAccessibilityElement = true + self.accessibilityLabel = accessibilityLabel + self.accessibilityTraits = .allowsDirectInteraction + } } private extension DashboardBlazeCampaignView { @@ -103,10 +115,10 @@ struct BlazeCampaignViewModel { let impressions: Int let clicks: Int let budget: String - var status: BlazeCampaignStatusViewModel { .init(status: campaign.status) } + var status: BlazeCampaignStatusViewModel { .init(campaign: campaign) } var isShowingStats: Bool { - switch campaign.status { + switch campaign.uiStatus { case .created, .processing, .canceled, .approved, .rejected, .scheduled, .unknown: return false case .active, .finished: diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignsCardView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignsCardView.swift index 683a5036a41c..78f08de1a331 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignsCardView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignsCardView.swift @@ -14,18 +14,25 @@ final class DashboardBlazeCampaignsCardView: UIView { private lazy var createCampaignButton: UIButton = { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false + button.configuration = { + var configuration = UIButton.Configuration.plain() + configuration.attributedTitle = { + var string = AttributedString(Strings.createCampaignButton) + string.font = WPStyleGuide.fontForTextStyle(.callout, fontWeight: .bold) + string.foregroundColor = UIColor.primary + return string + }() + configuration.contentInsets = Constants.createCampaignInsets + return configuration + }() button.contentHorizontalAlignment = .leading - button.setTitle(Strings.createCampaignButton, for: .normal) button.addTarget(self, action: #selector(buttonCreateCampaignTapped), for: .touchUpInside) - button.titleLabel?.font = WPStyleGuide.fontForTextStyle(.callout, fontWeight: .bold) - button.titleLabel?.adjustsFontForContentSizeCategory = true - button.setTitleColor(UIColor.primary, for: .normal) - button.contentEdgeInsets = Constants.createCampaignInsets return button }() private var blog: Blog? private weak var presentingViewController: BlogDashboardViewController? + private var campaign: BlazeCampaign? // MARK: - Initializers @@ -69,6 +76,8 @@ final class DashboardBlazeCampaignsCardView: UIView { frameView.onHeaderTap = { [weak self] in self?.showCampaignList() } + + campaignView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(campainViewTapped))) } private func showCampaignList() { @@ -82,15 +91,21 @@ final class DashboardBlazeCampaignsCardView: UIView { } } + @objc private func campainViewTapped() { + guard let presentingViewController, let blog, let campaign else { return } + BlazeFlowCoordinator.presentBlazeCampaignDetails(in: presentingViewController, source: .dashboardCard, blog: blog, campaignID: campaign.campaignID) + } + @objc private func buttonCreateCampaignTapped() { guard let presentingViewController, let blog else { return } BlazeEventsTracker.trackEntryPointTapped(for: .dashboardCard) BlazeFlowCoordinator.presentBlaze(in: presentingViewController, source: .dashboardCard, blog: blog) } - func configure(blog: Blog, viewController: BlogDashboardViewController?) { + func configure(blog: Blog, viewController: BlogDashboardViewController?, campaign: BlazeCampaign) { self.blog = blog self.presentingViewController = viewController + self.campaign = campaign frameView.addMoreMenu(items: [ UIMenu(options: .displayInline, children: [ @@ -101,7 +116,7 @@ final class DashboardBlazeCampaignsCardView: UIView { ]) ], card: .blaze) - let viewModel = BlazeCampaignViewModel(campaign: mockResponse.campaigns!.first!) + let viewModel = BlazeCampaignViewModel(campaign: campaign) campaignView.configure(with: viewModel, blog: blog) } } @@ -115,39 +130,6 @@ private extension DashboardBlazeCampaignsCardView { enum Constants { static let campaignViewInsets = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) - static let createCampaignInsets = UIEdgeInsets(top: 16, left: 16, bottom: 8, right: 16) + static let createCampaignInsets = NSDirectionalEdgeInsets(top: 16, leading: 16, bottom: 8, trailing: 16) } } - -private let mockResponse: BlazeCampaignsSearchResponse = { - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - decoder.dateDecodingStrategy = .iso8601 - return try! decoder.decode(BlazeCampaignsSearchResponse.self, from: """ - { - "totalItems": 3, - "campaigns": [ - { - "campaign_id": 26916, - "name": "Test Post - don't approve", - "start_date": "2023-06-13T00:00:00Z", - "end_date": "2023-06-01T19:15:45Z", - "status": "finished", - "avatar_url": "https://0.gravatar.com/avatar/614d27bcc21db12e7c49b516b4750387?s=96&d=identicon&r=G", - "budget_cents": 500, - "target_url": "https://alextest9123.wordpress.com/2023/06/01/test-post/", - "content_config": { - "title": "Test Post - don't approve", - "snippet": "Test Post Empty Empty", - "clickUrl": "https://alextest9123.wordpress.com/2023/06/01/test-post/", - "imageUrl": "https://i0.wp.com/public-api.wordpress.com/wpcom/v2/wordads/dsp/api/v1/dsp/creatives/56259/image?w=600&zoom=2" - }, - "campaign_stats": { - "impressions_total": 1000, - "clicks_total": 235 - } - } - ] - } - """.data(using: .utf8)!) -}() diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCardCell.swift index 0be8fe967cef..35a47c94fb16 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCardCell.swift @@ -1,18 +1,40 @@ import UIKit +import WordPressKit final class DashboardBlazeCardCell: DashboardCollectionViewCell { + private var blog: Blog? + private var viewController: BlogDashboardViewController? + private var viewModel: DashboardBlazeCardCellViewModel? + func configure(blog: Blog, viewController: BlogDashboardViewController?, apiResponse: BlogDashboardRemoteEntity?) { + self.blog = blog + self.viewController = viewController + BlazeEventsTracker.trackEntryPointDisplayed(for: .dashboardCard) + } - if RemoteFeatureFlag.blazeManageCampaigns.enabled() { - // Display campaigns - let cardView = DashboardBlazeCampaignsCardView() - cardView.configure(blog: blog, viewController: viewController) - setCardView(cardView, subtype: .campaigns) - } else { - // Display promo + func configure(_ viewModel: DashboardBlazeCardCellViewModel) { + guard viewModel !== self.viewModel else { return } + self.viewModel = viewModel + + viewModel.onRefresh = { [weak self] in + self?.update(with: $0) + self?.viewController?.collectionView.collectionViewLayout.invalidateLayout() + } + update(with: viewModel) + } + + private func update(with viewModel: DashboardBlazeCardCellViewModel) { + guard let blog, let viewController else { return } + + switch viewModel.state { + case .promo: let cardView = DashboardBlazePromoCardView(.make(with: blog, viewController: viewController)) - setCardView(cardView, subtype: .promo) + self.setCardView(cardView, subtype: .promo) + case .campaign(let campaign): + let cardView = DashboardBlazeCampaignsCardView() + cardView.configure(blog: blog, viewController: viewController, campaign: campaign) + self.setCardView(cardView, subtype: .campaigns) } } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCardCellViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCardCellViewModel.swift new file mode 100644 index 000000000000..fecbb50eaac3 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCardCellViewModel.swift @@ -0,0 +1,74 @@ +import Foundation +import WordPressKit + +final class DashboardBlazeCardCellViewModel { + private(set) var state: State = .promo + + private let blog: Blog + private let service: BlazeServiceProtocol + private let store: DashboardBlazeStoreProtocol + private var isRefreshing = false + private let isBlazeCampaignsFlagEnabled: () -> Bool + + enum State { + /// Showing "Promote you content with Blaze" promo card. + case promo + /// Showing the latest Blaze campaign. + case campaign(BlazeCampaign) + } + + var onRefresh: ((DashboardBlazeCardCellViewModel) -> Void)? + + init(blog: Blog, + service: BlazeServiceProtocol = BlazeService(), + store: DashboardBlazeStoreProtocol = BlogDashboardPersistence(), + isBlazeCampaignsFlagEnabled: @escaping () -> Bool = { RemoteFeatureFlag.blazeManageCampaigns.enabled() }) { + self.blog = blog + self.service = service + self.store = store + self.isBlazeCampaignsFlagEnabled = isBlazeCampaignsFlagEnabled + + if isBlazeCampaignsFlagEnabled(), + let blogID = blog.dotComID?.intValue, + let campaign = store.getBlazeCampaign(forBlogID: blogID) { + self.state = .campaign(campaign) + } + + NotificationCenter.default.addObserver(self, selector: #selector(refresh), name: .blazeCampaignCreated, object: nil) + } + + @objc func refresh() { + guard isBlazeCampaignsFlagEnabled() else { + return // Continue showing the default `Promo` card + } + + guard !isRefreshing else { return } + isRefreshing = true + + service.getRecentCampaigns(for: blog, page: 1) { [weak self] in + self?.didRefresh(with: $0) + } + } + + private func didRefresh(with result: Result) { + if case .success(let response) = result { + let campaign = response.campaigns?.first + if let blogID = blog.dotComID?.intValue { + store.setBlazeCampaign(campaign, forBlogID: blogID) + } + if let campaign { + state = .campaign(campaign) + } else { + state = .promo + } + } + + isRefreshing = false + onRefresh?(self) + } +} + +protocol DashboardBlazeStoreProtocol { + func getBlazeCampaign(forBlogID blogID: Int) -> BlazeCampaign? + func setBlazeCampaign(_ campaign: BlazeCampaign?, forBlogID blogID: Int) +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Free to Paid Plans/FreeToPaidPlansCoordinator.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Free to Paid Plans/FreeToPaidPlansCoordinator.swift index 5275cda78594..d7662557ea9c 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Free to Paid Plans/FreeToPaidPlansCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Free to Paid Plans/FreeToPaidPlansCoordinator.swift @@ -13,9 +13,7 @@ import SwiftUI includeSupportButton: false ) - let navigationController = UINavigationController(rootViewController: domainSuggestionsViewController) - - let purchaseCallback = { (domainName: String) in + let purchaseCallback = { (checkoutViewController: CheckoutViewController, domainName: String) in let blogService = BlogService(coreDataStack: ContextManager.shared) blogService.syncBlogAndAllMetadata(blog) { } @@ -25,32 +23,33 @@ import SwiftUI dashboardViewController.reloadCardsLocally() } let viewController = UIHostingController(rootView: resultView) - navigationController.setNavigationBarHidden(true, animated: false) - navigationController.pushViewController(viewController, animated: true) + checkoutViewController.navigationController?.setNavigationBarHidden(true, animated: false) + checkoutViewController.navigationController?.pushViewController(viewController, animated: true) PlansTracker.trackPurchaseResult(source: "plan_selection") } - let planSelected = { (domainName: String, checkoutURL: URL) in + let planSelected = { (planSelectionViewController: PlanSelectionViewController, domainName: String, checkoutURL: URL) in let viewModel = CheckoutViewModel(url: checkoutURL) - let checkoutViewController = CheckoutViewController(viewModel: viewModel, purchaseCallback: { - purchaseCallback(domainName) + let checkoutViewController = CheckoutViewController(viewModel: viewModel, purchaseCallback: { checkoutViewController in + purchaseCallback(checkoutViewController, domainName) }) checkoutViewController.configureSandboxStore { - navigationController.pushViewController(checkoutViewController, animated: true) + planSelectionViewController.navigationController?.pushViewController(checkoutViewController, animated: true) } } - let domainAddedToCart = { (domainName: String) in + let domainAddedToCart = { (domainViewController: RegisterDomainSuggestionsViewController, domainName: String) in guard let viewModel = PlanSelectionViewModel(blog: blog) else { return } let planSelectionViewController = PlanSelectionViewController(viewModel: viewModel) - planSelectionViewController.planSelectedCallback = { checkoutURL in - planSelected(domainName, checkoutURL) + planSelectionViewController.planSelectedCallback = { planSelectionViewController, checkoutURL in + planSelected(planSelectionViewController, domainName, checkoutURL) } - navigationController.pushViewController(planSelectionViewController, animated: true) + domainViewController.navigationController?.pushViewController(planSelectionViewController, animated: true) } domainSuggestionsViewController.domainAddedToCartCallback = domainAddedToCart + let navigationController = UINavigationController(rootViewController: domainSuggestionsViewController) dashboardViewController.present(navigationController, animated: true) } } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Free to Paid Plans/FreeToPaidPlansDashboardCardHelper.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Free to Paid Plans/FreeToPaidPlansDashboardCardHelper.swift index 65193f6a22b8..6f5ffdd459f4 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Free to Paid Plans/FreeToPaidPlansDashboardCardHelper.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Free to Paid Plans/FreeToPaidPlansDashboardCardHelper.swift @@ -12,15 +12,7 @@ import Foundation return false } - /// If this propery is empty, it indicates that domain information is not yet loaded - let hasLoadedDomains = blog.domains?.isEmpty == false - let hasMappedDomain = blog.hasMappedDomain() - let hasFreePlan = !blog.hasPaidPlan - - return blog.supports(.domains) - && hasFreePlan - && hasLoadedDomains - && !hasMappedDomain + return blog.supports(.domains) && !blog.hasPaidPlan } static func hideCard(for blog: Blog?) { diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Pages/DashboardPageCreationCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Pages/DashboardPageCreationCell.swift index d74428f7c97b..f5c332503bba 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Pages/DashboardPageCreationCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Pages/DashboardPageCreationCell.swift @@ -52,21 +52,14 @@ class DashboardPageCreationCell: UITableViewCell { button.addTarget(self, action: #selector(createPageButtonTapped), for: .touchUpInside) let font = WPStyleGuide.fontForTextStyle(.callout, fontWeight: .bold) - if #available(iOS 15.0, *) { - var buttonConfig: UIButton.Configuration = .plain() - buttonConfig.contentInsets = Metrics.createPageButtonContentInsets - buttonConfig.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer({ incoming in - var outgoing = incoming - outgoing.font = font - return outgoing - }) - button.configuration = buttonConfig - } else { - button.titleLabel?.font = font - button.setTitleColor(.jetpackGreen, for: .normal) - button.contentEdgeInsets = Metrics.createPageButtonContentEdgeInsets - button.flipInsetsForRightToLeftLayoutDirection() - } + var buttonConfig: UIButton.Configuration = .plain() + buttonConfig.contentInsets = Metrics.createPageButtonContentInsets + buttonConfig.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer({ incoming in + var outgoing = incoming + outgoing.font = font + return outgoing + }) + button.configuration = buttonConfig return button }() diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/DashboardPostsListCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/DashboardPostsListCardCell.swift index 3fbacf3395c3..b42cfa44e9e6 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/DashboardPostsListCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/DashboardPostsListCardCell.swift @@ -90,16 +90,17 @@ extension DashboardPostsListCardCell { switch cardType { case .draftPosts: + addDraftsContextMenu(card: cardType, blog: blog) configureDraftsList(blog: blog) status = .draft case .scheduledPosts: + addScheduledContextMenu(card: cardType, blog: blog) configureScheduledList(blog: blog) status = .scheduled default: assertionFailure("Cell used with wrong card type") return } - addContextMenu(card: cardType, blog: blog) viewModel = PostsCardViewModel(blog: blog, status: status, view: self) viewModel?.viewDidLoad() @@ -107,16 +108,46 @@ extension DashboardPostsListCardCell { viewModel?.refresh() } - private func addContextMenu(card: DashboardCard, blog: Blog) { + private func addDraftsContextMenu(card: DashboardCard, blog: Blog) { guard FeatureFlag.personalizeHomeTab.enabled else { return } frameView.addMoreMenu(items: [ - BlogDashboardHelpers.makeHideCardAction(for: card, blog: blog) + UIMenu(options: .displayInline, children: [ + makeDraftsListMenuAction() + ]), + UIMenu(options: .displayInline, children: [ + BlogDashboardHelpers.makeHideCardAction(for: card, blog: blog) + ]) ], card: card) } + private func addScheduledContextMenu(card: DashboardCard, blog: Blog) { + guard FeatureFlag.personalizeHomeTab.enabled else { return } + + frameView.addMoreMenu(items: [ + UIMenu(options: .displayInline, children: [ + makeScheduledListMenuAction() + ]), + UIMenu(options: .displayInline, children: [ + BlogDashboardHelpers.makeHideCardAction(for: card, blog: blog) + ]) + ], card: card) + } + + private func makeDraftsListMenuAction() -> UIAction { + UIAction(title: Strings.viewAllDrafts, image: UIImage(systemName: "square.and.pencil")) { [weak self] _ in + self?.presentPostList(with: .draft) + } + } + + private func makeScheduledListMenuAction() -> UIAction { + UIAction(title: Strings.viewAllScheduledPosts, image: UIImage(systemName: "calendar")) { [weak self] _ in + self?.presentPostList(with: .scheduled) + } + } + private func configureDraftsList(blog: Blog) { - frameView.setTitle(Strings.draftsTitle, titleHint: Strings.draftsTitleHint) + frameView.setTitle(Strings.draftsTitle) frameView.onHeaderTap = { [weak self] in self?.presentPostList(with: .draft) } @@ -185,8 +216,9 @@ private extension DashboardPostsListCardCell { private enum Strings { static let draftsTitle = NSLocalizedString("my-sites.drafts.card.title", value: "Work on a draft post", comment: "Title for the card displaying draft posts.") - static let draftsTitleHint = NSLocalizedString("my-sites.drafts.card.title.hint", value: "draft post", comment: "The part in the title that should be highlighted.") static let scheduledTitle = NSLocalizedString("Upcoming scheduled posts", comment: "Title for the card displaying upcoming scheduled posts.") + static let viewAllDrafts = NSLocalizedString("my-sites.drafts.card.viewAllDrafts", value: "View all drafts", comment: "Title for the View all drafts button in the More menu") + static let viewAllScheduledPosts = NSLocalizedString("my-sites.scheduled.card.viewAllScheduledPosts", value: "View all scheduled posts", comment: "Title for the View all scheduled drafts button in the More menu") } enum Constants { diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift index 0b6c87a24543..dec2db18d784 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift @@ -12,23 +12,11 @@ class DashboardPromptsCardCell: UICollectionViewCell, Reusable { frameView.translatesAutoresizingMaskIntoConstraints = false frameView.setTitle(Strings.cardFrameTitle) - // NOTE: Remove the logic when support for iOS 14 is dropped - if #available (iOS 15.0, *) { - // assign an empty closure so the button appears. - frameView.onEllipsisButtonTap = { - BlogDashboardAnalytics.trackContextualMenuAccessed(for: .prompts) - } - frameView.ellipsisButton.showsMenuAsPrimaryAction = true - frameView.ellipsisButton.menu = contextMenu - } else { - // Show a fallback implementation using `MenuSheetViewController`. - // iOS 13 doesn't support showing UIMenu programmatically. - // iOS 14 doesn't support `UIDeferredMenuElement.uncached`. - frameView.onEllipsisButtonTap = { [weak self] in - BlogDashboardAnalytics.trackContextualMenuAccessed(for: .prompts) - self?.showMenuSheet() - } + frameView.onEllipsisButtonTap = { + BlogDashboardAnalytics.trackContextualMenuAccessed(for: .prompts) } + frameView.ellipsisButton.showsMenuAsPrimaryAction = true + frameView.ellipsisButton.menu = contextMenu return frameView }() @@ -313,7 +301,6 @@ class DashboardPromptsCardCell: UICollectionViewCell, Reusable { return [defaultItems, [.learnMore(learnMoreTapped)]] } - @available(iOS 15.0, *) private var contextMenu: UIMenu { return .init(title: String(), options: .displayInline, children: contextMenuItems.map { menuSection in UIMenu(title: String(), options: .displayInline, children: [ @@ -526,27 +513,6 @@ private extension DashboardPromptsCardCell { BloggingPromptsIntroductionPresenter(interactionType: .actionable(blog: blog)).present(from: presenterViewController) } - // Fallback context menu implementation for iOS 13. - func showMenuSheet() { - guard let presenterViewController = presenterViewController else { - return - } - WPAnalytics.track(.promptsDashboardCardMenu) - - let menuViewController = MenuSheetViewController(items: contextMenuItems.map { menuSection in - menuSection.map { $0.toMenuSheetItem } - }) - - menuViewController.modalPresentationStyle = .popover - if let popoverPresentationController = menuViewController.popoverPresentationController { - popoverPresentationController.delegate = presenterViewController - popoverPresentationController.sourceView = cardFrameView.ellipsisButton - popoverPresentationController.sourceRect = cardFrameView.ellipsisButton.bounds - } - - presenterViewController.present(menuViewController, animated: true) - } - // MARK: Constants struct Strings { @@ -648,21 +614,6 @@ private extension DashboardPromptsCardCell { } } } - - var toMenuSheetItem: MenuSheetViewController.MenuItem { - switch self { - case .viewMore(let handler), - .skip(let handler), - .remove(let handler), - .learnMore(let handler): - return MenuSheetViewController.MenuItem( - title: title, - image: image, - destructive: menuAttributes.contains(.destructive), - handler: handler - ) - } - } } } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift index 1657074621ec..6fef2411bba3 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift @@ -57,10 +57,6 @@ final class DashboardQuickActionsCardCell: UICollectionViewCell, Reusable { required init?(coder: NSCoder) { fatalError("Not implemented") } - - deinit { - stopObservingQuickStart() - } } // MARK: - Button Actions @@ -142,35 +138,34 @@ extension DashboardQuickActionsCardCell { } private func startObservingQuickStart() { - NotificationCenter.default.addObserver(forName: .QuickStartTourElementChangedNotification, object: nil, queue: nil) { [weak self] notification in - - if let info = notification.userInfo, - let element = info[QuickStartTourGuide.notificationElementKey] as? QuickStartTourElement { - - switch element { - case .stats: - guard QuickStartTourGuide.shared.entryPointForCurrentTour == .blogDashboard else { - return - } - - self?.autoScrollToStatsButton() - case .mediaScreen: - guard QuickStartTourGuide.shared.entryPointForCurrentTour == .blogDashboard else { - return - } - - self?.autoScrollToMediaButton() - default: - break - } - self?.statsButton.shouldShowSpotlight = element == .stats - self?.mediaButton.shouldShowSpotlight = element == .mediaScreen - } - } + NotificationCenter.default.addObserver(self, selector: #selector(handleQuickStartTourElementChangedNotification(_:)), name: .QuickStartTourElementChangedNotification, object: nil) } - private func stopObservingQuickStart() { - NotificationCenter.default.removeObserver(self) + @objc private func handleQuickStartTourElementChangedNotification(_ notification: Foundation.Notification) { + guard let info = notification.userInfo, + let element = info[QuickStartTourGuide.notificationElementKey] as? QuickStartTourElement + else { + return + } + + switch element { + case .stats: + guard QuickStartTourGuide.shared.entryPointForCurrentTour == .blogDashboard else { + return + } + + autoScrollToStatsButton() + case .mediaScreen: + guard QuickStartTourGuide.shared.entryPointForCurrentTour == .blogDashboard else { + return + } + + autoScrollToMediaButton() + default: + break + } + statsButton.shouldShowSpotlight = element == .stats + mediaButton.shouldShowSpotlight = element == .mediaScreen } private func autoScrollToStatsButton() { diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Start/NewQuickStartChecklistView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Start/NewQuickStartChecklistView.swift index 0a340b22366b..d39586676348 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Start/NewQuickStartChecklistView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Start/NewQuickStartChecklistView.swift @@ -112,10 +112,6 @@ final class NewQuickStartChecklistView: UIView, QuickStartChecklistConfigurable fatalError("Not implemented") } - deinit { - stopObservingQuickStart() - } - // MARK: - Trait Collection override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -197,20 +193,18 @@ extension NewQuickStartChecklistView { } private func startObservingQuickStart() { - NotificationCenter.default.addObserver(forName: .QuickStartTourElementChangedNotification, object: nil, queue: nil) { [weak self] notification in - - guard let userInfo = notification.userInfo, - let element = userInfo[QuickStartTourGuide.notificationElementKey] as? QuickStartTourElement, - element == .tourCompleted else { - return - } + NotificationCenter.default.addObserver(self, selector: #selector(handleQuickStartTourElementChangedNotification(_:)), name: .QuickStartTourElementChangedNotification, object: nil) + } - self?.updateViews() + @objc private func handleQuickStartTourElementChangedNotification(_ notification: Foundation.Notification) { + guard let userInfo = notification.userInfo, + let element = userInfo[QuickStartTourGuide.notificationElementKey] as? QuickStartTourElement, + element == .tourCompleted + else { + return } - } - private func stopObservingQuickStart() { - NotificationCenter.default.removeObserver(self) + updateViews() } @objc private func didTap() { diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Start/QuickStartChecklistView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Start/QuickStartChecklistView.swift index 300c7477e2ec..4f8002793e44 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Start/QuickStartChecklistView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Start/QuickStartChecklistView.swift @@ -80,10 +80,6 @@ final class QuickStartChecklistView: UIView, QuickStartChecklistConfigurable { fatalError("Not implemented") } - deinit { - stopObservingQuickStart() - } - func configure(collection: QuickStartToursCollection, blog: Blog) { self.tours = collection.tours self.blog = blog @@ -134,20 +130,18 @@ extension QuickStartChecklistView { } private func startObservingQuickStart() { - NotificationCenter.default.addObserver(forName: .QuickStartTourElementChangedNotification, object: nil, queue: nil) { [weak self] notification in - - guard let userInfo = notification.userInfo, - let element = userInfo[QuickStartTourGuide.notificationElementKey] as? QuickStartTourElement, - element == .tourCompleted else { - return - } + NotificationCenter.default.addObserver(self, selector: #selector(handleQuickStartTourElementChangedNotification(_:)), name: .QuickStartTourElementChangedNotification, object: nil) + } - self?.updateViews() + @objc private func handleQuickStartTourElementChangedNotification(_ notification: Foundation.Notification) { + guard let userInfo = notification.userInfo, + let element = userInfo[QuickStartTourGuide.notificationElementKey] as? QuickStartTourElement, + element == .tourCompleted + else { + return } - } - private func stopObservingQuickStart() { - NotificationCenter.default.removeObserver(self) + updateViews() } @objc private func didTap() { diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Stats/DashboardStatsCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Stats/DashboardStatsCardCell.swift index 10745d10adf9..31e10f8870c7 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Stats/DashboardStatsCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Stats/DashboardStatsCardCell.swift @@ -39,7 +39,7 @@ class DashboardStatsCardCell: UICollectionViewCell, Reusable { } private func addSubviews() { - frameView.setTitle(Strings.statsTitle, titleHint: Strings.statsTitleHint) + frameView.setTitle(Strings.statsTitle) let statsStackview = DashboardStatsStackView() frameView.add(subview: statsStackview) @@ -76,7 +76,12 @@ extension DashboardStatsCardCell: BlogDashboardCardConfigurable { if FeatureFlag.personalizeHomeTab.enabled { frameView.addMoreMenu(items: [ - BlogDashboardHelpers.makeHideCardAction(for: .todaysStats, blog: blog) + UIMenu(options: .displayInline, children: [ + makeShowStatsMenuAction(for: blog, in: viewController) + ]), + UIMenu(options: .displayInline, children: [ + BlogDashboardHelpers.makeHideCardAction(for: .todaysStats, blog: blog) + ]) ], card: .todaysStats) } @@ -95,6 +100,12 @@ extension DashboardStatsCardCell: BlogDashboardCardConfigurable { blog: blog) } + private func makeShowStatsMenuAction(for blog: Blog, in viewController: UIViewController) -> UIAction { + UIAction(title: Strings.viewStats, image: UIImage(systemName: "chart.bar.xaxis")) { [weak self] _ in + self?.showStats(for: blog, from: viewController) + } + } + private func showStats(for blog: Blog, from sourceController: UIViewController) { WPAnalytics.track(.dashboardCardItemTapped, properties: ["type": DashboardCard.todaysStats.rawValue], @@ -133,9 +144,9 @@ private extension DashboardStatsCardCell { enum Strings { static let statsTitle = NSLocalizedString("my-sites.stats.card.title", value: "Today's Stats", comment: "Title for the card displaying today's stats.") - static let statsTitleHint = NSLocalizedString("my-sites.stats.card.title.hint", value: "Stats", comment: "The part of the title that needs to be emphasized") static let nudgeButtonTitle = NSLocalizedString("Interested in building your audience? Check out our top tips", comment: "Title for a button that opens up the 'Getting More Views and Traffic' support page when tapped.") static let nudgeButtonHint = NSLocalizedString("top tips", comment: "The part of the nudge title that should be emphasized, this content needs to match a string in 'If you want to try get more...'") + static let viewStats = NSLocalizedString("dashboardCard.stats.viewStats", value: "View stats", comment: "Title for the View stats button in the More menu") } enum Constants { diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Service/BlogDashboardPersistence.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Service/BlogDashboardPersistence.swift index 950911e7c18e..1b9712fb1480 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Service/BlogDashboardPersistence.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Service/BlogDashboardPersistence.swift @@ -27,3 +27,34 @@ class BlogDashboardPersistence { "cards_\(blogID).json" } } + +extension BlogDashboardPersistence: DashboardBlazeStoreProtocol { + func getBlazeCampaign(forBlogID blogID: Int) -> BlazeCampaign? { + do { + let url = try makeBlazeCampaignURL(for: blogID) + let data = try Data(contentsOf: url) + return try JSONDecoder().decode(BlazeCampaign.self, from: data) + } catch { + DDLogError("Failed to retrieve blaze campaign: \(error)") + return nil + } + } + + func setBlazeCampaign(_ campaign: BlazeCampaign?, forBlogID blogID: Int) { + do { + let url = try makeBlazeCampaignURL(for: blogID) + if let campaign { + try JSONEncoder().encode(campaign).write(to: url) + } else { + try? FileManager.default.removeItem(at: url) + } + } catch { + DDLogError("Failed to store blaze campaign: \(error)") + } + } + + private func makeBlazeCampaignURL(for blogID: Int) throws -> URL { + try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false) + .appendingPathComponent("recent_blaze_campaign_\(blogID).json", isDirectory: false) + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift index ca49a2f3e0e5..9f7335f6ce2b 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift @@ -67,6 +67,7 @@ class BlogDashboardViewModel { cellConfigurable.row = indexPath.row cellConfigurable.configure(blog: blog, viewController: viewController, apiResponse: cardModel.apiResponse) } + (cell as? DashboardBlazeCardCell)?.configure(blazeViewModel) return cell case .migrationSuccess: let cellType = DashboardMigrationSuccessCell.self @@ -74,14 +75,16 @@ class BlogDashboardViewModel { cell?.configure(with: viewController) return cell } - } }() + private let blazeViewModel: DashboardBlazeCardCellViewModel + init(viewController: BlogDashboardViewController, managedObjectContext: NSManagedObjectContext = ContextManager.shared.mainContext, blog: Blog) { self.viewController = viewController self.managedObjectContext = managedObjectContext self.blog = blog + self.blazeViewModel = DashboardBlazeCardCellViewModel(blog: blog) registerNotifications() } @@ -105,6 +108,8 @@ class BlogDashboardViewModel { completion?(cards) }) + + blazeViewModel.refresh() } @objc func loadCardsFromCache() { diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+DomainCredit.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+DomainCredit.swift index d16a1f98bdc0..023947bda2c6 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+DomainCredit.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+DomainCredit.swift @@ -22,7 +22,7 @@ extension BlogDetailsViewController { @objc func showDomainCreditRedemption() { let controller = RegisterDomainSuggestionsViewController - .instance(site: blog, domainSelectionType: .registerWithPaidPlan, domainPurchasedCallback: { [weak self] domain in + .instance(site: blog, domainSelectionType: .registerWithPaidPlan, domainPurchasedCallback: { [weak self] _, domain in WPAnalytics.track(.domainCreditRedemptionSuccess) self?.presentDomainCreditRedemptionSuccess(domain: domain) }) diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlow.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlow.swift index 334c7e5dcbd3..40ae94d7981d 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlow.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlow.swift @@ -11,7 +11,7 @@ class BloggingRemindersFlow { delegate: BloggingRemindersFlowDelegate? = nil, onDismiss: DismissClosure? = nil) { - guard Feature.enabled(.bloggingReminders) && JetpackNotificationMigrationService.shared.shouldPresentNotifications() else { + guard blog.areBloggingRemindersAllowed() else { return } diff --git a/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController+QuickStart.swift b/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController+QuickStart.swift index be911fbddf9c..d07ae63326a2 100644 --- a/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController+QuickStart.swift +++ b/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController+QuickStart.swift @@ -3,28 +3,31 @@ import UIKit extension MySiteViewController { func startObservingQuickStart() { - NotificationCenter.default.addObserver(forName: .QuickStartTourElementChangedNotification, object: nil, queue: nil) { [weak self] (notification) in + NotificationCenter.default.addObserver(self, selector: #selector(handleQuickStartTourElementChangedNotification(_:)), name: .QuickStartTourElementChangedNotification, object: nil) + } - if let info = notification.userInfo, - let element = info[QuickStartTourGuide.notificationElementKey] as? QuickStartTourElement { + @objc private func handleQuickStartTourElementChangedNotification(_ notification: Foundation.Notification) { + guard let info = notification.userInfo, + let element = info[QuickStartTourGuide.notificationElementKey] as? QuickStartTourElement + else { + return + } - self?.siteMenuSpotlightIsShown = element == .siteMenu + siteMenuSpotlightIsShown = element == .siteMenu - switch element { - case .noSuchElement, .newpost: - self?.additionalSafeAreaInsets = .zero + switch element { + case .noSuchElement, .newpost: + additionalSafeAreaInsets = .zero - case .siteIcon, .siteTitle, .viewSite: - self?.scrollView.scrollToTop(animated: true) - fallthrough + case .siteIcon, .siteTitle, .viewSite: + scrollView.scrollToTop(animated: true) + fallthrough - case .siteMenu, .pages, .sharing, .stats, .readerTab, .notifications, .mediaScreen: - self?.additionalSafeAreaInsets = Constants.quickStartNoticeInsets + case .siteMenu, .pages, .sharing, .stats, .readerTab, .notifications, .mediaScreen: + additionalSafeAreaInsets = Constants.quickStartNoticeInsets - default: - break - } - } + default: + break } } diff --git a/WordPress/Classes/ViewRelated/Blog/QuickStartChecklistViewController.swift b/WordPress/Classes/ViewRelated/Blog/QuickStartChecklistViewController.swift index 2120abfc1090..dd0b957235fb 100644 --- a/WordPress/Classes/ViewRelated/Blog/QuickStartChecklistViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/QuickStartChecklistViewController.swift @@ -107,9 +107,7 @@ private extension QuickStartChecklistViewController { navigationItem.standardAppearance = appearance navigationItem.compactAppearance = appearance navigationItem.scrollEdgeAppearance = appearance - if #available(iOS 15.0, *) { - navigationItem.compactScrollEdgeAppearance = appearance - } + navigationItem.compactScrollEdgeAppearance = appearance } func startObservingForQuickStart() { diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+QuickStart.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+QuickStart.swift index 528b4d52a487..b828d41e7085 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+QuickStart.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+QuickStart.swift @@ -10,19 +10,17 @@ extension SitePickerViewController { } func startObservingQuickStart() { - NotificationCenter.default.addObserver(forName: .QuickStartTourElementChangedNotification, object: nil, queue: nil) { [weak self] (notification) in - guard self?.blog.managedObjectContext != nil else { - return - } - - self?.blogDetailHeaderView.toggleSpotlightOnSiteTitle() - self?.blogDetailHeaderView.toggleSpotlightOnSiteUrl() - self?.blogDetailHeaderView.refreshIconImage() - } + NotificationCenter.default.addObserver(self, selector: #selector(handleQuickStartTourElementChangedNotification(_:)), name: .QuickStartTourElementChangedNotification, object: nil) } - func stopObservingQuickStart() { - NotificationCenter.default.removeObserver(self) + @objc private func handleQuickStartTourElementChangedNotification(_ notification: Foundation.Notification) { + guard blog.managedObjectContext != nil else { + return + } + + blogDetailHeaderView.toggleSpotlightOnSiteTitle() + blogDetailHeaderView.toggleSpotlightOnSiteUrl() + blogDetailHeaderView.refreshIconImage() } func startAlertTimer() { diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift index bc41dc10ac05..381d5cbc695c 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift @@ -49,10 +49,6 @@ final class SitePickerViewController: UIViewController { startObservingTitleChanges() } - deinit { - stopObservingQuickStart() - } - private func setupHeaderView() { blogDetailHeaderView.blog = blog blogDetailHeaderView.delegate = self diff --git a/WordPress/Classes/ViewRelated/Blog/Site Settings/Related Posts/RelatedPostsPreviewTableViewCell.h b/WordPress/Classes/ViewRelated/Blog/Site Settings/Related Posts/RelatedPostsPreviewTableViewCell.h deleted file mode 100644 index 914725e40f5c..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Site Settings/Related Posts/RelatedPostsPreviewTableViewCell.h +++ /dev/null @@ -1,11 +0,0 @@ -#import -#import - -@interface RelatedPostsPreviewTableViewCell : WPTableViewCell - -@property (nonatomic, assign) BOOL enabledHeader; -@property (nonatomic, assign) BOOL enabledImages; - -- (CGFloat)heightForWidth:(CGFloat)availableWidth; - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Site Settings/Related Posts/RelatedPostsPreviewTableViewCell.m b/WordPress/Classes/ViewRelated/Blog/Site Settings/Related Posts/RelatedPostsPreviewTableViewCell.m deleted file mode 100644 index 83729754bd1b..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Site Settings/Related Posts/RelatedPostsPreviewTableViewCell.m +++ /dev/null @@ -1,211 +0,0 @@ -#import "RelatedPostsPreviewTableViewCell.h" -#import -#import -#import "WordPress-Swift.h" - -static CGFloat HorizontalMargin = 0.0; -static CGFloat VerticalMargin = 5.0; -static CGFloat ImageHeight = 96.0; - -@interface RelatedPostsPreview : NSObject - -@property (nonatomic, copy) NSString *title; -@property (nonatomic, copy) NSString *site; -@property (nonatomic, copy) NSString *imageName; - -@property (nonatomic, strong) UILabel *titleLabel; -@property (nonatomic, strong) UILabel *siteLabel; -@property (nonatomic, strong) UIImageView *imageView; - -- (instancetype)initWithTitle:(NSString *)title site:(NSString *)site imageName:(NSString *)imageName; - -@end - -@implementation RelatedPostsPreview - -- (instancetype)initWithTitle:(NSString *)title site:(NSString *)site imageName:(NSString *)imageName -{ - self = [super init]; - if (self) { - _title = title; - _site = site; - _imageName = imageName; - } - - return self; -} - -- (UILabel *)titleLabel -{ - if (!_titleLabel) { - _titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; - _titleLabel.textColor = [UIColor murielNeutral70]; - _titleLabel.font = [WPFontManager systemSemiBoldFontOfSize:14.0]; - _titleLabel.numberOfLines = 0; - } - _titleLabel.text = self.title; - return _titleLabel; -} - -- (UILabel *)siteLabel -{ - if (!_siteLabel) { - _siteLabel = [[UILabel alloc] initWithFrame:CGRectZero]; - _siteLabel.textColor = [UIColor murielNeutral]; - _siteLabel.font = [WPFontManager systemItalicFontOfSize:11.0]; - _siteLabel.numberOfLines = 0; - } - _siteLabel.text = self.site; - return _siteLabel; -} - -- (UIImageView *)imageView -{ - if (!_imageView){ - _imageView = [[UIImageView alloc] init]; - [_imageView setContentMode:UIViewContentModeScaleAspectFill]; - [_imageView setClipsToBounds:YES]; - } - _imageView.image = [UIImage imageNamed:self.imageName]; - return _imageView; -} - -@end - -// Temporary container view for helping to follow readable margins until we can properly adopt this view for readability. -// Brent C. Jul/22/2016 -@protocol RelatedPostsPreviewReadableContentViewDelegate; - -@interface RelatedPostsPreviewReadableContentView : UIView -@property (nonatomic, weak) id delegate; -@end - -@protocol RelatedPostsPreviewReadableContentViewDelegate -- (void)postPreviewReadableContentViewDidLayoutSubviews:(RelatedPostsPreviewReadableContentView *)readableContentView; -@end - -@interface RelatedPostsPreviewTableViewCell() - -@property (nonatomic, strong) UIView *readableContentView; -@property (nonatomic, strong) UILabel *headerLabel; -@property (nonatomic, strong) NSArray *previewPosts; - -@end; - -@implementation RelatedPostsPreviewTableViewCell - -- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier -{ - self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier]; - if (self) { - - RelatedPostsPreviewReadableContentView *readableContentView = [[RelatedPostsPreviewReadableContentView alloc] init]; - readableContentView.delegate = self; - readableContentView.translatesAutoresizingMaskIntoConstraints = NO; - readableContentView.backgroundColor = [UIColor clearColor]; - [self.contentView addSubview:readableContentView]; - - UILayoutGuide *readableGuide = self.contentView.readableContentGuide; - [NSLayoutConstraint activateConstraints:@[ - [readableContentView.leadingAnchor constraintEqualToAnchor:readableGuide.leadingAnchor], - [readableContentView.trailingAnchor constraintEqualToAnchor:readableGuide.trailingAnchor], - [readableContentView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor], - [readableContentView.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor] - ]]; - _readableContentView = readableContentView; - - _enabledHeader = YES; - _enabledImages = YES; - _headerLabel = [[UILabel alloc] initWithFrame:CGRectZero]; - _headerLabel.text = NSLocalizedString(@"Related Posts", @"Label for Related Post header preview"); - _headerLabel.textColor = [UIColor murielNeutral]; - _headerLabel.font = [WPFontManager systemSemiBoldFontOfSize:11.0]; - [readableContentView addSubview:_headerLabel]; - - RelatedPostsPreview *preview1 = [[RelatedPostsPreview alloc] initWithTitle:NSLocalizedString(@"Big iPhone/iPad Update Now Available", @"Text for related post cell preview") - site:NSLocalizedString(@"in \"Mobile\"", @"Text for related post cell preview") - imageName:@"relatedPostsPreview1"]; - RelatedPostsPreview *preview2 = [[RelatedPostsPreview alloc] initWithTitle:NSLocalizedString(@"The WordPress for Android App Gets a Big Facelift", @"Text for related post cell preview") - site:NSLocalizedString(@"in \"Apps\"", @"Text for related post cell preview") - imageName:@"relatedPostsPreview2"]; - RelatedPostsPreview *preview3 = [[RelatedPostsPreview alloc] initWithTitle:NSLocalizedString(@"Upgrade Focus: VideoPress For Weddings", @"Text for related post cell preview") - site:NSLocalizedString(@"in \"Upgrade\"", @"Text for related post cell preview") - imageName:@"relatedPostsPreview3"]; - - _previewPosts = @[preview1, preview2, preview3]; - - for (RelatedPostsPreview *relatedPostPreview in _previewPosts) { - [readableContentView addSubview:relatedPostPreview.imageView]; - [readableContentView addSubview:relatedPostPreview.titleLabel]; - [readableContentView addSubview:relatedPostPreview.siteLabel]; - } - } - return self; -} - -- (CGFloat)heightForWidth:(CGFloat)availableWidth -{ - CGFloat width = self.readableContentView.frame.size.width - (2 * HorizontalMargin); - CGFloat height = 0; - CGSize sizeRestriction = CGSizeMake(width, CGFLOAT_MAX); - if (self.enabledHeader) { - height += ceilf([self.headerLabel sizeThatFits:sizeRestriction].height) + VerticalMargin; - } - for (RelatedPostsPreview *relatedPostPreview in _previewPosts) { - if (self.enabledImages) { - height += ImageHeight + (2 * VerticalMargin); - } - height += ceilf([relatedPostPreview.titleLabel sizeThatFits:sizeRestriction].height) + VerticalMargin; - height += ceilf([relatedPostPreview.siteLabel sizeThatFits:sizeRestriction].height); - } - height += VerticalMargin; - - return height; -} - -#pragma mark - RelatedPostsPreviewReadableContentViewDelegate - -- (void)postPreviewReadableContentViewDidLayoutSubviews:(RelatedPostsPreviewReadableContentView *)readableContentView -{ - CGFloat width = self.readableContentView.frame.size.width - (2 * HorizontalMargin); - CGFloat height = 0; - CGSize sizeRestriction = CGSizeMake(width, CGFLOAT_MAX); - if (self.enabledHeader) { - height = ceilf([self.headerLabel sizeThatFits:sizeRestriction].height); - self.headerLabel.frame = CGRectMake(HorizontalMargin, VerticalMargin, width, height); - } else { - self.headerLabel.frame = CGRectZero; - } - UIView *referenceView = self.headerLabel; - for (RelatedPostsPreview *relatedPostPreview in _previewPosts) { - if (self.enabledImages) { - relatedPostPreview.imageView.frame = CGRectMake(HorizontalMargin, CGRectGetMaxY(referenceView.frame) + (2 * VerticalMargin), width, ImageHeight); - relatedPostPreview.imageView.hidden = NO; - referenceView = relatedPostPreview.imageView; - } else { - relatedPostPreview.imageView.frame = CGRectZero; - relatedPostPreview.imageView.hidden = YES; - } - - height = ceilf([relatedPostPreview.titleLabel sizeThatFits:sizeRestriction].height); - relatedPostPreview.titleLabel.frame = CGRectMake(HorizontalMargin, CGRectGetMaxY(referenceView.frame) + VerticalMargin, width, height); - referenceView = relatedPostPreview.titleLabel; - - height = ceilf([relatedPostPreview.siteLabel sizeThatFits:sizeRestriction].height); - relatedPostPreview.siteLabel.frame = CGRectMake(HorizontalMargin, CGRectGetMaxY(referenceView.frame), width, height); - referenceView = relatedPostPreview.siteLabel; - } -} - -@end - -@implementation RelatedPostsPreviewReadableContentView - -- (void)layoutSubviews -{ - [super layoutSubviews]; - - [self.delegate postPreviewReadableContentViewDidLayoutSubviews:self]; -} - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Site Settings/Related Posts/RelatedPostsSettingsViewController.h b/WordPress/Classes/ViewRelated/Blog/Site Settings/Related Posts/RelatedPostsSettingsViewController.h deleted file mode 100644 index 15799d188360..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Site Settings/Related Posts/RelatedPostsSettingsViewController.h +++ /dev/null @@ -1,9 +0,0 @@ -#import - -@class Blog; - -@interface RelatedPostsSettingsViewController : UITableViewController - -- (instancetype)initWithBlog:(Blog *)blog; - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Site Settings/Related Posts/RelatedPostsSettingsViewController.m b/WordPress/Classes/ViewRelated/Blog/Site Settings/Related Posts/RelatedPostsSettingsViewController.m deleted file mode 100644 index a870160e916b..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Site Settings/Related Posts/RelatedPostsSettingsViewController.m +++ /dev/null @@ -1,264 +0,0 @@ -#import "RelatedPostsSettingsViewController.h" - -#import "Blog.h" -#import "BlogService.h" -#import "CoreDataStack.h" -#import "SettingTableViewCell.h" -#import "SVProgressHud+Dismiss.h" -#import "RelatedPostsPreviewTableViewCell.h" - -#import -#import "WordPress-Swift.h" - - -static const CGFloat RelatePostsSettingsCellHeight = 44; - -typedef NS_ENUM(NSInteger, RelatedPostsSettingsSection) { - RelatedPostsSettingsSectionOptions = 0, - RelatedPostsSettingsSectionPreview, - RelatedPostsSettingsSectionCount -}; - -typedef NS_ENUM(NSInteger, RelatedPostsSettingsOptions) { - RelatedPostsSettingsOptionsEnabled = 0, - RelatedPostsSettingsOptionsShowHeader, - RelatedPostsSettingsOptionsShowThumbnails, - RelatedPostsSettingsOptionsCount, -}; - -@interface RelatedPostsSettingsViewController() - -@property (nonatomic, strong) Blog *blog; - -@property (nonatomic, strong) SwitchTableViewCell *relatedPostsEnabledCell; -@property (nonatomic, strong) SwitchTableViewCell *relatedPostsShowHeaderCell; -@property (nonatomic, strong) SwitchTableViewCell *relatedPostsShowThumbnailsCell; - -@property (nonatomic, strong) RelatedPostsPreviewTableViewCell *relatedPostsPreviewTableViewCell; - -@end - -@implementation RelatedPostsSettingsViewController - -- (instancetype)initWithBlog:(Blog *)blog -{ - NSParameterAssert([blog isKindOfClass:[Blog class]]); - self = [super initWithStyle:UITableViewStyleInsetGrouped]; - if (self) { - _blog = blog; - } - return self; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - [WPStyleGuide configureColorsForView:self.view andTableView:self.tableView]; - self.navigationItem.title = NSLocalizedString(@"Related Posts", @"Title for screen that allows configuration of your blog/site related posts settings."); - self.tableView.allowsSelection = NO; -} - - -#pragma mark - Properties - -- (BlogSettings *)settings -{ - return self.blog.settings; -} - - -#pragma mark - UITableViewDataSource Methods - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - if (self.settings.relatedPostsEnabled) { - return RelatedPostsSettingsSectionCount; - } else { - return RelatedPostsSettingsSectionCount-1; - } -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - switch (section) { - case RelatedPostsSettingsSectionOptions:{ - if (self.settings.relatedPostsEnabled) { - return RelatedPostsSettingsOptionsCount; - } else { - return 1; - } - } - break; - case RelatedPostsSettingsSectionPreview: - return 1; - break; - } - return 0; -} - -- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section -{ - switch (section) { - case RelatedPostsSettingsSectionPreview: - return NSLocalizedString(@"Preview", @"Section title for related posts section preview"); - default: - return nil; - } -} - -- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section -{ - switch (section) { - case RelatedPostsSettingsSectionOptions: - return NSLocalizedString(@"Related Posts displays relevant content from your site below your posts", @"Information of what related post are and how they are presented");; - default: - return nil; - } -} - -- (void)tableView:(UITableView *)tableView willDisplayFooterView:(UIView *)view forSection:(NSInteger)section -{ - [WPStyleGuide configureTableViewSectionFooter:view]; -} - -- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { - switch (indexPath.section) { - case RelatedPostsSettingsSectionOptions:{ - return RelatePostsSettingsCellHeight; - } - break; - case RelatedPostsSettingsSectionPreview:{ - return [self.relatedPostsPreviewTableViewCell heightForWidth:tableView.frame.size.width]; - } - break; - case RelatedPostsSettingsSectionCount: - break; - } - return 0; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - RelatedPostsSettingsSection section = (RelatedPostsSettingsSection)indexPath.section; - switch (section) { - case RelatedPostsSettingsSectionOptions:{ - RelatedPostsSettingsOptions row = indexPath.row; - return [self tableView:tableView cellForOptionsRow:row]; - } - break; - case RelatedPostsSettingsSectionPreview:{ - return [self relatedPostsPreviewTableViewCell]; - } - break; - case RelatedPostsSettingsSectionCount: - break; - } - return [UITableViewCell new]; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForOptionsRow:(RelatedPostsSettingsOptions)row -{ - switch (row) { - case RelatedPostsSettingsOptionsEnabled:{ - self.relatedPostsEnabledCell.on = self.settings.relatedPostsEnabled; - return self.relatedPostsEnabledCell; - } - break; - case RelatedPostsSettingsOptionsShowHeader:{ - self.relatedPostsShowHeaderCell.on = self.settings.relatedPostsShowHeadline; - return self.relatedPostsShowHeaderCell; - } - break; - case RelatedPostsSettingsOptionsShowThumbnails:{ - self.relatedPostsShowThumbnailsCell.on = self.settings.relatedPostsShowThumbnails; - return self.relatedPostsShowThumbnailsCell; - } - break; - case RelatedPostsSettingsOptionsCount: - break; - } - return nil; -} - - -#pragma mark - Cell Helpers - -- (SwitchTableViewCell *)relatedPostsEnabledCell -{ - if (!_relatedPostsEnabledCell) { - _relatedPostsEnabledCell = [SwitchTableViewCell new]; - _relatedPostsEnabledCell.name = NSLocalizedString(@"Show Related Posts", @"Label for configuration switch to enable/disable related posts"); - __weak RelatedPostsSettingsViewController *weakSelf = self; - _relatedPostsEnabledCell.onChange = ^(BOOL value){ - [WPAnalytics trackSettingsChange:@"related_posts" fieldName:@"show_related_posts" value:@(value)]; - - [weakSelf updateRelatedPostsSettings:nil]; - }; - } - return _relatedPostsEnabledCell; -} - -- (SwitchTableViewCell *)relatedPostsShowHeaderCell -{ - if (!_relatedPostsShowHeaderCell) { - _relatedPostsShowHeaderCell = [SwitchTableViewCell new]; - _relatedPostsShowHeaderCell.name = NSLocalizedString(@"Show Header", @"Label for configuration switch to show/hide the header for the related posts section"); - __weak RelatedPostsSettingsViewController *weakSelf = self; - _relatedPostsShowHeaderCell.onChange = ^(BOOL value){ - [WPAnalytics trackSettingsChange:@"related_posts" fieldName:@"show_related_posts_header" value:@(value)]; - [weakSelf updateRelatedPostsSettings:nil]; - }; - } - - return _relatedPostsShowHeaderCell; -} - -- (SwitchTableViewCell *)relatedPostsShowThumbnailsCell -{ - if (!_relatedPostsShowThumbnailsCell) { - _relatedPostsShowThumbnailsCell = [SwitchTableViewCell new]; - _relatedPostsShowThumbnailsCell.name = NSLocalizedString(@"Show Images", @"Label for configuration switch to show/hide images thumbnail for the related posts"); - __weak RelatedPostsSettingsViewController *weakSelf = self; - _relatedPostsShowThumbnailsCell.onChange = ^(BOOL value){ - [WPAnalytics trackSettingsChange:@"related_posts" fieldName:@"show_related_posts_thumbnail" value:@(value)]; - - [weakSelf updateRelatedPostsSettings:nil]; - }; - } - - return _relatedPostsShowThumbnailsCell; -} - - -- (RelatedPostsPreviewTableViewCell *)relatedPostsPreviewTableViewCell -{ - if (!_relatedPostsPreviewTableViewCell) { - _relatedPostsPreviewTableViewCell = [[RelatedPostsPreviewTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault - reuseIdentifier:nil]; - } - _relatedPostsPreviewTableViewCell.enabledImages = self.settings.relatedPostsShowThumbnails; - _relatedPostsPreviewTableViewCell.enabledHeader = self.settings.relatedPostsShowHeadline; - - return _relatedPostsPreviewTableViewCell; - -} - -#pragma mark - Helpers - -- (IBAction)updateRelatedPostsSettings:(id)sender -{ - self.settings.relatedPostsEnabled = self.relatedPostsEnabledCell.on; - self.settings.relatedPostsShowHeadline = self.relatedPostsShowHeaderCell.on; - self.settings.relatedPostsShowThumbnails = self.relatedPostsShowThumbnailsCell.on; - - BlogService *blogService = [[BlogService alloc] initWithCoreDataStack:[ContextManager sharedInstance]]; - [blogService updateSettingsForBlog:self.blog success:^{ - [self.tableView reloadData]; - } failure:^(NSError * __unused error) { - [SVProgressHUD showDismissibleErrorWithStatus:NSLocalizedString(@"Settings update failed", @"Message to show when setting save failed")]; - [self.tableView reloadData]; - }]; - [self.tableView reloadData]; -} - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsRelatedPostsView.swift b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsRelatedPostsView.swift new file mode 100644 index 000000000000..cfe48f3ba904 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsRelatedPostsView.swift @@ -0,0 +1,143 @@ +import Foundation +import SwiftUI +import SVProgressHUD +import WordPressShared + +struct RelatedPostsSettingsView: View { + private let blog: Blog + @ObservedObject private var settings: BlogSettings + + var title: String { Strings.title } + + init(blog: Blog) { + self.blog = blog + assert(blog.settings != nil, "Settings should never be nil") + self.settings = blog.settings ?? BlogSettings(context: ContextManager.shared.mainContext) + } + + var body: some View { + Form { + settingsSection + if settings.relatedPostsEnabled { + previewsSection + } + } + .toggleStyle(SwitchToggleStyle(tint: Color(UIColor.jetpackGreen))) + .onChange(of: settings.relatedPostsEnabled) { + save(field: "show_related_posts", value: $0) + } + .onChange(of: settings.relatedPostsShowHeadline) { + save(field: "show_related_posts_header", value: $0) + } + .onChange(of: settings.relatedPostsShowThumbnails) { + save(field: "show_related_posts_thumbnail", value: $0) + } + .navigationTitle(Strings.title) + .navigationBarTitleDisplayMode(.inline) + } + + private var settingsSection: some View { + let section = Section { + Toggle(Strings.showRelatedPosts, isOn: $settings.relatedPostsEnabled) + if settings.relatedPostsEnabled { + Toggle(Strings.showHeader, isOn: $settings.relatedPostsShowHeadline) + Toggle(Strings.showThumbnail, isOn: $settings.relatedPostsShowThumbnails) + } + } footer: { + Text(Strings.optionsFooter) + } + if #available(iOS 15, *) { + return section.tint(Color(UIColor.jetpackGreen)) + } else { + return section.toggleStyle(SwitchToggleStyle(tint: Color(UIColor.jetpackGreen))) + } + } + + private var previewsSection: some View { + Section { + VStack(spacing: settings.relatedPostsShowThumbnails ? 10 : 5) { + if settings.relatedPostsShowHeadline { + Text(Strings.relatedPostsHeader) + .font(.system(size: 11, weight: .semibold)) + .foregroundColor(Color(UIColor.neutral)) + .frame(maxWidth: .infinity, alignment: .leading) + } + ForEach(PreviewViewModel.previews, content: makePreview) + } + } header: { + Text(Strings.previewsHeader) + } + } + + private func makePreview(for viewModel: PreviewViewModel) -> some View { + VStack(spacing: 5) { + if settings.relatedPostsShowThumbnails { + Image(viewModel.imageName) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(height: Constants.imageViewHeight) + .clipped() + } + HStack { + VStack(alignment: .leading, spacing: 0) { + Text(viewModel.title) + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(Color(UIColor.neutral(.shade70))) + Text(viewModel.details) + .font(.system(size: 11).italic()) + .foregroundColor(Color(UIColor.neutral)) + } + Spacer() + } + } + } + + private func save(field: String, value: Any) { + WPAnalytics.trackSettingsChange("related_posts", fieldName: field, value: value) + BlogService(coreDataStack: ContextManager.shared).updateSettings(for: blog, success: nil, failure: { _ in + SVProgressHUD.showDismissibleError(withStatus: Strings.saveFailed) + }) + } +} + +private struct PreviewViewModel: Identifiable { + let id = UUID() + let title: String + let details: String + let imageName: String + + static let previews: [PreviewViewModel] = [ + PreviewViewModel( + title: NSLocalizedString("relatedPostsSettings.preview1.title", value: "Big iPhone/iPad Update Now Available", comment: "Text for related post cell preview"), + details: NSLocalizedString("relatedPostsSettings.preview1.details", value: "in \"Mobile\"", comment: "Text for related post cell preview"), + imageName: "relatedPostsPreview1" + ), + PreviewViewModel( + title: NSLocalizedString("relatedPostsSettings.preview2.title", value: "The WordPress for Android App Gets a Big Facelift", comment: "Text for related post cell preview"), + details: NSLocalizedString("relatedPostsSettings.preview2.details", value: "in \"Apps\"", comment: "Text for related post cell preview"), + imageName: "relatedPostsPreview2" + ), + PreviewViewModel( + title: NSLocalizedString("relatedPostsSettings.preview3.title", value: "Upgrade Focus: VideoPress For Weddings", comment: "Text for related post cell preview"), + details: NSLocalizedString("relatedPostsSettings.preview3.details", value: "in \"Upgrade\"", comment: "Text for related post cell preview"), + imageName: "relatedPostsPreview3" + ) + ] +} + +private extension RelatedPostsSettingsView { + enum Strings { + static let title = NSLocalizedString("relatedPostsSettings.title", value: "Related Posts", comment: "Title for screen that allows configuration of your blog/site related posts settings.") + static let showRelatedPosts = NSLocalizedString("relatedPostsSettings.showRelatedPosts", value: "Show Related Posts", comment: "Label for configuration switch to enable/disable related posts") + static let showHeader = NSLocalizedString("relatedPostsSettings.showHeader", value: "Show Header", comment: "Label for configuration switch to show/hide the header for the related posts section") + static let showThumbnail = NSLocalizedString("relatedPostsSettings.showThumbnail", value: "Show Images", comment: "Label for configuration switch to show/hide images thumbnail for the related posts") + static let optionsFooter = NSLocalizedString("relatedPostsSettings.optionsFooter", value: "Related Posts displays relevant content from your site below your posts", comment: "Information of what related post are and how they are presented") + static let previewsHeader = NSLocalizedString("relatedPostsSettings.previewsHeaders", value: "Preview", comment: "Section title for related posts section preview") + static let relatedPostsHeader = NSLocalizedString("relatedPostsSettings.relatedPostsHeader", value: "Related Posts", comment: "Label for Related Post header preview") + static let saveFailed = NSLocalizedString("relatedPostsSettings.settingsUpdateFailed", value: "Settings update failed", comment: "Message to show when setting save failed") + } + + enum Constants { + static let imageViewHeight: CGFloat = 96 + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController+Swift.swift index 07616be964ef..a6531757e709 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController+Swift.swift @@ -1,6 +1,7 @@ import Foundation import SwiftUI import WordPressFlux +import SwiftUI // This is just a wrapper for the receipts, since Receipt isn't exposed to Obj-C @objc class TimeZoneObserver: NSObject { @@ -145,6 +146,13 @@ extension SiteSettingsViewController { navigationController?.pushViewController(speedUpSiteSettingsViewController, animated: true) } + @objc func showRelatedPostsSettings() { + let view = RelatedPostsSettingsView(blog: blog) + let host = UIHostingController(rootView: view) + host.title = view.title // Make sure title is available before push + navigationController?.pushViewController(host, animated: true) + } + // MARK: Footers @objc(getTrafficSettingsSectionFooterView) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController.m b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController.m index c70232b92e36..9f6b525c38f1 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController.m @@ -6,7 +6,6 @@ #import "NSURL+IDN.h" #import "PostCategory.h" #import "PostCategoryService.h" -#import "RelatedPostsSettingsViewController.h" #import "SettingsSelectionViewController.h" #import "SettingsMultiTextViewController.h" #import "SettingTableViewCell.h" @@ -859,13 +858,6 @@ - (void)showPostFormatSelector [self.navigationController pushViewController:vc animated:YES]; } -- (void)showRelatedPostsSettings -{ - RelatedPostsSettingsViewController *relatedPostsViewController = [[RelatedPostsSettingsViewController alloc] initWithBlog:self.blog]; - - [self.navigationController pushViewController:relatedPostsViewController animated:YES]; -} - - (void)tableView:(UITableView *)tableView didSelectInWritingSectionRow:(NSInteger)row { NSInteger writingRow = [self.writingSectionRows[row] integerValue]; diff --git a/WordPress/Classes/ViewRelated/Blog/WPStyleGuide+Sharing.swift b/WordPress/Classes/ViewRelated/Blog/WPStyleGuide+Sharing.swift index 86d845df4d09..b5a780414504 100644 --- a/WordPress/Classes/ViewRelated/Blog/WPStyleGuide+Sharing.swift +++ b/WordPress/Classes/ViewRelated/Blog/WPStyleGuide+Sharing.swift @@ -69,6 +69,14 @@ extension WPStyleGuide { return image!.withRenderingMode(.alwaysTemplate) } + @objc public class func socialIcon(for service: NSString) -> UIImage { + guard FeatureFlag.jetpackSocial.enabled else { + return iconForService(service) + } + + return UIImage(named: "icon-\(service)") ?? iconForService(service) + } + /// Get's the tint color to use for the specified service when it is connected. /// diff --git a/WordPress/Classes/ViewRelated/Comments/CommentDetailViewController.swift b/WordPress/Classes/ViewRelated/Comments/CommentDetailViewController.swift index 2555daadf2e6..f385541a66b7 100644 --- a/WordPress/Classes/ViewRelated/Comments/CommentDetailViewController.swift +++ b/WordPress/Classes/ViewRelated/Comments/CommentDetailViewController.swift @@ -195,18 +195,6 @@ class CommentDetailViewController: UIViewController, NoResultsViewHost { return appearance }() - /// Convenience property that keeps track of whether the content has scrolled. - private var isContentScrolled: Bool = false { - didSet { - if isContentScrolled == oldValue { - return - } - - // show blurred navigation bar when content is scrolled, or opaque style when the scroll position is at the top. - updateNavigationBarAppearance(isBlurred: isContentScrolled) - } - } - // MARK: Nav Bar Buttons private(set) lazy var editBarButtonItem: UIBarButtonItem = { @@ -380,26 +368,11 @@ private extension CommentDetailViewController { } func configureNavigationBar() { - if #available(iOS 15, *) { - // In iOS 15, to apply visual blur only when content is scrolled, keep the scrollEdgeAppearance unchanged as it applies to ALL navigation bars. - navigationItem.standardAppearance = blurredBarAppearance - } else { - // For iOS 14 and below, scrollEdgeAppearance only affects large title navigation bars. Therefore we need to manually detect if the content - // has been scrolled and change the appearance accordingly. - updateNavigationBarAppearance() - } - + navigationItem.standardAppearance = blurredBarAppearance navigationController?.navigationBar.isTranslucent = true configureNavBarButton() } - /// Updates the navigation bar style based on the `isBlurred` boolean parameter. The intent is to show a visual blur effect when the content is scrolled, - /// but reverts to opaque style when the scroll position is at the top. This method may be called multiple times since it's triggered by the `didSet` - /// property observer on the `isContentScrolled` property. - func updateNavigationBarAppearance(isBlurred: Bool = false) { - navigationItem.standardAppearance = isBlurred ? blurredBarAppearance : opaqueBarAppearance - } - func configureNavBarButton() { var barItems: [UIBarButtonItem] = [] barItems.append(shareBarButtonItem) @@ -1044,17 +1017,6 @@ extension CommentDetailViewController: UITableViewDelegate, UITableViewDataSourc } } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - // keep track of whether the content has scrolled or not. This is used to update the navigation bar style in iOS 14 and below. - // in iOS 15, we don't need to do this since it's been handled automatically; hence the early return. - if #available(iOS 15, *) { - return - } - - isContentScrolled = scrollView.contentOffset.y > contentScrollThreshold - } - } // MARK: - Reply Handling diff --git a/WordPress/Classes/ViewRelated/Comments/CommentTableHeaderView.swift b/WordPress/Classes/ViewRelated/Comments/CommentTableHeaderView.swift index 0cdfbbf15e91..f1a38a3410b7 100644 --- a/WordPress/Classes/ViewRelated/Comments/CommentTableHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Comments/CommentTableHeaderView.swift @@ -96,18 +96,6 @@ private struct CommentHeaderView: View { @State var showsDisclosureIndicator = true var body: some View { - if #available(iOS 15.0, *) { - // Material ShapeStyles are only available from iOS 15.0. - content.background(.ultraThinMaterial) - } else { - ZStack { - VisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial)) - content - } - } - } - - var content: some View { HStack { text Spacer() @@ -116,6 +104,7 @@ private struct CommentHeaderView: View { } } .padding(EdgeInsets(top: 10, leading: 16, bottom: 10, trailing: 16)) + .background(.ultraThinMaterial) } var text: some View { @@ -139,17 +128,3 @@ private struct CommentHeaderView: View { .imageScale(.large) } } - -// MARK: SwiftUI VisualEffect support for iOS 14 - -private struct VisualEffectView: UIViewRepresentable { - var effect: UIVisualEffect - - func makeUIView(context: Context) -> UIVisualEffectView { - return UIVisualEffectView() - } - - func updateUIView(_ uiView: UIVisualEffectView, context: Context) { - uiView.effect = effect - } -} diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/CheckoutViewController.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/CheckoutViewController.swift index 9d03f782ec68..c7202961949d 100644 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/CheckoutViewController.swift +++ b/WordPress/Classes/ViewRelated/Domains/Domain registration/CheckoutViewController.swift @@ -5,12 +5,14 @@ struct CheckoutViewModel { } final class CheckoutViewController: WebKitViewController { + typealias PurchaseCallback = ((CheckoutViewController) -> Void) + let viewModel: CheckoutViewModel - let purchaseCallback: (() -> Void)? + let purchaseCallback: PurchaseCallback? private var webViewURLChangeObservation: NSKeyValueObservation? - init(viewModel: CheckoutViewModel, purchaseCallback: (() -> Void)?) { + init(viewModel: CheckoutViewModel, purchaseCallback: PurchaseCallback?) { self.viewModel = viewModel self.purchaseCallback = purchaseCallback @@ -40,7 +42,7 @@ final class CheckoutViewController: WebKitViewController { } if newURL.absoluteString.hasPrefix("https://wordpress.com/checkout/thank-you") { - self.purchaseCallback?() + self.purchaseCallback?(self) /// Stay on Checkout page self.webView.goBack() diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/PlanSelectionViewController.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/PlanSelectionViewController.swift index 3ada117f8c2b..663ba63f3c0e 100644 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/PlanSelectionViewController.swift +++ b/WordPress/Classes/ViewRelated/Domains/Domain registration/PlanSelectionViewController.swift @@ -37,8 +37,10 @@ struct PlanSelectionViewModel { } final class PlanSelectionViewController: WebKitViewController { + typealias PlanSelectionCallback = (PlanSelectionViewController, URL) -> Void + let viewModel: PlanSelectionViewModel - var planSelectedCallback: ((URL) -> Void)? + var planSelectedCallback: PlanSelectionCallback? private var webViewURLChangeObservation: NSKeyValueObservation? @@ -65,7 +67,7 @@ final class PlanSelectionViewController: WebKitViewController { } if self.viewModel.isPlanSelected(newURL) { - self.planSelectedCallback?(newURL) + self.planSelectedCallback?(self, newURL) /// Stay on Plan Selection page self.webView.goBack() diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/DomainSuggestionsTableViewController.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/DomainSuggestionsTableViewController.swift index 76e027713cc7..868ce858ef5f 100644 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/DomainSuggestionsTableViewController.swift +++ b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/DomainSuggestionsTableViewController.swift @@ -3,7 +3,7 @@ import SVProgressHUD import WordPressAuthenticator -protocol DomainSuggestionsTableViewControllerDelegate { +protocol DomainSuggestionsTableViewControllerDelegate: AnyObject { func domainSelected(_ domain: FullyQuotedDomainSuggestion) func newSearchStarted() } @@ -30,7 +30,7 @@ class DomainSuggestionsTableViewController: UITableViewController { var blog: Blog? var siteName: String? - var delegate: DomainSuggestionsTableViewControllerDelegate? + weak var delegate: DomainSuggestionsTableViewControllerDelegate? var domainSuggestionType: DomainsServiceRemote.DomainSuggestionType = .noWordpressDotCom var domainSelectionType: DomainSelectionType? var freeSiteAddress: String = "" diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/RegisterDomainSuggestionsViewController.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/RegisterDomainSuggestionsViewController.swift index cb9a81b49317..526b589fa840 100644 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/RegisterDomainSuggestionsViewController.swift +++ b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainSuggestions/RegisterDomainSuggestionsViewController.swift @@ -11,14 +11,17 @@ enum DomainSelectionType { } class RegisterDomainSuggestionsViewController: UIViewController { + typealias DomainPurchasedCallback = ((RegisterDomainSuggestionsViewController, String) -> Void) + typealias DomainAddedToCartCallback = ((RegisterDomainSuggestionsViewController, String) -> Void) + @IBOutlet weak var buttonContainerBottomConstraint: NSLayoutConstraint! @IBOutlet weak var buttonContainerViewHeightConstraint: NSLayoutConstraint! private var constraintsInitialized = false private var site: Blog! - var domainPurchasedCallback: ((String) -> Void)! - var domainAddedToCartCallback: ((String) -> Void)? + var domainPurchasedCallback: DomainPurchasedCallback! + var domainAddedToCartCallback: DomainAddedToCartCallback? private var domain: FullyQuotedDomainSuggestion? private var siteName: String? @@ -53,7 +56,7 @@ class RegisterDomainSuggestionsViewController: UIViewController { static func instance(site: Blog, domainSelectionType: DomainSelectionType = .registerWithPaidPlan, includeSupportButton: Bool = true, - domainPurchasedCallback: ((String) -> Void)? = nil) -> RegisterDomainSuggestionsViewController { + domainPurchasedCallback: DomainPurchasedCallback? = nil) -> RegisterDomainSuggestionsViewController { let storyboard = UIStoryboard(name: Constants.storyboardIdentifier, bundle: Bundle.main) let controller = storyboard.instantiateViewController(withIdentifier: Constants.viewControllerIdentifier) as! RegisterDomainSuggestionsViewController controller.site = site @@ -243,8 +246,12 @@ extension RegisterDomainSuggestionsViewController: NUXButtonViewControllerDelega createCart( domain, onSuccess: { [weak self] in - self?.domainAddedToCartCallback?(domain.domainName) - self?.setPrimaryButtonLoading(false, afterDelay: 0.25) + guard let self = self else { + return + } + + self.domainAddedToCartCallback?(self, domain.domainName) + self.setPrimaryButtonLoading(false, afterDelay: 0.25) }, onFailure: onFailure ) @@ -267,7 +274,13 @@ extension RegisterDomainSuggestionsViewController: NUXButtonViewControllerDelega } let controller = RegisterDomainDetailsViewController() - controller.viewModel = RegisterDomainDetailsViewModel(siteID: siteID, domain: domain, domainPurchasedCallback: domainPurchasedCallback) + controller.viewModel = RegisterDomainDetailsViewModel(siteID: siteID, domain: domain) { [weak self] name in + guard let self = self else { + return + } + + self.domainPurchasedCallback?(self, name) + } self.navigationController?.pushViewController(controller, animated: true) } @@ -354,7 +367,11 @@ extension RegisterDomainSuggestionsViewController: NUXButtonViewControllerDelega navController.dismiss(animated: true) }) { domain in self.dismiss(animated: true, completion: { [weak self] in - self?.domainPurchasedCallback(domain) + guard let self = self else { + return + } + + self.domainPurchasedCallback(self, domain) }) } } diff --git a/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardFactory.swift b/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardFactory.swift index ed7d8b2f862b..b720d367e9c6 100644 --- a/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardFactory.swift +++ b/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardFactory.swift @@ -14,7 +14,7 @@ struct DomainsDashboardFactory { domainSelectionType: domainSelectionType, includeSupportButton: false) - viewController.domainPurchasedCallback = { domain in + viewController.domainPurchasedCallback = { viewController, domain in let blogService = BlogService(coreDataStack: ContextManager.shared) blogService.syncBlogAndAllMetadata(blog) { } WPAnalytics.track(.domainCreditRedemptionSuccess) diff --git a/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardView.swift b/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardView.swift index c2b64c51c70b..ca2e83a37d50 100644 --- a/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardView.swift +++ b/WordPress/Classes/ViewRelated/Domains/Views/DomainsDashboardView.swift @@ -55,8 +55,7 @@ struct DomainsDashboardView: View { /// Builds the site address section for the given blog private func makeSiteAddressSection(blog: Blog) -> some View { - Section(header: makeSiteAddressHeader(), - footer: Text(TextContent.primarySiteSectionFooter(blog.hasPaidPlan))) { + Section(footer: Text(TextContent.primarySiteSectionFooter(blog.hasPaidPlan))) { VStack(alignment: .leading) { Text(TextContent.siteAddressTitle) Text(blog.freeSiteAddress) @@ -129,13 +128,6 @@ struct DomainsDashboardView: View { .foregroundColor(domain.domain.expirySoon || domain.domain.expired ? Color(UIColor.error) : Color(UIColor.textSubtle)) } - private func makeSiteAddressHeader() -> Divider? { - if #available(iOS 15, *) { - return nil - } - return Divider() - } - /// Instantiates the proper search depending if it's for claiming a free domain with a paid plan or purchasing a new one private func makeDomainSearch(for blog: Blog, onDismiss: @escaping () -> Void) -> some View { return DomainSuggestionViewControllerWrapper( diff --git a/WordPress/Classes/ViewRelated/Gutenberg/Collapsable Header/CollapsableHeaderViewController.swift b/WordPress/Classes/ViewRelated/Gutenberg/Collapsable Header/CollapsableHeaderViewController.swift index d63f085bf29a..3c9c36d6dab2 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/Collapsable Header/CollapsableHeaderViewController.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/Collapsable Header/CollapsableHeaderViewController.swift @@ -759,19 +759,22 @@ extension CollapsableHeaderViewController: UIScrollViewDelegate { // MARK: - Keyboard Adjustments extension CollapsableHeaderViewController { private func startObservingKeyboardChanges() { - let willShowObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { (notification) in + let willShowObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { [weak self] (notification) in + guard let self else { return } UIView.animate(withKeyboard: notification) { (_, endFrame) in self.scrollableContainerBottomConstraint.constant = endFrame.height - self.footerHeight } } - let willHideObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { (notification) in + let willHideObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { [weak self] (notification) in + guard let self else { return } UIView.animate(withKeyboard: notification) { (_, _) in self.scrollableContainerBottomConstraint.constant = 0 } } - let willChangeFrameObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillChangeFrameNotification, object: nil, queue: .main) { (notification) in + let willChangeFrameObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillChangeFrameNotification, object: nil, queue: .main) { [weak self] (notification) in + guard let self else { return } UIView.animate(withKeyboard: notification) { (_, endFrame) in self.scrollableContainerBottomConstraint.constant = endFrame.height - self.footerHeight } diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Fullscreen Overlay/JetpackFullscreenOverlayViewController.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Fullscreen Overlay/JetpackFullscreenOverlayViewController.swift index 88d863db4fa7..29589db8777b 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/Fullscreen Overlay/JetpackFullscreenOverlayViewController.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/Fullscreen Overlay/JetpackFullscreenOverlayViewController.swift @@ -104,9 +104,7 @@ class JetpackFullscreenOverlayViewController: UIViewController { navigationItem.standardAppearance = appearance navigationItem.compactAppearance = appearance navigationItem.scrollEdgeAppearance = appearance - if #available(iOS 15.0, *) { - navigationItem.compactScrollEdgeAppearance = appearance - } + navigationItem.compactScrollEdgeAppearance = appearance } private func addCloseButtonIfNeeded() { @@ -188,33 +186,21 @@ class JetpackFullscreenOverlayViewController: UIViewController { } private func setupButtonInsets() { - if #available(iOS 15.0, *) { - // Continue & Switch Buttons - var buttonConfig: UIButton.Configuration = .plain() - buttonConfig.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer({ incoming in - var outgoing = incoming - outgoing.font = WPStyleGuide.fontForTextStyle(.body, fontWeight: .semibold) - return outgoing - }) - buttonConfig.contentInsets = Metrics.mainButtonsContentInsets - continueButton.configuration = buttonConfig - switchButton.configuration = buttonConfig - - // Learn More Button - var learnMoreButtonConfig: UIButton.Configuration = .plain() - learnMoreButtonConfig.contentInsets = Metrics.learnMoreButtonContentInsets - learnMoreButton.configuration = learnMoreButtonConfig - } else { - // Continue Button - continueButton.contentEdgeInsets = Metrics.mainButtonsContentEdgeInsets - - // Switch Button - switchButton.contentEdgeInsets = Metrics.mainButtonsContentEdgeInsets - - // Learn More Button - learnMoreButton.contentEdgeInsets = Metrics.learnMoreButtonContentEdgeInsets - learnMoreButton.flipInsetsForRightToLeftLayoutDirection() - } + // Continue & Switch Buttons + var buttonConfig: UIButton.Configuration = .plain() + buttonConfig.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer({ incoming in + var outgoing = incoming + outgoing.font = WPStyleGuide.fontForTextStyle(.body, fontWeight: .semibold) + return outgoing + }) + buttonConfig.contentInsets = Metrics.mainButtonsContentInsets + continueButton.configuration = buttonConfig + switchButton.configuration = buttonConfig + + // Learn More Button + var learnMoreButtonConfig: UIButton.Configuration = .plain() + learnMoreButtonConfig.contentInsets = Metrics.learnMoreButtonContentInsets + learnMoreButton.configuration = learnMoreButtonConfig } private func setupLearnMoreButtonTitle() { diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/JetpackBrandingMenuCardCell.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/JetpackBrandingMenuCardCell.swift index d059838d1701..fbbf031067bd 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/JetpackBrandingMenuCardCell.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/JetpackBrandingMenuCardCell.swift @@ -97,14 +97,9 @@ class JetpackBrandingMenuCardCell: UITableViewCell { button.setTitle(Strings.learnMoreButtonText, for: .normal) button.addTarget(self, action: #selector(learnMoreButtonTapped), for: .touchUpInside) - if #available(iOS 15.0, *) { - var learnMoreButtonConfig: UIButton.Configuration = .plain() - learnMoreButtonConfig.contentInsets = Metrics.learnMoreButtonContentInsets - button.configuration = learnMoreButtonConfig - } else { - button.contentEdgeInsets = Metrics.learnMoreButtonContentEdgeInsets - button.flipInsetsForRightToLeftLayoutDirection() - } + var learnMoreButtonConfig: UIButton.Configuration = .plain() + learnMoreButtonConfig.contentInsets = Metrics.learnMoreButtonContentInsets + button.configuration = learnMoreButtonConfig return button }() diff --git a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift b/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift index a74082783c53..b604d0804cf0 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift @@ -2,15 +2,14 @@ import SwiftUI struct JetpackSocialNoConnectionView: View { - private let viewModel: JetpackSocialNoConnectionViewModel + @StateObject private var viewModel: JetpackSocialNoConnectionViewModel var body: some View { VStack(alignment: .leading, spacing: 12.0) { HStack(spacing: -5.0) { - iconImage("icon-tumblr") - iconImage("icon-facebook") - iconImage("icon-twitter") - iconImage("icon-linkedin") + ForEach(viewModel.icons, id: \.self) { icon in + iconImage(icon) + } } .accessibilityElement() .accessibilityLabel(Constants.iconGroupAccessibilityLabel) @@ -38,10 +37,11 @@ struct JetpackSocialNoConnectionView: View { .background(Color(UIColor.listForeground)) } - func iconImage(_ image: String) -> some View { - Image(image) + func iconImage(_ image: UIImage) -> some View { + Image(uiImage: image) .resizable() .frame(width: 32.0, height: 32.0) + .background(Color(UIColor.listForeground)) .clipShape(Circle()) .overlay(Circle().stroke(Color(UIColor.listForeground), lineWidth: 2.0)) } @@ -53,19 +53,22 @@ extension JetpackSocialNoConnectionView { static func createHostController(with viewModel: JetpackSocialNoConnectionViewModel = JetpackSocialNoConnectionViewModel()) -> UIHostingController { let hostController = UIHostingController(rootView: JetpackSocialNoConnectionView(viewModel: viewModel)) hostController.view.translatesAutoresizingMaskIntoConstraints = false + hostController.view.backgroundColor = .listForeground return hostController } } // MARK: - View model -struct JetpackSocialNoConnectionViewModel { +class JetpackSocialNoConnectionViewModel: ObservableObject { let padding: EdgeInsets let hideNotNow: Bool let onConnectTap: (() -> Void)? let onNotNowTap: (() -> Void)? + @MainActor @Published var icons: [UIImage] = [UIImage()] - init(padding: EdgeInsets = Constants.defaultPadding, + init(services: [PublicizeService] = [], + padding: EdgeInsets = Constants.defaultPadding, hideNotNow: Bool = false, onConnectTap: (() -> Void)? = nil, onNotNowTap: (() -> Void)? = nil) { @@ -73,6 +76,52 @@ struct JetpackSocialNoConnectionViewModel { self.hideNotNow = hideNotNow self.onConnectTap = onConnectTap self.onNotNowTap = onNotNowTap + updateIcons(services) + } + + enum JetpackSocialService: String { + case facebook + case twitter + case tumblr + case linkedin + case instagram = "instagram-business" + case mastodon + case unknown + } + + private func updateIcons(_ services: [PublicizeService]) { + var icons: [UIImage] = [] + var downloadTasks: [(url: URL, index: Int)] = [] + for (index, service) in services.enumerated() { + let serviceType = JetpackSocialService(rawValue: service.serviceID) ?? .unknown + let icon = WPStyleGuide.socialIcon(for: service.serviceID as NSString) + icons.append(icon) + + if serviceType == .unknown { + guard let iconUrl = URL(string: service.icon) else { + continue + } + downloadTasks.append((url: iconUrl, index: index)) + } + } + + DispatchQueue.main.async { + self.icons = icons + + for task in downloadTasks { + let (url, index) = task + WPImageSource.shared().downloadImage(for: url) { image in + guard let image else { + return + } + DispatchQueue.main.async { + self.icons[index] = image + } + } failure: { error in + DDLogError("Error downloading icon: \(String(describing: error))") + } + } + } } } diff --git a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialSettingsRemainingSharesView.swift b/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialSettingsRemainingSharesView.swift new file mode 100644 index 000000000000..5ae47080ef46 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialSettingsRemainingSharesView.swift @@ -0,0 +1,71 @@ +import SwiftUI + +struct JetpackSocialSettingsRemainingSharesView: View { + + let viewModel: JetpackSocialRemainingSharesViewModel + + var body: some View { + VStack(alignment: .leading, spacing: 4.0) { + HStack(spacing: 8.0) { + if viewModel.displayWarning { + Image("icon-warning") + .resizable() + .frame(width: 16.0, height: 16.0) + } + remainingText + } + Text(Constants.subscribeText) + .font(.callout) + .foregroundColor(Color(UIColor.primary)) + .onTapGesture { + viewModel.onSubscribeTap() + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(EdgeInsets(top: 12.0, leading: 16.0, bottom: 12.0, trailing: 16.0)) + .background(Color(UIColor.listForeground)) + } + + private var remainingText: some View { + let sharesRemainingString = String(format: Constants.remainingTextFormat, viewModel.remaining, viewModel.limit) + let sharesRemaining = Text(sharesRemainingString).font(.callout) + if viewModel.displayWarning { + return sharesRemaining + } + let remainingDays = Text(Constants.remainingEndText).font(.callout).foregroundColor(.secondary) + return sharesRemaining + remainingDays + } + + private struct Constants { + static let remainingTextFormat = NSLocalizedString("postsettings.social.remainingshares.text.format", + value: "%1$d/%2$d social shares remaining", + comment: "Beginning text of the remaining social shares a user has left." + + " %1$d is their current remaining shares. %2$d is their share limit." + + " This text is combined with ' in the next 30 days' if there is no warning displayed.") + static let remainingEndText = NSLocalizedString("postsettings.social.remainingshares.text.part", + value: " in the next 30 days", + comment: "The second half of the remaining social shares a user has." + + " This is only displayed when there is no social limit warning.") + static let subscribeText = NSLocalizedString("postsettings.social.remainingshares.subscribe", + value: "Subscribe now to share more", + comment: "Title for the button to subscribe to Jetpack Social on the remaining shares view") + } + +} + +struct JetpackSocialRemainingSharesViewModel { + let remaining: Int + let limit: Int + let displayWarning: Bool + let onSubscribeTap: () -> Void + + init(remaining: Int = 27, + limit: Int = 30, + displayWarning: Bool = false, + onSubscribeTap: @escaping () -> Void) { + self.remaining = remaining + self.limit = limit + self.displayWarning = displayWarning + self.onSubscribeTap = onSubscribeTap + } +} diff --git a/WordPress/Classes/ViewRelated/Likes/LikesListController.swift b/WordPress/Classes/ViewRelated/Likes/LikesListController.swift index 5f0b99ea7b09..b4a8bcfb400b 100644 --- a/WordPress/Classes/ViewRelated/Likes/LikesListController.swift +++ b/WordPress/Classes/ViewRelated/Likes/LikesListController.swift @@ -231,7 +231,7 @@ class LikesListController: NSObject { case .post(let postID): likingUsers = postService.likeUsersFor(postID: postID, siteID: siteID) case .comment(let commentID): - likingUsers = commentService.likeUsersFor(commentID: commentID, siteID: siteID) + likingUsers = LikeUserHelper.likeUsersFor(commentID: commentID, siteID: siteID, in: ContextManager.shared.mainContext) } } @@ -283,7 +283,7 @@ class LikesListController: NSObject { case .post(let postID): fetchedUsers = postService.likeUsersFor(postID: postID, siteID: siteID, after: modifiedDate) case .comment(let commentID): - fetchedUsers = commentService.likeUsersFor(commentID: commentID, siteID: siteID, after: modifiedDate) + fetchedUsers = LikeUserHelper.likeUsersFor(commentID: commentID, siteID: siteID, after: modifiedDate, in: ContextManager.shared.mainContext) } excludeUserIDs = fetchedUsers.map { NSNumber(value: $0.userID) } diff --git a/WordPress/Classes/ViewRelated/Me/My Profile/Change Username/ChangeUsernameViewController.swift b/WordPress/Classes/ViewRelated/Me/My Profile/Change Username/ChangeUsernameViewController.swift index 062ba6d33b29..8dd57cb91311 100644 --- a/WordPress/Classes/ViewRelated/Me/My Profile/Change Username/ChangeUsernameViewController.swift +++ b/WordPress/Classes/ViewRelated/Me/My Profile/Change Username/ChangeUsernameViewController.swift @@ -1,3 +1,4 @@ +import Combine import WordPressAuthenticator class ChangeUsernameViewController: SignupUsernameTableViewController { @@ -15,7 +16,13 @@ class ChangeUsernameViewController: SignupUsernameTableViewController { } return saveItem }() - private var changeUsernameAction: UIAlertAction? + + private var confirmationTextObserver: AnyCancellable? + private weak var confirmationController: UIAlertController? { + didSet { + observeConfirmationTextField() + } + } init(service: AccountSettingsService, settings: AccountSettings?, completionBlock: @escaping CompletionBlock) { self.viewModel = ChangeUsernameViewModel(service: service, settings: settings) @@ -105,7 +112,9 @@ private extension ChangeUsernameViewController { } func save() { - present(changeUsernameConfirmationPrompt(), animated: true) + let controller = changeUsernameConfirmationPrompt() + present(controller, animated: true) + confirmationController = controller } func changeUsername() { @@ -135,44 +144,66 @@ private extension ChangeUsernameViewController { preferredStyle: .alert) alertController.addAttributeMessage(String(format: Constants.Alert.message, viewModel.selectedUsername), highlighted: viewModel.selectedUsername) - alertController.addCancelActionWithTitle(Constants.Alert.cancel, handler: { [weak alertController] _ in - if let textField = alertController?.textFields?.first { - NotificationCenter.default.removeObserver(textField, name: UITextField.textDidChangeNotification, object: nil) - } + alertController.addCancelActionWithTitle(Constants.Alert.cancel, handler: { _ in DDLogInfo("User cancelled alert") }) - changeUsernameAction = alertController.addDefaultActionWithTitle(Constants.Alert.change, handler: { [weak alertController] _ in - guard let textField = alertController?.textFields?.first, + let action = alertController.addDefaultActionWithTitle(Constants.Alert.change, handler: { [weak alertController, weak self] _ in + guard let self, let alertController else { return } + guard let textField = alertController.textFields?.first, textField.text == self.viewModel.selectedUsername else { DDLogInfo("Username confirmation failed") return } DDLogInfo("User changes username") - NotificationCenter.default.removeObserver(textField, name: UITextField.textDidChangeNotification, object: nil) self.changeUsername() }) - changeUsernameAction?.isEnabled = false - alertController.addTextField { [weak self] textField in + action.isEnabled = false + alertController.addTextField { textField in textField.placeholder = Constants.Alert.confirm - NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, - object: textField, - queue: .main) {_ in - if let text = textField.text, - !text.isEmpty, - let username = self?.viewModel.selectedUsername, - text == username { - self?.changeUsernameAction?.isEnabled = true - textField.textColor = .success - return - } - self?.changeUsernameAction?.isEnabled = false - textField.textColor = .text - } } DDLogInfo("Prompting user for confirmation of change username") return alertController } + func observeConfirmationTextField() { + confirmationTextObserver?.cancel() + confirmationTextObserver = nil + + guard let confirmationController, + let textField = confirmationController.textFields?.first + else { + return + } + + // We need to add another condition to check if the text field is the username confirmation text field, if there + // are more than one text field in the prompt. + assert(confirmationController.textFields?.count == 1, "There should be only one text field in the prompt") + + confirmationTextObserver = NotificationCenter.default + .publisher(for: UITextField.textDidChangeNotification, object: textField) + .sink(receiveValue: { [weak self] in + self?.handleTextDidChangeNotification($0) + }) + } + + func handleTextDidChangeNotification(_ notification: Foundation.Notification) { + guard notification.name == UITextField.textDidChangeNotification, + let confirmationController, + let textField = notification.object as? UITextField + else { + DDLogInfo("The notification is not sent from the text field within the change username confirmation prompt") + return + } + + let actions = confirmationController.actions.filter({ $0.title == Constants.Alert.change }) + precondition(actions.count == 1, "More than one 'Change username' action found") + let changeUsernameAction = actions.first + + let enabled = textField.text?.isEmpty == false && textField.text == self.viewModel.selectedUsername + changeUsernameAction?.isEnabled = enabled + textField.textColor = enabled ? .success : .text + } + enum Constants { static let actionButtonTitle = NSLocalizedString("Save", comment: "Settings Text save button title") static let username = NSLocalizedString("Username", comment: "The header and main title") diff --git a/WordPress/Classes/ViewRelated/Menus/MenuItemView.m b/WordPress/Classes/ViewRelated/Menus/MenuItemView.m index f9f6908e8b20..aabfdec0888c 100644 --- a/WordPress/Classes/ViewRelated/Menus/MenuItemView.m +++ b/WordPress/Classes/ViewRelated/Menus/MenuItemView.m @@ -64,16 +64,10 @@ - (void)setupCancelButton button.titleLabel.adjustsFontForContentSizeCategory = YES; [button setTitle:NSLocalizedString(@"Cancel", @"") forState:UIControlStateNormal]; - if (@available(iOS 15, *)) { - UIButtonConfiguration *configuration = [UIButtonConfiguration plainButtonConfiguration]; - configuration.contentInsets = NSDirectionalEdgeInsetsMake(0, 6, 0, 6); - button.configuration = configuration; - } else { - UIEdgeInsets inset = button.contentEdgeInsets; - inset.left = 6.0; - inset.right = inset.left; - button.contentEdgeInsets = inset; - } + UIButtonConfiguration *configuration = [UIButtonConfiguration plainButtonConfiguration]; + configuration.contentInsets = NSDirectionalEdgeInsetsMake(0, 6, 0, 6); + button.configuration = configuration; + button.hidden = YES; [self.accessoryStackView addArrangedSubview:button]; diff --git a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController.swift b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController.swift index 5b769ed57fc7..ac451be152ef 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController.swift @@ -1373,7 +1373,7 @@ extension NotificationsViewController: WPTableViewHandlerDelegate { return ContextManager.sharedInstance().mainContext } - func fetchRequest() -> NSFetchRequest { + func fetchRequest() -> NSFetchRequest? { let request = NSFetchRequest(entityName: entityName()) request.sortDescriptors = [NSSortDescriptor(key: Filter.sortKey, ascending: false)] request.predicate = predicateForFetchRequest() @@ -1817,7 +1817,9 @@ extension NotificationsViewController: WPSplitViewControllerDetailProvider { private func fetchFirstNotification() -> Notification? { let context = managedObjectContext() - let fetchRequest = self.fetchRequest() + guard let fetchRequest = self.fetchRequest() else { + return nil + } fetchRequest.fetchLimit = 1 if let results = try? context.fetch(fetchRequest) as? [Notification] { diff --git a/WordPress/Classes/ViewRelated/Plugins/PluginViewModel.swift b/WordPress/Classes/ViewRelated/Plugins/PluginViewModel.swift index b01a48adea0f..db15ba3332b9 100644 --- a/WordPress/Classes/ViewRelated/Plugins/PluginViewModel.swift +++ b/WordPress/Classes/ViewRelated/Plugins/PluginViewModel.swift @@ -540,7 +540,7 @@ class PluginViewModel: Observable { return } - let controller = RegisterDomainSuggestionsViewController.instance(site: blog, domainPurchasedCallback: { [weak self] domain in + let controller = RegisterDomainSuggestionsViewController.instance(site: blog, domainPurchasedCallback: { [weak self] _, domain in guard let strongSelf = self, let atHelper = AutomatedTransferHelper(site: strongSelf.site, plugin: directoryEntry) else { diff --git a/WordPress/Classes/ViewRelated/Post/AbstractPostListViewController.swift b/WordPress/Classes/ViewRelated/Post/AbstractPostListViewController.swift index 5a8c7d549669..d1eb6df8f35d 100644 --- a/WordPress/Classes/ViewRelated/Post/AbstractPostListViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/AbstractPostListViewController.swift @@ -434,7 +434,7 @@ class AbstractPostListViewController: UIViewController, return ContextManager.sharedInstance().mainContext } - func fetchRequest() -> NSFetchRequest { + func fetchRequest() -> NSFetchRequest? { let fetchRequest = NSFetchRequest(entityName: entityName()) fetchRequest.predicate = predicateForFetchRequest() fetchRequest.sortDescriptors = sortDescriptorsForFetchRequest() diff --git a/WordPress/Classes/ViewRelated/Post/PostEditorNavigationBarManager.swift b/WordPress/Classes/ViewRelated/Post/PostEditorNavigationBarManager.swift index 227f233a7c63..b0ba43ef65fc 100644 --- a/WordPress/Classes/ViewRelated/Post/PostEditorNavigationBarManager.swift +++ b/WordPress/Classes/ViewRelated/Post/PostEditorNavigationBarManager.swift @@ -25,15 +25,12 @@ class PostEditorNavigationBarManager { /// lazy var closeButton: UIButton = { let button = UIButton(type: .system) - if #available(iOS 15, *) { - var configuration = UIButton.Configuration.plain() - configuration.image = Assets.closeButtonModalImage - configuration.contentInsets = Constants.closeButtonInsets - button.configuration = configuration - } else { - button.setImage(Assets.closeButtonModalImage, for: .normal) - button.contentEdgeInsets = Constants.closeButtonEdgeInsets - } + + var configuration = UIButton.Configuration.plain() + configuration.image = Assets.closeButtonModalImage + configuration.contentInsets = Constants.closeButtonInsets + button.configuration = configuration + button.addTarget(self, action: #selector(closeWasPressed), for: .touchUpInside) button.setContentHuggingPriority(.required, for: .horizontal) button.accessibilityIdentifier = "editor-close-button" diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+JetpackSocial.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+JetpackSocial.swift index b06e52d05248..9d360d61ec96 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+JetpackSocial.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+JetpackSocial.swift @@ -1,23 +1,28 @@ +import SwiftUI extension PostSettingsViewController { + // MARK: - No connection view + @objc func showNoConnection() -> Bool { let isJetpackSocialEnabled = FeatureFlag.jetpackSocial.enabled + let isNoConnectionViewHidden = UserPersistentStoreFactory.instance().bool(forKey: hideNoConnectionViewKey()) let blogSupportsPublicize = apost.blog.supportsPublicize() let blogHasNoConnections = publicizeConnections.count == 0 - return isJetpackSocialEnabled && blogSupportsPublicize && blogHasNoConnections + let blogHasServices = availableServices().count > 0 + + return isJetpackSocialEnabled + && !isNoConnectionViewHidden + && blogSupportsPublicize + && blogHasNoConnections + && blogHasServices } @objc func createNoConnectionView() -> UIView { - let viewModel = JetpackSocialNoConnectionViewModel( - onConnectTap: { - // TODO: Open the social screen - print("Connect tap") - }, onNotNowTap: { - // TODO: Add condition to hide the connection view after not now is tapped - print("Not now tap") - } - ) + let services = availableServices() + let viewModel = JetpackSocialNoConnectionViewModel(services: services, + onConnectTap: onConnectTap(), + onNotNowTap: onNotNowTap()) let viewController = JetpackSocialNoConnectionView.createHostController(with: viewModel) // Returning just the view means the view controller will deallocate but we don't need a @@ -25,4 +30,82 @@ extension PostSettingsViewController { return viewController.view } + // MARK: - Remaining shares view + + @objc func showRemainingShares() -> Bool { + let isJetpackSocialEnabled = FeatureFlag.jetpackSocial.enabled + let blogSupportsPublicize = apost.blog.supportsPublicize() + let blogHasConnections = publicizeConnections.count > 0 + // TODO: Check if there's a share limit + + return isJetpackSocialEnabled + && blogSupportsPublicize + && blogHasConnections + } + + @objc func createRemainingSharesView() -> UIView { + let viewModel = JetpackSocialRemainingSharesViewModel { [weak self] in + guard let blog = self?.apost.blog, + let hostname = blog.hostname, + let url = URL(string: "https://wordpress.com/checkout/\(hostname)/jetpack_social_basic_yearly") else { + return + } + let webViewController = WebViewControllerFactory.controller(url: url, + blog: blog, + source: "post_settings_remaining_shares_subscribe_now") + let navigationController = UINavigationController(rootViewController: webViewController) + self?.present(navigationController, animated: true) + } + let hostController = UIHostingController(rootView: JetpackSocialSettingsRemainingSharesView(viewModel: viewModel)) + hostController.view.translatesAutoresizingMaskIntoConstraints = false + hostController.view.backgroundColor = .listForeground + return hostController.view + } + +} + +// MARK: - Private methods + +private extension PostSettingsViewController { + + func hideNoConnectionViewKey() -> String { + guard let dotComID = apost.blog.dotComID?.stringValue else { + return Constants.hideNoConnectionViewKey + } + + return "\(dotComID)-\(Constants.hideNoConnectionViewKey)" + } + + func onConnectTap() -> () -> Void { + return { [weak self] in + guard let blog = self?.apost.blog, + let controller = SharingViewController(blog: blog, delegate: nil) else { + return + } + self?.navigationController?.pushViewController(controller, animated: true) + } + } + + func onNotNowTap() -> () -> Void { + return { [weak self] in + guard let key = self?.hideNoConnectionViewKey() else { + return + } + UserPersistentStoreFactory.instance().set(true, forKey: key) + self?.tableView.reloadData() + } + } + + func availableServices() -> [PublicizeService] { + let context = apost.managedObjectContext ?? ContextManager.shared.mainContext + let services = try? PublicizeService.allPublicizeServices(in: context) + return services ?? [] + } + + // MARK: - Constants + + struct Constants { + static let hideNoConnectionViewKey = "post-settings-social-no-connection-view-hidden" + } + } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h index a611ab1d8b16..a1409455e82f 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h @@ -17,4 +17,6 @@ @property (nonatomic, weak, nullable) id featuredImageDelegate; +- (void)reloadData; + @end diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index bd1af6d5988b..f2527725dc2f 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -41,7 +41,9 @@ typedef NS_ENUM(NSInteger, PostSettingsRow) { PostSettingsRowShareConnection, PostSettingsRowShareMessage, PostSettingsRowSlug, - PostSettingsRowExcerpt + PostSettingsRowExcerpt, + PostSettingsRowSocialNoConnections, + PostSettingsRowSocialRemainingShares }; static CGFloat CellHeight = 44.0f; @@ -359,6 +361,7 @@ - (void)reloadData { self.passwordTextField.text = self.apost.password; + [self configureSections]; [self.tableView reloadData]; } @@ -393,14 +396,16 @@ - (void)configureSections { NSNumber *stickyPostSection = @(PostSettingsSectionStickyPost); NSNumber *disabledTwitterSection = @(PostSettingsSectionDisabledTwitter); + NSNumber *remainingSharesSection = @(PostSettingsSectionSharesRemaining); NSMutableArray *sections = [@[ @(PostSettingsSectionTaxonomy), - @(PostSettingsSectionMeta), - @(PostSettingsSectionFormat), - @(PostSettingsSectionFeaturedImage), - stickyPostSection, - @(PostSettingsSectionShare), - disabledTwitterSection, - @(PostSettingsSectionMoreOptions) ] mutableCopy]; + @(PostSettingsSectionMeta), + @(PostSettingsSectionFormat), + @(PostSettingsSectionFeaturedImage), + stickyPostSection, + @(PostSettingsSectionShare), + disabledTwitterSection, + remainingSharesSection, + @(PostSettingsSectionMoreOptions) ] mutableCopy]; // Remove sticky post section for self-hosted non Jetpack site // and non admin user // @@ -412,6 +417,10 @@ - (void)configureSections [sections removeObject:disabledTwitterSection]; } + if (![self showRemainingShares]) { + [sections removeObject:remainingSharesSection]; + } + self.sections = [sections copy]; } @@ -428,28 +437,22 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger NSInteger sec = [[self.sections objectAtIndex:section] integerValue]; if (sec == PostSettingsSectionTaxonomy) { return 2; - } else if (sec == PostSettingsSectionMeta) { return [self.postMetaSectionRows count]; - } else if (sec == PostSettingsSectionFormat) { return 1; - } else if (sec == PostSettingsSectionFeaturedImage) { return 1; - } else if (sec == PostSettingsSectionStickyPost) { return 1; - } else if (sec == PostSettingsSectionShare) { return [self numberOfRowsForShareSection]; - } else if (sec == PostSettingsSectionDisabledTwitter) { return self.unsupportedConnections.count; - + } else if (sec == PostSettingsSectionSharesRemaining) { + return 1; } else if (sec == PostSettingsSectionMoreOptions) { return 2; - } return 0; @@ -564,6 +567,8 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N cell = [self configureStickyPostCellForIndexPath:indexPath]; } else if (sec == PostSettingsSectionShare || sec == PostSettingsSectionDisabledTwitter) { cell = [self showNoConnection] ? [self configureNoConnectionCell] : [self configureShareCellForIndexPath:indexPath]; + } else if (sec == PostSettingsSectionSharesRemaining) { + cell = [self configureRemainingSharesCell]; } else if (sec == PostSettingsSectionMoreOptions) { cell = [self configureMoreOptionsCellForIndexPath:indexPath]; } @@ -870,6 +875,55 @@ - (nullable NSURL *)urlForFeaturedImage { return featuredURL; } +- (UITableViewCell *)configureSocialCellForIndexPath:(NSIndexPath *)indexPath + connection:(PublicizeConnection *)connection + canEditSharing:(BOOL)canEditSharing + section:(NSInteger)section +{ + BOOL isJetpackSocialEnabled = [Feature enabled:FeatureFlagJetpackSocial]; + UITableViewCell *cell = [self getWPTableViewImageAndAccessoryCell]; + UIImage *image = [WPStyleGuide socialIconFor:connection.service]; + if (isJetpackSocialEnabled) { + image = [image resizedImageWithContentMode:UIViewContentModeScaleAspectFill + bounds:CGSizeMake(28.0, 28.0) + interpolationQuality:kCGInterpolationDefault]; + } + [cell.imageView setImage:image]; + if (canEditSharing && !isJetpackSocialEnabled) { + cell.imageView.tintColor = [WPStyleGuide tintColorForConnectedService: connection.service]; + } + cell.textLabel.text = connection.externalDisplay; + cell.textLabel.enabled = canEditSharing; + if (connection.isBroken) { + cell.accessoryView = section == PostSettingsSectionShare ? + [WPStyleGuide sharingCellWarningAccessoryImageView] : + [WPStyleGuide sharingCellErrorAccessoryImageView]; + } else { + UISwitch *switchAccessory = [[UISwitch alloc] initWithFrame:CGRectZero]; + // This interaction is handled at a cell level + switchAccessory.userInteractionEnabled = NO; + switchAccessory.on = ![self.post publicizeConnectionDisabledForKeyringID:connection.keyringConnectionID]; + switchAccessory.enabled = canEditSharing; + cell.accessoryView = switchAccessory; + } + cell.selectionStyle = UITableViewCellSelectionStyleNone; + cell.tag = PostSettingsRowShareConnection; + cell.accessibilityIdentifier = [NSString stringWithFormat:@"%@ %@", connection.service, connection.externalDisplay]; + return cell; +} + +- (UITableViewCell *)configureDisclosureCellWithSharing:(BOOL)canEditSharing +{ + UITableViewCell *cell = [self getWPTableViewDisclosureCell]; + cell.textLabel.text = NSLocalizedString(@"Message", @"Label for the share message field on the post settings."); + cell.textLabel.enabled = canEditSharing; + cell.detailTextLabel.text = self.post.publicizeMessage ? self.post.publicizeMessage : self.post.titleForDisplay; + cell.detailTextLabel.enabled = canEditSharing; + cell.tag = PostSettingsRowShareMessage; + cell.accessibilityIdentifier = @"Customize the message"; + return cell; +} + - (UITableViewCell *)configureShareCellForIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell; @@ -878,38 +932,12 @@ - (UITableViewCell *)configureShareCellForIndexPath:(NSIndexPath *)indexPath NSArray *connections = sec == PostSettingsSectionShare ? self.publicizeConnections : self.unsupportedConnections; if (indexPath.row < connections.count) { - cell = [self getWPTableViewImageAndAccessoryCell]; - PublicizeConnection *connection = connections[indexPath.row]; - UIImage *image = [WPStyleGuide iconForService: connection.service]; - [cell.imageView setImage:image]; - if (canEditSharing) { - cell.imageView.tintColor = [WPStyleGuide tintColorForConnectedService: connection.service]; - } - cell.textLabel.text = connection.externalDisplay; - cell.textLabel.enabled = canEditSharing; - if (connection.isBroken) { - cell.accessoryView = sec == PostSettingsSectionShare ? - [WPStyleGuide sharingCellWarningAccessoryImageView] : - [WPStyleGuide sharingCellErrorAccessoryImageView]; - } else { - UISwitch *switchAccessory = [[UISwitch alloc] initWithFrame:CGRectZero]; - // This interaction is handled at a cell level - switchAccessory.userInteractionEnabled = NO; - switchAccessory.on = ![self.post publicizeConnectionDisabledForKeyringID:connection.keyringConnectionID]; - switchAccessory.enabled = canEditSharing; - cell.accessoryView = switchAccessory; - } - cell.selectionStyle = UITableViewCellSelectionStyleNone; - cell.tag = PostSettingsRowShareConnection; - cell.accessibilityIdentifier = [NSString stringWithFormat:@"%@ %@", connection.service, connection.externalDisplay]; + cell = [self configureSocialCellForIndexPath:indexPath + connection:connections[indexPath.row] + canEditSharing:canEditSharing + section:sec]; } else { - cell = [self getWPTableViewDisclosureCell]; - cell.textLabel.text = NSLocalizedString(@"Message", @"Label for the share message field on the post settings."); - cell.textLabel.enabled = canEditSharing; - cell.detailTextLabel.text = self.post.publicizeMessage ? self.post.publicizeMessage : self.post.titleForDisplay; - cell.detailTextLabel.enabled = canEditSharing; - cell.tag = PostSettingsRowShareMessage; - cell.accessibilityIdentifier = @"Customize the message"; + cell = [self configureDisclosureCellWithSharing:canEditSharing]; } cell.userInteractionEnabled = canEditSharing; return cell; @@ -1362,12 +1390,27 @@ - (void)updateSearchBarForPicker:(WPMediaPickerViewController *)picker #pragma mark - Jetpack Social +- (UITableViewCell *)configureGenericCellWith:(UIView *)view { + UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:TableViewGenericCellIdentifier]; + for (UIView *subview in cell.contentView.subviews) { + [subview removeFromSuperview]; + } + [cell.contentView addSubview:view]; + [cell.contentView pinSubviewToAllEdges:view]; + return cell; +} + - (UITableViewCell *)configureNoConnectionCell { - UIView *noConnectionView = [self createNoConnectionView]; - UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:TableViewGenericCellIdentifier]; - [cell.contentView addSubview:noConnectionView]; - [cell.contentView pinSubviewToAllEdges:noConnectionView]; + UITableViewCell *cell = [self configureGenericCellWith:[self createNoConnectionView]]; + cell.tag = PostSettingsRowSocialNoConnections; + return cell; +} + +- (UITableViewCell *)configureRemainingSharesCell +{ + UITableViewCell *cell = [self configureGenericCellWith:[self createRemainingSharesView]]; + cell.tag = PostSettingsRowSocialRemainingShares; return cell; } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h index 6406aff1b79a..bccf3b37aa83 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h @@ -8,6 +8,7 @@ typedef enum { PostSettingsSectionStickyPost, PostSettingsSectionShare, PostSettingsSectionDisabledTwitter, // NOTE: Clean up when Twitter has been removed from Publicize services. + PostSettingsSectionSharesRemaining, PostSettingsSectionGeolocation, PostSettingsSectionMoreOptions } PostSettingsSection; diff --git a/WordPress/Classes/ViewRelated/Post/Revisions/RevisionsTableViewController.swift b/WordPress/Classes/ViewRelated/Post/Revisions/RevisionsTableViewController.swift index 83f96b3ee6bc..4a87ad95ad75 100644 --- a/WordPress/Classes/ViewRelated/Post/Revisions/RevisionsTableViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Revisions/RevisionsTableViewController.swift @@ -173,7 +173,7 @@ extension RevisionsTableViewController: WPTableViewHandlerDelegate { return ContextManager.sharedInstance().mainContext } - func fetchRequest() -> NSFetchRequest { + func fetchRequest() -> NSFetchRequest? { guard let postId = post?.postID, let siteId = post?.blog.dotComID else { preconditionFailure("Expected a postId or a siteId") } diff --git a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsFollowPresenter.swift b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsFollowPresenter.swift index d3d24c21c008..6736584f10a2 100644 --- a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsFollowPresenter.swift +++ b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsFollowPresenter.swift @@ -15,7 +15,7 @@ class ReaderCommentsFollowPresenter: NSObject { private let post: ReaderPost private weak var delegate: ReaderCommentsFollowPresenterDelegate? - private let presentingViewController: UIViewController + private unowned let presentingViewController: UIViewController private let followCommentsService: FollowCommentsService? // MARK: - Initialization diff --git a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m index ed8e238ddb43..baaf195788c2 100644 --- a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m +++ b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m @@ -1122,10 +1122,7 @@ - (void)configureCell:(UITableViewCell *)aCell atIndexPath:(NSIndexPath *)indexP __weak __typeof(self) weakSelf = self; cell.accessoryButtonAction = ^(UIView * _Nonnull sourceView) { - if (comment && [self isModerationMenuEnabledFor:comment]) { - // NOTE: Remove when minimum version is bumped to iOS 14. - [self showMenuSheetFor:comment indexPath:indexPath handler:weakSelf.tableViewHandler sourceView:sourceView]; - } else if (comment) { + if (comment) { [self shareComment:comment sourceView:sourceView]; } }; diff --git a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift index 68a2dd5b7d39..3c3edacd4b8f 100644 --- a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift @@ -65,13 +65,11 @@ extension NSNotification.Name { // if the comment can be moderated, show the context menu when tapping the accessory button. // Note that accessoryButtonAction will be ignored when the menu is assigned. - if #available (iOS 14.0, *) { - cell.accessoryButton.showsMenuAsPrimaryAction = isModerationMenuEnabled(for: comment) - cell.accessoryButton.menu = isModerationMenuEnabled(for: comment) ? menu(for: comment, - indexPath: indexPath, - handler: handler, - sourceView: cell.accessoryButton) : nil - } + cell.accessoryButton.showsMenuAsPrimaryAction = isModerationMenuEnabled(for: comment) + cell.accessoryButton.menu = isModerationMenuEnabled(for: comment) ? menu(for: comment, + indexPath: indexPath, + handler: handler, + sourceView: cell.accessoryButton) : nil cell.configure(with: comment, renderMethod: .richContent) { _ in // don't adjust cell height when it's already scrolled out of viewport. @@ -99,28 +97,6 @@ extension NSNotification.Name { present(activityViewController, animated: true, completion: nil) } - /// Shows a contextual menu through `UIPopoverPresentationController`. This is a fallback implementation for iOS 13, since the menu can't be - /// shown programmatically or through a single tap. - /// - /// NOTE: Remove this once we bump the minimum version to iOS 14. - /// - func showMenuSheet(for comment: Comment, indexPath: IndexPath, handler: WPTableViewHandler, sourceView: UIView?) { - let commentMenus = commentMenu(for: comment, indexPath: indexPath, handler: handler, sourceView: sourceView) - let menuViewController = MenuSheetViewController(items: commentMenus.map { menuSection in - // Convert ReaderCommentMenu to MenuSheetViewController.MenuItem - menuSection.map { $0.toMenuItem } - }) - - menuViewController.modalPresentationStyle = .popover - if let popoverPresentationController = menuViewController.popoverPresentationController { - popoverPresentationController.delegate = self - popoverPresentationController.sourceView = sourceView - popoverPresentationController.sourceRect = sourceView?.bounds ?? .null - } - - present(menuViewController, animated: true) - } - func isModerationMenuEnabled(for comment: Comment) -> Bool { return comment.allowsModeration() } @@ -408,18 +384,4 @@ enum ReaderCommentMenu { } } } - - /// NOTE: Remove when minimum version is bumped to iOS 14. - var toMenuItem: MenuSheetViewController.MenuItem { - switch self { - case .unapprove(let handler), - .spam(let handler), - .trash(let handler), - .edit(let handler), - .share(let handler): - return MenuSheetViewController.MenuItem(title: title, image: image) { - handler() - } - } - } } diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailCommentsTableViewDelegate.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailCommentsTableViewDelegate.swift index 4cf701ed5f16..362e679b03b8 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailCommentsTableViewDelegate.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailCommentsTableViewDelegate.swift @@ -7,7 +7,7 @@ class ReaderDetailCommentsTableViewDelegate: NSObject, UITableViewDataSource, UI private(set) var totalComments = 0 private var post: ReaderPost? - private var presentingViewController: UIViewController? + private weak var presentingViewController: UIViewController? private weak var buttonDelegate: BorderedButtonTableViewCellDelegate? private(set) var headerView: ReaderDetailCommentsHeader? var followButtonTappedClosure: (() ->Void)? diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift index 3de61d8d4301..bacec85bac52 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift @@ -340,9 +340,7 @@ class ReaderDetailFeaturedImageView: UIView, NibLoadable { navigationItem.standardAppearance = appearance navigationItem.compactAppearance = appearance navigationItem.scrollEdgeAppearance = appearance - if #available(iOS 15.0, *) { - navigationItem.compactScrollEdgeAppearance = appearance - } + navigationItem.compactScrollEdgeAppearance = appearance if isLoaded, imageView.image == nil { navBarTintColor = Styles.endTintColor diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift index e568c03ecef8..7109c297eb0e 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift @@ -1,7 +1,7 @@ import UIKit import AutomatticTracks -protocol ReaderDetailHeaderViewDelegate { +protocol ReaderDetailHeaderViewDelegate: AnyObject { func didTapBlogName() func didTapMenuButton(_ sender: UIView) func didTapHeaderAvatar() @@ -43,7 +43,7 @@ class ReaderDetailHeaderView: UIStackView, NibLoadable { /// Any interaction with the header is sent to the delegate /// - var delegate: ReaderDetailHeaderViewDelegate? + weak var delegate: ReaderDetailHeaderViewDelegate? func configure(for post: ReaderPost) { self.post = post diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailLikesView.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailLikesView.swift index 6e220b113af7..be506522818d 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailLikesView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailLikesView.swift @@ -1,6 +1,6 @@ import UIKit -protocol ReaderDetailLikesViewDelegate { +protocol ReaderDetailLikesViewDelegate: AnyObject { func didTapLikesView() } @@ -13,7 +13,7 @@ class ReaderDetailLikesView: UIView, NibLoadable { @IBOutlet private weak var selfAvatarImageView: CircularImageView! static let maxAvatarsDisplayed = 5 - var delegate: ReaderDetailLikesViewDelegate? + weak var delegate: ReaderDetailLikesViewDelegate? /// Stores the number of total likes _without_ adding the like from self. private var totalLikes: Int = 0 diff --git a/WordPress/Classes/ViewRelated/Reader/Manage/ReaderTagsTableViewModel.swift b/WordPress/Classes/ViewRelated/Reader/Manage/ReaderTagsTableViewModel.swift index 811f6f7b958b..b47e448c9ae6 100644 --- a/WordPress/Classes/ViewRelated/Reader/Manage/ReaderTagsTableViewModel.swift +++ b/WordPress/Classes/ViewRelated/Reader/Manage/ReaderTagsTableViewModel.swift @@ -27,7 +27,7 @@ extension ReaderTagsTableViewModel: WPTableViewHandlerDelegate { return context } - func fetchRequest() -> NSFetchRequest { + func fetchRequest() -> NSFetchRequest? { return ReaderTagTopic.tagsFetchRequest } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderCardsStreamViewController.swift b/WordPress/Classes/ViewRelated/Reader/ReaderCardsStreamViewController.swift index 151348ea572a..d408e6f715e6 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderCardsStreamViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderCardsStreamViewController.swift @@ -175,7 +175,7 @@ class ReaderCardsStreamViewController: ReaderStreamViewController { // MARK: - TableViewHandler - override func fetchRequest() -> NSFetchRequest { + override func fetchRequest() -> NSFetchRequest? { let fetchRequest = NSFetchRequest(entityName: ReaderCard.classNameWithoutNamespaces()) fetchRequest.sortDescriptors = sortDescriptorsForFetchRequest(ascending: true) return fetchRequest diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderFollowedSitesViewController.swift b/WordPress/Classes/ViewRelated/Reader/ReaderFollowedSitesViewController.swift index c97a5963bdc6..7bacde97f10e 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderFollowedSitesViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderFollowedSitesViewController.swift @@ -393,7 +393,7 @@ extension ReaderFollowedSitesViewController: WPTableViewHandlerDelegate { } - func fetchRequest() -> NSFetchRequest { + func fetchRequest() -> NSFetchRequest? { let fetchRequest = NSFetchRequest(entityName: "ReaderSiteTopic") fetchRequest.predicate = NSPredicate(format: "following = YES") diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderListStreamHeader.swift b/WordPress/Classes/ViewRelated/Reader/ReaderListStreamHeader.swift index 1f2659a9bc76..9529a374a07d 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderListStreamHeader.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderListStreamHeader.swift @@ -7,7 +7,7 @@ import WordPressShared.WPStyleGuide @IBOutlet fileprivate weak var detailLabel: UILabel! // Required by ReaderStreamHeader protocol. - open var delegate: ReaderStreamHeaderDelegate? + open weak var delegate: ReaderStreamHeaderDelegate? // MARK: - Lifecycle Methods diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderRecommendedSiteCardCell.swift b/WordPress/Classes/ViewRelated/Reader/ReaderRecommendedSiteCardCell.swift index 5f59ecec911c..3c30c5a570fe 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderRecommendedSiteCardCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderRecommendedSiteCardCell.swift @@ -9,7 +9,7 @@ class ReaderRecommendedSiteCardCell: UITableViewCell { @IBOutlet weak var descriptionLabel: UILabel! @IBOutlet weak var infoTrailingConstraint: NSLayoutConstraint! - var delegate: ReaderRecommendedSitesCardCellDelegate? + weak var delegate: ReaderRecommendedSitesCardCellDelegate? override func awakeFromNib() { super.awakeFromNib() @@ -110,6 +110,6 @@ class ReaderRecommendedSiteCardCell: UITableViewCell { } } -protocol ReaderRecommendedSitesCardCellDelegate { +protocol ReaderRecommendedSitesCardCellDelegate: AnyObject { func handleFollowActionForCell(_ cell: ReaderRecommendedSiteCardCell) } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderSearchSuggestionsViewController.swift b/WordPress/Classes/ViewRelated/Reader/ReaderSearchSuggestionsViewController.swift index c8483a61d375..7c6b37389444 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderSearchSuggestionsViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderSearchSuggestionsViewController.swift @@ -5,7 +5,7 @@ import WordPressShared /// Defines methods that a delegate should implement for clearing suggestions /// and for responding to a selected suggestion. /// -protocol ReaderSearchSuggestionsDelegate { +protocol ReaderSearchSuggestionsDelegate: AnyObject { func searchSuggestionsController(_ controller: ReaderSearchSuggestionsViewController, selectedItem: String) } @@ -28,7 +28,7 @@ class ReaderSearchSuggestionsViewController: UIViewController { } @objc var tableViewHandler: WPTableViewHandler! - var delegate: ReaderSearchSuggestionsDelegate? + weak var delegate: ReaderSearchSuggestionsDelegate? @objc let cellIdentifier = "CellIdentifier" @objc let rowAndButtonHeight = CGFloat(44.0) @@ -176,7 +176,7 @@ extension ReaderSearchSuggestionsViewController: WPTableViewHandlerDelegate { } - func fetchRequest() -> NSFetchRequest { + func fetchRequest() -> NSFetchRequest? { let request = NSFetchRequest(entityName: "ReaderSearchSuggestion") request.predicate = predicateForFetchRequest() request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)] diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderSiteStreamHeader.swift b/WordPress/Classes/ViewRelated/Reader/ReaderSiteStreamHeader.swift index 71cad564149b..b4437e4e2a05 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderSiteStreamHeader.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderSiteStreamHeader.swift @@ -36,7 +36,7 @@ fileprivate func > (lhs: T?, rhs: T?) -> Bool { @IBOutlet fileprivate weak var descriptionLabel: UILabel! @IBOutlet fileprivate weak var descriptionLabelTopConstraint: NSLayoutConstraint! - open var delegate: ReaderStreamHeaderDelegate? + open weak var delegate: ReaderStreamHeaderDelegate? fileprivate var defaultBlavatar = "blavatar-default" // MARK: - Lifecycle Methods diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderStreamHeader.swift b/WordPress/Classes/ViewRelated/Reader/ReaderStreamHeader.swift index d970a7c02cbf..137cbc363037 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderStreamHeader.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderStreamHeader.swift @@ -1,11 +1,11 @@ import Foundation -public protocol ReaderStreamHeaderDelegate: NSObjectProtocol { +@objc public protocol ReaderStreamHeaderDelegate { func handleFollowActionForHeader(_ header: ReaderStreamHeader, completion: @escaping () -> Void) } -public protocol ReaderStreamHeader: NSObjectProtocol { - var delegate: ReaderStreamHeaderDelegate? {get set} +@objc public protocol ReaderStreamHeader { + weak var delegate: ReaderStreamHeaderDelegate? {get set} func enableLoggedInFeatures(_ enable: Bool) func configureHeader(_ topic: ReaderAbstractTopic) } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderStreamViewController.swift b/WordPress/Classes/ViewRelated/Reader/ReaderStreamViewController.swift index 94b5ecbaa75a..fa55dfab257f 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderStreamViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderStreamViewController.swift @@ -1494,7 +1494,7 @@ extension ReaderStreamViewController: WPTableViewHandlerDelegate { } - func fetchRequest() -> NSFetchRequest { + func fetchRequest() -> NSFetchRequest? { let fetchRequest = NSFetchRequest(entityName: ReaderPost.classNameWithoutNamespaces()) fetchRequest.predicate = predicateForFetchRequest() fetchRequest.sortDescriptors = sortDescriptorsForFetchRequest() diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagStreamHeader.swift b/WordPress/Classes/ViewRelated/Reader/ReaderTagStreamHeader.swift index 0ce723d11d09..55278fcbe80f 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderTagStreamHeader.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagStreamHeader.swift @@ -5,7 +5,7 @@ import WordPressShared @IBOutlet fileprivate weak var titleLabel: UILabel! @IBOutlet fileprivate weak var followButton: UIButton! - open var delegate: ReaderStreamHeaderDelegate? + open weak var delegate: ReaderStreamHeaderDelegate? // MARK: - Lifecycle Methods open override func awakeFromNib() { diff --git a/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabViewController.swift b/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabViewController.swift index 5a2ef070bc2c..92b63fbc272b 100644 --- a/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabViewController.swift @@ -140,11 +140,13 @@ extension ReaderTabViewController { // MARK: Observing Quick Start extension ReaderTabViewController { private func startObservingQuickStart() { - NotificationCenter.default.addObserver(forName: .QuickStartTourElementChangedNotification, object: nil, queue: nil) { [weak self] notification in - if let info = notification.userInfo, - let element = info[QuickStartTourGuide.notificationElementKey] as? QuickStartTourElement { - self?.settingsButton.shouldShowSpotlight = element == .readerDiscoverSettings - } + NotificationCenter.default.addObserver(self, selector: #selector(handleQuickStartTourElementChangedNotification(_:)), name: .QuickStartTourElementChangedNotification, object: nil) + } + + @objc private func handleQuickStartTourElementChangedNotification(_ notification: Foundation.Notification) { + if let info = notification.userInfo, + let element = info[QuickStartTourGuide.notificationElementKey] as? QuickStartTourElement { + settingsButton.shouldShowSpotlight = element == .readerDiscoverSettings } } } diff --git a/WordPress/Classes/ViewRelated/Reader/Tags View/ReaderTopicCollectionViewCoordinator.swift b/WordPress/Classes/ViewRelated/Reader/Tags View/ReaderTopicCollectionViewCoordinator.swift index 5cc04a108593..f07b1623081d 100644 --- a/WordPress/Classes/ViewRelated/Reader/Tags View/ReaderTopicCollectionViewCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Reader/Tags View/ReaderTopicCollectionViewCoordinator.swift @@ -40,45 +40,46 @@ class ReaderTopicCollectionViewCoordinator: NSObject { weak var delegate: ReaderTopicCollectionViewCoordinatorDelegate? - let collectionView: UICollectionView + weak var collectionView: UICollectionView? + var topics: [String] { didSet { reloadData() } } - deinit { - guard let layout = collectionView.collectionViewLayout as? ReaderInterestsCollectionViewFlowLayout else { - return - } - - layout.isExpanded = false - layout.invalidateLayout() - } - init(collectionView: UICollectionView, topics: [String]) { self.collectionView = collectionView self.topics = topics super.init() - configureCollectionView() + configure(collectionView) + } + + func invalidate() { + guard let layout = collectionView?.collectionViewLayout as? ReaderInterestsCollectionViewFlowLayout else { + return + } + + layout.isExpanded = false + layout.invalidateLayout() } func reloadData() { - collectionView.reloadData() - collectionView.invalidateIntrinsicContentSize() + collectionView?.reloadData() + collectionView?.invalidateIntrinsicContentSize() } func changeState(_ state: ReaderTopicCollectionViewState) { - guard let layout = collectionView.collectionViewLayout as? ReaderInterestsCollectionViewFlowLayout else { + guard let layout = collectionView?.collectionViewLayout as? ReaderInterestsCollectionViewFlowLayout else { return } layout.isExpanded = state == .expanded } - private func configureCollectionView() { + private func configure(_ collectionView: UICollectionView) { collectionView.isAccessibilityElement = false collectionView.delegate = self collectionView.dataSource = self @@ -105,7 +106,7 @@ class ReaderTopicCollectionViewCoordinator: NSObject { layout.allowsCentering = false } - private func sizeForCell(title: String) -> CGSize { + private func sizeForCell(title: String, of collectionView: UICollectionView)-> CGSize { let attributes: [NSAttributedString.Key: Any] = [ .font: ReaderInterestsStyleGuide.compactCellLabelTitleFont ] @@ -193,7 +194,7 @@ extension ReaderTopicCollectionViewCoordinator: UICollectionViewDelegateFlowLayo } @objc func toggleExpanded(_ sender: ReaderInterestsCollectionViewCell) { - guard let layout = collectionView.collectionViewLayout as? ReaderInterestsCollectionViewFlowLayout else { + guard let layout = collectionView?.collectionViewLayout as? ReaderInterestsCollectionViewFlowLayout else { return } @@ -206,7 +207,7 @@ extension ReaderTopicCollectionViewCoordinator: UICollectionViewDelegateFlowLayo } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - return sizeForCell(title: topics[indexPath.row]) + return sizeForCell(title: topics[indexPath.row], of: collectionView) } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { @@ -225,6 +226,6 @@ extension ReaderTopicCollectionViewCoordinator: ReaderInterestsCollectionViewFlo func collectionView(_ collectionView: UICollectionView, layout: ReaderInterestsCollectionViewFlowLayout, sizeForOverflowItem at: IndexPath, remainingItems: Int?) -> CGSize { let title = string(for: remainingItems) - return sizeForCell(title: title) + return sizeForCell(title: title, of: collectionView) } } diff --git a/WordPress/Classes/ViewRelated/Reader/Tags View/TopicsCollectionView.swift b/WordPress/Classes/ViewRelated/Reader/Tags View/TopicsCollectionView.swift index 90d0e1484556..a6984eba92e2 100644 --- a/WordPress/Classes/ViewRelated/Reader/Tags View/TopicsCollectionView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Tags View/TopicsCollectionView.swift @@ -27,6 +27,10 @@ class TopicsCollectionView: DynamicHeightCollectionView { commonInit() } + deinit { + coordinator?.invalidate() + } + func commonInit() { collectionViewLayout = ReaderInterestsCollectionViewFlowLayout() diff --git a/WordPress/Classes/ViewRelated/Site Creation/Shared/UITableView+Header.swift b/WordPress/Classes/ViewRelated/Site Creation/Shared/UITableView+Header.swift index 9e4c28b3b6d5..716db7dca6e5 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Shared/UITableView+Header.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Shared/UITableView+Header.swift @@ -48,4 +48,31 @@ extension UITableView { self.tableHeaderView = tableHeaderView } } + + /// Resizes the `tableFooterView` to fit its content. + /// + /// The `tableFooterView` doesn't adjust its size automatically like a `UITableViewCell`, so this method + /// should be called whenever the `tableView`'s bounds changes or when the `tableFooterView` content changes. + /// + /// This method should typically be called in `UIViewController.viewDidLayoutSubviews`. + /// + /// Source: https://gist.github.com/smileyborg/50de5da1c921b73bbccf7f76b3694f6a + /// + func sizeToFitFooterView() { + guard let tableFooterView else { + return + } + let fittingSize = CGSize(width: bounds.width - (safeAreaInsets.left + safeAreaInsets.right), height: 0) + let size = tableFooterView.systemLayoutSizeFitting( + fittingSize, + withHorizontalFittingPriority: .required, + verticalFittingPriority: .fittingSizeLevel + ) + let newFrame = CGRect(origin: .zero, size: size) + if tableFooterView.frame.height != newFrame.height { + tableFooterView.frame = newFrame + self.tableFooterView = tableFooterView + } + } + } diff --git a/WordPress/Classes/ViewRelated/Stats/StatsForegroundObservable.swift b/WordPress/Classes/ViewRelated/Stats/StatsForegroundObservable.swift index 447af1b75cf2..63d45d12e1ef 100644 --- a/WordPress/Classes/ViewRelated/Stats/StatsForegroundObservable.swift +++ b/WordPress/Classes/ViewRelated/Stats/StatsForegroundObservable.swift @@ -4,9 +4,11 @@ protocol StatsForegroundObservable: AnyObject { func reloadStatsData() } +private var observerKey = 0 + extension StatsForegroundObservable where Self: UIViewController { func addWillEnterForegroundObserver() { - NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, + enterForegroundObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil) { [weak self] _ in self?.reloadStatsData() @@ -14,8 +16,18 @@ extension StatsForegroundObservable where Self: UIViewController { } func removeWillEnterForegroundObserver() { - NotificationCenter.default.removeObserver(self, - name: UIApplication.willEnterForegroundNotification, - object: nil) + if let enterForegroundObserver { + NotificationCenter.default.removeObserver(enterForegroundObserver) + } + enterForegroundObserver = nil + } + + private var enterForegroundObserver: NSObjectProtocol? { + get { + objc_getAssociatedObject(self, &observerKey) as? NSObjectProtocol + } + set { + objc_setAssociatedObject(self, &observerKey, newValue, .OBJC_ASSOCIATION_RETAIN) + } } } diff --git a/WordPress/Classes/ViewRelated/System/Action Sheet/ActionSheetViewController.swift b/WordPress/Classes/ViewRelated/System/Action Sheet/ActionSheetViewController.swift index 246ce62f37ab..0ec101e40121 100644 --- a/WordPress/Classes/ViewRelated/System/Action Sheet/ActionSheetViewController.swift +++ b/WordPress/Classes/ViewRelated/System/Action Sheet/ActionSheetViewController.swift @@ -33,8 +33,8 @@ class ActionSheetViewController: UIViewController { enum Button { static let height: CGFloat = 54 - static let contentInsets: UIEdgeInsets = UIEdgeInsets(top: 0, left: 18, bottom: 0, right: 35) - static let titleInsets: UIEdgeInsets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 0) + static let contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 18, bottom: 0, trailing: 35) + static let imagePadding: CGFloat = 16 static let imageTintColor: UIColor = .neutral(.shade30) static let font: UIFont = .preferredFont(forTextStyle: .callout) static let textColor: UIColor = .text @@ -147,27 +147,32 @@ class ActionSheetViewController: UIViewController { updateScrollViewHeight() } - private func createButton(_ handler: @escaping () -> Void) -> UIButton { - let button = UIButton(type: .custom, primaryAction: UIAction(handler: { _ in handler() })) - button.titleLabel?.font = Constants.Button.font - button.setTitleColor(Constants.Button.textColor, for: .normal) - button.imageView?.tintColor = Constants.Button.imageTintColor - button.setBackgroundImage(UIImage(color: .divider), for: .highlighted) - button.titleEdgeInsets = Constants.Button.titleInsets - button.naturalContentHorizontalAlignment = .leading - button.contentEdgeInsets = Constants.Button.contentInsets - button.translatesAutoresizingMaskIntoConstraints = false - button.flipInsetsForRightToLeftLayoutDirection() - button.titleLabel?.adjustsFontForContentSizeCategory = true - return button - } - private func button(_ info: ActionSheetButton) -> UIButton { - let button = createButton(info.action) - - button.setTitle(info.title, for: .normal) - button.setImage(info.image, for: .normal) + let button = UIButton(type: .system, primaryAction: UIAction(handler: { _ in info.action() })) + + button.configuration = { + var configuration = UIButton.Configuration.plain() + configuration.attributedTitle = { + var string = AttributedString(info.title) + string.font = Constants.Button.font + string.foregroundColor = Constants.Button.textColor + return string + }() + configuration.image = info.image + configuration.imageColorTransformer = UIConfigurationColorTransformer { _ in + Constants.Button.imageTintColor + } + configuration.imagePadding = Constants.Button.imagePadding + configuration.contentInsets = Constants.Button.contentInsets + configuration.background.cornerRadius = 0 + return configuration + }() + button.configurationUpdateHandler = { button in + button.configuration?.background.backgroundColor = button.isHighlighted ? .divider : .clear + } + button.contentHorizontalAlignment = .leading button.accessibilityIdentifier = info.identifier + button.translatesAutoresizingMaskIntoConstraints = false if let badge = info.badge { button.addSubview(badge) diff --git a/WordPress/Classes/ViewRelated/System/Action Sheet/BloggingPromptsHeaderView.swift b/WordPress/Classes/ViewRelated/System/Action Sheet/BloggingPromptsHeaderView.swift index 71dc149fbef0..471de1910ae1 100644 --- a/WordPress/Classes/ViewRelated/System/Action Sheet/BloggingPromptsHeaderView.swift +++ b/WordPress/Classes/ViewRelated/System/Action Sheet/BloggingPromptsHeaderView.swift @@ -91,15 +91,10 @@ private extension BloggingPromptsHeaderView { } func configureInsets() { - if #available(iOS 15.0, *) { - var config: UIButton.Configuration = .plain() - config.contentInsets = Constants.buttonContentInsets - answerPromptButton.configuration = config - shareButton.configuration = config - } else { - answerPromptButton.contentEdgeInsets = Constants.buttonContentEdgeInsets - shareButton.contentEdgeInsets = Constants.buttonContentEdgeInsets - } + var config: UIButton.Configuration = .plain() + config.contentInsets = Constants.buttonContentInsets + answerPromptButton.configuration = config + shareButton.configuration = config } func configure(_ prompt: BloggingPrompt?) { @@ -143,7 +138,6 @@ private extension BloggingPromptsHeaderView { static let promptSpacing: CGFloat = 8.0 static let answeredViewSpacing: CGFloat = 9.0 static let answerPromptButtonSpacing: CGFloat = 9.0 - static let buttonContentEdgeInsets = UIEdgeInsets(top: 16.0, left: 0.0, bottom: 16.0, right: 0.0) static let buttonContentInsets = NSDirectionalEdgeInsets(top: 16.0, leading: 0.0, bottom: 16.0, trailing: 0.0) } diff --git a/WordPress/Classes/ViewRelated/System/Notices/NoticePresenter.swift b/WordPress/Classes/ViewRelated/System/Notices/NoticePresenter.swift index 7546c7916a28..33d80c8c0616 100644 --- a/WordPress/Classes/ViewRelated/System/Notices/NoticePresenter.swift +++ b/WordPress/Classes/ViewRelated/System/Notices/NoticePresenter.swift @@ -1,4 +1,5 @@ import Foundation +import Combine import UIKit import UserNotifications import WordPressFlux @@ -59,6 +60,8 @@ class NoticePresenter { private var currentNoticePresentation: NoticePresentation? private var currentKeyboardPresentation: KeyboardPresentation = .notPresent + private var notificationObservers = Set() + init(store: NoticeStore = StoreContainer.shared.notice, animator: NoticeAnimator = NoticeAnimator(duration: Animations.appearanceDuration, springDampening: Animations.appearanceSpringDamping, springVelocity: NoticePresenter.Animations.appearanceSpringVelocity)) { self.store = store @@ -97,54 +100,62 @@ class NoticePresenter { // MARK: - Events private func listenToKeyboardEvents() { - NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: nil) { [weak self] (notification) in - guard let self = self, - let userInfo = notification.userInfo, - let keyboardFrameValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue, - let durationValue = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber else { - return - } - - self.currentKeyboardPresentation = .present(height: keyboardFrameValue.cgRectValue.size.height) - - guard let currentContainer = self.currentNoticePresentation?.containerView else { - return - } + NotificationCenter.default + .publisher(for: UIResponder.keyboardWillShowNotification) + .sink { [weak self] notification in + guard let self, + let userInfo = notification.userInfo, + let keyboardFrameValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue, + let durationValue = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber else { + return + } - UIView.animate(withDuration: durationValue.doubleValue, animations: { - currentContainer.bottomConstraint?.constant = self.onScreenNoticeContainerBottomConstraintConstant - self.view.layoutIfNeeded() - }) - } - NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: nil) { [weak self] (notification) in - self?.currentKeyboardPresentation = .notPresent + self.currentKeyboardPresentation = .present(height: keyboardFrameValue.cgRectValue.size.height) - guard let self = self, - let currentContainer = self.currentNoticePresentation?.containerView, - let userInfo = notification.userInfo, - let durationValue = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber else { + guard let currentContainer = self.currentNoticePresentation?.containerView else { return + } + + UIView.animate(withDuration: durationValue.doubleValue, animations: { + currentContainer.bottomConstraint?.constant = self.onScreenNoticeContainerBottomConstraintConstant + self.view.layoutIfNeeded() + }) } + .store(in: ¬ificationObservers) + + NotificationCenter.default + .publisher(for: UIResponder.keyboardWillHideNotification) + .sink { [weak self] notification in + self?.currentKeyboardPresentation = .notPresent + + guard let self, + let currentContainer = self.currentNoticePresentation?.containerView, + let userInfo = notification.userInfo, + let durationValue = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber else { + return + } - UIView.animate(withDuration: durationValue.doubleValue, animations: { - currentContainer.bottomConstraint?.constant = self.onScreenNoticeContainerBottomConstraintConstant - self.view.layoutIfNeeded() - }) - } + UIView.animate(withDuration: durationValue.doubleValue, animations: { + currentContainer.bottomConstraint?.constant = self.onScreenNoticeContainerBottomConstraintConstant + self.view.layoutIfNeeded() + }) + } + .store(in: ¬ificationObservers) } /// Adjust the current Notice so it will always be in the correct y-position after the /// device is rotated. private func listenToOrientationChangeEvents() { - let nc = NotificationCenter.default - nc.addObserver(forName: NSNotification.Name.WPTabBarHeightChanged, object: nil, queue: nil) { [weak self] _ in - guard let self = self, - let containerView = self.currentNoticePresentation?.containerView else { - return - } + NotificationCenter.default.publisher(for: .WPTabBarHeightChanged) + .sink { [weak self] _ in + guard let self = self, + let containerView = self.currentNoticePresentation?.containerView else { + return + } - containerView.bottomConstraint?.constant = -self.window.untouchableViewController.offsetOnscreen - } + containerView.bottomConstraint?.constant = -self.window.untouchableViewController.offsetOnscreen + } + .store(in: ¬ificationObservers) } /// Handle all changes in the `NoticeStore`. diff --git a/WordPress/Classes/ViewRelated/Views/MenuSheetViewController.swift b/WordPress/Classes/ViewRelated/Views/MenuSheetViewController.swift deleted file mode 100644 index a8b09c0c3f8f..000000000000 --- a/WordPress/Classes/ViewRelated/Views/MenuSheetViewController.swift +++ /dev/null @@ -1,171 +0,0 @@ -import UIKit -import WordPressUI - -/// Provides a fallback implementation for showing `UIMenu` in iOS 13. To "mimic" the `UIContextMenu` appearance, this -/// view controller should be presented modally with a `.popover` presentation style. Note that to simplify things, -/// nested elements will be displayed as if `UIMenuOptions.displayInline` is applied. -/// -/// In iOS 13, `UIMenu` can only appear through long press gesture. There is no way to make it appear programmatically -/// or through different gestures. However, in iOS 14 menus can be configured to appear on tap events. Refer to -/// `showsMenuAsPrimaryAction` for more details. -/// -/// TODO: Remove this component (and its usage) in favor of `UIMenu` when the minimum version is bumped to iOS 14. -/// -class MenuSheetViewController: UITableViewController { - - struct MenuItem { - let title: String - let image: UIImage? - let handler: () -> Void - let destructive: Bool - - init(title: String, image: UIImage? = nil, destructive: Bool = false, handler: @escaping () -> Void) { - self.title = title - self.image = image - self.handler = handler - self.destructive = destructive - } - - var foregroundColor: UIColor { - return destructive ? .error : .text - } - } - - private let itemSource: [[MenuItem]] - private let orientation: UIDeviceOrientation // used to track if orientation changes. - - // MARK: Lifecycle - - required init(items: [[MenuItem]]) { - self.itemSource = items - self.orientation = UIDevice.current.orientation - - super.init(style: .plain) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - configureTable() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - preferredContentSize = CGSize(width: min(tableView.contentSize.width, Constants.maxWidth), height: tableView.contentSize.height) - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - - // Dismiss the menu when the orientation changes. This mimics the behavior of UIContextMenu/UIMenu. - if UIDevice.current.orientation != orientation { - dismissMenu() - } - } - -} - -// MARK: - Table View - -extension MenuSheetViewController { - - override func numberOfSections(in tableView: UITableView) -> Int { - return itemSource.count - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - guard let items = itemSource[safe: section] else { - return 0 - } - return items.count - } - - /// Override separator color in dark mode so it kinda matches the separator color in `UIContextMenu`. - /// With system colors, somehow dark colors won't go darker below the cell's background color. - /// Note that returning nil means falling back to the default behavior. - override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - guard traitCollection.userInterfaceStyle == .dark else { - return nil - } - - let headerView = UIView() - headerView.backgroundColor = Constants.darkSeparatorColor - return headerView - } - - override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return section == 0 ? tableView.sectionHeaderHeight : Constants.tableSectionHeight - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let items = itemSource[safe: indexPath.section], - let item = items[safe: indexPath.row] else { - return .init() - } - - let cell = tableView.dequeueReusableCell(withIdentifier: Constants.cellIdentifier, for: indexPath) - cell.tintColor = item.foregroundColor - cell.textLabel?.textColor = item.foregroundColor - cell.textLabel?.setText(item.title) - cell.textLabel?.numberOfLines = 0 - cell.accessoryView = UIImageView(image: item.image?.withTintColor(.text)) - - return cell - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - - dismissMenu { - guard let items = self.itemSource[safe: indexPath.section], - let item = items[safe: indexPath.row] else { - return - } - - item.handler() - } - } -} - -// MARK: - Private Helpers - -private extension MenuSheetViewController { - struct Constants { - // maximum width follows the approximate width of `UIContextMenu`. - static let maxWidth: CGFloat = 250 - static let tableSectionHeight: CGFloat = 8 - static let darkSeparatorColor = UIColor(fromRGBColorWithRed: 11, green: 11, blue: 11) - static let cellIdentifier = "cell" - } - - func configureTable() { - tableView.register(UITableViewCell.self, forCellReuseIdentifier: Constants.cellIdentifier) - tableView.sectionHeaderHeight = 0 - tableView.bounces = false - - // draw the separators from edge to edge. - tableView.separatorInset = .zero - - // hide separators for the last row. - tableView.tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 0)) - } - - func dismissMenu(completion: (() -> Void)? = nil) { - if let controller = popoverPresentationController { - controller.delegate?.presentationControllerWillDismiss?(controller) - } - - dismiss(animated: true) { - defer { - if let controller = self.popoverPresentationController { - controller.delegate?.presentationControllerDidDismiss?(controller) - } - } - completion?() - } - } -} diff --git a/WordPress/Classes/ViewRelated/Views/PagingFooterView.swift b/WordPress/Classes/ViewRelated/Views/PagingFooterView.swift new file mode 100644 index 000000000000..61e80b6cef40 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Views/PagingFooterView.swift @@ -0,0 +1,56 @@ +import UIKit + +final class PagingFooterView: UIView { + enum State { + case loading, error + } + + let buttonRetry: UIButton = { + let button = UIButton(type: .system) + var configuration = UIButton.Configuration.plain() + configuration.title = Strings.retry + configuration.contentInsets = NSDirectionalEdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) + button.configuration = configuration + return button + }() + + private lazy var errorView: UIView = { + let label = UILabel() + label.font = UIFont.preferredFont(forTextStyle: .body) + label.textColor = .secondaryLabel + label.adjustsFontForContentSizeCategory = true + label.text = Strings.errorMessage + return UIStackView(arrangedSubviews: [label, UIView(), buttonRetry]) + }() + + private let spinner = UIActivityIndicatorView(style: .medium) + + init(state: State) { + super.init(frame: .zero) + + // Add errorView to ensure the footer has the same height in both states + addSubview(errorView) + errorView.translatesAutoresizingMaskIntoConstraints = false + errorView.isHidden = true + pinSubviewToAllEdges(errorView, insets: UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 0), priority: .init(999)) + + switch state { + case .error: + errorView.isHidden = false + case .loading: + addSubview(spinner) + spinner.startAnimating() + spinner.translatesAutoresizingMaskIntoConstraints = false + pinSubviewAtCenter(spinner) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private struct Strings { + static let errorMessage = NSLocalizedString("general.pagingFooterView.errorMessage", value: "An error occurred", comment: "A generic error message for a footer view in a list with pagination") + static let retry = NSLocalizedString("general.pagingFooterView.retry", value: "Retry", comment: "A footer retry button") + } +} diff --git a/WordPress/Classes/ViewRelated/Views/RichTextView/AnimatedGifAttachmentViewProvider.swift b/WordPress/Classes/ViewRelated/Views/RichTextView/AnimatedGifAttachmentViewProvider.swift index da4fa28c8a34..65b20773431c 100644 --- a/WordPress/Classes/ViewRelated/Views/RichTextView/AnimatedGifAttachmentViewProvider.swift +++ b/WordPress/Classes/ViewRelated/Views/RichTextView/AnimatedGifAttachmentViewProvider.swift @@ -5,7 +5,6 @@ import UIKit * This can be used by using: `NSTextAttachment.registerViewProviderClass` * */ -@available(iOS 15.0, *) class AnimatedGifAttachmentViewProvider: NSTextAttachmentViewProvider { deinit { guard let animatedImageView = view as? CachedAnimatedImageView else { diff --git a/WordPress/Classes/ViewRelated/Views/RichTextView/RichTextView.swift b/WordPress/Classes/ViewRelated/Views/RichTextView/RichTextView.swift index e17ea7ae1a6d..0ca9d176431f 100644 --- a/WordPress/Classes/ViewRelated/Views/RichTextView/RichTextView.swift +++ b/WordPress/Classes/ViewRelated/Views/RichTextView/RichTextView.swift @@ -163,9 +163,7 @@ import UniformTypeIdentifiers pinSubviewToAllEdges(textView) // Allow animatable gifs to be displayed - if #available(iOS 15.0, *) { - NSTextAttachment.registerViewProviderClass(AnimatedGifAttachmentViewProvider.self, forFileType: UTType.gif.identifier) - } + NSTextAttachment.registerViewProviderClass(AnimatedGifAttachmentViewProvider.self, forFileType: UTType.gif.identifier) } fileprivate func renderAttachments() { diff --git a/WordPress/Classes/WordPress.xcdatamodeld/.xccurrentversion b/WordPress/Classes/WordPress.xcdatamodeld/.xccurrentversion index ebc7bef3f617..42d23e05307f 100644 --- a/WordPress/Classes/WordPress.xcdatamodeld/.xccurrentversion +++ b/WordPress/Classes/WordPress.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - WordPress 150.xcdatamodel + WordPress 151.xcdatamodel diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 151.xcdatamodel/contents b/WordPress/Classes/WordPress.xcdatamodeld/WordPress 151.xcdatamodel/contents new file mode 100644 index 000000000000..6ec523b96192 --- /dev/null +++ b/WordPress/Classes/WordPress.xcdatamodeld/WordPress 151.xcdatamodel/contents @@ -0,0 +1,1040 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WordPress/Jetpack/AppImages.xcassets/Social/icon-instagram-business.imageset/Contents.json b/WordPress/Jetpack/AppImages.xcassets/Social/icon-instagram-business.imageset/Contents.json new file mode 100644 index 000000000000..16fc84e28443 --- /dev/null +++ b/WordPress/Jetpack/AppImages.xcassets/Social/icon-instagram-business.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon-instagram.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/Jetpack/AppImages.xcassets/Social/icon-instagram-business.imageset/icon-instagram.svg b/WordPress/Jetpack/AppImages.xcassets/Social/icon-instagram-business.imageset/icon-instagram.svg new file mode 100644 index 000000000000..69c10588109f --- /dev/null +++ b/WordPress/Jetpack/AppImages.xcassets/Social/icon-instagram-business.imageset/icon-instagram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/WordPress/Jetpack/AppImages.xcassets/Social/icon-mastodon.imageset/Contents.json b/WordPress/Jetpack/AppImages.xcassets/Social/icon-mastodon.imageset/Contents.json new file mode 100644 index 000000000000..65dfffd6be37 --- /dev/null +++ b/WordPress/Jetpack/AppImages.xcassets/Social/icon-mastodon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon-mastodon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/Jetpack/AppImages.xcassets/Social/icon-mastodon.imageset/icon-mastodon.svg b/WordPress/Jetpack/AppImages.xcassets/Social/icon-mastodon.imageset/icon-mastodon.svg new file mode 100644 index 000000000000..fcea8ed22762 --- /dev/null +++ b/WordPress/Jetpack/AppImages.xcassets/Social/icon-mastodon.imageset/icon-mastodon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/WordPress/Jetpack/AppImages.xcassets/Social/icon-tumblr.imageset/icon-tumblr-dark.svg b/WordPress/Jetpack/AppImages.xcassets/Social/icon-tumblr.imageset/icon-tumblr-dark.svg index 34eed67cc0b9..57292a204027 100644 --- a/WordPress/Jetpack/AppImages.xcassets/Social/icon-tumblr.imageset/icon-tumblr-dark.svg +++ b/WordPress/Jetpack/AppImages.xcassets/Social/icon-tumblr.imageset/icon-tumblr-dark.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/WordPress/Jetpack/AppImages.xcassets/Social/icon-warning.imageset/Contents.json b/WordPress/Jetpack/AppImages.xcassets/Social/icon-warning.imageset/Contents.json new file mode 100644 index 000000000000..5e1bdf242188 --- /dev/null +++ b/WordPress/Jetpack/AppImages.xcassets/Social/icon-warning.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon-warning.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/Jetpack/AppImages.xcassets/Social/icon-warning.imageset/icon-warning.svg b/WordPress/Jetpack/AppImages.xcassets/Social/icon-warning.imageset/icon-warning.svg new file mode 100644 index 000000000000..2fb81dfc2ef3 --- /dev/null +++ b/WordPress/Jetpack/AppImages.xcassets/Social/icon-warning.imageset/icon-warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Navigation/MigrationNavigationController.swift b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Navigation/MigrationNavigationController.swift index a0567d625138..997f044b38cd 100644 --- a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Navigation/MigrationNavigationController.swift +++ b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/Navigation/MigrationNavigationController.swift @@ -52,9 +52,7 @@ class MigrationNavigationController: UINavigationController { navigationBar.standardAppearance = standardAppearance navigationBar.scrollEdgeAppearance = scrollEdgeAppearance navigationBar.compactAppearance = standardAppearance - if #available(iOS 15.0, *) { - navigationBar.compactScrollEdgeAppearance = scrollEdgeAppearance - } + navigationBar.compactScrollEdgeAppearance = scrollEdgeAppearance navigationBar.isTranslucent = true listenForStateChanges() } diff --git a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Delete WordPress/MigrationDeleteWordPressViewController.swift b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Delete WordPress/MigrationDeleteWordPressViewController.swift index 82a7472e99fe..42a27de4b6ea 100644 --- a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Delete WordPress/MigrationDeleteWordPressViewController.swift +++ b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Delete WordPress/MigrationDeleteWordPressViewController.swift @@ -63,9 +63,7 @@ final class MigrationDeleteWordPressViewController: UIViewController { navigationItem.standardAppearance = appearance navigationItem.scrollEdgeAppearance = appearance navigationItem.compactAppearance = appearance - if #available(iOS 15.0, *) { - navigationItem.compactScrollEdgeAppearance = appearance - } + navigationItem.compactScrollEdgeAppearance = appearance } private func setupDismissButton() { diff --git a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Welcome/MigrationWelcomeViewController.swift b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Welcome/MigrationWelcomeViewController.swift index 383b6cb2bf73..e27c55ea2f8a 100644 --- a/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Welcome/MigrationWelcomeViewController.swift +++ b/WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Welcome/MigrationWelcomeViewController.swift @@ -116,13 +116,7 @@ final class MigrationWelcomeViewController: UIViewController { static let tableViewLeadingMargin = CGFloat(30) /// Used for the `tableHeaderView` layout guide margins. - static let tableHeaderViewMargins: NSDirectionalEdgeInsets = { - var insets = NSDirectionalEdgeInsets(top: 20, leading: 30, bottom: 30, trailing: 30) - if #available(iOS 15, *) { - insets.top = 0 - } - return insets - }() + static let tableHeaderViewMargins = NSDirectionalEdgeInsets(top: 0, leading: 30, bottom: 30, trailing: 30) } } diff --git a/WordPress/Jetpack/Resources/release_notes.txt b/WordPress/Jetpack/Resources/release_notes.txt index 3c0fb643df2b..42816e59d62b 100644 --- a/WordPress/Jetpack/Resources/release_notes.txt +++ b/WordPress/Jetpack/Resources/release_notes.txt @@ -1,6 +1,8 @@ -We fixed an issue with the home screen’s “Work on a draft post” card. The app will no longer crash when you access drafts while they’re in the middle of uploading. +* [*] Blogging Reminders: Disabled prompt for self-hosted sites not connected to Jetpack. [#20970] +* [**] [internal] Do not save synced blogs if the app has signed out. [#20959] +* [**] [internal] Make sure synced posts are saved before calling completion block. [#20960] +* [**] [internal] Fix observing Quick Start notifications. [#20997] +* [**] [internal] Fixed an issue that was causing a memory leak in the domain selection flow. [#20813] +* [*] [Jetpack-only] Block editor: Rename "Reusable blocks" to "Synced patterns", aligning with the web editor. [https://github.com/wordpress-mobile/gutenberg-mobile/pull/5885] +* [**] [internal] Block editor: Fix a crash related to Reanimated when closing the editor [https://github.com/wordpress-mobile/gutenberg-mobile/pull/5938] -We also solved a couple of problems in the block editor. - -- Right on—image blocks now display the correct aspect ratio, whether or not the image has a set width and height. -- When you’re dictating text, the cursor’s position will stay where it’s supposed to—no more jumping around. Keep calm and dictate on. diff --git a/WordPress/Resources/en.lproj/Localizable.strings b/WordPress/Resources/en.lproj/Localizable.strings index 863ed3860a00..73a4d4d9dbb8 100644 --- a/WordPress/Resources/en.lproj/Localizable.strings +++ b/WordPress/Resources/en.lproj/Localizable.strings @@ -230,11 +230,8 @@ /* translators: accessibility text for blocks with invalid content. %d: localized block title */ "%s block. This block has invalid content" = "%s block. This block has invalid content"; -/* translators: %s: name of the reusable block */ -"%s converted to regular block" = "%s converted to regular block"; - -/* translators: %s: name of the reusable block */ -"%s converted to regular blocks" = "%s converted to regular blocks"; +/* translators: %s: name of the synced block */ +"%s detached" = "%s detached"; /* translators: %s: embed block variant's label e.g: \"Twitter\". */ "%s embed block previews are coming soon" = "%s embed block previews are coming soon"; @@ -770,10 +767,10 @@ "Alt Text" = "Alt Text"; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit these blocks separately by tapping “Convert to regular blocks”." = "Alternatively, you can detach and edit these blocks separately by tapping “Convert to regular blocks”."; +"Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”." = "Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”."; /* No comment provided by engineer. */ -"Alternatively, you can detach and edit this block separately by tapping “Convert to regular block”." = "Alternatively, you can detach and edit this block separately by tapping “Convert to regular block”."; +"Alternatively, you can detach and edit this block separately by tapping “Detach pattern”." = "Alternatively, you can detach and edit this block separately by tapping “Detach pattern”."; /* Instruction text to explain to help users type their password instead of using magic link login option. */ "Alternatively, you may enter the password for this account." = "Alternatively, you may enter the password for this account."; @@ -1173,9 +1170,6 @@ /* All Time Stats 'Best views ever' label */ "Best views ever" = "Best views ever"; -/* Text for related post cell preview */ -"Big iPhone/iPad Update Now Available" = "Big iPhone/iPad Update Now Available"; - /* Notice that a page without content has been created */ "Blank page created" = "Blank page created"; @@ -1188,6 +1182,12 @@ /* Title displayed when there are no Blaze campaigns to display. */ "blaze.campaigns.empty.title" = "You have no campaigns"; +/* Text displayed when there is a failure loading Blaze campaigns. */ +"blaze.campaigns.errorMessage" = "There was an error loading campaigns."; + +/* Title for the view when there's an error loading Blaze campiagns. */ +"blaze.campaigns.errorTitle" = "Oops"; + /* Displayed while Blaze campaigns are being loaded. */ "blaze.campaigns.loading.title" = "Loading campaigns..."; @@ -1240,10 +1240,10 @@ "blazeCampaign.status.completed" = "Completed"; /* Short status description */ -"blazeCampaign.status.created" = "Created"; +"blazeCampaign.status.inmoderation" = "In Moderation"; /* Short status description */ -"blazeCampaign.status.inmoderation" = "In Moderation"; +"blazeCampaign.status.processing" = "Processing"; /* Short status description */ "blazeCampaign.status.rejected" = "Rejected"; @@ -2518,6 +2518,9 @@ marketing activities based on your consent and our legitimate interest."; /* Title for the Pages dashboard card. */ "dashboardCard.Pages.title" = "Pages"; +/* Title for the View stats button in the More menu */ +"dashboardCard.stats.viewStats" = "View stats"; + /* Title for a threat that includes the number of database rows affected */ "Database %1$d threats" = "Database %1$d threats"; @@ -3066,10 +3069,10 @@ marketing activities based on your consent and our legitimate interest."; "Edit video" = "Edit video"; /* translators: %s: name of the host app (e.g. WordPress) */ -"Editing reusable blocks is not yet supported on %s for Android" = "Editing reusable blocks is not yet supported on %s for Android"; +"Editing synced patterns is not yet supported on %s for Android" = "Editing synced patterns is not yet supported on %s for Android"; /* translators: %s: name of the host app (e.g. WordPress) */ -"Editing reusable blocks is not yet supported on %s for iOS" = "Editing reusable blocks is not yet supported on %s for iOS"; +"Editing synced patterns is not yet supported on %s for iOS" = "Editing synced patterns is not yet supported on %s for iOS"; /* Editing GIF alert message. */ "Editing this GIF will remove its animation." = "Editing this GIF will remove its animation."; @@ -3490,6 +3493,9 @@ marketing activities based on your consent and our legitimate interest."; /* Option to select the Fastmail app when logging in with magic links */ "Fastmail" = "Fastmail"; +/* Title of screen the displays the details of an advertisement campaign. */ +"feature.blaze.campaignDetails.title" = "Campaign Details"; + /* Name of a feature that allows the user to promote their posts. */ "feature.blaze.title" = "Blaze"; @@ -3752,6 +3758,12 @@ marketing activities based on your consent and our legitimate interest."; Title for the general section in site settings screen */ "General" = "General"; +/* A generic error message for a footer view in a list with pagination */ +"general.pagingFooterView.errorMessage" = "An error occurred"; + +/* A footer retry button */ +"general.pagingFooterView.retry" = "Retry"; + /* Title. A call to action to generate a new invite link. */ "Generate new link" = "Generate new link"; @@ -4182,15 +4194,6 @@ marketing activities based on your consent and our legitimate interest."; /* Footer for the Serve images from our servers setting */ "Improve your site's speed by only loading images visible on the screen. New images will load just before they scroll into view. This prevents viewers from having to download all the images on a page all at once, even ones they can't see." = "Improve your site's speed by only loading images visible on the screen. New images will load just before they scroll into view. This prevents viewers from having to download all the images on a page all at once, even ones they can't see."; -/* Text for related post cell preview */ -"in \"Apps\"" = "in \"Apps\""; - -/* Text for related post cell preview */ -"in \"Mobile\"" = "in \"Mobile\""; - -/* Text for related post cell preview */ -"in \"Upgrade\"" = "in \"Upgrade\""; - /* Explain what is the purpose of the tagline */ "In a few words, explain what this site is about." = "In a few words, explain what this site is about."; @@ -5531,15 +5534,15 @@ Please install the %3$@ to use the app with this site."; /* Title for the card displaying draft posts. */ "my-sites.drafts.card.title" = "Work on a draft post"; -/* The part in the title that should be highlighted. */ -"my-sites.drafts.card.title.hint" = "draft post"; +/* Title for the View all drafts button in the More menu */ +"my-sites.drafts.card.viewAllDrafts" = "View all drafts"; + +/* Title for the View all scheduled drafts button in the More menu */ +"my-sites.scheduled.card.viewAllScheduledPosts" = "View all scheduled posts"; /* Title for the card displaying today's stats. */ "my-sites.stats.card.title" = "Today's Stats"; -/* The part of the title that needs to be emphasized */ -"my-sites.stats.card.title.hint" = "Stats"; - /* Accessibility label for the Email text field. Header for a comment author's name, shown when editing a comment. Name text field placeholder @@ -6074,7 +6077,6 @@ Please install the %3$@ to use the app with this site."; /* An informal exclaimation that means `something went wrong`. Title for the view when there's an error loading a comment. Title for the view when there's an error loading Activity Log - Title for the view when there's an error loading Blaze campiagns. Title for the view when there's an error loading blogging prompts. Title for the view when there's an error loading scan status Title for the view when there's an error loading the history @@ -6718,6 +6720,15 @@ Please install the %3$@ to use the app with this site."; /* Section title for the disabled Twitter service in the Post Settings screen */ "postSettings.section.disabledTwitter.header" = "Twitter Auto-Sharing Is No Longer Available"; +/* Title for the button to subscribe to Jetpack Social on the remaining shares view */ +"postsettings.social.remainingshares.subscribe" = "Subscribe now to share more"; + +/* Beginning text of the remaining social shares a user has left. %1$d is their current remaining shares. %2$d is their share limit. This text is combined with ' in the next 30 days' if there is no warning displayed. */ +"postsettings.social.remainingshares.text.format" = "%1$d/%2$d social shares remaining"; + +/* The second half of the remaining social shares a user has. This is only displayed when there is no social limit warning. */ +"postsettings.social.remainingshares.text.part" = " in the next 30 days"; + /* Subtitle for placeholder in Tenor picker. `The company name 'Tenor' should always be written as it is. */ "Powered by Tenor" = "Powered by Tenor"; @@ -6734,7 +6745,6 @@ Please install the %3$@ to use the app with this site."; "Preparing..." = "Preparing..."; /* Displays the Post Preview Interface - Section title for related posts section preview Title for button to preview a selected layout Title for screen to preview a selected homepage design. Title for screen to preview a static content. */ @@ -7073,13 +7083,50 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Displayed in the Notifications Tab as a message, when the Unread Filter shows no notifications */ "Reignite the conversation: write a new post." = "Reignite the conversation: write a new post."; -/* Label for Related Post header preview - Label for selecting the related posts options - Title for screen that allows configuration of your blog/site related posts settings. */ +/* Label for selecting the related posts options */ "Related Posts" = "Related Posts"; /* Information of what related post are and how they are presented */ -"Related Posts displays relevant content from your site below your posts" = "Related Posts displays relevant content from your site below your posts"; +"relatedPostsSettings.optionsFooter" = "Related Posts displays relevant content from your site below your posts"; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview1.details" = "in \"Mobile\""; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview1.title" = "Big iPhone/iPad Update Now Available"; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview2.details" = "in \"Apps\""; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview2.title" = "The WordPress for Android App Gets a Big Facelift"; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview3.details" = "in \"Upgrade\""; + +/* Text for related post cell preview */ +"relatedPostsSettings.preview3.title" = "Upgrade Focus: VideoPress For Weddings"; + +/* Section title for related posts section preview */ +"relatedPostsSettings.previewsHeaders" = "Preview"; + +/* Label for Related Post header preview */ +"relatedPostsSettings.relatedPostsHeader" = "Related Posts"; + +/* Message to show when setting save failed */ +"relatedPostsSettings.settingsUpdateFailed" = "Settings update failed"; + +/* Label for configuration switch to show/hide the header for the related posts section */ +"relatedPostsSettings.showHeader" = "Show Header"; + +/* Label for configuration switch to enable/disable related posts */ +"relatedPostsSettings.showRelatedPosts" = "Show Related Posts"; + +/* Label for configuration switch to show/hide images thumbnail for the related posts */ +"relatedPostsSettings.showThumbnail" = "Show Images"; + +/* Title for screen that allows configuration of your blog/site related posts settings. */ +"relatedPostsSettings.title" = "Related Posts"; /* Button title on the blogging prompt's feature introduction view to set a reminder. */ "Remind me" = "Remind me"; @@ -7328,9 +7375,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Share extension error dialog cancel button text */ "Return to post" = "Return to post"; -/* No comment provided by engineer. */ -"Reusable" = "Reusable"; - /* Cancels a pending Email Change */ "Revert Pending Change" = "Revert Pending Change"; @@ -7865,12 +7909,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Share extension error dialog title. */ "Sharing Error" = "Sharing Error"; -/* Label for configuration switch to show/hide the header for the related posts section */ -"Show Header" = "Show Header"; - -/* Label for configuration switch to show/hide images thumbnail for the related posts */ -"Show Images" = "Show Images"; - /* Title for the `show like button` setting */ "Show Like button" = "Show Like button"; @@ -7890,9 +7928,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the `show reblog button` setting */ "Show Reblog button" = "Show Reblog button"; -/* Label for configuration switch to enable/disable related posts */ -"Show Related Posts" = "Show Related Posts"; - /* Alert title picking theme type to browse */ "Show themes:" = "Show themes:"; @@ -8923,9 +8958,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Message shown when a video failed to load while trying to add it to the Media library. */ "The video could not be added to the Media Library." = "The video could not be added to the Media Library."; -/* Text for related post cell preview */ -"The WordPress for Android App Gets a Big Facelift" = "The WordPress for Android App Gets a Big Facelift"; - /* Example post title used in the login prologue screens. This is a post about football fans. */ "The World's Best Fans" = "The World's Best Fans"; @@ -9019,9 +9051,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Text displayed when there is a failure loading the activity feed */ "There was an error loading activities" = "There was an error loading activities"; -/* Text displayed when there is a failure loading Blaze campaigns. */ -"There was an error loading campaigns." = "There was an error loading campaigns."; - /* Text displayed when there is a failure loading the plan list */ "There was an error loading plans" = "There was an error loading plans"; @@ -9759,9 +9788,6 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Text displayed in HUD while a draft or scheduled post is being updated. */ "Updating..." = "Updating..."; -/* Text for related post cell preview */ -"Upgrade Focus: VideoPress For Weddings" = "Upgrade Focus: VideoPress For Weddings"; - /* No comment provided by engineer. */ "Upgrade your plan to upload audio" = "Upgrade your plan to upload audio"; diff --git a/WordPress/Resources/id.lproj/Localizable.strings b/WordPress/Resources/id.lproj/Localizable.strings index b57ee70158fa..200041fc6e3a 100644 --- a/WordPress/Resources/id.lproj/Localizable.strings +++ b/WordPress/Resources/id.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-06-27 09:54:29+0000 */ +/* Translation-Revision-Date: 2023-06-30 15:06:09+0000 */ /* Plural-Forms: nplurals=2; plural=n > 1; */ /* Generator: GlotPress/4.0.0-alpha.4 */ /* Language: id */ @@ -9728,6 +9728,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Short status description */ "blazeCampaign.status.rejected" = "Ditolak"; +/* Short status description */ +"blazeCampaign.status.scheduled" = "Terjadwal"; + /* Title for budget stats view */ "blazeCampaigns.budget" = "Anggaran:"; diff --git a/WordPress/Resources/release_notes.txt b/WordPress/Resources/release_notes.txt index 3c0fb643df2b..5622d406fc18 100644 --- a/WordPress/Resources/release_notes.txt +++ b/WordPress/Resources/release_notes.txt @@ -1,6 +1,7 @@ -We fixed an issue with the home screen’s “Work on a draft post” card. The app will no longer crash when you access drafts while they’re in the middle of uploading. +* [*] Blogging Reminders: Disabled prompt for self-hosted sites not connected to Jetpack. [#20970] +* [**] [internal] Do not save synced blogs if the app has signed out. [#20959] +* [**] [internal] Make sure synced posts are saved before calling completion block. [#20960] +* [**] [internal] Fix observing Quick Start notifications. [#20997] +* [**] [internal] Fixed an issue that was causing a memory leak in the domain selection flow. [#20813] +* [**] [internal] Block editor: Fix a crash related to Reanimated when closing the editor [https://github.com/wordpress-mobile/gutenberg-mobile/pull/5938] -We also solved a couple of problems in the block editor. - -- Right on—image blocks now display the correct aspect ratio, whether or not the image has a set width and height. -- When you’re dictating text, the cursor’s position will stay where it’s supposed to—no more jumping around. Keep calm and dictate on. diff --git a/WordPress/Resources/ro.lproj/Localizable.strings b/WordPress/Resources/ro.lproj/Localizable.strings index d8ed769b33d1..28720b3d26f6 100644 --- a/WordPress/Resources/ro.lproj/Localizable.strings +++ b/WordPress/Resources/ro.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2023-06-26 13:11:37+0000 */ +/* Translation-Revision-Date: 2023-07-02 16:09:18+0000 */ /* Plural-Forms: nplurals=3; plural=(n == 1) ? 0 : ((n == 0 || n % 100 >= 2 && n % 100 <= 19) ? 1 : 2); */ /* Generator: GlotPress/4.0.0-alpha.4 */ /* Language: ro */ @@ -2314,7 +2314,7 @@ translators: Block name. %s: The localized block name */ /* Label for selecting the default post format Title for screen to select a default post format for a blog */ -"Default Post Format" = "Format articol implicit"; +"Default Post Format" = "Format implicit pentru articole"; /* Placeholder for the reader CSS URL */ "Default URL" = "URL implicit"; diff --git a/WordPress/UITests/Flows/LoginFlow.swift b/WordPress/UITests/Flows/LoginFlow.swift index 5d778d81a22c..794be5eff30c 100644 --- a/WordPress/UITests/Flows/LoginFlow.swift +++ b/WordPress/UITests/Flows/LoginFlow.swift @@ -10,7 +10,6 @@ class LoginFlow { .proceedWith(email: email) .proceedWithValidPassword() .continueWithSelectedSite(title: selectedSiteTitle) - .dismissNotificationAlertIfNeeded() } // Login with self-hosted site via Site Address. @@ -21,7 +20,6 @@ class LoginFlow { .proceedWith(siteUrl: siteUrl) .proceedWith(username: username, password: password) .continueWithSelectedSite() - .dismissNotificationAlertIfNeeded() } // Login with WP site via Site Address. @@ -33,7 +31,6 @@ class LoginFlow { .proceedWith(email: email) .proceedWithValidPassword() .continueWithSelectedSite() - .dismissNotificationAlertIfNeeded() } // Login with self-hosted site via Site Address. diff --git a/WordPress/UITests/README.md b/WordPress/UITests/README.md index 97e898bd1e7a..e2d66a9a0e5d 100644 --- a/WordPress/UITests/README.md +++ b/WordPress/UITests/README.md @@ -32,8 +32,7 @@ The following flows are covered/planned to be covered by UI tests. Tests that ar - [x] Publish Basic Public Post with Category and Tag - [x] Add and Remove Featured Image - [x] Add Gallery Block - - [ ] Add Image Block - - [ ] Add Video Block + - [x] Add Media Blocks (Image, Video and Audio) - [ ] Create Scheduled Post - [ ] Pages: - [ ] Create Page from Layout diff --git a/WordPress/UITests/Tests/EditorGutenbergTests.swift b/WordPress/UITests/Tests/EditorGutenbergTests.swift index 7fa174656578..0dabfb3c009d 100644 --- a/WordPress/UITests/Tests/EditorGutenbergTests.swift +++ b/WordPress/UITests/Tests/EditorGutenbergTests.swift @@ -10,10 +10,9 @@ class EditorGutenbergTests: XCTestCase { email: WPUITestCredentials.testWPcomUserEmail, password: WPUITestCredentials.testWPcomPassword ) - try EditorFlow - .goToMySiteScreen() - .tabBar.gotoBlockEditorScreen() - .dismissNotificationAlertIfNeeded(.accept) + + try TabNavComponent() + .gotoBlockEditorScreen() } override func tearDownWithError() throws { @@ -22,6 +21,8 @@ class EditorGutenbergTests: XCTestCase { let title = "Rich post title" let content = "Some text, and more text" + let videoUrlPath = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" + let audioUrlPath = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4" func testTextPostPublish() throws { @@ -77,4 +78,12 @@ class EditorGutenbergTests: XCTestCase { .addImageGallery() .verifyContentStructure(blocks: 2, words: content.components(separatedBy: " ").count, characters: content.count) } + + func testAddMediaBlocks() throws { + try BlockEditorScreen() + .addImage() + .addVideoFromUrl(urlPath: videoUrlPath) + .addAudioFromUrl(urlPath: audioUrlPath) + .verifyMediaBlocksDisplayed() + } } diff --git a/WordPress/UITestsFoundation/Screens/ActionSheetComponent.swift b/WordPress/UITestsFoundation/Screens/ActionSheetComponent.swift index 3f2bb2ce082a..d9675b8dac68 100644 --- a/WordPress/UITestsFoundation/Screens/ActionSheetComponent.swift +++ b/WordPress/UITestsFoundation/Screens/ActionSheetComponent.swift @@ -18,8 +18,7 @@ public class ActionSheetComponent: ScreenObject { public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [Self.getBlogPostButton, Self.getSitePageButton], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/ActivityLogScreen.swift b/WordPress/UITestsFoundation/Screens/ActivityLogScreen.swift index f7d13c501c19..604702eac863 100644 --- a/WordPress/UITestsFoundation/Screens/ActivityLogScreen.swift +++ b/WordPress/UITestsFoundation/Screens/ActivityLogScreen.swift @@ -20,8 +20,7 @@ public class ActivityLogScreen: ScreenObject { try super.init( expectedElementGetters: [ dateRangeButtonGetter, activityTypeButtonGetter ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/CommentsScreen.swift b/WordPress/UITestsFoundation/Screens/CommentsScreen.swift index a56aa97cda52..da892230e8b4 100644 --- a/WordPress/UITestsFoundation/Screens/CommentsScreen.swift +++ b/WordPress/UITestsFoundation/Screens/CommentsScreen.swift @@ -29,8 +29,7 @@ public class CommentsScreen: ScreenObject { navigationBarTitleGetter, replyFieldGetter ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/DomainsScreen.swift b/WordPress/UITestsFoundation/Screens/DomainsScreen.swift index f604112f60b3..0227cdfff314 100644 --- a/WordPress/UITestsFoundation/Screens/DomainsScreen.swift +++ b/WordPress/UITestsFoundation/Screens/DomainsScreen.swift @@ -15,8 +15,7 @@ public class DomainsScreen: ScreenObject { try super.init( expectedElementGetters: [ siteDomainsNavbarHeaderGetter ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/DomainsSuggestionsScreen.swift b/WordPress/UITestsFoundation/Screens/DomainsSuggestionsScreen.swift index b9de5528d52b..eedb233add83 100644 --- a/WordPress/UITestsFoundation/Screens/DomainsSuggestionsScreen.swift +++ b/WordPress/UITestsFoundation/Screens/DomainsSuggestionsScreen.swift @@ -12,8 +12,7 @@ public class DomainsSuggestionsScreen: ScreenObject { public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [ siteDomainsNavbarHeaderGetter ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Editor/AztecEditorScreen.swift b/WordPress/UITestsFoundation/Screens/Editor/AztecEditorScreen.swift index ea9281f7b287..9d9f467fb947 100644 --- a/WordPress/UITestsFoundation/Screens/Editor/AztecEditorScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Editor/AztecEditorScreen.swift @@ -49,8 +49,7 @@ public class AztecEditorScreen: ScreenObject { try super.init( expectedElementGetters: [ textViewGetter(textField) ], - app: app, - waitTimeout: 7 + app: app ) showOptionsStrip() diff --git a/WordPress/UITestsFoundation/Screens/Editor/BlockEditorScreen.swift b/WordPress/UITestsFoundation/Screens/Editor/BlockEditorScreen.swift index e8b37d253bb5..fbb2d566ca6f 100644 --- a/WordPress/UITestsFoundation/Screens/Editor/BlockEditorScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Editor/BlockEditorScreen.swift @@ -8,19 +8,27 @@ public class BlockEditorScreen: ScreenObject { $0.navigationBars["Gutenberg Editor Navigation Bar"].buttons["Close"] } - var editorCloseButton: XCUIElement { editorCloseButtonGetter(app) } - let addBlockButtonGetter: (XCUIApplication) -> XCUIElement = { - $0.buttons["add-block-button"] // Uses a testID + $0.buttons["add-block-button"] } - var addBlockButton: XCUIElement { addBlockButtonGetter(app) } - let moreButtonGetter: (XCUIApplication) -> XCUIElement = { $0.buttons["more_post_options"] } + let insertFromUrlButtonGetter: (XCUIApplication) -> XCUIElement = { + $0.buttons["Insert from URL"] + } + + let applyButtonGetter: (XCUIApplication) -> XCUIElement = { + $0.buttons["Apply"] + } + + var editorCloseButton: XCUIElement { editorCloseButtonGetter(app) } + var addBlockButton: XCUIElement { addBlockButtonGetter(app) } var moreButton: XCUIElement { moreButtonGetter(app) } + var insertFromUrlButton: XCUIElement { insertFromUrlButtonGetter(app) } + var applyButton: XCUIElement { applyButtonGetter(app) } public init(app: XCUIApplication = XCUIApplication()) throws { // The block editor has _many_ elements but most are loaded on-demand. To verify the screen @@ -28,8 +36,7 @@ public class BlockEditorScreen: ScreenObject { // expect to encase the screen. try super.init( expectedElementGetters: [ editorCloseButtonGetter, addBlockButtonGetter ], - app: app, - waitTimeout: 10 + app: app ) } @@ -80,6 +87,44 @@ public class BlockEditorScreen: ScreenObject { return self } + public func addVideoFromUrl(urlPath: String) -> Self { + addMediaBlockFromUrl( + blockType: "Video block", + UrlPath: urlPath + ) + + return self + } + + public func addAudioFromUrl(urlPath: String) -> Self { + addMediaBlockFromUrl( + blockType: "Audio block", + UrlPath: urlPath + ) + + return self + } + + private func addMediaBlockFromUrl(blockType: String, UrlPath: String) { + addBlock(blockType) + insertFromUrlButton.tap() + app.textFields.element.typeText(UrlPath) + applyButton.tap() + } + + @discardableResult + public func verifyMediaBlocksDisplayed() -> Self { + let imagePredicate = NSPredicate(format: "label == 'Image Block. Row 1'") + let videoPredicate = NSPredicate(format: "label == 'Video Block. Row 2'") + let audioPredicate = NSPredicate(format: "label == 'Audio Block. Row 3'") + + XCTAssertTrue(app.buttons.containing(imagePredicate).firstMatch.exists) + XCTAssertTrue(app.buttons.containing(videoPredicate).firstMatch.exists) + XCTAssertTrue(app.buttons.containing(audioPredicate).firstMatch.exists) + + return self + } + /** Selects a block based on part of the block label (e.g. partial text in a paragraph block) */ @@ -186,7 +231,7 @@ public class BlockEditorScreen: ScreenObject { private func addBlock(_ blockLabel: String) { addBlockButton.tap() let blockButton = app.buttons[blockLabel] - XCTAssertTrue(blockButton.waitForIsHittable(timeout: 3)) + if !blockButton.isHittable { app.scrollDownToElement(element: blockButton) } blockButton.tap() } diff --git a/WordPress/UITestsFoundation/Screens/Editor/ChooseLayoutScreen.swift b/WordPress/UITestsFoundation/Screens/Editor/ChooseLayoutScreen.swift index 82efcabbe5f7..3c871a6b8f4f 100644 --- a/WordPress/UITestsFoundation/Screens/Editor/ChooseLayoutScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Editor/ChooseLayoutScreen.swift @@ -10,8 +10,7 @@ public class ChooseLayoutScreen: ScreenObject { init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [closeButtonGetter], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Editor/EditorNoticeComponent.swift b/WordPress/UITestsFoundation/Screens/Editor/EditorNoticeComponent.swift index fa697fbc05bb..21d96321b673 100644 --- a/WordPress/UITestsFoundation/Screens/Editor/EditorNoticeComponent.swift +++ b/WordPress/UITestsFoundation/Screens/Editor/EditorNoticeComponent.swift @@ -19,8 +19,7 @@ public class EditorNoticeComponent: ScreenObject { try super.init( expectedElementGetters: [ noticeTitleGetter, noticeActionGetter ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Editor/EditorPostSettings.swift b/WordPress/UITestsFoundation/Screens/Editor/EditorPostSettings.swift index 82fad5c1b2a2..0ab2e404d0f4 100644 --- a/WordPress/UITestsFoundation/Screens/Editor/EditorPostSettings.swift +++ b/WordPress/UITestsFoundation/Screens/Editor/EditorPostSettings.swift @@ -15,8 +15,7 @@ public class EditorPostSettings: ScreenObject { init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [ { $0.tables["SettingsTable"] } ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Editor/EditorPublishEpilogueScreen.swift b/WordPress/UITestsFoundation/Screens/Editor/EditorPublishEpilogueScreen.swift index 498889a66639..b8540f16bac7 100644 --- a/WordPress/UITestsFoundation/Screens/Editor/EditorPublishEpilogueScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Editor/EditorPublishEpilogueScreen.swift @@ -22,8 +22,7 @@ public class EditorPublishEpilogueScreen: ScreenObject { try super.init( expectedElementGetters: [ getDoneButton, getViewButton, publishedPostStatusGetter ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Editor/EditorSettingsComponents/CategoriesComponent.swift b/WordPress/UITestsFoundation/Screens/Editor/EditorSettingsComponents/CategoriesComponent.swift index e85876fe2a2f..81a4785ff1d4 100644 --- a/WordPress/UITestsFoundation/Screens/Editor/EditorSettingsComponents/CategoriesComponent.swift +++ b/WordPress/UITestsFoundation/Screens/Editor/EditorSettingsComponents/CategoriesComponent.swift @@ -6,8 +6,7 @@ public class CategoriesComponent: ScreenObject { init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [ { $0.tables["CategoriesList"] } ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Editor/EditorSettingsComponents/TagsComponent.swift b/WordPress/UITestsFoundation/Screens/Editor/EditorSettingsComponents/TagsComponent.swift index a1971d7c459a..70d1dce5d570 100644 --- a/WordPress/UITestsFoundation/Screens/Editor/EditorSettingsComponents/TagsComponent.swift +++ b/WordPress/UITestsFoundation/Screens/Editor/EditorSettingsComponents/TagsComponent.swift @@ -9,8 +9,7 @@ public class TagsComponent: ScreenObject { init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [ { $0.textViews["add-tags"] } ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Editor/FeaturedImageScreen.swift b/WordPress/UITestsFoundation/Screens/Editor/FeaturedImageScreen.swift index 85fa28be4af4..25b53ae7eaed 100644 --- a/WordPress/UITestsFoundation/Screens/Editor/FeaturedImageScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Editor/FeaturedImageScreen.swift @@ -9,8 +9,7 @@ public class FeaturedImageScreen: ScreenObject { public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [ { $0.navigationBars.buttons["Remove Featured Image"] } ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Jetpack/JetpackBackupOptionsScreen.swift b/WordPress/UITestsFoundation/Screens/Jetpack/JetpackBackupOptionsScreen.swift index 8f70944b0ad5..e58c7e2b39c4 100644 --- a/WordPress/UITestsFoundation/Screens/Jetpack/JetpackBackupOptionsScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Jetpack/JetpackBackupOptionsScreen.swift @@ -6,8 +6,7 @@ public class JetpackBackupOptionsScreen: ScreenObject { public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [ { $0.otherElements.firstMatch } ], - app: app, - waitTimeout: 7 + app: app ) } } diff --git a/WordPress/UITestsFoundation/Screens/Jetpack/JetpackBackupScreen.swift b/WordPress/UITestsFoundation/Screens/Jetpack/JetpackBackupScreen.swift index 9d76d50092a4..999f5daae144 100644 --- a/WordPress/UITestsFoundation/Screens/Jetpack/JetpackBackupScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Jetpack/JetpackBackupScreen.swift @@ -13,8 +13,7 @@ public class JetpackBackupScreen: ScreenObject { public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [ellipsisButtonGetter], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Jetpack/JetpackScanScreen.swift b/WordPress/UITestsFoundation/Screens/Jetpack/JetpackScanScreen.swift index c7f4ffc71168..e8163214592d 100644 --- a/WordPress/UITestsFoundation/Screens/Jetpack/JetpackScanScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Jetpack/JetpackScanScreen.swift @@ -6,8 +6,7 @@ public class JetpackScanScreen: ScreenObject { public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [ { $0.otherElements.firstMatch } ], - app: app, - waitTimeout: 7 + app: app ) } } diff --git a/WordPress/UITestsFoundation/Screens/Login/FeatureIntroductionScreen.swift b/WordPress/UITestsFoundation/Screens/Login/FeatureIntroductionScreen.swift index 286aec0b8ac7..cc24651c330d 100644 --- a/WordPress/UITestsFoundation/Screens/Login/FeatureIntroductionScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Login/FeatureIntroductionScreen.swift @@ -11,8 +11,7 @@ public class FeatureIntroductionScreen: ScreenObject { public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [closeButtonGetter], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Login/LinkOrPasswordScreen.swift b/WordPress/UITestsFoundation/Screens/Login/LinkOrPasswordScreen.swift index 112c4874552b..047fc58dbfa1 100644 --- a/WordPress/UITestsFoundation/Screens/Login/LinkOrPasswordScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Login/LinkOrPasswordScreen.swift @@ -15,8 +15,7 @@ public class LinkOrPasswordScreen: ScreenObject { init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [passwordOptionGetter, linkButtonGetter], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Login/LoginCheckMagicLinkScreen.swift b/WordPress/UITestsFoundation/Screens/Login/LoginCheckMagicLinkScreen.swift index bef86bbdd037..b139ef346caa 100644 --- a/WordPress/UITestsFoundation/Screens/Login/LoginCheckMagicLinkScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Login/LoginCheckMagicLinkScreen.swift @@ -14,8 +14,7 @@ public class LoginCheckMagicLinkScreen: ScreenObject { // swiftlint:disable:next opening_brace { $0.buttons["Open Mail Button"] } ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Login/LoginEmailScreen.swift b/WordPress/UITestsFoundation/Screens/Login/LoginEmailScreen.swift index 6cda9eb87a1b..0891e67a5501 100644 --- a/WordPress/UITestsFoundation/Screens/Login/LoginEmailScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Login/LoginEmailScreen.swift @@ -19,8 +19,7 @@ public class LoginEmailScreen: ScreenObject { init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [emailTextFieldGetter, nextButtonGetter], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Login/LoginEpilogueScreen.swift b/WordPress/UITestsFoundation/Screens/Login/LoginEpilogueScreen.swift index bc604324c582..92d52d0f4d38 100644 --- a/WordPress/UITestsFoundation/Screens/Login/LoginEpilogueScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Login/LoginEpilogueScreen.swift @@ -13,7 +13,7 @@ public class LoginEpilogueScreen: ScreenObject { try super.init( expectedElementGetters: [loginEpilogueTableGetter], app: app, - waitTimeout: 60 + waitTimeout: 70 ) } diff --git a/WordPress/UITestsFoundation/Screens/Login/LoginPasswordScreen.swift b/WordPress/UITestsFoundation/Screens/Login/LoginPasswordScreen.swift index 6efe5deee507..925beaae4f39 100644 --- a/WordPress/UITestsFoundation/Screens/Login/LoginPasswordScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Login/LoginPasswordScreen.swift @@ -12,8 +12,7 @@ class LoginPasswordScreen: ScreenObject { init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [passwordTextFieldGetter], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Login/LoginSiteAddressScreen.swift b/WordPress/UITestsFoundation/Screens/Login/LoginSiteAddressScreen.swift index badf06ea22bb..fb26648ded3d 100644 --- a/WordPress/UITestsFoundation/Screens/Login/LoginSiteAddressScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Login/LoginSiteAddressScreen.swift @@ -26,8 +26,7 @@ public class LoginSiteAddressScreen: ScreenObject { init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [siteAddressTextFieldGetter], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Login/LoginUsernamePasswordScreen.swift b/WordPress/UITestsFoundation/Screens/Login/LoginUsernamePasswordScreen.swift index cf09c4119dbb..e4cad8fc1423 100644 --- a/WordPress/UITestsFoundation/Screens/Login/LoginUsernamePasswordScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Login/LoginUsernamePasswordScreen.swift @@ -44,8 +44,7 @@ public class LoginUsernamePasswordScreen: ScreenObject { usernameTextFieldGetter, passwordTextFieldGetter ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Login/OnboardingQuestionsPromptScreen.swift b/WordPress/UITestsFoundation/Screens/Login/OnboardingQuestionsPromptScreen.swift index 2040762b4965..e635424686a8 100644 --- a/WordPress/UITestsFoundation/Screens/Login/OnboardingQuestionsPromptScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Login/OnboardingQuestionsPromptScreen.swift @@ -11,8 +11,7 @@ public class OnboardingQuestionsPromptScreen: ScreenObject { public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [skipButtonGetter], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Login/QuickStartPromptScreen.swift b/WordPress/UITestsFoundation/Screens/Login/QuickStartPromptScreen.swift index df95937f0914..7030d33819a0 100644 --- a/WordPress/UITestsFoundation/Screens/Login/QuickStartPromptScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Login/QuickStartPromptScreen.swift @@ -12,8 +12,7 @@ public class QuickStartPromptScreen: ScreenObject { public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [noThanksButtonGetter], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Login/Unified/GetStartedScreen.swift b/WordPress/UITestsFoundation/Screens/Login/Unified/GetStartedScreen.swift index 5dfd2c9cbd96..9ca50cf44f44 100644 --- a/WordPress/UITestsFoundation/Screens/Login/Unified/GetStartedScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Login/Unified/GetStartedScreen.swift @@ -38,8 +38,7 @@ public class GetStartedScreen: ScreenObject { emailTextFieldGetter, helpButtonGetter ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Login/Unified/PasswordScreen.swift b/WordPress/UITestsFoundation/Screens/Login/Unified/PasswordScreen.swift index 72391366b384..acfb9af7c8d1 100644 --- a/WordPress/UITestsFoundation/Screens/Login/Unified/PasswordScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Login/Unified/PasswordScreen.swift @@ -22,8 +22,7 @@ public class PasswordScreen: ScreenObject { public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [ passwordTextFieldGetter, continueButtonGetter ], - app: app, - waitTimeout: 10 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Login/Unified/PrologueScreen.swift b/WordPress/UITestsFoundation/Screens/Login/Unified/PrologueScreen.swift index 09aaa5754d66..0553f0f62187 100644 --- a/WordPress/UITestsFoundation/Screens/Login/Unified/PrologueScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Login/Unified/PrologueScreen.swift @@ -17,8 +17,7 @@ public class PrologueScreen: ScreenObject { public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [continueButtonGetter, siteAddressButtonGetter], - app: app, - waitTimeout: 3 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Login/WelcomeScreen.swift b/WordPress/UITestsFoundation/Screens/Login/WelcomeScreen.swift index ef5359088ae4..89b7a16bb708 100644 --- a/WordPress/UITestsFoundation/Screens/Login/WelcomeScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Login/WelcomeScreen.swift @@ -19,8 +19,7 @@ public class WelcomeScreen: ScreenObject { public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [logInButtonGetter, signupButtonGetter], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Login/WelcomeScreenLoginComponent.swift b/WordPress/UITestsFoundation/Screens/Login/WelcomeScreenLoginComponent.swift index 90b8adc111e4..fb4b2b8510cf 100644 --- a/WordPress/UITestsFoundation/Screens/Login/WelcomeScreenLoginComponent.swift +++ b/WordPress/UITestsFoundation/Screens/Login/WelcomeScreenLoginComponent.swift @@ -15,8 +15,7 @@ public class WelcomeScreenLoginComponent: ScreenObject { init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [emailLoginButtonGetter, siteAddressButtonGetter], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Me/ContactUsScreen.swift b/WordPress/UITestsFoundation/Screens/Me/ContactUsScreen.swift index 8c6532cd9789..d72765b6fa03 100644 --- a/WordPress/UITestsFoundation/Screens/Me/ContactUsScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Me/ContactUsScreen.swift @@ -34,8 +34,7 @@ public class ContactUsScreen: ScreenObject { closeButtonGetter, attachButtonGetter, ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Me/SupportScreen.swift b/WordPress/UITestsFoundation/Screens/Me/SupportScreen.swift index 96c415b73623..2a5f47896790 100644 --- a/WordPress/UITestsFoundation/Screens/Me/SupportScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Me/SupportScreen.swift @@ -39,8 +39,7 @@ public class SupportScreen: ScreenObject { { $0.cells["activity-logs-button"] } // swiftlint:enable opening_brace ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/MeTabScreen.swift b/WordPress/UITestsFoundation/Screens/MeTabScreen.swift index 5e1d0a5e6309..e0381e8f4788 100644 --- a/WordPress/UITestsFoundation/Screens/MeTabScreen.swift +++ b/WordPress/UITestsFoundation/Screens/MeTabScreen.swift @@ -20,8 +20,7 @@ public class MeTabScreen: ScreenObject { try super.init( expectedElementGetter: { $0.cells["appSettings"] }, - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Media/MediaPickerAlbumListScreen.swift b/WordPress/UITestsFoundation/Screens/Media/MediaPickerAlbumListScreen.swift index 4b9ed8d1b3bf..78cc4ea51377 100644 --- a/WordPress/UITestsFoundation/Screens/Media/MediaPickerAlbumListScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Media/MediaPickerAlbumListScreen.swift @@ -10,8 +10,7 @@ public class MediaPickerAlbumListScreen: ScreenObject { public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetter: albumListGetter, - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Media/MediaPickerAlbumScreen.swift b/WordPress/UITestsFoundation/Screens/Media/MediaPickerAlbumScreen.swift index 8f4f73d97de6..133381f8689b 100644 --- a/WordPress/UITestsFoundation/Screens/Media/MediaPickerAlbumScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Media/MediaPickerAlbumScreen.swift @@ -9,8 +9,7 @@ public class MediaPickerAlbumScreen: ScreenObject { public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [mediaCollectionGetter], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/MediaScreen.swift b/WordPress/UITestsFoundation/Screens/MediaScreen.swift index 7e6bc5a7e9a6..771bf1f50c12 100644 --- a/WordPress/UITestsFoundation/Screens/MediaScreen.swift +++ b/WordPress/UITestsFoundation/Screens/MediaScreen.swift @@ -6,8 +6,7 @@ public class MediaScreen: ScreenObject { public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [ { $0.collectionViews["MediaCollection"] } ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/MySiteScreen.swift b/WordPress/UITestsFoundation/Screens/MySiteScreen.swift index 3dec24a98350..6178c4e7cae8 100644 --- a/WordPress/UITestsFoundation/Screens/MySiteScreen.swift +++ b/WordPress/UITestsFoundation/Screens/MySiteScreen.swift @@ -128,8 +128,7 @@ public class MySiteScreen: ScreenObject { mediaButtonGetter, createButtonGetter ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/MySitesScreen.swift b/WordPress/UITestsFoundation/Screens/MySitesScreen.swift index 3400d18d604d..f04bf90a0627 100644 --- a/WordPress/UITestsFoundation/Screens/MySitesScreen.swift +++ b/WordPress/UITestsFoundation/Screens/MySitesScreen.swift @@ -24,8 +24,7 @@ public class MySitesScreen: ScreenObject { cancelButtonGetter, plusButtonGetter ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/NotificationsScreen.swift b/WordPress/UITestsFoundation/Screens/NotificationsScreen.swift index d32f47c3926c..ad7699648e2c 100644 --- a/WordPress/UITestsFoundation/Screens/NotificationsScreen.swift +++ b/WordPress/UITestsFoundation/Screens/NotificationsScreen.swift @@ -6,8 +6,7 @@ public class NotificationsScreen: ScreenObject { init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [ { $0.tables["Notifications Table"] } ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/PagesScreen.swift b/WordPress/UITestsFoundation/Screens/PagesScreen.swift index 607e6cb68118..020b04fddcd8 100644 --- a/WordPress/UITestsFoundation/Screens/PagesScreen.swift +++ b/WordPress/UITestsFoundation/Screens/PagesScreen.swift @@ -15,8 +15,7 @@ public class PagesScreen: ScreenObject { try super.init( expectedElementGetters: [ pagesTableGetter ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/PeopleScreen.swift b/WordPress/UITestsFoundation/Screens/PeopleScreen.swift index 2c4c3feec618..378c44a7cb3a 100644 --- a/WordPress/UITestsFoundation/Screens/PeopleScreen.swift +++ b/WordPress/UITestsFoundation/Screens/PeopleScreen.swift @@ -17,8 +17,7 @@ public class PeopleScreen: ScreenObject { filterButtonGetter("followers"), filterButtonGetter("email") ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/PlanSelectionScreen.swift b/WordPress/UITestsFoundation/Screens/PlanSelectionScreen.swift index c7f25de4f311..66d59d41658a 100644 --- a/WordPress/UITestsFoundation/Screens/PlanSelectionScreen.swift +++ b/WordPress/UITestsFoundation/Screens/PlanSelectionScreen.swift @@ -9,8 +9,7 @@ public class PlanSelectionScreen: ScreenObject { public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [ webViewGetter ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/PostsScreen.swift b/WordPress/UITestsFoundation/Screens/PostsScreen.swift index 91e4aa7f60ec..a74d99b906bd 100644 --- a/WordPress/UITestsFoundation/Screens/PostsScreen.swift +++ b/WordPress/UITestsFoundation/Screens/PostsScreen.swift @@ -13,8 +13,7 @@ public class PostsScreen: ScreenObject { init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [ { $0.tables["PostsTable"] } ], - app: app, - waitTimeout: 7 + app: app ) showOnly(.published) } diff --git a/WordPress/UITestsFoundation/Screens/ReaderScreen.swift b/WordPress/UITestsFoundation/Screens/ReaderScreen.swift index 7a8e88b4670e..3262e451c5e0 100644 --- a/WordPress/UITestsFoundation/Screens/ReaderScreen.swift +++ b/WordPress/UITestsFoundation/Screens/ReaderScreen.swift @@ -16,8 +16,7 @@ public class ReaderScreen: ScreenObject { { $0.tables["Reader"] }, discoverButtonGetter ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Signup/SignupCheckMagicLinkScreen.swift b/WordPress/UITestsFoundation/Screens/Signup/SignupCheckMagicLinkScreen.swift index 5f1c5e354e7b..4f5e23593c74 100644 --- a/WordPress/UITestsFoundation/Screens/Signup/SignupCheckMagicLinkScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Signup/SignupCheckMagicLinkScreen.swift @@ -7,8 +7,7 @@ public class SignupCheckMagicLinkScreen: ScreenObject { try super.init( // swiftlint:disable:next opening_brace expectedElementGetters: [{ $0.buttons["Open Mail Button"] }], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Signup/SignupEmailScreen.swift b/WordPress/UITestsFoundation/Screens/Signup/SignupEmailScreen.swift index 96300e13f8a5..3d164b737497 100644 --- a/WordPress/UITestsFoundation/Screens/Signup/SignupEmailScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Signup/SignupEmailScreen.swift @@ -19,8 +19,7 @@ public class SignupEmailScreen: ScreenObject { init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [emailTextFieldGetter, nextButtonGetter], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Signup/SignupEpilogueScreen.swift b/WordPress/UITestsFoundation/Screens/Signup/SignupEpilogueScreen.swift index 8da53c217c21..a73de050180c 100644 --- a/WordPress/UITestsFoundation/Screens/Signup/SignupEpilogueScreen.swift +++ b/WordPress/UITestsFoundation/Screens/Signup/SignupEpilogueScreen.swift @@ -6,8 +6,7 @@ public class SignupEpilogueScreen: ScreenObject { init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [ { $0.staticTexts["New Account Header"] } ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/Signup/WelcomeScreenSignupComponent.swift b/WordPress/UITestsFoundation/Screens/Signup/WelcomeScreenSignupComponent.swift index ff23b0cd0dc9..7f9a73546a38 100644 --- a/WordPress/UITestsFoundation/Screens/Signup/WelcomeScreenSignupComponent.swift +++ b/WordPress/UITestsFoundation/Screens/Signup/WelcomeScreenSignupComponent.swift @@ -14,8 +14,7 @@ public class WelcomeScreenSignupComponent: ScreenObject { init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [emailSignupButtonGetter], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/SiteIntentScreen.swift b/WordPress/UITestsFoundation/Screens/SiteIntentScreen.swift index 36036dd0aad0..6fe6aac44eb5 100644 --- a/WordPress/UITestsFoundation/Screens/SiteIntentScreen.swift +++ b/WordPress/UITestsFoundation/Screens/SiteIntentScreen.swift @@ -14,8 +14,7 @@ public class SiteIntentScreen: ScreenObject { { $0.tables["Site Intent Table"] }, cancelButtonGetter ], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/SiteSettingsScreen.swift b/WordPress/UITestsFoundation/Screens/SiteSettingsScreen.swift index 04f2fa1752c2..66b8e84fd0f1 100644 --- a/WordPress/UITestsFoundation/Screens/SiteSettingsScreen.swift +++ b/WordPress/UITestsFoundation/Screens/SiteSettingsScreen.swift @@ -16,8 +16,7 @@ public class SiteSettingsScreen: ScreenObject { public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( expectedElementGetters: [blockEditorToggleGetter], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/StatsScreen.swift b/WordPress/UITestsFoundation/Screens/StatsScreen.swift index 48b6aa3b6c7c..909287fbac4b 100644 --- a/WordPress/UITestsFoundation/Screens/StatsScreen.swift +++ b/WordPress/UITestsFoundation/Screens/StatsScreen.swift @@ -19,8 +19,7 @@ public class StatsScreen: ScreenObject { try super.init( // swiftlint:disable:next opening_brace expectedElementGetters: [{ $0.otherElements.firstMatch }], - app: app, - waitTimeout: 7 + app: app ) } diff --git a/WordPress/UITestsFoundation/Screens/TabNavComponent.swift b/WordPress/UITestsFoundation/Screens/TabNavComponent.swift index 93bc9eab9555..00e05583f4a6 100644 --- a/WordPress/UITestsFoundation/Screens/TabNavComponent.swift +++ b/WordPress/UITestsFoundation/Screens/TabNavComponent.swift @@ -30,8 +30,7 @@ public class TabNavComponent: ScreenObject { readerTabButtonGetter, notificationsTabButtonGetter ], - app: app, - waitTimeout: 7 + app: app ) } @@ -56,10 +55,11 @@ public class TabNavComponent: ScreenObject { return try AztecEditorScreen(mode: .rich) } + @discardableResult public func gotoBlockEditorScreen() throws -> BlockEditorScreen { - let mySite = try goToMySiteScreen() - let actionSheet = try mySite.goToCreateSheet() - actionSheet.goToBlogPost() + try goToMySiteScreen() + .goToCreateSheet() + .goToBlogPost() return try BlockEditorScreen() } diff --git a/WordPress/UITestsFoundation/XCUIApplication+SavePassword.swift b/WordPress/UITestsFoundation/XCUIApplication+SavePassword.swift index 8edf6e440b7b..dac04c35873d 100644 --- a/WordPress/UITestsFoundation/XCUIApplication+SavePassword.swift +++ b/WordPress/UITestsFoundation/XCUIApplication+SavePassword.swift @@ -6,7 +6,7 @@ extension XCUIApplication { // This method encapsulates the logic to dimiss the prompt. func dismissSavePasswordPrompt() { XCTContext.runActivity(named: "Dismiss save password prompt if needed.") { _ in - guard buttons["Save Password"].waitForExistence(timeout: 5) else { return } + guard buttons["Save Password"].waitForExistence(timeout: 10) else { return } // There should be no need to wait for this button to exist since it's part of the same // alert where "Save Password" is. diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index afa2b53ffc11..98b298c557dd 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -349,6 +349,8 @@ 0A9610F928B2E56300076EBA /* UserSuggestion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9610F828B2E56300076EBA /* UserSuggestion+Comparable.swift */; }; 0A9610FA28B2E56300076EBA /* UserSuggestion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9610F828B2E56300076EBA /* UserSuggestion+Comparable.swift */; }; 0A9687BC28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9687BB28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift */; }; + 0C0D3B0D2A4C79DE0050A00D /* BlazeCampaignsStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */; }; + 0C0D3B0E2A4C79DE0050A00D /* BlazeCampaignsStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */; }; 0C35FFF129CB81F700D224EB /* BlogDashboardHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C35FFF029CB81F700D224EB /* BlogDashboardHelpers.swift */; }; 0C35FFF229CB81F700D224EB /* BlogDashboardHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C35FFF029CB81F700D224EB /* BlogDashboardHelpers.swift */; }; 0C35FFF429CBA6DA00D224EB /* BlogDashboardPersonalizationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C35FFF329CBA6DA00D224EB /* BlogDashboardPersonalizationViewModelTests.swift */; }; @@ -360,10 +362,19 @@ 0C391E622A3002950040EA91 /* BlazeCampaignStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391E602A3002950040EA91 /* BlazeCampaignStatusView.swift */; }; 0C391E642A312DB20040EA91 /* BlazeCampaignViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391E632A312DB20040EA91 /* BlazeCampaignViewModelTests.swift */; }; 0C63266F2A3D1305000B8C57 /* GutenbergFilesAppMediaSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C63266E2A3D1305000B8C57 /* GutenbergFilesAppMediaSourceTests.swift */; }; + 0C6C4CD02A4F0A000049E762 /* BlazeCampaignsStreamTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6C4CCF2A4F0A000049E762 /* BlazeCampaignsStreamTests.swift */; }; + 0C6C4CD42A4F0AD90049E762 /* blaze-search-page-1.json in Resources */ = {isa = PBXBuildFile; fileRef = 0C6C4CD32A4F0AD80049E762 /* blaze-search-page-1.json */; }; + 0C6C4CD62A4F0AEE0049E762 /* blaze-search-page-2.json in Resources */ = {isa = PBXBuildFile; fileRef = 0C6C4CD52A4F0AEE0049E762 /* blaze-search-page-2.json */; }; + 0C6C4CD82A4F0F2C0049E762 /* Bundle+TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6C4CD72A4F0F2C0049E762 /* Bundle+TestExtensions.swift */; }; + 0C71959B2A3CA582002EA18C /* SiteSettingsRelatedPostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C71959A2A3CA582002EA18C /* SiteSettingsRelatedPostsView.swift */; }; + 0C71959C2A3CA582002EA18C /* SiteSettingsRelatedPostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C71959A2A3CA582002EA18C /* SiteSettingsRelatedPostsView.swift */; }; + 0C7D481A2A4DB9300023CF84 /* blaze-search-response.json in Resources */ = {isa = PBXBuildFile; fileRef = 0C7D48192A4DB9300023CF84 /* blaze-search-response.json */; }; 0C7E09202A4286A00052324C /* PostMetaButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 0C7E091F2A4286A00052324C /* PostMetaButton.m */; }; 0C7E09212A4286A00052324C /* PostMetaButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 0C7E091F2A4286A00052324C /* PostMetaButton.m */; }; 0C7E09242A4286F40052324C /* PostMetaButton+Swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7E09232A4286F40052324C /* PostMetaButton+Swift.swift */; }; 0C7E09252A4286F40052324C /* PostMetaButton+Swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7E09232A4286F40052324C /* PostMetaButton+Swift.swift */; }; + 0C8078AB2A4E01A5002ABF29 /* PagingFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8078AA2A4E01A5002ABF29 /* PagingFooterView.swift */; }; + 0C8078AC2A4E01A5002ABF29 /* PagingFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8078AA2A4E01A5002ABF29 /* PagingFooterView.swift */; }; 0C896DDE2A3A762200D7D4E7 /* SettingsPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C896DDD2A3A762200D7D4E7 /* SettingsPicker.swift */; }; 0C896DE02A3A763400D7D4E7 /* SettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C896DDF2A3A763400D7D4E7 /* SettingsCell.swift */; }; 0C896DE22A3A767200D7D4E7 /* SiteVisibility+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C896DE12A3A767200D7D4E7 /* SiteVisibility+Extensions.swift */; }; @@ -380,6 +391,9 @@ 0CB4057A29C8DDEE008EED0A /* BlogDashboardPersonalizationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4057029C8DCF4008EED0A /* BlogDashboardPersonalizationViewModel.swift */; }; 0CB4057D29C8DF83008EED0A /* BlogDashboardPersonalizeCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4057B29C8DEE1008EED0A /* BlogDashboardPersonalizeCardCell.swift */; }; 0CB4057E29C8DF84008EED0A /* BlogDashboardPersonalizeCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4057B29C8DEE1008EED0A /* BlogDashboardPersonalizeCardCell.swift */; }; + 0CD382832A4B699E00612173 /* DashboardBlazeCardCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD382822A4B699E00612173 /* DashboardBlazeCardCellViewModel.swift */; }; + 0CD382842A4B699E00612173 /* DashboardBlazeCardCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD382822A4B699E00612173 /* DashboardBlazeCardCellViewModel.swift */; }; + 0CD382862A4B6FCF00612173 /* DashboardBlazeCardCellViewModelTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD382852A4B6FCE00612173 /* DashboardBlazeCardCellViewModelTest.swift */; }; 0CDEC40C2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDEC40B2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift */; }; 0CDEC40D2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDEC40B2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift */; }; 1702BBDC1CEDEA6B00766A33 /* BadgeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1702BBDB1CEDEA6B00766A33 /* BadgeLabel.swift */; }; @@ -857,6 +871,9 @@ 3F73BE5F24EB3B4400BE99FF /* WhatIsNewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F73BE5E24EB3B4400BE99FF /* WhatIsNewView.swift */; }; 3F751D462491A93D0008A2B1 /* ZendeskUtilsTests+Plans.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F751D452491A93D0008A2B1 /* ZendeskUtilsTests+Plans.swift */; }; 3F758FD524F6FB4900BBA2FC /* AnnouncementsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F758FD424F6FB4900BBA2FC /* AnnouncementsStore.swift */; }; + 3F759FBA2A2DA93B0039A845 /* WPAccount+Fixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F759FB92A2DA93B0039A845 /* WPAccount+Fixture.swift */; }; + 3F759FBC2A2DB2CF0039A845 /* TestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F759FBB2A2DB2CF0039A845 /* TestError.swift */; }; + 3F759FBE2A2DB3280039A845 /* AccountSettingsRemoteInterfaceStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F759FBD2A2DB3280039A845 /* AccountSettingsRemoteInterfaceStub.swift */; }; 3F762E9326784A950088CD45 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F762E9226784A950088CD45 /* Logger.swift */; }; 3F762E9526784B540088CD45 /* WireMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F762E9426784B540088CD45 /* WireMock.swift */; }; 3F762E9726784BED0088CD45 /* FancyAlertComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F762E9626784BED0088CD45 /* FancyAlertComponent.swift */; }; @@ -1755,6 +1772,9 @@ 805CC0C2296CF3B3002941DC /* JetpackNewUsersOverlaySecondaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 805CC0C0296CF3B3002941DC /* JetpackNewUsersOverlaySecondaryView.swift */; }; 8067340A27E3A50900ABC95E /* UIViewController+RemoveQuickStart.m in Sources */ = {isa = PBXBuildFile; fileRef = 8067340927E3A50900ABC95E /* UIViewController+RemoveQuickStart.m */; }; 8067340B27E3A50900ABC95E /* UIViewController+RemoveQuickStart.m in Sources */ = {isa = PBXBuildFile; fileRef = 8067340927E3A50900ABC95E /* UIViewController+RemoveQuickStart.m */; }; + 806BA1192A492A2700052422 /* BlazeCampaignDetailsWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 806BA1182A492A2700052422 /* BlazeCampaignDetailsWebViewModel.swift */; }; + 806BA11A2A492A2700052422 /* BlazeCampaignDetailsWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 806BA1182A492A2700052422 /* BlazeCampaignDetailsWebViewModel.swift */; }; + 806BA11C2A492B0F00052422 /* BlazeCampaignDetailsWebViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 806BA11B2A492B0F00052422 /* BlazeCampaignDetailsWebViewModelTests.swift */; }; 806E53E127E01C7F0064315E /* DashboardStatsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 806E53E027E01C7F0064315E /* DashboardStatsViewModel.swift */; }; 806E53E227E01C7F0064315E /* DashboardStatsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 806E53E027E01C7F0064315E /* DashboardStatsViewModel.swift */; }; 806E53E427E01CFE0064315E /* DashboardStatsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 806E53E327E01CFE0064315E /* DashboardStatsViewModelTests.swift */; }; @@ -1927,9 +1947,9 @@ 80B016D22803AB9F00D15566 /* DashboardPostsListCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80B016D02803AB9F00D15566 /* DashboardPostsListCardCell.swift */; }; 80C523A429959DE000B1C14B /* BlazeWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80C523A329959DE000B1C14B /* BlazeWebViewController.swift */; }; 80C523A529959DE000B1C14B /* BlazeWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80C523A329959DE000B1C14B /* BlazeWebViewController.swift */; }; - 80C523A72995D73C00B1C14B /* BlazeWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80C523A62995D73C00B1C14B /* BlazeWebViewModel.swift */; }; - 80C523A82995D73C00B1C14B /* BlazeWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80C523A62995D73C00B1C14B /* BlazeWebViewModel.swift */; }; - 80C523AB29AE6C2200B1C14B /* BlazeWebViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80C523AA29AE6C2200B1C14B /* BlazeWebViewModelTests.swift */; }; + 80C523A72995D73C00B1C14B /* BlazeCreateCampaignWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80C523A62995D73C00B1C14B /* BlazeCreateCampaignWebViewModel.swift */; }; + 80C523A82995D73C00B1C14B /* BlazeCreateCampaignWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80C523A62995D73C00B1C14B /* BlazeCreateCampaignWebViewModel.swift */; }; + 80C523AB29AE6C2200B1C14B /* BlazeCreateCampaignWebViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80C523AA29AE6C2200B1C14B /* BlazeCreateCampaignWebViewModelTests.swift */; }; 80C740FB2989FC4600199027 /* PostStatsTableViewController+JetpackBannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80C740FA2989FC4600199027 /* PostStatsTableViewController+JetpackBannerViewController.swift */; }; 80C740FC2989FC4600199027 /* PostStatsTableViewController+JetpackBannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80C740FA2989FC4600199027 /* PostStatsTableViewController+JetpackBannerViewController.swift */; }; 80D9CFF429DCA53E00FE3400 /* DashboardPagesListCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D9CFF329DCA53E00FE3400 /* DashboardPagesListCardCell.swift */; }; @@ -2094,6 +2114,8 @@ 83B1D038282C62620061D911 /* BloggingPromptsAttribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B1D036282C62620061D911 /* BloggingPromptsAttribution.swift */; }; 83C972E0281C45AB0049E1FE /* Post+BloggingPrompts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83C972DF281C45AB0049E1FE /* Post+BloggingPrompts.swift */; }; 83C972E1281C45AB0049E1FE /* Post+BloggingPrompts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83C972DF281C45AB0049E1FE /* Post+BloggingPrompts.swift */; }; + 83DC5C462A4B769000DAA422 /* JetpackSocialSettingsRemainingSharesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DC5C452A4B769000DAA422 /* JetpackSocialSettingsRemainingSharesView.swift */; }; + 83DC5C472A4B769000DAA422 /* JetpackSocialSettingsRemainingSharesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DC5C452A4B769000DAA422 /* JetpackSocialSettingsRemainingSharesView.swift */; }; 83EF3D7B2937D703000AF9BF /* SharedDataIssueSolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED65D78293511E4008071BF /* SharedDataIssueSolver.swift */; }; 83EF3D7F2937F08C000AF9BF /* SharedDataIssueSolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83EF3D7C2937E969000AF9BF /* SharedDataIssueSolverTests.swift */; }; 83F3E26011275E07004CD686 /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 83F3E25F11275E07004CD686 /* MapKit.framework */; }; @@ -3488,7 +3510,6 @@ F18B43781F849F580089B817 /* PostAttachmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F18B43771F849F580089B817 /* PostAttachmentTests.swift */; }; F18CB8962642E58700B90794 /* FixedSizeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F18CB8952642E58700B90794 /* FixedSizeImageView.swift */; }; F18CB8972642E58700B90794 /* FixedSizeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F18CB8952642E58700B90794 /* FixedSizeImageView.swift */; }; - F19153BD2549ED0800629EC4 /* UITextField+WorkaroundContinueIssue.swift in Sources */ = {isa = PBXBuildFile; fileRef = F19153BC2549ED0800629EC4 /* UITextField+WorkaroundContinueIssue.swift */; }; F195C42B26DFBDC2000EC884 /* BackgroundTasksCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F181EDE426B2AC7200C61241 /* BackgroundTasksCoordinator.swift */; }; F195C42C26DFBE21000EC884 /* WeeklyRoundupBackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D8C6EF26C17A6C002E3323 /* WeeklyRoundupBackgroundTask.swift */; }; F195C42D26DFBE3A000EC884 /* WordPressBackgroundTaskEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D8C6E826BA94DF002E3323 /* WordPressBackgroundTaskEventHandler.swift */; }; @@ -5114,7 +5135,6 @@ FABB24EE2602FC2C00C8785C /* Suggestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = B03B9235250BC5FD000A40AF /* Suggestion.swift */; }; FABB24EF2602FC2C00C8785C /* ShareNoticeConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74EA3B87202A0462004F802D /* ShareNoticeConstants.swift */; }; FABB24F02602FC2C00C8785C /* FeatureFlagOverrideStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A09B98238FE13B0022AE0D /* FeatureFlagOverrideStore.swift */; }; - FABB24F12602FC2C00C8785C /* RelatedPostsPreviewTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = FF1933FE1BB17DA3006825B8 /* RelatedPostsPreviewTableViewCell.m */; }; FABB24F22602FC2C00C8785C /* IntrinsicTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54075D31D3D7D5B0095C318 /* IntrinsicTableView.swift */; }; FABB24F32602FC2C00C8785C /* HomeWidgetTodayData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FB34ACA25672A90001A74A6 /* HomeWidgetTodayData.swift */; }; FABB24F42602FC2C00C8785C /* NoticePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172E27D21FD98135003EA321 /* NoticePresenter.swift */; }; @@ -5311,7 +5331,6 @@ FABB25B62602FC2C00C8785C /* Product.swift in Sources */ = {isa = PBXBuildFile; fileRef = E148362E1C6DF7D8005ACF53 /* Product.swift */; }; FABB25B72602FC2C00C8785C /* PostTag.m in Sources */ = {isa = PBXBuildFile; fileRef = 082AB9DC1C4F035E000CA523 /* PostTag.m */; }; FABB25B82602FC2C00C8785C /* ActivityListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82FC61291FA8B6F000A1757E /* ActivityListViewModel.swift */; }; - FABB25B92602FC2C00C8785C /* UITextField+WorkaroundContinueIssue.swift in Sources */ = {isa = PBXBuildFile; fileRef = F19153BC2549ED0800629EC4 /* UITextField+WorkaroundContinueIssue.swift */; }; FABB25BA2602FC2C00C8785C /* StoreKit+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = E177807F1C97FA9500FA7E14 /* StoreKit+Debug.swift */; }; FABB25BB2602FC2C00C8785C /* PrivacySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ADE0EA20A9EF6200D6AADC /* PrivacySettingsViewController.swift */; }; FABB25BC2602FC2C00C8785C /* SharingDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = E6431DE21C4E892900FD8D90 /* SharingDetailViewController.m */; }; @@ -5326,7 +5345,6 @@ FABB25C52602FC2C00C8785C /* MenusSelectionItemView.m in Sources */ = {isa = PBXBuildFile; fileRef = 08D3454D1CD7F50900358E8C /* MenusSelectionItemView.m */; }; FABB25C62602FC2C00C8785C /* TenorReponseParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81CCD6D243AF09900A83E27 /* TenorReponseParser.swift */; }; FABB25C72602FC2C00C8785C /* WPStyleGuide+Gridicon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4020B2BC2007AC850002C963 /* WPStyleGuide+Gridicon.swift */; }; - FABB25C82602FC2C00C8785C /* RelatedPostsSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FF42584F1BA092EE00580C68 /* RelatedPostsSettingsViewController.m */; }; FABB25C92602FC2C00C8785C /* CreateButtonCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E032D5240889EB003AF350 /* CreateButtonCoordinator.swift */; }; FABB25CA2602FC2C00C8785C /* SearchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74729CA22056FA0900D1394D /* SearchManager.swift */; }; FABB25CB2602FC2C00C8785C /* BlogToBlogMigration87to88.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8980C922E8C7A600C567B0 /* BlogToBlogMigration87to88.swift */; }; @@ -5511,6 +5529,13 @@ FE06AC8326C3BD0900B69DE4 /* ShareAppContentPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE06AC8226C3BD0900B69DE4 /* ShareAppContentPresenter.swift */; }; FE06AC8526C3C2F800B69DE4 /* ShareAppTextActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE06AC8426C3C2F800B69DE4 /* ShareAppTextActivityItemSource.swift */; }; FE18495827F5ACBA00D26879 /* DashboardPromptsCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE18495727F5ACBA00D26879 /* DashboardPromptsCardCell.swift */; }; + FE1E20152A47042500CE7C90 /* PublicizeInfo+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1E20142A47042500CE7C90 /* PublicizeInfo+CoreDataProperties.swift */; }; + FE1E20162A47042500CE7C90 /* PublicizeInfo+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1E20142A47042500CE7C90 /* PublicizeInfo+CoreDataProperties.swift */; }; + FE1E20172A47042500CE7C90 /* PublicizeInfo+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1E20132A47042400CE7C90 /* PublicizeInfo+CoreDataClass.swift */; }; + FE1E20182A47042500CE7C90 /* PublicizeInfo+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1E20132A47042400CE7C90 /* PublicizeInfo+CoreDataClass.swift */; }; + FE1E201A2A473E0800CE7C90 /* JetpackSocialService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1E20192A473E0800CE7C90 /* JetpackSocialService.swift */; }; + FE1E201B2A473E0800CE7C90 /* JetpackSocialService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1E20192A473E0800CE7C90 /* JetpackSocialService.swift */; }; + FE1E201E2A49D59400CE7C90 /* JetpackSocialServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1E201D2A49D59400CE7C90 /* JetpackSocialServiceTests.swift */; }; FE23EB4926E7C91F005A1698 /* richCommentTemplate.html in Resources */ = {isa = PBXBuildFile; fileRef = FE23EB4726E7C91F005A1698 /* richCommentTemplate.html */; }; FE23EB4A26E7C91F005A1698 /* richCommentTemplate.html in Resources */ = {isa = PBXBuildFile; fileRef = FE23EB4726E7C91F005A1698 /* richCommentTemplate.html */; }; FE23EB4B26E7C91F005A1698 /* richCommentStyle.css in Resources */ = {isa = PBXBuildFile; fileRef = FE23EB4826E7C91F005A1698 /* richCommentStyle.css */; }; @@ -5522,8 +5547,6 @@ FE2E3729281C839C00A1E82A /* BloggingPromptsServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2E3728281C839C00A1E82A /* BloggingPromptsServiceTests.swift */; }; FE320CC5294705990046899B /* ReaderPostBackupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE320CC4294705990046899B /* ReaderPostBackupTests.swift */; }; FE32E7F12844971000744D80 /* ReminderScheduleCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE32E7F02844971000744D80 /* ReminderScheduleCoordinatorTests.swift */; }; - FE32EFFF275914390040BE67 /* MenuSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE32EFFE275914390040BE67 /* MenuSheetViewController.swift */; }; - FE32F000275914390040BE67 /* MenuSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE32EFFE275914390040BE67 /* MenuSheetViewController.swift */; }; FE32F002275F602E0040BE67 /* CommentContentRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE32F001275F602E0040BE67 /* CommentContentRenderer.swift */; }; FE32F003275F602E0040BE67 /* CommentContentRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE32F001275F602E0040BE67 /* CommentContentRenderer.swift */; }; FE32F006275F62620040BE67 /* WebCommentContentRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE32F005275F62620040BE67 /* WebCommentContentRenderer.swift */; }; @@ -5597,6 +5620,9 @@ FEDA1AD9269D475D0038EC98 /* ListTableViewCell+Comments.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDA1AD7269D475D0038EC98 /* ListTableViewCell+Comments.swift */; }; FEDDD46F26A03DE900F8942B /* ListTableViewCell+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDDD46E26A03DE900F8942B /* ListTableViewCell+Notifications.swift */; }; FEDDD47026A03DE900F8942B /* ListTableViewCell+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDDD46E26A03DE900F8942B /* ListTableViewCell+Notifications.swift */; }; + FEE48EFC2A4C8312008A48E0 /* Blog+JetpackSocial.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE48EFB2A4C8312008A48E0 /* Blog+JetpackSocial.swift */; }; + FEE48EFD2A4C8312008A48E0 /* Blog+JetpackSocial.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE48EFB2A4C8312008A48E0 /* Blog+JetpackSocial.swift */; }; + FEE48EFF2A4C9855008A48E0 /* Blog+PublicizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE48EFE2A4C9855008A48E0 /* Blog+PublicizeTests.swift */; }; FEF4DC5528439357003806BE /* ReminderScheduleCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF4DC5428439357003806BE /* ReminderScheduleCoordinator.swift */; }; FEF4DC5628439357003806BE /* ReminderScheduleCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEF4DC5428439357003806BE /* ReminderScheduleCoordinator.swift */; }; FEFA263E26C58427009CCB7E /* ShareAppTextActivityItemSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEFA263D26C58427009CCB7E /* ShareAppTextActivityItemSourceTests.swift */; }; @@ -5615,7 +5641,6 @@ FF0B2567237A023C004E255F /* GutenbergVideoUploadProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0B2566237A023C004E255F /* GutenbergVideoUploadProcessorTests.swift */; }; FF0D8146205809C8000EE505 /* PostCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0D8145205809C8000EE505 /* PostCoordinator.swift */; }; FF0F722C206E5345000DAAB5 /* Post+RefreshStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0F722B206E5345000DAAB5 /* Post+RefreshStatus.swift */; }; - FF1933FF1BB17DA3006825B8 /* RelatedPostsPreviewTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = FF1933FE1BB17DA3006825B8 /* RelatedPostsPreviewTableViewCell.m */; }; FF1B11E5238FDFE70038B93E /* GutenbergGalleryUploadProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1B11E4238FDFE70038B93E /* GutenbergGalleryUploadProcessor.swift */; }; FF1B11E7238FE27A0038B93E /* GutenbergGalleryUploadProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1B11E6238FE27A0038B93E /* GutenbergGalleryUploadProcessorTests.swift */; }; FF1FD0242091268900186384 /* URL+LinkNormalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1FD0232091268900186384 /* URL+LinkNormalization.swift */; }; @@ -5627,7 +5652,6 @@ FF2EC3C22209AC19006176E1 /* GutenbergImgUploadProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2EC3C12209AC19006176E1 /* GutenbergImgUploadProcessorTests.swift */; }; FF355D981FB492DD00244E6D /* ExportableAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF355D971FB492DD00244E6D /* ExportableAsset.swift */; }; FF37F90922385CA000AFA3DB /* RELEASE-NOTES.txt in Resources */ = {isa = PBXBuildFile; fileRef = FF37F90822385C9F00AFA3DB /* RELEASE-NOTES.txt */; }; - FF4258501BA092EE00580C68 /* RelatedPostsSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FF42584F1BA092EE00580C68 /* RelatedPostsSettingsViewController.m */; }; FF4C069F206560E500E0B2BC /* MediaThumbnailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4C069E206560E500E0B2BC /* MediaThumbnailCoordinator.swift */; }; FF4DEAD8244B56E300ACA032 /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FF4DEAD7244B56E200ACA032 /* CoreServices.framework */; }; FF5371631FDFF64F00619A3F /* MediaService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5371621FDFF64F00619A3F /* MediaService.swift */; }; @@ -6072,6 +6096,7 @@ 0A69300A28B5AA5E00E98DE1 /* FullScreenCommentReplyViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCommentReplyViewModelTests.swift; sourceTree = ""; }; 0A9610F828B2E56300076EBA /* UserSuggestion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserSuggestion+Comparable.swift"; sourceTree = ""; }; 0A9687BB28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCommentReplyViewModelMock.swift; sourceTree = ""; }; + 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignsStream.swift; sourceTree = ""; }; 0C35FFF029CB81F700D224EB /* BlogDashboardHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardHelpers.swift; sourceTree = ""; }; 0C35FFF329CBA6DA00D224EB /* BlogDashboardPersonalizationViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizationViewModelTests.swift; sourceTree = ""; }; 0C35FFF529CBB5DE00D224EB /* BlogDashboardEmptyStateCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardEmptyStateCell.swift; sourceTree = ""; }; @@ -6079,9 +6104,16 @@ 0C391E602A3002950040EA91 /* BlazeCampaignStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignStatusView.swift; sourceTree = ""; }; 0C391E632A312DB20040EA91 /* BlazeCampaignViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignViewModelTests.swift; sourceTree = ""; }; 0C63266E2A3D1305000B8C57 /* GutenbergFilesAppMediaSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergFilesAppMediaSourceTests.swift; sourceTree = ""; }; + 0C6C4CCF2A4F0A000049E762 /* BlazeCampaignsStreamTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignsStreamTests.swift; sourceTree = ""; }; + 0C6C4CD32A4F0AD80049E762 /* blaze-search-page-1.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "blaze-search-page-1.json"; sourceTree = ""; }; + 0C6C4CD52A4F0AEE0049E762 /* blaze-search-page-2.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blaze-search-page-2.json"; sourceTree = ""; }; + 0C6C4CD72A4F0F2C0049E762 /* Bundle+TestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+TestExtensions.swift"; sourceTree = ""; }; + 0C71959A2A3CA582002EA18C /* SiteSettingsRelatedPostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSettingsRelatedPostsView.swift; sourceTree = ""; }; + 0C7D48192A4DB9300023CF84 /* blaze-search-response.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blaze-search-response.json"; sourceTree = ""; }; 0C7E091F2A4286A00052324C /* PostMetaButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PostMetaButton.m; sourceTree = ""; }; 0C7E09222A4286AA0052324C /* PostMetaButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PostMetaButton.h; sourceTree = ""; }; 0C7E09232A4286F40052324C /* PostMetaButton+Swift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostMetaButton+Swift.swift"; sourceTree = ""; }; + 0C8078AA2A4E01A5002ABF29 /* PagingFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingFooterView.swift; sourceTree = ""; }; 0C896DDD2A3A762200D7D4E7 /* SettingsPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPicker.swift; sourceTree = ""; }; 0C896DDF2A3A763400D7D4E7 /* SettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = ""; }; 0C896DE12A3A767200D7D4E7 /* SiteVisibility+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SiteVisibility+Extensions.swift"; sourceTree = ""; }; @@ -6091,6 +6123,8 @@ 0CB4057029C8DCF4008EED0A /* BlogDashboardPersonalizationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizationViewModel.swift; sourceTree = ""; }; 0CB4057229C8DD01008EED0A /* BlogDashboardPersonalizationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizationView.swift; sourceTree = ""; }; 0CB4057B29C8DEE1008EED0A /* BlogDashboardPersonalizeCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizeCardCell.swift; sourceTree = ""; }; + 0CD382822A4B699E00612173 /* DashboardBlazeCardCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBlazeCardCellViewModel.swift; sourceTree = ""; }; + 0CD382852A4B6FCE00612173 /* DashboardBlazeCardCellViewModelTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBlazeCardCellViewModelTest.swift; sourceTree = ""; }; 0CDEC40B2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBlazeCampaignsCardView.swift; sourceTree = ""; }; 131D0EE49695795ECEDAA446 /* Pods-WordPressTest.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressTest.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-WordPressTest/Pods-WordPressTest.release-alpha.xcconfig"; sourceTree = ""; }; 150B6590614A28DF9AD25491 /* Pods-Apps-Jetpack.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Apps-Jetpack.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-Apps-Jetpack/Pods-Apps-Jetpack.release-alpha.xcconfig"; sourceTree = ""; }; @@ -6543,6 +6577,9 @@ 3F73BE5E24EB3B4400BE99FF /* WhatIsNewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatIsNewView.swift; sourceTree = ""; }; 3F751D452491A93D0008A2B1 /* ZendeskUtilsTests+Plans.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ZendeskUtilsTests+Plans.swift"; sourceTree = ""; }; 3F758FD424F6FB4900BBA2FC /* AnnouncementsStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnnouncementsStore.swift; sourceTree = ""; }; + 3F759FB92A2DA93B0039A845 /* WPAccount+Fixture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WPAccount+Fixture.swift"; sourceTree = ""; }; + 3F759FBB2A2DB2CF0039A845 /* TestError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestError.swift; sourceTree = ""; }; + 3F759FBD2A2DB3280039A845 /* AccountSettingsRemoteInterfaceStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsRemoteInterfaceStub.swift; sourceTree = ""; }; 3F762E9226784A950088CD45 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 3F762E9426784B540088CD45 /* WireMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireMock.swift; sourceTree = ""; }; 3F762E9626784BED0088CD45 /* FancyAlertComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FancyAlertComponent.swift; sourceTree = ""; }; @@ -7329,6 +7366,8 @@ 805CC0C0296CF3B3002941DC /* JetpackNewUsersOverlaySecondaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackNewUsersOverlaySecondaryView.swift; sourceTree = ""; }; 8067340827E3A50900ABC95E /* UIViewController+RemoveQuickStart.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIViewController+RemoveQuickStart.h"; sourceTree = ""; }; 8067340927E3A50900ABC95E /* UIViewController+RemoveQuickStart.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+RemoveQuickStart.m"; sourceTree = ""; }; + 806BA1182A492A2700052422 /* BlazeCampaignDetailsWebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignDetailsWebViewModel.swift; sourceTree = ""; }; + 806BA11B2A492B0F00052422 /* BlazeCampaignDetailsWebViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignDetailsWebViewModelTests.swift; sourceTree = ""; }; 806E53E027E01C7F0064315E /* DashboardStatsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardStatsViewModel.swift; sourceTree = ""; }; 806E53E327E01CFE0064315E /* DashboardStatsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardStatsViewModelTests.swift; sourceTree = ""; }; 8070EB3D28D807CB005C6513 /* InMemoryUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemoryUserDefaults.swift; sourceTree = ""; }; @@ -7352,8 +7391,8 @@ 80B016CE27FEBDC900D15566 /* DashboardCardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardCardTests.swift; sourceTree = ""; }; 80B016D02803AB9F00D15566 /* DashboardPostsListCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardPostsListCardCell.swift; sourceTree = ""; }; 80C523A329959DE000B1C14B /* BlazeWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeWebViewController.swift; sourceTree = ""; }; - 80C523A62995D73C00B1C14B /* BlazeWebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeWebViewModel.swift; sourceTree = ""; }; - 80C523AA29AE6C2200B1C14B /* BlazeWebViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeWebViewModelTests.swift; sourceTree = ""; }; + 80C523A62995D73C00B1C14B /* BlazeCreateCampaignWebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCreateCampaignWebViewModel.swift; sourceTree = ""; }; + 80C523AA29AE6C2200B1C14B /* BlazeCreateCampaignWebViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCreateCampaignWebViewModelTests.swift; sourceTree = ""; }; 80C740FA2989FC4600199027 /* PostStatsTableViewController+JetpackBannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostStatsTableViewController+JetpackBannerViewController.swift"; sourceTree = ""; }; 80D65C1129CC0813008E69D5 /* JetpackUITests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "JetpackUITests-Info.plist"; sourceTree = ""; }; 80D9CFF329DCA53E00FE3400 /* DashboardPagesListCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardPagesListCardCell.swift; sourceTree = ""; }; @@ -7453,6 +7492,7 @@ 839B150A2795DEE0009F5E77 /* UIView+Margins.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Margins.swift"; sourceTree = ""; }; 83B1D036282C62620061D911 /* BloggingPromptsAttribution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloggingPromptsAttribution.swift; sourceTree = ""; }; 83C972DF281C45AB0049E1FE /* Post+BloggingPrompts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post+BloggingPrompts.swift"; sourceTree = ""; }; + 83DC5C452A4B769000DAA422 /* JetpackSocialSettingsRemainingSharesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackSocialSettingsRemainingSharesView.swift; sourceTree = ""; }; 83EF3D7C2937E969000AF9BF /* SharedDataIssueSolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedDataIssueSolverTests.swift; sourceTree = ""; }; 83F3E25F11275E07004CD686 /* MapKit.framework */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; }; 83F3E2D211276371004CD686 /* CoreLocation.framework */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; }; @@ -8900,7 +8940,6 @@ F1863715253E49B8003D4BEF /* AddSiteAlertFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddSiteAlertFactory.swift; sourceTree = ""; }; F18B43771F849F580089B817 /* PostAttachmentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PostAttachmentTests.swift; path = Posts/PostAttachmentTests.swift; sourceTree = ""; }; F18CB8952642E58700B90794 /* FixedSizeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixedSizeImageView.swift; sourceTree = ""; }; - F19153BC2549ED0800629EC4 /* UITextField+WorkaroundContinueIssue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+WorkaroundContinueIssue.swift"; sourceTree = ""; }; F198FF6E256D4914001266EB /* WordPressIntents.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WordPressIntents.entitlements; sourceTree = ""; }; F198FF7F256D498A001266EB /* WordPressIntentsRelease-Internal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "WordPressIntentsRelease-Internal.entitlements"; sourceTree = ""; }; F198FFB1256D4AB2001266EB /* WordPressIntentsRelease-Alpha.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "WordPressIntentsRelease-Alpha.entitlements"; sourceTree = ""; }; @@ -9326,6 +9365,11 @@ FE06AC8226C3BD0900B69DE4 /* ShareAppContentPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppContentPresenter.swift; sourceTree = ""; }; FE06AC8426C3C2F800B69DE4 /* ShareAppTextActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppTextActivityItemSource.swift; sourceTree = ""; }; FE18495727F5ACBA00D26879 /* DashboardPromptsCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardPromptsCardCell.swift; sourceTree = ""; }; + FE1E200F2A45ACE900CE7C90 /* WordPress 151.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 151.xcdatamodel"; sourceTree = ""; }; + FE1E20132A47042400CE7C90 /* PublicizeInfo+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicizeInfo+CoreDataClass.swift"; sourceTree = ""; }; + FE1E20142A47042500CE7C90 /* PublicizeInfo+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicizeInfo+CoreDataProperties.swift"; sourceTree = ""; }; + FE1E20192A473E0800CE7C90 /* JetpackSocialService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackSocialService.swift; sourceTree = ""; }; + FE1E201D2A49D59400CE7C90 /* JetpackSocialServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackSocialServiceTests.swift; sourceTree = ""; }; FE23EB4726E7C91F005A1698 /* richCommentTemplate.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = richCommentTemplate.html; path = Resources/HTML/richCommentTemplate.html; sourceTree = ""; }; FE23EB4826E7C91F005A1698 /* richCommentStyle.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; name = richCommentStyle.css; path = Resources/HTML/richCommentStyle.css; sourceTree = ""; }; FE25C234271F23000084E1DB /* ReaderCommentsNotificationSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderCommentsNotificationSheetViewController.swift; sourceTree = ""; }; @@ -9334,7 +9378,6 @@ FE320CC4294705990046899B /* ReaderPostBackupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderPostBackupTests.swift; sourceTree = ""; }; FE32E7F02844971000744D80 /* ReminderScheduleCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderScheduleCoordinatorTests.swift; sourceTree = ""; }; FE32E7F32846A68800744D80 /* WordPress 142.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 142.xcdatamodel"; sourceTree = ""; }; - FE32EFFE275914390040BE67 /* MenuSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuSheetViewController.swift; sourceTree = ""; }; FE32F001275F602E0040BE67 /* CommentContentRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentContentRenderer.swift; sourceTree = ""; }; FE32F005275F62620040BE67 /* WebCommentContentRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebCommentContentRenderer.swift; sourceTree = ""; }; FE341704275FA157005D5CA7 /* RichCommentContentRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichCommentContentRenderer.swift; sourceTree = ""; }; @@ -9380,6 +9423,8 @@ FED77257298BC5B300C2346E /* PluginJetpackProxyService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginJetpackProxyService.swift; sourceTree = ""; }; FEDA1AD7269D475D0038EC98 /* ListTableViewCell+Comments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ListTableViewCell+Comments.swift"; sourceTree = ""; }; FEDDD46E26A03DE900F8942B /* ListTableViewCell+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ListTableViewCell+Notifications.swift"; sourceTree = ""; }; + FEE48EFB2A4C8312008A48E0 /* Blog+JetpackSocial.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Blog+JetpackSocial.swift"; sourceTree = ""; }; + FEE48EFE2A4C9855008A48E0 /* Blog+PublicizeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Blog+PublicizeTests.swift"; sourceTree = ""; }; FEF4DC5428439357003806BE /* ReminderScheduleCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderScheduleCoordinator.swift; sourceTree = ""; }; FEFA263D26C58427009CCB7E /* ShareAppTextActivityItemSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppTextActivityItemSourceTests.swift; sourceTree = ""; }; FEFC0F872730510F001F7F1D /* WordPress 136.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 136.xcdatamodel"; sourceTree = ""; }; @@ -9396,8 +9441,6 @@ FF0B2566237A023C004E255F /* GutenbergVideoUploadProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergVideoUploadProcessorTests.swift; sourceTree = ""; }; FF0D8145205809C8000EE505 /* PostCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostCoordinator.swift; sourceTree = ""; }; FF0F722B206E5345000DAAB5 /* Post+RefreshStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post+RefreshStatus.swift"; sourceTree = ""; }; - FF1933FD1BB17DA3006825B8 /* RelatedPostsPreviewTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RelatedPostsPreviewTableViewCell.h; sourceTree = ""; }; - FF1933FE1BB17DA3006825B8 /* RelatedPostsPreviewTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RelatedPostsPreviewTableViewCell.m; sourceTree = ""; }; FF1B11E4238FDFE70038B93E /* GutenbergGalleryUploadProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GutenbergGalleryUploadProcessor.swift; sourceTree = ""; }; FF1B11E6238FE27A0038B93E /* GutenbergGalleryUploadProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GutenbergGalleryUploadProcessorTests.swift; path = Gutenberg/GutenbergGalleryUploadProcessorTests.swift; sourceTree = ""; }; FF1FD0232091268900186384 /* URL+LinkNormalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+LinkNormalization.swift"; sourceTree = ""; }; @@ -9412,8 +9455,6 @@ FF2EC3C12209AC19006176E1 /* GutenbergImgUploadProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = GutenbergImgUploadProcessorTests.swift; path = Gutenberg/GutenbergImgUploadProcessorTests.swift; sourceTree = ""; }; FF355D971FB492DD00244E6D /* ExportableAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportableAsset.swift; sourceTree = ""; }; FF37F90822385C9F00AFA3DB /* RELEASE-NOTES.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "RELEASE-NOTES.txt"; path = "../RELEASE-NOTES.txt"; sourceTree = ""; }; - FF42584E1BA092EE00580C68 /* RelatedPostsSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RelatedPostsSettingsViewController.h; sourceTree = ""; }; - FF42584F1BA092EE00580C68 /* RelatedPostsSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RelatedPostsSettingsViewController.m; sourceTree = ""; }; FF4C069E206560E500E0B2BC /* MediaThumbnailCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaThumbnailCoordinator.swift; sourceTree = ""; }; FF4DEAD7244B56E200ACA032 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; FF5371621FDFF64F00619A3F /* MediaService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaService.swift; sourceTree = ""; }; @@ -9802,7 +9843,6 @@ 37EAAF4C1A11799A006D6306 /* CircularImageView.swift */, ADF544C0195A0F620092213D /* CustomHighlightButton.h */, ADF544C1195A0F620092213D /* CustomHighlightButton.m */, - FE32EFFE275914390040BE67 /* MenuSheetViewController.swift */, B5B410B51B1772B000CFCF8D /* NavigationTitleView.swift */, 982A4C3420227D6700B5518E /* NoResultsViewController.swift */, 98B33C87202283860071E1E2 /* NoResults.storyboard */, @@ -9822,6 +9862,7 @@ F18CB8952642E58700B90794 /* FixedSizeImageView.swift */, FADFBD25265F580500039C41 /* MultilineButton.swift */, 808C578E27C7FB1A0099A92C /* ButtonScrollView.swift */, + 0C8078AA2A4E01A5002ABF29 /* PagingFooterView.swift */, ); path = Views; sourceTree = ""; @@ -10067,6 +10108,16 @@ path = Mocks; sourceTree = ""; }; + 0C7D48182A4DB91B0023CF84 /* Blaze */ = { + isa = PBXGroup; + children = ( + 0C7D48192A4DB9300023CF84 /* blaze-search-response.json */, + 0C6C4CD32A4F0AD80049E762 /* blaze-search-page-1.json */, + 0C6C4CD52A4F0AEE0049E762 /* blaze-search-page-2.json */, + ); + name = Blaze; + sourceTree = ""; + }; 0C896DDC2A3A761600D7D4E7 /* Settings */ = { isa = PBXGroup; children = ( @@ -10749,6 +10800,8 @@ 082AB9DC1C4F035E000CA523 /* PostTag.m */, 08B6D6F01C8F7DCE0052C52B /* PostType.h */, 08B6D6F11C8F7DCE0052C52B /* PostType.m */, + FE1E20132A47042400CE7C90 /* PublicizeInfo+CoreDataClass.swift */, + FE1E20142A47042500CE7C90 /* PublicizeInfo+CoreDataProperties.swift */, E6374DBD1C444D8B00F24720 /* PublicizeConnection.swift */, 4A1E77C82988997C006281CC /* PublicizeConnection+Creation.swift */, E6374DBE1C444D8B00F24720 /* PublicizeService.swift */, @@ -11268,17 +11321,6 @@ path = "Blog Selector"; sourceTree = ""; }; - 3F43603723F369A9001DEE70 /* Related Posts */ = { - isa = PBXGroup; - children = ( - FF42584E1BA092EE00580C68 /* RelatedPostsSettingsViewController.h */, - FF42584F1BA092EE00580C68 /* RelatedPostsSettingsViewController.m */, - FF1933FD1BB17DA3006825B8 /* RelatedPostsPreviewTableViewCell.h */, - FF1933FE1BB17DA3006825B8 /* RelatedPostsPreviewTableViewCell.m */, - ); - path = "Related Posts"; - sourceTree = ""; - }; 3F43603823F36A76001DEE70 /* Site Settings */ = { isa = PBXGroup; children = ( @@ -11295,7 +11337,7 @@ 8313B9ED298B1ACD000AF26E /* SiteSettingsViewController+Blogging.swift */, 82C420751FE44BD900CFB15B /* SiteSettingsViewController+Swift.swift */, 0C896DE12A3A767200D7D4E7 /* SiteVisibility+Extensions.swift */, - 3F43603723F369A9001DEE70 /* Related Posts */, + 0C71959A2A3CA582002EA18C /* SiteSettingsRelatedPostsView.swift */, ); path = "Site Settings"; sourceTree = ""; @@ -12207,13 +12249,14 @@ 59B48B601B99E0B0008EBB84 /* TestUtilities */ = { isa = PBXGroup; children = ( - 59B48B611B99E132008EBB84 /* JSONObject.swift */, - E157D5DF1C690A6C00F04FB9 /* ImmuTableTestUtils.swift */, - 570BFD8F2282418A007859A8 /* PostBuilder.swift */, + 2481B1D4260D4E8B00AE59DB /* AccountBuilder.swift */, 57B71D4D230DB5F200789A68 /* BlogBuilder.swift */, + E157D5DF1C690A6C00F04FB9 /* ImmuTableTestUtils.swift */, + 59B48B611B99E132008EBB84 /* JSONObject.swift */, F11023A223186BCA00C4E84A /* MediaBuilder.swift */, 57889AB723589DF100DAE56D /* PageBuilder.swift */, - 2481B1D4260D4E8B00AE59DB /* AccountBuilder.swift */, + 570BFD8F2282418A007859A8 /* PostBuilder.swift */, + 3F759FBB2A2DB2CF0039A845 /* TestError.swift */, ); name = TestUtilities; sourceTree = ""; @@ -12371,25 +12414,27 @@ 5D7A577D1AFBFD7C0097C028 /* Models */ = { isa = PBXGroup; children = ( - F1B1E7A224098FA100549E2A /* BlogTests.swift */, + E6A2158F1D1065F200DE5270 /* AbstractPostTest.swift */, + 8B8C814C2318073300A0E620 /* BasePostTests.swift */, 246D0A0225E97D5D0028B83F /* Blog+ObjcTests.m */, + FEE48EFE2A4C9855008A48E0 /* Blog+PublicizeTests.swift */, 4AD5657128E543A30054C676 /* BlogQueryTests.swift */, + B55F1AA11C107CE200FD04D4 /* BlogSettingsDiscussionTests.swift */, + F1B1E7A224098FA100549E2A /* BlogTests.swift */, + 24A2948225D602710000A51E /* BlogTimeZoneTests.m */, D848CC1620FF38EA00A9038F /* FormattableCommentRangeTests.swift */, + 0879FC151E9301DD00E1EFC8 /* MediaTests.swift */, + C38C5D8027F61D2C002F517E /* MenuItemTests.swift */, D848CC1420FF33FC00A9038F /* NotificationContentRangeTests.swift */, + D826D67E211D21C700A5D8FE /* NullMockUserDefaults.swift */, + 5960967E1CF7959300848496 /* PostTests.swift */, 8BBBEBB124B8F8C0005E358E /* ReaderCardTests.swift */, E6B9B8A91B94E1FE0001B92F /* ReaderPostTest.m */, - B55F1AA11C107CE200FD04D4 /* BlogSettingsDiscussionTests.swift */, - E6A2158F1D1065F200DE5270 /* AbstractPostTest.swift */, - 5960967E1CF7959300848496 /* PostTests.swift */, - 0879FC151E9301DD00E1EFC8 /* MediaTests.swift */, - D826D67E211D21C700A5D8FE /* NullMockUserDefaults.swift */, - 8B8C814C2318073300A0E620 /* BasePostTests.swift */, - 24A2948225D602710000A51E /* BlogTimeZoneTests.m */, 24C69A8A2612421900312D9A /* UserSettingsTests.swift */, 24C69AC12612467C00312D9A /* UserSettingsTestsObjc.m */, + 3F759FB92A2DA93B0039A845 /* WPAccount+Fixture.swift */, 2481B1E7260D4EAC00AE59DB /* WPAccount+LookupTests.swift */, 2481B20B260D8FED00AE59DB /* WPAccount+ObjCLookupTests.m */, - C38C5D8027F61D2C002F517E /* MenuItemTests.swift */, ); name = Models; sourceTree = ""; @@ -13253,7 +13298,8 @@ 80C523A929AE6BF300B1C14B /* Blaze */ = { isa = PBXGroup; children = ( - 80C523AA29AE6C2200B1C14B /* BlazeWebViewModelTests.swift */, + 80C523AA29AE6C2200B1C14B /* BlazeCreateCampaignWebViewModelTests.swift */, + 806BA11B2A492B0F00052422 /* BlazeCampaignDetailsWebViewModelTests.swift */, ); path = Blaze; sourceTree = ""; @@ -13393,6 +13439,7 @@ isa = PBXGroup; children = ( 83914BD02A2E89F30017A588 /* JetpackSocialNoConnectionView.swift */, + 83DC5C452A4B769000DAA422 /* JetpackSocialSettingsRemainingSharesView.swift */, ); path = Social; sourceTree = ""; @@ -13770,7 +13817,9 @@ 0C35FFF329CBA6DA00D224EB /* BlogDashboardPersonalizationViewModelTests.swift */, 011F52C52A15413800B04114 /* FreeToPaidPlansDashboardCardHelperTests.swift */, FA3FBF8D2A2777E00012FC90 /* DashboardActivityLogViewModelTests.swift */, + 0CD382852A4B6FCE00612173 /* DashboardBlazeCardCellViewModelTest.swift */, 0C391E632A312DB20040EA91 /* BlazeCampaignViewModelTests.swift */, + 0C6C4CCF2A4F0A000049E762 /* BlazeCampaignsStreamTests.swift */, ); path = Dashboard; sourceTree = ""; @@ -14077,6 +14126,7 @@ 8B749E7125AF522900023F03 /* JetpackCapabilitiesService.swift */, FAB8AB8A25AFFE7500F9F8A0 /* JetpackRestoreService.swift */, 8C6A22E325783D2000A79950 /* JetpackScanService.swift */, + FE1E20192A473E0800CE7C90 /* JetpackSocialService.swift */, C7A09A4928401B7B003096ED /* QRLoginService.swift */, 9A2D0B35225E2511009E585F /* JetpackService.swift */, 1751E5901CE0E552000CA08D /* KeyValueDatabase.swift */, @@ -14967,6 +15017,7 @@ F10D634E26F0B78E00E46CC7 /* Blog+Organization.swift */, 8B4EDADC27DF9D5E004073B6 /* Blog+MySite.swift */, 4AD5656E28E413160054C676 /* Blog+History.swift */, + FEE48EFB2A4C8312008A48E0 /* Blog+JetpackSocial.swift */, ); name = Blog; sourceTree = ""; @@ -15252,7 +15303,6 @@ 171CC15724FCEBF7008B7180 /* UINavigationBar+Appearance.swift */, 7326A4A7221C8F4100B4EB8C /* UIStackView+Subviews.swift */, 8BF0B606247D88EB009A7457 /* UITableViewCell+enableDisable.swift */, - F19153BC2549ED0800629EC4 /* UITextField+WorkaroundContinueIssue.swift */, D829C33A21B12EFE00B09F12 /* UIView+Borders.swift */, F17A2A1D23BFBD72001E96AC /* UIView+ExistingConstraints.swift */, 8BA125EA27D8F5E4008B779F /* UIView+PinSubviewPriority.swift */, @@ -15373,6 +15423,7 @@ children = ( 9363113E19FA996700B0C739 /* AccountServiceTests.swift */, 4A9948E129714EF1006282A9 /* AccountSettingsServiceTests.swift */, + 3F759FBD2A2DB3280039A845 /* AccountSettingsRemoteInterfaceStub.swift */, F1F083F5241FFE930056D3B1 /* AtomicAuthenticationServiceTests.swift */, 46F58500262605930010A723 /* BlockEditorSettingsServiceTests.swift */, FEA312832987FB0100FFD405 /* BlogJetpackTests.swift */, @@ -15387,6 +15438,7 @@ FAB4F32624EDE12A00F259BA /* FollowCommentsServiceTests.swift */, B5772AC51C9C84900031F97E /* GravatarServiceTests.swift */, 8B749E8F25AF8D2E00023F03 /* JetpackCapabilitiesServiceTests.swift */, + FE1E201D2A49D59400CE7C90 /* JetpackSocialServiceTests.swift */, F1BB660B274E704D00A319BE /* LikeUserHelperTests.swift */, 59A9AB391B4C3ECD00A433DC /* LocalCoreDataServiceTests.m */, F11023A0231863CE00C4E84A /* MediaServiceTests.swift */, @@ -16474,6 +16526,7 @@ 805CC0B6296680CF002941DC /* RemoteFeatureFlagStoreMock.swift */, 805CC0B8296680F7002941DC /* RemoteConfigStoreMock.swift */, 805CC0BE29668A97002941DC /* MockCurrentDateProvider.swift */, + 0C6C4CD72A4F0F2C0049E762 /* Bundle+TestExtensions.swift */, ); name = Helpers; sourceTree = ""; @@ -16617,6 +16670,7 @@ C8567490243F371D001A995E /* Tenor */, D8A468DE2181C5B50094B82F /* Site Creation */, 7E442FC820F6783600DEACA5 /* ActivityLog */, + 0C7D48182A4DB91B0023CF84 /* Blaze */, 8BEE845627B1DC5E0001A93C /* Dashboard */, B5DA8A5E20ADAA1C00D5BDE1 /* plugin-directory-jetpack.json */, 855408851A6F105700DDBD79 /* app-review-prompt-all-enabled.json */, @@ -17718,6 +17772,7 @@ FA111E372A2F38FC00896FCE /* BlazeCampaignsViewController.swift */, FA3A28172A38D36900206D74 /* BlazeCampaignTableViewCell.swift */, FA3A281A2A39C8FF00206D74 /* BlazeCampaignSingleStatView.swift */, + 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */, ); path = "Blaze Campaigns"; sourceTree = ""; @@ -17893,6 +17948,7 @@ isa = PBXGroup; children = ( FA98B61529A3B76A0071AAE8 /* DashboardBlazeCardCell.swift */, + 0CD382822A4B699E00612173 /* DashboardBlazeCardCellViewModel.swift */, FA98B61829A3BF050071AAE8 /* DashboardBlazePromoCardView.swift */, 0CDEC40B2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift */, 0C391E5D2A2FE5350040EA91 /* DashboardBlazeCampaignView.swift */, @@ -17980,7 +18036,8 @@ children = ( FA4B202E29A619130089FE68 /* BlazeFlowCoordinator.swift */, 80C523A329959DE000B1C14B /* BlazeWebViewController.swift */, - 80C523A62995D73C00B1C14B /* BlazeWebViewModel.swift */, + 80C523A62995D73C00B1C14B /* BlazeCreateCampaignWebViewModel.swift */, + 806BA1182A492A2700052422 /* BlazeCampaignDetailsWebViewModel.swift */, ); path = Webview; sourceTree = ""; @@ -19338,12 +19395,14 @@ F4426FDB287F066400218003 /* site-suggestions.json in Resources */, DC772B0128200A3700664C02 /* stats-visits-day-4.json in Resources */, 93C882A21EEB18D700227A59 /* html_page_with_link_to_rsd.html in Resources */, + 0C6C4CD62A4F0AEE0049E762 /* blaze-search-page-2.json in Resources */, D848CC0520FF062100A9038F /* notifications-user-content-meta.json in Resources */, 8BB185CF24B62D7600A4CCE8 /* reader-cards-success.json in Resources */, 7E4A772320F7BE94001C706D /* activity-log-comment-content.json in Resources */, E12BE5EE1C5235DB000FD5CA /* get-me-settings-v1.1.json in Resources */, 933D1F6C1EA7A3AB009FB462 /* TestingMode.storyboard in Resources */, 08F8CD371EBD2AA80049D0C0 /* test-image-device-photo-gps.jpg in Resources */, + 0C7D481A2A4DB9300023CF84 /* blaze-search-response.json in Resources */, D88A64A6208D92B1008AE9BC /* stock-photos-media.json in Resources */, C8567492243F3751001A995E /* tenor-search-response.json in Resources */, 7E4A772520F7C5E5001C706D /* activity-log-theme-content.json in Resources */, @@ -19352,6 +19411,7 @@ 3211055A250C027D0048446F /* invalid-jpeg-header.jpg in Resources */, E15027621E03E51500B847E3 /* notes-action-push.json in Resources */, 3211056B250C0F750048446F /* valid-png-header in Resources */, + 0C6C4CD42A4F0AD90049E762 /* blaze-search-page-1.json in Resources */, 748BD8851F19234300813F9A /* notifications-mark-as-read.json in Resources */, 7E442FCA20F678D100DEACA5 /* activity-log-pingback-content.json in Resources */, F127FFD824213B5600B9D41A /* atomic-get-authentication-cookie-success.json in Resources */, @@ -20980,7 +21040,7 @@ F1C197A62670DDB100DE1FF7 /* BloggingRemindersTracker.swift in Sources */, 17E24F5420FCF1D900BD70A3 /* Routes+MySites.swift in Sources */, 0828D7FA1E6E09AE00C7C7D4 /* WPAppAnalytics+Media.swift in Sources */, - 80C523A72995D73C00B1C14B /* BlazeWebViewModel.swift in Sources */, + 80C523A72995D73C00B1C14B /* BlazeCreateCampaignWebViewModel.swift in Sources */, C7A09A4D28403A34003096ED /* QRLoginURLParser.swift in Sources */, 591AA5011CEF9BF20074934F /* Post.swift in Sources */, 1E0FF01E242BC572008DA898 /* GutenbergWebViewController.swift in Sources */, @@ -21007,6 +21067,7 @@ 8BA77BCF2483415400E1EBBF /* ReaderDetailToolbar.swift in Sources */, E102B7901E714F24007928E8 /* RecentSitesService.swift in Sources */, E1D7FF381C74EB0E00E7E5E5 /* PlanService.swift in Sources */, + 83DC5C462A4B769000DAA422 /* JetpackSocialSettingsRemainingSharesView.swift in Sources */, F1655B4822A6C2FA00227BFB /* Routes+Mbar.swift in Sources */, 32CA6EC02390C61F00B51347 /* PostListEditorPresenter.swift in Sources */, 80D9CFF429DCA53E00FE3400 /* DashboardPagesListCardCell.swift in Sources */, @@ -21285,6 +21346,7 @@ 3F73388226C1CE9B0075D1DD /* TimeSelectionButton.swift in Sources */, 5DED0E181B432E0400431FCD /* SourcePostAttribution.m in Sources */, 1715179420F4B5CD002C4A38 /* MySitesCoordinator.swift in Sources */, + 0C0D3B0D2A4C79DE0050A00D /* BlazeCampaignsStream.swift in Sources */, 08D978561CD2AF7D0054F19A /* MenuItem+ViewDesign.m in Sources */, 3254366C24ABA82100B2C5F5 /* ReaderInterestsStyleGuide.swift in Sources */, 988056032183CCE50083B643 /* SiteStatsInsightsTableViewController.swift in Sources */, @@ -21411,6 +21473,7 @@ 3FBB2D2B27FB6CB200C57BBF /* SiteNameViewController.swift in Sources */, 17BD4A192101D31B00975AC3 /* NavigationActionHelpers.swift in Sources */, 3F4D035028A56F9B00F0A4FD /* CircularImageButton.swift in Sources */, + 0C8078AB2A4E01A5002ABF29 /* PagingFooterView.swift in Sources */, B55FFCFA1F034F1A0070812C /* String+Ranges.swift in Sources */, 7E846FF320FD37BD00881F5A /* ActivityCommentRange.swift in Sources */, 1E672D95257663CE00421F13 /* GutenbergAudioUploadProcessor.swift in Sources */, @@ -21428,6 +21491,7 @@ 3F8B45A9292C1F2C00730FA4 /* DashboardMigrationSuccessCell.swift in Sources */, 8B5E1DD827EA5929002EBEE3 /* PostCoordinator+Dashboard.swift in Sources */, 98458CB821A39D350025D232 /* StatsNoDataRow.swift in Sources */, + FE1E20152A47042500CE7C90 /* PublicizeInfo+CoreDataProperties.swift in Sources */, 3234BB172530DFCA0068DA40 /* ReaderTableCardCell.swift in Sources */, 7462BFD42028CD4400B552D8 /* ShareNoticeNavigationCoordinator.swift in Sources */, 803BB97C2959559500B3F6D6 /* RootViewPresenter.swift in Sources */, @@ -21435,6 +21499,7 @@ 178810B52611D25600A98BD8 /* Text+BoldSubString.swift in Sources */, 3FA53E9C256571D800F4D9A2 /* HomeWidgetCache.swift in Sources */, FAB8AB5F25AFFD0600F9F8A0 /* JetpackRestoreStatusCoordinator.swift in Sources */, + FE1E20172A47042500CE7C90 /* PublicizeInfo+CoreDataClass.swift in Sources */, F913BB0E24B3C58B00C19032 /* EventLoggingDelegate.swift in Sources */, 4089C51022371B120031CE78 /* TodayStatsRecordValue+CoreDataClass.swift in Sources */, 3F8B45A8292C1F2500730FA4 /* MigrationSuccessCardView.swift in Sources */, @@ -21731,7 +21796,6 @@ 2481B17F260D4D4E00AE59DB /* WPAccount+Lookup.swift in Sources */, 7E3E9B702177C9DC00FD5797 /* GutenbergViewController.swift in Sources */, 800035C1292307E8007D2D26 /* ExtensionConfiguration.swift in Sources */, - FE32EFFF275914390040BE67 /* MenuSheetViewController.swift in Sources */, 3FC7F89E2612341900FD8728 /* UnifiedPrologueStatsContentView.swift in Sources */, 7D21280D251CF0850086DD2C /* EditPageViewController.swift in Sources */, 738B9A4F21B85CF20005062B /* SiteCreator.swift in Sources */, @@ -21850,6 +21914,7 @@ D816C1E920E0880400C4D82F /* NotificationAction.swift in Sources */, E19B17B01E5C69A5007517C6 /* NSManagedObject.swift in Sources */, FF355D981FB492DD00244E6D /* ExportableAsset.swift in Sources */, + FEE48EFC2A4C8312008A48E0 /* Blog+JetpackSocial.swift in Sources */, E15644EF1CE0E53B00D96E64 /* PlanListViewModel.swift in Sources */, 983002A822FA05D600F03DBB /* InsightsManagementViewController.swift in Sources */, 98CAD296221B4ED2003E8F45 /* StatSection.swift in Sources */, @@ -22114,6 +22179,8 @@ 8B74A9A8268E3C68003511CE /* RewindStatus+multiSite.swift in Sources */, 3F5B3EAF23A851330060FF1F /* ReaderReblogPresenter.swift in Sources */, 7E3E7A6220E44E6A0075D159 /* BodyContentGroup.swift in Sources */, + 806BA1192A492A2700052422 /* BlazeCampaignDetailsWebViewModel.swift in Sources */, + 0C71959B2A3CA582002EA18C /* SiteSettingsRelatedPostsView.swift in Sources */, FEA088012696E7F600193358 /* ListTableHeaderView.swift in Sources */, 17AD36D51D36C1A60044B10D /* WPStyleGuide+Search.swift in Sources */, F1E3536B25B9F74C00992E3A /* WindowManager.swift in Sources */, @@ -22129,6 +22196,7 @@ 37EAAF4D1A11799A006D6306 /* CircularImageView.swift in Sources */, 837966A2299E9C85004A92B9 /* JetpackInstallPluginHelper.swift in Sources */, 3FD0316F24201E08005C0993 /* GravatarButtonView.swift in Sources */, + 0CD382832A4B699E00612173 /* DashboardBlazeCardCellViewModel.swift in Sources */, 7EBB4126206C388100012D98 /* StockPhotosService.swift in Sources */, E17FEADA221494B2006E1D2D /* Blog+Analytics.swift in Sources */, E69551F61B8B6AE200CB8E4F /* ReaderStreamViewController+Helper.swift in Sources */, @@ -22372,7 +22440,6 @@ B03B9236250BC5FD000A40AF /* Suggestion.swift in Sources */, 74EA3B88202A0462004F802D /* ShareNoticeConstants.swift in Sources */, 17A09B99238FE13B0022AE0D /* FeatureFlagOverrideStore.swift in Sources */, - FF1933FF1BB17DA3006825B8 /* RelatedPostsPreviewTableViewCell.m in Sources */, B54075D41D3D7D5B0095C318 /* IntrinsicTableView.swift in Sources */, 08A4E12C289D2337001D9EC7 /* UserPersistentRepository.swift in Sources */, 3FB34ADA25672AA5001A74A6 /* HomeWidgetTodayData.swift in Sources */, @@ -22648,7 +22715,6 @@ 0107E16428FFED1800DE87DB /* WidgetConfiguration.swift in Sources */, 82FC612A1FA8B6F000A1757E /* ActivityListViewModel.swift in Sources */, 0CB4056B29C78F06008EED0A /* BlogDashboardPersonalizationService.swift in Sources */, - F19153BD2549ED0800629EC4 /* UITextField+WorkaroundContinueIssue.swift in Sources */, E17780801C97FA9500FA7E14 /* StoreKit+Debug.swift in Sources */, E1ADE0EB20A9EF6200D6AADC /* PrivacySettingsViewController.swift in Sources */, E6431DE61C4E892900FD8D90 /* SharingDetailViewController.m in Sources */, @@ -22671,7 +22737,6 @@ C7B7CC702812FDCE007B9807 /* MySiteViewController+OnboardingPrompt.swift in Sources */, C81CCD6F243AF7D700A83E27 /* TenorReponseParser.swift in Sources */, 4020B2BD2007AC850002C963 /* WPStyleGuide+Gridicon.swift in Sources */, - FF4258501BA092EE00580C68 /* RelatedPostsSettingsViewController.m in Sources */, 982D261F2788DDF200A41286 /* ReaderCommentsFollowPresenter.swift in Sources */, F5E032D6240889EB003AF350 /* CreateButtonCoordinator.swift in Sources */, 74729CA32056FA0900D1394D /* SearchManager.swift in Sources */, @@ -22731,6 +22796,7 @@ 08D345501CD7F50900358E8C /* MenusSelectionDetailView.m in Sources */, 8B0732F0242BF7E800E7FBD3 /* Blog+Title.swift in Sources */, C94C0B1B25DCFA0100F2F69B /* FilterableCategoriesViewController.swift in Sources */, + FE1E201A2A473E0800CE7C90 /* JetpackSocialService.swift in Sources */, 591AA5021CEF9BF20074934F /* Post+CoreDataProperties.swift in Sources */, E19B17AE1E5C6944007517C6 /* BasePost.swift in Sources */, C71AF533281064DE00F9E99E /* OnboardingQuestionsCoordinator.swift in Sources */, @@ -23375,6 +23441,7 @@ 570BFD8D22823DE5007859A8 /* PostActionSheetTests.swift in Sources */, FA4ADADA1C509FE400F858D7 /* SiteManagementServiceTests.swift in Sources */, 3F1B66A323A2F54B0075F09E /* ReaderReblogActionTests.swift in Sources */, + FEE48EFF2A4C9855008A48E0 /* Blog+PublicizeTests.swift in Sources */, 5749984722FA0F2E00CE86ED /* PostNoticeViewModelTests.swift in Sources */, 08B6E51C1F037ADD00268F57 /* MediaFileManagerTests.swift in Sources */, 5981FE051AB8A89A0009E080 /* WPUserAgentTests.m in Sources */, @@ -23393,6 +23460,7 @@ C396C80B280F2401006FE7AC /* SiteDesignTests.swift in Sources */, 806E53E427E01CFE0064315E /* DashboardStatsViewModelTests.swift in Sources */, D88A649E208D82D2008AE9BC /* XCTestCase+Wait.swift in Sources */, + 0C6C4CD82A4F0F2C0049E762 /* Bundle+TestExtensions.swift in Sources */, C38C5D8127F61D2C002F517E /* MenuItemTests.swift in Sources */, E18549DB230FBFEF003C620E /* BlogServiceDeduplicationTests.swift in Sources */, 400A2C8F2217AD7F000A8A59 /* ClicksStatsRecordValueTests.swift in Sources */, @@ -23448,7 +23516,7 @@ 3FDDFE9627C8178C00606933 /* SiteStatsInformationTests.swift in Sources */, 74B335EC1F06F9520053A184 /* MockWordPressComRestApi.swift in Sources */, 80EF92932810FA5A0064A971 /* QuickStartFactoryTests.swift in Sources */, - 80C523AB29AE6C2200B1C14B /* BlazeWebViewModelTests.swift in Sources */, + 80C523AB29AE6C2200B1C14B /* BlazeCreateCampaignWebViewModelTests.swift in Sources */, D848CBFF20FF010F00A9038F /* FormattableCommentContentTests.swift in Sources */, 9123471B221449E200BD9F97 /* GutenbergInformativeDialogTests.swift in Sources */, 8332DD2829259BEB00802F7D /* DataMigratorTests.swift in Sources */, @@ -23494,6 +23562,7 @@ 7E987F58210811CC00CAFB88 /* NotificationContentRouterTests.swift in Sources */, E1C9AA561C10427100732665 /* MathTest.swift in Sources */, 93A379EC19FFBF7900415023 /* KeychainTest.m in Sources */, + 3F759FBE2A2DB3280039A845 /* AccountSettingsRemoteInterfaceStub.swift in Sources */, F1BB660C274E704D00A319BE /* LikeUserHelperTests.swift in Sources */, 436D5655212209D600CEAA33 /* RegisterDomainDetailsServiceProxyMock.swift in Sources */, F1450CF92437EEBB00A28BFE /* MediaRequestAuthenticatorTests.swift in Sources */, @@ -23506,6 +23575,7 @@ FF9A6E7121F9361700D36D14 /* MediaUploadHashTests.swift in Sources */, B532ACCF1DC3AB8E00FFFA57 /* NotificationSyncMediatorTests.swift in Sources */, 7E4A772F20F7FDF8001C706D /* ActivityLogTestData.swift in Sources */, + 3F759FBC2A2DB2CF0039A845 /* TestError.swift in Sources */, 40E7FEC82211EEC00032834E /* LastPostStatsRecordValueTests.swift in Sources */, 57240224234E5BE200227067 /* PostServiceSelfHostedTests.swift in Sources */, B59D40A61DB522DF003D2D79 /* NSAttributedStringTests.swift in Sources */, @@ -23552,6 +23622,7 @@ 40F50B82221310F000CBBB73 /* StatsTestCase.swift in Sources */, 02BE5CC02281B53F00E351BA /* RegisterDomainDetailsViewModelLoadingStateTests.swift in Sources */, FEFA263E26C58427009CCB7E /* ShareAppTextActivityItemSourceTests.swift in Sources */, + 0C6C4CD02A4F0A000049E762 /* BlazeCampaignsStreamTests.swift in Sources */, 40EE948222132F5800CD264F /* PublicizeConectionStatsRecordValueTests.swift in Sources */, 577C2AAB22936DCB00AD1F03 /* PostCardCellGhostableTests.swift in Sources */, 08B954F328535EE800B07185 /* FeatureHighlightStoreTests.swift in Sources */, @@ -23610,6 +23681,7 @@ F17A2A2023BFBD84001E96AC /* UIView+ExistingConstraints.swift in Sources */, 9A9D34FD23607CCC00BC95A3 /* AsyncOperationTests.swift in Sources */, B5552D821CD1061F00B26DF6 /* StringExtensionsTests.swift in Sources */, + 3F759FBA2A2DA93B0039A845 /* WPAccount+Fixture.swift in Sources */, 8B821F3C240020E2006B697E /* PostServiceUploadingListTests.swift in Sources */, 73178C3521BEE9AC00E37C9A /* TitleSubtitleHeaderTests.swift in Sources */, 08AAD6A11CBEA610002B2418 /* MenusServiceTests.m in Sources */, @@ -23631,6 +23703,7 @@ D81C2F5820F86CEA002AE1F1 /* NetworkStatus.swift in Sources */, E1C545801C6C79BB001CEB0E /* MediaSettingsTests.swift in Sources */, C3439B5F27FE3A3C0058DA55 /* SiteCreationWizardLauncherTests.swift in Sources */, + 806BA11C2A492B0F00052422 /* BlazeCampaignDetailsWebViewModelTests.swift in Sources */, 7E987F5A2108122A00CAFB88 /* NotificationUtility.swift in Sources */, FE32E7F12844971000744D80 /* ReminderScheduleCoordinatorTests.swift in Sources */, 4688E6CC26AB571D00A5D894 /* RequestAuthenticatorTests.swift in Sources */, @@ -23681,6 +23754,7 @@ E6843840221F5A2200752258 /* PostListExcessiveLoadMoreTests.swift in Sources */, 325D3B3D23A8376400766DF6 /* FullScreenCommentReplyViewControllerTests.swift in Sources */, 805CC0B9296680F7002941DC /* RemoteConfigStoreMock.swift in Sources */, + 0CD382862A4B6FCF00612173 /* DashboardBlazeCardCellViewModelTest.swift in Sources */, 0147D651294B6EA600AA6410 /* StatsRevampStoreTests.swift in Sources */, 57D6C83E22945A10003DDC7E /* PostCompactCellTests.swift in Sources */, B030FE0A27EBF0BC000F6F5E /* SiteCreationIntentTracksEventTests.swift in Sources */, @@ -23737,6 +23811,7 @@ 3F50945F245537A700C4470B /* ReaderTabViewModelTests.swift in Sources */, 0885A3671E837AFE00619B4D /* URLIncrementalFilenameTests.swift in Sources */, D848CBF920FEF82100A9038F /* NotificationsContentFactoryTests.swift in Sources */, + FE1E201E2A49D59400CE7C90 /* JetpackSocialServiceTests.swift in Sources */, 575802132357C41200E4C63C /* MediaCoordinatorTests.swift in Sources */, F18B43781F849F580089B817 /* PostAttachmentTests.swift in Sources */, 400A2C972217B883000A8A59 /* VisitsSummaryStatsRecordValueTests.swift in Sources */, @@ -24018,7 +24093,6 @@ FABB21612602FC2C00C8785C /* ReaderShowAttributionAction.swift in Sources */, FABB21622602FC2C00C8785C /* LinkSettingsViewController.swift in Sources */, 9856A39E261FC21E008D6354 /* UserProfileUserInfoCell.swift in Sources */, - FE32F000275914390040BE67 /* MenuSheetViewController.swift in Sources */, FABB21632602FC2C00C8785C /* FeatureItemCell.swift in Sources */, C3FBF4E928AFEDF8003797DF /* JetpackBrandingAnalyticsHelper.swift in Sources */, 803C493C283A7C0C00003E9B /* QuickStartChecklistHeader.swift in Sources */, @@ -24050,6 +24124,7 @@ FABB21772602FC2C00C8785C /* JetpackConnectionViewController.swift in Sources */, 803BB97A2959543D00B3F6D6 /* RootViewCoordinator.swift in Sources */, FABB21782602FC2C00C8785C /* NotificationActionsService.swift in Sources */, + 83DC5C472A4B769000DAA422 /* JetpackSocialSettingsRemainingSharesView.swift in Sources */, FABB21792602FC2C00C8785C /* JetpackScanService.swift in Sources */, FABB217A2602FC2C00C8785C /* FilterSheetView.swift in Sources */, FABB217B2602FC2C00C8785C /* WPAddPostCategoryViewController.m in Sources */, @@ -24230,6 +24305,7 @@ FABB21F72602FC2C00C8785C /* GridCell.swift in Sources */, FA4F383727D766020068AAF5 /* MySiteSettings.swift in Sources */, C72A4F68264088E4009CA633 /* JetpackNotFoundErrorViewModel.swift in Sources */, + FE1E201B2A473E0800CE7C90 /* JetpackSocialService.swift in Sources */, FABB21F82602FC2C00C8785C /* AdaptiveNavigationController.swift in Sources */, FABB21F92602FC2C00C8785C /* RemotePostCategory+Extensions.swift in Sources */, FABB21FA2602FC2C00C8785C /* PostAutoUploadMessages.swift in Sources */, @@ -24401,6 +24477,7 @@ FABB22752602FC2C00C8785C /* NetworkAware.swift in Sources */, FABB22762602FC2C00C8785C /* LanguageViewController.swift in Sources */, FABB22772602FC2C00C8785C /* InlineEditableNameValueCell.swift in Sources */, + 0C0D3B0E2A4C79DE0050A00D /* BlazeCampaignsStream.swift in Sources */, FABB22782602FC2C00C8785C /* TodayStatsRecordValue+CoreDataProperties.swift in Sources */, FABB22792602FC2C00C8785C /* WPCrashLoggingProvider.swift in Sources */, FA332AD529C1FC7A00182FBB /* MovedToJetpackViewModel.swift in Sources */, @@ -24485,7 +24562,7 @@ FABB22B62602FC2C00C8785C /* WPAndDeviceMediaLibraryDataSource.m in Sources */, 011896A329D5AF0700D34BA9 /* BlogDashboardCardConfigurable.swift in Sources */, FABB22B72602FC2C00C8785C /* SettingsListEditorViewController.swift in Sources */, - 80C523A82995D73C00B1C14B /* BlazeWebViewModel.swift in Sources */, + 80C523A82995D73C00B1C14B /* BlazeCreateCampaignWebViewModel.swift in Sources */, 982DDF97263238A6002B3904 /* LikeUserPreferredBlog+CoreDataProperties.swift in Sources */, FABB22B82602FC2C00C8785C /* QuickStartChecklistManager.swift in Sources */, FABB22B92602FC2C00C8785C /* NoResultsTenorConfiguration.swift in Sources */, @@ -24530,6 +24607,7 @@ FABB22D42602FC2C00C8785C /* ManagedPerson+CoreDataProperties.swift in Sources */, FE32F003275F602E0040BE67 /* CommentContentRenderer.swift in Sources */, 3FAE0653287C8FC500F46508 /* JPScrollViewDelegate.swift in Sources */, + FEE48EFD2A4C8312008A48E0 /* Blog+JetpackSocial.swift in Sources */, FABB22D52602FC2C00C8785C /* TenorStrings.swift in Sources */, FAFC064F27D2360B002F0483 /* QuickStartCell.swift in Sources */, 086F2482284F52DD00032F39 /* TooltipAnchor.swift in Sources */, @@ -24639,6 +24717,7 @@ FA3A281C2A39C8FF00206D74 /* BlazeCampaignSingleStatView.swift in Sources */, FABB231D2602FC2C00C8785C /* PlanGroup.swift in Sources */, 8320BDE6283D9359009DF2DE /* BlogService+BloggingPrompts.swift in Sources */, + 0C8078AC2A4E01A5002ABF29 /* PagingFooterView.swift in Sources */, FABB231E2602FC2C00C8785C /* MenuItemPostsViewController.m in Sources */, FABB231F2602FC2C00C8785C /* SignupUsernameTableViewController.swift in Sources */, FABB23202602FC2C00C8785C /* Blog+Editor.swift in Sources */, @@ -24800,6 +24879,7 @@ 8313B9FB2995A03C000AF26E /* JetpackRemoteInstallCardView.swift in Sources */, FABB23962602FC2C00C8785C /* StatsTableFooter.swift in Sources */, FABB23972602FC2C00C8785C /* BlogSyncFacade.m in Sources */, + 806BA11A2A492A2700052422 /* BlazeCampaignDetailsWebViewModel.swift in Sources */, FABB23982602FC2C00C8785C /* PrepublishingNavigationController.swift in Sources */, 8384C64228AAC82600EABE26 /* KeychainUtils.swift in Sources */, FABB23992602FC2C00C8785C /* UINavigationController+KeyboardFix.m in Sources */, @@ -24958,6 +25038,7 @@ C79C307D26EA919F00E88514 /* ReferrerDetailsViewModel.swift in Sources */, 8BBC778C27B5531700DBA087 /* BlogDashboardPersistence.swift in Sources */, FABB24172602FC2C00C8785C /* BlogSettings.swift in Sources */, + FE1E20162A47042500CE7C90 /* PublicizeInfo+CoreDataProperties.swift in Sources */, FABB24182602FC2C00C8785C /* WKWebView+UserAgent.swift in Sources */, FABB24192602FC2C00C8785C /* JetpackCapabilitiesService.swift in Sources */, FABB241A2602FC2C00C8785C /* PostToPost30To31.m in Sources */, @@ -25045,6 +25126,7 @@ FABB24542602FC2C00C8785C /* StoriesIntroViewController.swift in Sources */, FABB24552602FC2C00C8785C /* WPStyleGuide+Suggestions.m in Sources */, FABB24562602FC2C00C8785C /* SiteStatsInsightsViewModel.swift in Sources */, + 0C71959C2A3CA582002EA18C /* SiteSettingsRelatedPostsView.swift in Sources */, FABB24572602FC2C00C8785C /* CheckmarkTableViewCell.swift in Sources */, FABB24582602FC2C00C8785C /* ReaderManageScenePresenter.swift in Sources */, FABB24592602FC2C00C8785C /* MediaService.swift in Sources */, @@ -25133,6 +25215,7 @@ FABB24942602FC2C00C8785C /* PluginViewController.swift in Sources */, 011F52C92A16551A00B04114 /* FreeToPaidPlansCoordinator.swift in Sources */, 179A70F12729834B006DAC0A /* Binding+OnChange.swift in Sources */, + FE1E20182A47042500CE7C90 /* PublicizeInfo+CoreDataClass.swift in Sources */, 17F11EDC268623BA00D1BBA7 /* BloggingRemindersScheduleFormatter.swift in Sources */, FABB24952602FC2C00C8785C /* ReaderSiteTopic.swift in Sources */, FABB24962602FC2C00C8785C /* JetpackBackupCompleteViewController.swift in Sources */, @@ -25260,7 +25343,6 @@ FABB24EE2602FC2C00C8785C /* Suggestion.swift in Sources */, FABB24EF2602FC2C00C8785C /* ShareNoticeConstants.swift in Sources */, FABB24F02602FC2C00C8785C /* FeatureFlagOverrideStore.swift in Sources */, - FABB24F12602FC2C00C8785C /* RelatedPostsPreviewTableViewCell.m in Sources */, FABB24F22602FC2C00C8785C /* IntrinsicTableView.swift in Sources */, FABB24F32602FC2C00C8785C /* HomeWidgetTodayData.swift in Sources */, FABB24F42602FC2C00C8785C /* NoticePresenter.swift in Sources */, @@ -25539,7 +25621,6 @@ FABB25B62602FC2C00C8785C /* Product.swift in Sources */, FABB25B72602FC2C00C8785C /* PostTag.m in Sources */, FABB25B82602FC2C00C8785C /* ActivityListViewModel.swift in Sources */, - FABB25B92602FC2C00C8785C /* UITextField+WorkaroundContinueIssue.swift in Sources */, FABB25BA2602FC2C00C8785C /* StoreKit+Debug.swift in Sources */, FABB25BB2602FC2C00C8785C /* PrivacySettingsViewController.swift in Sources */, 3FFDEF812917882800B625CE /* MigrationNavigationController.swift in Sources */, @@ -25562,7 +25643,6 @@ FABB25C52602FC2C00C8785C /* MenusSelectionItemView.m in Sources */, FABB25C62602FC2C00C8785C /* TenorReponseParser.swift in Sources */, FABB25C72602FC2C00C8785C /* WPStyleGuide+Gridicon.swift in Sources */, - FABB25C82602FC2C00C8785C /* RelatedPostsSettingsViewController.m in Sources */, FABB25C92602FC2C00C8785C /* CreateButtonCoordinator.swift in Sources */, 3FBF21B8267AA17A0098335F /* BloggingRemindersAnimator.swift in Sources */, FABB25CA2602FC2C00C8785C /* SearchManager.swift in Sources */, @@ -25678,6 +25758,7 @@ FABB26182602FC2C00C8785C /* RegisterDomainDetailsViewModel+SectionDefinitions.swift in Sources */, FABB26192602FC2C00C8785C /* ActionRow.swift in Sources */, C395FB272822148400AE7C11 /* RemoteSiteDesign+Thumbnail.swift in Sources */, + 0CD382842A4B699E00612173 /* DashboardBlazeCardCellViewModel.swift in Sources */, 9815D0B426B49A0600DF7226 /* Comment+CoreDataProperties.swift in Sources */, FABB261A2602FC2C00C8785C /* AppSettingsViewController.swift in Sources */, 4A82C43228D321A300486CFF /* Blog+Post.swift in Sources */, @@ -30777,6 +30858,7 @@ E125443B12BF5A7200D87A0A /* WordPress.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + FE1E200F2A45ACE900CE7C90 /* WordPress 151.xcdatamodel */, FA3A281D2A42049F00206D74 /* WordPress 150.xcdatamodel */, FE5096572A13D5BA00DDD071 /* WordPress 149.xcdatamodel */, FA98B61329A39DA80071AAE8 /* WordPress 148.xcdatamodel */, @@ -30928,7 +31010,7 @@ 8350E15911D28B4A00A7B073 /* WordPress.xcdatamodel */, E125443D12BF5A7200D87A0A /* WordPress 2.xcdatamodel */, ); - currentVersion = FA3A281D2A42049F00206D74 /* WordPress 150.xcdatamodel */; + currentVersion = FE1E200F2A45ACE900CE7C90 /* WordPress 151.xcdatamodel */; name = WordPress.xcdatamodeld; path = Classes/WordPress.xcdatamodeld; sourceTree = ""; diff --git a/WordPress/WordPressTest/AccountServiceTests.swift b/WordPress/WordPressTest/AccountServiceTests.swift index d41409d8f705..ba0abe17782c 100644 --- a/WordPress/WordPressTest/AccountServiceTests.swift +++ b/WordPress/WordPressTest/AccountServiceTests.swift @@ -147,24 +147,10 @@ class AccountServiceTests: CoreDataTestCase { func testMergeMultipleDuplicateAccounts() throws { let context = contextManager.mainContext - let account1 = WPAccount(context: context) - account1.userID = 1 - account1.username = "username" - account1.authToken = "authToken" - account1.uuid = UUID().uuidString - - let account2 = WPAccount(context: context) - account2.userID = 1 - account2.username = "username" - account2.authToken = "authToken" - account2.uuid = UUID().uuidString - - let account3 = WPAccount(context: context) - account3.userID = 1 - account3.username = "username" - account3.authToken = "authToken" - account3.uuid = UUID().uuidString + let account1 = WPAccount.fixture(context: context, userID: 1) + let account2 = WPAccount.fixture(context: context, userID: 1) + let account3 = WPAccount.fixture(context: context, userID: 1) account1.addBlogs(createMockBlogs(withIDs: [1, 2, 3, 4, 5, 6], in: context)) account2.addBlogs(createMockBlogs(withIDs: [1, 2, 3], in: context)) @@ -240,17 +226,8 @@ class AccountServiceTests: CoreDataTestCase { } func testPurgeAccount() throws { - let account1 = WPAccount(context: mainContext) - account1.userID = 1 - account1.username = "username" - account1.authToken = "authToken" - account1.uuid = UUID().uuidString - - let account2 = WPAccount(context: mainContext) - account2.userID = 1 - account2.username = "username" - account2.authToken = "authToken" - account2.uuid = UUID().uuidString + let account1 = WPAccount.fixture(context: mainContext, userID: 1) + let account2 = WPAccount.fixture(context: mainContext, userID: 2) contextManager.saveContextAndWait(mainContext) try XCTAssertEqual(mainContext.count(for: WPAccount.fetchRequest()), 2) diff --git a/WordPress/WordPressTest/AccountSettingsRemoteInterfaceStub.swift b/WordPress/WordPressTest/AccountSettingsRemoteInterfaceStub.swift new file mode 100644 index 000000000000..59cd089c1887 --- /dev/null +++ b/WordPress/WordPressTest/AccountSettingsRemoteInterfaceStub.swift @@ -0,0 +1,77 @@ +@testable import WordPress +import WordPressKit + +class AccountSettingsRemoteInterfaceStub: AccountSettingsRemoteInterface { + + let updateSettingResult: Result<(), Error> + let getSettingsResult: Result + let changeUsernameShouldSucceed: Bool + let suggestUsernamesResult: [String] + let updatePasswordResult: Result<(), Error> + let closeAccountResult: Result<(), Error> + + init( + updateSettingResult: Result = .success(()), + // Defaulting to failure to avoid having to create AccountSettings here, because it required an NSManagedContext + getSettingsResult: Result = .failure(TestError()), + changeUsernameShouldSucceed: Bool = true, + suggestUsernamesResult: [String] = [], + updatePasswordResult: Result = .success(()), + closeAccountResult: Result = .success(()) + ) { + self.updateSettingResult = updateSettingResult + self.getSettingsResult = getSettingsResult + self.changeUsernameShouldSucceed = changeUsernameShouldSucceed + self.suggestUsernamesResult = suggestUsernamesResult + self.updatePasswordResult = updatePasswordResult + self.closeAccountResult = closeAccountResult + } + + func updateSetting(_ change: AccountSettingsChange, success: @escaping () -> Void, failure: @escaping (Error) -> Void) { + switch updateSettingResult { + case .success: + success() + case .failure(let error): + failure(error) + } + } + + func getSettings(success: @escaping (WordPressKit.AccountSettings) -> Void, failure: @escaping (Error) -> Void) { + switch getSettingsResult { + case .success(let settings): + success(settings) + case .failure(let error): + failure(error) + } + } + + func changeUsername(to username: String, success: @escaping () -> Void, failure: @escaping () -> Void) { + if changeUsernameShouldSucceed { + success() + } else { + failure() + } + } + + func suggestUsernames(base: String, finished: @escaping ([String]) -> Void) { + finished(suggestUsernamesResult) + } + + func updatePassword(_ password: String, success: @escaping () -> Void, failure: @escaping (Error) -> Void) { + switch updatePasswordResult { + case .success: + success() + case .failure(let error): + failure(error) + } + } + + func closeAccount(success: @escaping () -> Void, failure: @escaping (Error) -> Void) { + switch closeAccountResult { + case .success: + success() + case .failure(let error): + failure(error) + } + } +} diff --git a/WordPress/WordPressTest/AccountSettingsServiceTests.swift b/WordPress/WordPressTest/AccountSettingsServiceTests.swift index b365eb39fa6d..0681613fed44 100644 --- a/WordPress/WordPressTest/AccountSettingsServiceTests.swift +++ b/WordPress/WordPressTest/AccountSettingsServiceTests.swift @@ -8,25 +8,10 @@ class AccountSettingsServiceTests: CoreDataTestCase { private var service: AccountSettingsService! override func setUp() { - let account = WPAccount(context: mainContext) - account.username = "test" - account.authToken = "token" - account.userID = 1 - account.uuid = UUID().uuidString - - let settings = ManagedAccountSettings(context: mainContext) - settings.account = account - settings.username = "Username" - settings.displayName = "Display Name" - settings.primarySiteID = 1 - settings.aboutMe = "" - settings.email = "test@email.com" - settings.firstName = "Test" - settings.lastName = "User" - settings.language = "en" - settings.webAddress = "https://test.wordpress.com" - + let account = WPAccount.fixture(context: mainContext) + _ = makeManagedAccountSettings(context: mainContext, account: account) contextManager.saveContextAndWait(mainContext) + service = makeService(contextManager: contextManager, account: account) service = AccountSettingsService( userID: account.userID.intValue, @@ -35,41 +20,44 @@ class AccountSettingsServiceTests: CoreDataTestCase { ) } - private func managedAccountSettings() -> ManagedAccountSettings? { - contextManager.performQuery { context in - let request = NSFetchRequest(entityName: ManagedAccountSettings.entityName()) - request.predicate = NSPredicate(format: "account.userID = %d", self.service.userID) - request.fetchLimit = 1 - guard let results = (try? context.fetch(request)) as? [ManagedAccountSettings] else { - return nil - } - return results.first - } - } - func testUpdateSuccess() throws { - stub(condition: isPath("/rest/v1.1/me/settings")) { _ in - HTTPStubsResponse(jsonObject: [String: Any](), statusCode: 200, headers: nil) - } + // We've seen some flakiness in CI on this test, and therefore are using a stub object rather than stubbing the HTTP requests. + // Since this approach bypasses the entire network stack, the hope is that it'll result in a more robust test. + // + // This is the second test in this class edited this way. + // If we'll need to update a third, we shall also take the time to update the rest of the tests. + let service = AccountSettingsService( + userID: 1, + remote: AccountSettingsRemoteInterfaceStub(updateSettingResult: .success(())), + coreDataStack: contextManager + ) + waitUntil { done in - self.service.saveChange(.firstName("Updated"), finished: { success in + service.saveChange(.firstName("Updated"), finished: { success in expect(success).to(beTrue()) done() }) } + expect(self.managedAccountSettings()?.firstName).to(equal("Updated")) } func testUpdateFailure() throws { - stub(condition: isPath("/rest/v1.1/me/settings")) { _ in - HTTPStubsResponse(jsonObject: [String: Any](), statusCode: 500, headers: nil) - } + // We've seen some flakiness in CI on this test, and therefore are using a stub object rather than stubbing the HTTP requests. + // Since this approach bypasses the entire network stack, the hope is that it'll result in a more robust test. + let service = AccountSettingsService( + userID: 1, + remote: AccountSettingsRemoteInterfaceStub(updateSettingResult: .failure(TestError())), + coreDataStack: contextManager + ) + waitUntil { done in - self.service.saveChange(.firstName("Updated"), finished: { success in + service.saveChange(.firstName("Updated"), finished: { success in expect(success).to(beFalse()) done() }) } + expect(self.managedAccountSettings()?.firstName).to(equal("Test")) } @@ -100,3 +88,44 @@ class AccountSettingsServiceTests: CoreDataTestCase { wait(for: [notCrash], timeout: 0.5) } } + +extension AccountSettingsServiceTests { + + private func makeManagedAccountSettings( + context: NSManagedObjectContext, + account: WPAccount + ) -> ManagedAccountSettings { + let settings = ManagedAccountSettings(context: context) + settings.account = account + settings.username = "Username" + settings.displayName = "Display Name" + settings.primarySiteID = 1 + settings.aboutMe = "" + settings.email = "test@email.com" + settings.firstName = "Test" + settings.lastName = "User" + settings.language = "en" + settings.webAddress = "https://test.wordpress.com" + return settings + } + + private func makeService(contextManager: ContextManager, account: WPAccount) -> AccountSettingsService { + AccountSettingsService( + userID: account.userID.intValue, + remote: AccountSettingsRemote(wordPressComRestApi: account.wordPressComRestApi), + coreDataStack: contextManager + ) + } + + private func managedAccountSettings() -> ManagedAccountSettings? { + contextManager.performQuery { context in + let request = NSFetchRequest(entityName: ManagedAccountSettings.entityName()) + request.predicate = NSPredicate(format: "account.userID = %d", self.service.userID) + request.fetchLimit = 1 + guard let results = (try? context.fetch(request)) as? [ManagedAccountSettings] else { + return nil + } + return results.first + } + } +} diff --git a/WordPress/WordPressTest/Blaze/BlazeCampaignDetailsWebViewModelTests.swift b/WordPress/WordPressTest/Blaze/BlazeCampaignDetailsWebViewModelTests.swift new file mode 100644 index 000000000000..078af985607a --- /dev/null +++ b/WordPress/WordPressTest/Blaze/BlazeCampaignDetailsWebViewModelTests.swift @@ -0,0 +1,109 @@ +import XCTest +@testable import WordPress + +final class BlazeCampaignDetailsWebViewModelTests: CoreDataTestCase { + + // MARK: Private Variables + + private var view: BlazeWebViewMock! + private var externalURLHandler: ExternalURLHandlerMock! + private var blog: Blog! + private static let blogURL = "test.blog.com" + + // MARK: Setup + + override func setUp() { + super.setUp() + view = BlazeWebViewMock() + externalURLHandler = ExternalURLHandlerMock() + contextManager.useAsSharedInstance(untilTestFinished: self) + blog = BlogBuilder(mainContext).with(url: Self.blogURL).build() + } + + // MARK: Tests + + func testInternalURLsAllowed() throws { + // Given + let viewModel = BlazeCampaignDetailsWebViewModel(source: .campaignsList, blog: blog, campaignID: 0, view: view, externalURLHandler: externalURLHandler) + let validURL = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com?source=menu_item")) + var validRequest = URLRequest(url: validURL) + validRequest.mainDocumentURL = validURL + + // When + let policy = viewModel.shouldNavigate(to: validRequest, with: .linkActivated) + + // Then + XCTAssertEqual(policy, .allow) + XCTAssertFalse(externalURLHandler.openURLCalled) + } + + func testExternalURLsBlocked() throws { + // Given + let viewModel = BlazeCampaignDetailsWebViewModel(source: .campaignsList, blog: blog, campaignID: 0, view: view, externalURLHandler: externalURLHandler) + let invalidURL = try XCTUnwrap(URL(string: "https://test.com/test?example=test")) + var invalidRequest = URLRequest(url: invalidURL) + invalidRequest.mainDocumentURL = invalidURL + + // When + let policy = viewModel.shouldNavigate(to: invalidRequest, with: .linkActivated) + + // Then + XCTAssertEqual(policy, .cancel) + XCTAssertTrue(externalURLHandler.openURLCalled) + XCTAssertEqual(externalURLHandler.urlOpened?.absoluteString, invalidURL.absoluteString) + } + + func testCallingDismissTappedDismissesTheView() { + // Given + let viewModel = BlazeCampaignDetailsWebViewModel(source: .campaignsList, blog: blog, campaignID: 0, view: view, externalURLHandler: externalURLHandler) + + // When + viewModel.dismissTapped() + + // Then + XCTAssertTrue(view.dismissViewCalled) + } + + func testCallingStartBlazeSiteFlowLoadsTheView() throws { + // Given + let viewModel = BlazeCampaignDetailsWebViewModel(source: .campaignsList, blog: blog, campaignID: 0, view: view, externalURLHandler: externalURLHandler) + + // When + viewModel.startBlazeFlow() + + // Then + XCTAssertTrue(view.loadCalled) + XCTAssertEqual(view.requestLoaded?.url?.absoluteString, "https://wordpress.com/advertising/test.blog.com/campaigns/0?source=campaigns_list") + } +} + +private class BlazeWebViewMock: NSObject, BlazeWebView { + + var loadCalled = false + var requestLoaded: URLRequest? + var dismissViewCalled = false + + func load(request: URLRequest) { + loadCalled = true + requestLoaded = request + } + + func reloadNavBar() {} + + func dismissView() { + dismissViewCalled = true + } + + var cookieJar: WordPress.CookieJar = MockCookieJar() +} + +private class ExternalURLHandlerMock: ExternalURLHandler { + + var openURLCalled = false + var urlOpened: URL? + + func open(_ url: URL) { + self.openURLCalled = true + self.urlOpened = url + } +} diff --git a/WordPress/WordPressTest/Blaze/BlazeWebViewModelTests.swift b/WordPress/WordPressTest/Blaze/BlazeCreateCampaignWebViewModelTests.swift similarity index 76% rename from WordPress/WordPressTest/Blaze/BlazeWebViewModelTests.swift rename to WordPress/WordPressTest/Blaze/BlazeCreateCampaignWebViewModelTests.swift index b5604a134771..7f01f32103fd 100644 --- a/WordPress/WordPressTest/Blaze/BlazeWebViewModelTests.swift +++ b/WordPress/WordPressTest/Blaze/BlazeCreateCampaignWebViewModelTests.swift @@ -1,7 +1,7 @@ import XCTest @testable import WordPress -final class BlazeWebViewModelTests: CoreDataTestCase { +final class BlazeCreateCampaignWebViewModelTests: CoreDataTestCase { // MARK: Private Variables @@ -9,7 +9,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { private var externalURLHandler: ExternalURLHandlerMock! private var remoteConfigStore = RemoteConfigStoreMock() private var blog: Blog! - private static let blogURL = "test.blog.com" + private static let blogURL = "test.blog.com" // MARK: Setup @@ -27,7 +27,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testPostsListStep() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) let url = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com?source=menu_item")) let request = URLRequest(url: url) @@ -40,7 +40,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testPostsListStepWithPostsPath() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) let url = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com/posts?source=menu_item")) let request = URLRequest(url: url) @@ -53,7 +53,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testCampaignsStep() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) let url = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com/campaigns?source=menu_item")) let request = URLRequest(url: url) @@ -66,7 +66,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testDefaultWidgetStep() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) let url = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com?blazepress-widget=post-2&source=menu_item")) let request = URLRequest(url: url) @@ -79,7 +79,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testDefaultWidgetStepWithPostsPath() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) let url = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com/posts?blazepress-widget=post-2&source=menu_item")) let request = URLRequest(url: url) @@ -92,7 +92,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testExtractStepFromFragment() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) let url = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com?blazepress-widget=post-2&source=menu_item#step-2")) let request = URLRequest(url: url) @@ -105,7 +105,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testExtractStepFromFragmentPostsPath() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) let url = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com/posts?blazepress-widget=post-2&source=menu_item#step-3")) let request = URLRequest(url: url) @@ -118,7 +118,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testPostsListStepWithoutQuery() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) let url = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com")) let request = URLRequest(url: url) @@ -131,7 +131,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testPostsListStepWithPostsPathWithoutQuery() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) let url = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com/posts")) let request = URLRequest(url: url) @@ -144,7 +144,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testCampaignsStepWithoutQuery() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) let url = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com/campaigns")) let request = URLRequest(url: url) @@ -157,7 +157,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testDefaultWidgetStepWithoutQuery() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) let url = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com?blazepress-widget=post-2")) let request = URLRequest(url: url) @@ -170,7 +170,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testDefaultWidgetStepWithPostsPathWithoutQuery() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) let url = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com/posts?blazepress-widget=post-2")) let request = URLRequest(url: url) @@ -183,7 +183,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testExtractStepFromFragmentWithoutQuery() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) let url = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com?blazepress-widget=post-2#step-2")) let request = URLRequest(url: url) @@ -196,7 +196,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testExtractStepFromFragmentPostsPathWithoutQuery() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) let url = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com/posts?blazepress-widget=post-2#step-3")) let request = URLRequest(url: url) @@ -209,7 +209,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testInitialStep() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) // Then XCTAssertEqual(viewModel.currentStep, "unspecified") @@ -217,7 +217,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testCurrentStepMaintainedIfExtractionFails() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) let postsListURL = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com?source=menu_item")) let postsListRequest = URLRequest(url: postsListURL) let invalidURL = try XCTUnwrap(URL(string: "https://test.com/test?example=test")) @@ -238,7 +238,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testInternalURLsAllowed() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) let validURL = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com?source=menu_item")) var validRequest = URLRequest(url: validURL) validRequest.mainDocumentURL = validURL @@ -253,7 +253,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testExternalURLsBlocked() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) let invalidURL = try XCTUnwrap(URL(string: "https://test.com/test?example=test")) var invalidRequest = URLRequest(url: invalidURL) invalidRequest.mainDocumentURL = invalidURL @@ -269,7 +269,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testCallingShouldNavigateReloadsTheNavBar() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) let url = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com?source=menu_item")) let request = URLRequest(url: url) @@ -282,7 +282,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testCallingDismissTappedDismissesTheView() { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) // When viewModel.dismissTapped() @@ -293,7 +293,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testCallingStartBlazeSiteFlowLoadsTheView() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, externalURLHandler: externalURLHandler) // When viewModel.startBlazeFlow() @@ -305,7 +305,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testCallingStartBlazePostFlowLoadsTheView() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: 1, view: view, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: 1, view: view, externalURLHandler: externalURLHandler) // When viewModel.startBlazeFlow() @@ -317,7 +317,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testIsCurrentStepDismissible() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, remoteConfigStore: remoteConfigStore, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, remoteConfigStore: remoteConfigStore, externalURLHandler: externalURLHandler) // When var url = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com/posts?blazepress-widget=post-2#step-1")) @@ -346,7 +346,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { func testIsFlowCompleted() throws { // Given - let viewModel = BlazeWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, remoteConfigStore: remoteConfigStore, externalURLHandler: externalURLHandler) + let viewModel = BlazeCreateCampaignWebViewModel(source: .menuItem, blog: blog, postID: nil, view: view, remoteConfigStore: remoteConfigStore, externalURLHandler: externalURLHandler) // When var url = try XCTUnwrap(URL(string: "https://wordpress.com/advertising/test.blog.com/posts?blazepress-widget=post-2#step-1")) @@ -374,7 +374,7 @@ final class BlazeWebViewModelTests: CoreDataTestCase { } } -private class BlazeWebViewMock: BlazeWebView { +private class BlazeWebViewMock: NSObject, BlazeWebView { var loadCalled = false var requestLoaded: URLRequest? diff --git a/WordPress/WordPressTest/Blog+PublicizeTests.swift b/WordPress/WordPressTest/Blog+PublicizeTests.swift new file mode 100644 index 000000000000..287f88b52d78 --- /dev/null +++ b/WordPress/WordPressTest/Blog+PublicizeTests.swift @@ -0,0 +1,68 @@ +import Foundation +import XCTest + +@testable import WordPress + +final class Blog_PublicizeTests: CoreDataTestCase { + + func testAutoSharingInfoForDotComBlog() { + let blog = makeBlog(hostedAtWPCom: true, activeSocialFeature: false) + + // all dotcom sites should have no sharing limitations. + XCTAssertFalse(blog.isSocialSharingLimited) + } + + func testAutoSharingInfoForDotComBlogWithStoredSharingLimit() { + // unlikely case, but let's test anyway. + let blog = makeBlog(hostedAtWPCom: true, activeSocialFeature: false, hasPreExistingData: true) + + XCTAssertFalse(blog.isSocialSharingLimited) + XCTAssertNil(blog.sharingLimit) + } + + func testAutoSharingInfoForSelfHostedBlog() { + let blog = makeBlog(hostedAtWPCom: false, activeSocialFeature: false) + + XCTAssertTrue(blog.isSocialSharingLimited) + } + + func testAutoSharingInfoForSelfHostedBlogWithStoredSharingLimit() { + let blog = makeBlog(hostedAtWPCom: false, activeSocialFeature: false, hasPreExistingData: true) + + XCTAssertNotNil(blog.sharingLimit) + } + + func testAutoSharingInfoForSelfHostedBlogWithSocialFeature() { + let blog = makeBlog(hostedAtWPCom: false, activeSocialFeature: true) + + XCTAssertFalse(blog.isSocialSharingLimited) + } + + func testAutoSharingInfoForSelfHostedBlogWithSocialFeatureAndStoredSharingLimit() { + // Example: a free site purchased an individual Social Basic plan. In this case, there should still be a + // `PublicizeInfo` data stored in Core Data, but the blog might still have the social feature active. + let blog = makeBlog(hostedAtWPCom: false, activeSocialFeature: true, hasPreExistingData: true) + + // the sharing limit should be nil regardless of the existence of stored data. + XCTAssertNil(blog.sharingLimit) + } + + // MARK: - Helpers + + private func makeBlog(hostedAtWPCom: Bool, activeSocialFeature: Bool, hasPreExistingData: Bool = false) -> Blog { + let blog = BlogBuilder(mainContext) + .with(isHostedAtWPCom: hostedAtWPCom) + .with(planActiveFeatures: activeSocialFeature ? ["social-shares-1000"] : []) + .build() + + if hasPreExistingData { + let publicizeInfo = PublicizeInfo(context: mainContext) + publicizeInfo.shareLimit = 30 + publicizeInfo.sharesRemaining = 25 + blog.publicizeInfo = publicizeInfo + } + + return blog + } + +} diff --git a/WordPress/WordPressTest/BlogBuilder.swift b/WordPress/WordPressTest/BlogBuilder.swift index 6242bbe5d624..e90b62319d7d 100644 --- a/WordPress/WordPressTest/BlogBuilder.swift +++ b/WordPress/WordPressTest/BlogBuilder.swift @@ -40,6 +40,11 @@ final class BlogBuilder { return self } + func with(planActiveFeatures: [String]) -> Self { + blog.planActiveFeatures = planActiveFeatures + return self + } + func withJetpack(version: String? = nil, username: String? = nil, email: String? = nil) -> Self { set(blogOption: "jetpack_client_id", value: 1) set(blogOption: "jetpack_version", value: version as Any) diff --git a/WordPress/WordPressTest/BlogJetpackTests.swift b/WordPress/WordPressTest/BlogJetpackTests.swift index 10234b71b2cb..468d8fb933c5 100644 --- a/WordPress/WordPressTest/BlogJetpackTests.swift +++ b/WordPress/WordPressTest/BlogJetpackTests.swift @@ -151,6 +151,45 @@ class BlogJetpackTests: CoreDataTestCase { XCTAssertEqual(2, Blog.count(in: mainContext)) } + /// Verify an account's blogs won't be saved if the account is deleted during a blog sync. + func testSyncBlogsAndSignOut() throws { + let wpComAccount = try createOrUpdateAccount(username: "user", authToken: "token") + XCTAssertEqual(Blog.count(in: mainContext), 0) + var deleted = false + + // Blog sync makes a series of HTTP requests: the first one fetchs all blogs, followed by a few + // requests to get blog capabilities (one for each blog). + // + // See also https://github.com/wordpress-mobile/WordPress-iOS/issues/20964 + HTTPStubs.stubRequest(forEndpoint: "me/sites", + withFileAtPath: OHPathForFile("me-sites-with-jetpack.json", Self.self)!) + HTTPStubs.stubRequests { request in + (request.url?.path.matches(regex: "sites/\\d+/rewind/capabilities").count ?? 0) > 0 + } withStubResponse: { _ in + // We can't delete the `Account` instance until the first API request completes. Because the URLSession instance + // used in the `me/sites` API request will be invalidated upon account deletion (see `WPAccount.prepareForDeletion` method). + self.mainContext.performAndWait { + // Delete the account to simulate user signing out of the app. + guard !deleted else { return } + self.mainContext.delete(wpComAccount) + try! self.mainContext.save() + deleted = true + } + return HTTPStubsResponse(jsonObject: [String: Int](), statusCode: 200, headers: nil) + } + + let syncExpectation = expectation(description: "Blogs sync") + blogService.syncBlogs(for: wpComAccount) { + syncExpectation.fulfill() + } failure: { error in + XCTFail("Sync blogs shouldn't fail: \(error)") + } + + // No blogs should be saved after the sync blogs operation finishes. + wait(for: [syncExpectation], timeout: 1.0) + XCTAssertEqual(Blog.count(in: mainContext), 0) + } + // MARK: Jetpack Individual Plugins func testJetpackIsConnectedWithoutFullPluginGivenIndividualPluginOnlyReturnsTrue() { diff --git a/WordPress/WordPressTest/BlogTests.swift b/WordPress/WordPressTest/BlogTests.swift index c5048fdc2f80..b50b72a9f566 100644 --- a/WordPress/WordPressTest/BlogTests.swift +++ b/WordPress/WordPressTest/BlogTests.swift @@ -199,9 +199,7 @@ final class BlogTests: CoreDataTestCase { // Create an account with duplicated blogs let xmlrpc = "https://xmlrpc.test.wordpress.com" let account = try await contextManager.performAndSave { context in - let account = WPAccount(context: context) - account.username = "username" - account.authToken = "authToken" + let account = WPAccount.fixture(context: context) account.blogs = Set( (1...10).map { _ in let blog = BlogBuilder(context).build() diff --git a/WordPress/WordPressTest/Bundle+TestExtensions.swift b/WordPress/WordPressTest/Bundle+TestExtensions.swift new file mode 100644 index 000000000000..000bdeb77d12 --- /dev/null +++ b/WordPress/WordPressTest/Bundle+TestExtensions.swift @@ -0,0 +1,14 @@ +import Foundation +import XCTest + +extension Bundle { + static var test: Bundle { Bundle(for: TestBundleToken.self) } + + func json(named name: String) throws -> Data { + let url = try XCTUnwrap(Bundle(for: TestBundleToken.self) + .url(forResource: name, withExtension: "json")) + return try Data(contentsOf: url) + } +} + +private final class TestBundleToken {} diff --git a/WordPress/WordPressTest/ContentMigrationCoordinatorTests.swift b/WordPress/WordPressTest/ContentMigrationCoordinatorTests.swift index 6051d5f717dc..f80e4a678123 100644 --- a/WordPress/WordPressTest/ContentMigrationCoordinatorTests.swift +++ b/WordPress/WordPressTest/ContentMigrationCoordinatorTests.swift @@ -203,7 +203,7 @@ final class ContentMigrationCoordinatorTests: CoreDataTestCase { } func test_coordinatorShouldObserveLogoutNotifications() { - XCTAssertNotNil(mockNotificationCenter.observerBlock) + XCTAssertNotNil(mockNotificationCenter.observerSelector) XCTAssertNotNil(mockNotificationCenter.observedNotificationName) XCTAssertEqual(mockNotificationCenter.observedNotificationName, Foundation.Notification.Name.WPAccountDefaultWordPressComAccountChanged) } @@ -213,7 +213,7 @@ final class ContentMigrationCoordinatorTests: CoreDataTestCase { let loginNotification = mockNotificationCenter.makeLoginNotification() // When - mockNotificationCenter.observerBlock?(loginNotification) + mockNotificationCenter.post(loginNotification) // Then XCTAssertFalse(mockDataMigrator.exportCalled) @@ -226,7 +226,7 @@ final class ContentMigrationCoordinatorTests: CoreDataTestCase { let logoutNotification = mockNotificationCenter.makeLogoutNotification() // When - mockNotificationCenter.observerBlock?(logoutNotification) + mockNotificationCenter.post(logoutNotification) // Then XCTAssertFalse(mockDataMigrator.exportCalled) @@ -300,15 +300,12 @@ private extension ContentMigrationCoordinatorTests { private final class MockNotificationCenter: NotificationCenter { var observedNotificationName: NSNotification.Name? = nil - var observerBlock: ((Foundation.Notification) -> Void)? = nil - - override func addObserver(forName name: NSNotification.Name?, - object obj: Any?, - queue: OperationQueue?, - using block: @escaping @Sendable (Foundation.Notification) -> Void) -> NSObjectProtocol { - observedNotificationName = name - observerBlock = block - return NSNull() + var observerSelector: Selector? = nil + + override func addObserver(_ observer: Any, selector aSelector: Selector, name aName: NSNotification.Name?, object anObject: Any?) { + observedNotificationName = aName + observerSelector = aSelector + super.addObserver(observer, selector: aSelector, name: aName, object: anObject) } func makeLoginNotification() -> Foundation.Notification { diff --git a/WordPress/WordPressTest/ContextManagerTests.swift b/WordPress/WordPressTest/ContextManagerTests.swift index 5a9f0f16a3f6..c9268ab92fcb 100644 --- a/WordPress/WordPressTest/ContextManagerTests.swift +++ b/WordPress/WordPressTest/ContextManagerTests.swift @@ -146,9 +146,7 @@ class ContextManagerTests: XCTestCase { let derivedContext = contextManager.newDerivedContext() derivedContext.perform { - let account = WPAccount(context: derivedContext) - account.userID = 1 - account.username = "First User" + _ = WPAccount.fixture(context: derivedContext, userID: 1, username: "First User") contextManager.saveContextAndWait(derivedContext) } @@ -165,9 +163,7 @@ class ContextManagerTests: XCTestCase { // Save another user waitUntil { done in derivedContext.perform { - let account = WPAccount(context: derivedContext) - account.userID = 2 - account.username = "Second account" + _ = WPAccount.fixture(context: derivedContext, userID: 2) contextManager.saveContextAndWait(derivedContext) done() } @@ -193,17 +189,13 @@ class ContextManagerTests: XCTestCase { XCTAssertEqual(numberOfAccounts(), 0) try await contextManager.performAndSave { context in - let account = WPAccount(context: context) - account.userID = 1 - account.username = "First User" + _ = WPAccount.fixture(context: context, userID: 1) } XCTAssertEqual(numberOfAccounts(), 1) do { try await contextManager.performAndSave { context in - let account = WPAccount(context: context) - account.userID = 100 - account.username = "Unknown User" + _ = WPAccount.fixture(context: context, userID: 100) throw NSError(domain: "save", code: 1) } XCTFail("The above call should throw") @@ -221,9 +213,7 @@ class ContextManagerTests: XCTestCase { // > "In non-async functions, and closures without any await expression, the compiler selects the non-async overload" let sync: () -> Void = { contextManager.performAndSave { context in - let account = WPAccount(context: context) - account.userID = 2 - account.username = "Second User" + _ = WPAccount.fixture(context: context, userID: 2) } } sync() @@ -245,14 +235,10 @@ class ContextManagerTests: XCTestCase { ] contextManager.performAndSave({ - let account = WPAccount(context: $0) - account.userID = 1 - account.username = "First User" + _ = WPAccount.fixture(context: $0, userID: 1, username: "First User") contextManager.performAndSave { - let account = WPAccount(context: $0) - account.userID = 2 - account.username = "Second User" + _ = WPAccount.fixture(context: $0, userID: 2, username: "Second User") } saveOperations[1].fulfill() @@ -279,14 +265,10 @@ class ContextManagerTests: XCTestCase { ] contextManager.performAndSave({ - let account = WPAccount(context: $0) - account.userID = 1 - account.username = "First User" + _ = WPAccount.fixture(context: $0, userID: 1, username: "First User") contextManager.performAndSave({ - let account = WPAccount(context: $0) - account.userID = 2 - account.username = "Second User" + _ = WPAccount.fixture(context: $0, userID: 2, username: "Second User") }, completion: { saveOperations[1].fulfill() }, on: .main) @@ -380,9 +362,7 @@ class ContextManagerTests: XCTestCase { // First, insert an account into the database. let contextManager = ContextManager.forTesting() contextManager.performAndSave { context in - let account = WPAccount(context: context) - account.userID = 1 - account.username = "First User" + _ = WPAccount.fixture(context: context, userID: 1, username: "First User") } // Fetch the account in the main context @@ -441,8 +421,8 @@ class ContextManagerTests: XCTestCase { private func createOrUpdateAccount(username: String, newToken: String, in context: NSManagedObjectContext) throws { var account = try WPAccount.lookup(withUsername: username, in: context) if account == nil { - account = WPAccount(context: context) - account?.username = username + // Will this make tests fail because of the default userID in the fixture? + account = WPAccount.fixture(context: context, username: username) } account?.authToken = newToken } diff --git a/WordPress/WordPressTest/Dashboard/BlazeCampaignsStreamTests.swift b/WordPress/WordPressTest/Dashboard/BlazeCampaignsStreamTests.swift new file mode 100644 index 000000000000..c06a9bff27de --- /dev/null +++ b/WordPress/WordPressTest/Dashboard/BlazeCampaignsStreamTests.swift @@ -0,0 +1,269 @@ +import XCTest +import WordPressKit + +@testable import WordPress + +final class BlazeCampaignsStreamTests: CoreDataTestCase { + private var sut: BlazeCampaignsStream! + private var blog: Blog! + private var delegate = MockCampaignsStreamDelegate() + private let service = MockBlazePaginatedService() + + override func setUp() { + super.setUp() + + blog = ModelTestHelper.insertDotComBlog(context: mainContext) + blog.dotComID = 1 + + sut = BlazeCampaignsStream(blog: blog, service: service) + sut.delegate = delegate + } + + func testThatPagesAreLoaded() throws { + XCTAssertTrue(sut.campaigns.isEmpty) + XCTAssertFalse(sut.isLoading) + XCTAssertNil(sut.error) + + // When loading the first page + try loadNextPage() + + // Then the first page is loaded + XCTAssertEqual(service.numberOfRequests, 1) + XCTAssertEqual(delegate.appendedIndexPaths, [[IndexPath(row: 0, section: 0)]]) + guard delegate.states.count == 2 else { + return XCTFail("Unexpected state updates recorded: \(delegate.states)") + } + do { + let state = delegate.states[0] + XCTAssertEqual(state.campaignIDs, []) + XCTAssertTrue(state.isLoading) + XCTAssertNil(state.error) + } + do { + let state = delegate.states[1] + XCTAssertEqual(state.campaignIDs, [1]) + XCTAssertFalse(state.isLoading) + XCTAssertNil(state.error) + } + + // When loading the second page + delegate.reset() + try loadNextPage() + + // Then the second page is loaded + XCTAssertEqual(service.numberOfRequests, 2) + XCTAssertEqual(delegate.appendedIndexPaths, [[IndexPath(row: 1, section: 0)]]) + guard delegate.states.count == 2 else { + return XCTFail("Unexpected state updates recorded: \(delegate.states)") + } + do { + let state = delegate.states[0] + XCTAssertEqual(state.campaignIDs, [1]) + XCTAssertTrue(state.isLoading) + XCTAssertNil(state.error) + } + do { + let state = delegate.states[1] + // Then the duplicated campaign with ID "1" is skipped + XCTAssertEqual(state.campaignIDs, [1, 2]) + XCTAssertFalse(state.isLoading) + XCTAssertNil(state.error) + } + + // When loading with not more pages available + delegate.reset() + sut.load() + + // Then no requests are made + XCTAssertTrue(delegate.appendedIndexPaths.isEmpty) + XCTAssertTrue(delegate.states.isEmpty) + XCTAssertEqual(service.numberOfRequests, 2) + } + + func testFirstPageFailedToLoad() throws { + // Given + service.isFailing = true + + // When loading the first page + do { + try loadNextPage() + XCTFail("Expected the request to fail") + } catch { + XCTAssertEqual((error as? URLError)?.code, .notConnectedToInternet) + } + + // Then + XCTAssertEqual(service.numberOfRequests, 1) + XCTAssertEqual(delegate.appendedIndexPaths, []) + guard delegate.states.count == 2 else { + return XCTFail("Unexpected state updates recorded: \(delegate.states)") + } + do { + let state = delegate.states[0] + XCTAssertEqual(state.campaignIDs, []) + XCTAssertTrue(state.isLoading) + XCTAssertNil(state.error) + } + do { + let state = delegate.states[1] + XCTAssertEqual(state.campaignIDs, []) + XCTAssertFalse(state.isLoading) + let error = try XCTUnwrap(state.error) + XCTAssertEqual((error as? URLError)?.code, .notConnectedToInternet) + } + + // When connection restored + service.isFailing = false + + // When + delegate.reset() + try loadNextPage() + + // Then the first page is loaded + XCTAssertEqual(service.numberOfRequests, 2) + XCTAssertEqual(delegate.appendedIndexPaths, [[IndexPath(row: 0, section: 0)]]) + guard delegate.states.count == 2 else { + return XCTFail("Unexpected state updates recorded: \(delegate.states)") + } + do { + let state = delegate.states[0] + XCTAssertEqual(state.campaignIDs, []) + XCTAssertTrue(state.isLoading) + XCTAssertNil(state.error) + } + do { + let state = delegate.states[1] + XCTAssertEqual(state.campaignIDs, [1]) + XCTAssertFalse(state.isLoading) + XCTAssertNil(state.error) + } + } + + func testSecondPageFailedToLoad() throws { + // Given first page already loaded + try loadNextPage() + + // When connection fails + service.isFailing = true + + // When + delegate.reset() + do { + try loadNextPage() + XCTFail("Expected the request to fail") + } catch { + XCTAssertEqual((error as? URLError)?.code, .notConnectedToInternet) + } + + // Then the second page fails to load + XCTAssertEqual(service.numberOfRequests, 2) + XCTAssertEqual(delegate.appendedIndexPaths, []) + guard delegate.states.count == 2 else { + return XCTFail("Unexpected state updates recorded: \(delegate.states)") + } + do { + let state = delegate.states[0] + XCTAssertEqual(state.campaignIDs, [1]) + XCTAssertTrue(state.isLoading) + XCTAssertNil(state.error) + } + do { + let state = delegate.states[1] + XCTAssertEqual(state.campaignIDs, [1]) + XCTAssertFalse(state.isLoading) + let error = try XCTUnwrap(state.error) + XCTAssertEqual((error as? URLError)?.code, .notConnectedToInternet) + } + + // When connection is restored + service.isFailing = false + + // When loading the second page + delegate.reset() + try loadNextPage() + + // Then the second page is loaded + XCTAssertEqual(service.numberOfRequests, 3) + XCTAssertEqual(delegate.appendedIndexPaths, [[IndexPath(row: 1, section: 0)]]) + guard delegate.states.count == 2 else { + return XCTFail("Unexpected state updates recorded: \(delegate.states)") + } + do { + let state = delegate.states[0] + XCTAssertEqual(state.campaignIDs, [1]) + XCTAssertTrue(state.isLoading) + XCTAssertNil(state.error) + } + do { + let state = delegate.states[1] + // Then the duplicated campaign with ID "1" is skipped + XCTAssertEqual(state.campaignIDs, [1, 2]) + XCTAssertFalse(state.isLoading) + XCTAssertNil(state.error) + } + } + + @discardableResult + private func loadNextPage() throws -> BlazeCampaignsSearchResponse { + let expectation = self.expectation(description: "didLoadNextPage") + var result: Result? + sut.load { + result = $0 + expectation.fulfill() + } + wait(for: [expectation], timeout: 1) + return try XCTUnwrap(result).get() + } +} + +private final class MockCampaignsStreamDelegate: BlazeCampaignsStreamDelegate { + var appendedIndexPaths: [[IndexPath]] = [] + var states: [RecordedBlazeCampaignsStreamState] = [] + + func reset() { + appendedIndexPaths = [] + states = [] + } + + func stream(_ stream: BlazeCampaignsStream, didAppendItemsAt indexPaths: [IndexPath]) { + appendedIndexPaths.append(indexPaths) + } + + func streamDidRefreshState(_ stream: BlazeCampaignsStream) { + states.append(RecordedBlazeCampaignsStreamState(campaigns: stream.campaigns, isLoading: stream.isLoading, error: stream.error)) + } +} + +private struct RecordedBlazeCampaignsStreamState { + let campaigns: [BlazeCampaign] + var campaignIDs: [Int] { campaigns.map(\.campaignID) } + let isLoading: Bool + let error: Error? +} + +private final class MockBlazePaginatedService: BlazeServiceProtocol { + var numberOfRequests = 0 + var isFailing = false + + func getRecentCampaigns(for blog: Blog, page: Int, completion: @escaping (Result) -> Void) { + XCTAssertEqual(blog.dotComID, 1) + XCTAssertTrue([1, 2].contains(page)) + + numberOfRequests += 1 + + guard !isFailing else { + return completion(.failure(URLError(.notConnectedToInternet))) + } + + do { + let data = try Bundle.test.json(named: "blaze-search-page-\(page)") + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .iso8601 + let response = try decoder.decode(BlazeCampaignsSearchResponse.self, from: data) + completion(.success(response)) + } catch { + completion(.failure(error)) + } + } +} diff --git a/WordPress/WordPressTest/Dashboard/DashboardBlazeCardCellViewModelTest.swift b/WordPress/WordPressTest/Dashboard/DashboardBlazeCardCellViewModelTest.swift new file mode 100644 index 000000000000..31e543cf21ed --- /dev/null +++ b/WordPress/WordPressTest/Dashboard/DashboardBlazeCardCellViewModelTest.swift @@ -0,0 +1,129 @@ +import XCTest + +@testable import WordPress + +final class DashboardBlazeCardCellViewModelTest: CoreDataTestCase { + private let service = MockBlazeService() + private let store = MockDashboardBlazeStore() + private var blog: Blog! + private var sut: DashboardBlazeCardCellViewModel! + private var isBlazeCampaignsFlagEnabled = true + + override func setUp() { + super.setUp() + + blog = ModelTestHelper.insertDotComBlog(context: mainContext) + blog.dotComID = 1 + + createSUT() + } + + private func createSUT() { + sut = DashboardBlazeCardCellViewModel( + blog: blog, + service: service, + store: store, + isBlazeCampaignsFlagEnabled: { [unowned self] in self.isBlazeCampaignsFlagEnabled } + ) + } + + func testInitialState() { + switch sut.state { + case .promo: + break // Expected + case .campaign: + XCTFail("The card should show promo before the app fetches the data") + } + } + + func testCampaignRefresh() { + let expectation = self.expectation(description: "didRefresh") + sut.onRefresh = { _ in + expectation.fulfill() + } + + // When + sut.refresh() + XCTAssertTrue(service.didPerformRequest) + wait(for: [expectation], timeout: 1) + + // Then + switch sut.state { + case .promo: + XCTFail("The card should display the latest campaign") + case .campaign(let campaign): + XCTAssertEqual(campaign.name, "Test Post - don't approve") + } + } + + func testThatCampaignIsCached() { + // Given + let expectation = self.expectation(description: "didRefresh") + sut.onRefresh = { _ in expectation.fulfill() } + sut.refresh() + wait(for: [expectation], timeout: 1) + + // When the ViewModel is re-created + createSUT() + + // Then it shows the cached campaign + switch sut.state { + case .promo: + XCTFail("The card should display the latest campaign") + case .campaign(let campaign): + XCTAssertEqual(campaign.name, "Test Post - don't approve") + } + } + + func testThatNoRequestsAreMadeWhenFlagDisabled() { + // Given + isBlazeCampaignsFlagEnabled = false + createSUT() + + // When + sut.refresh() + + // Then + XCTAssertFalse(service.didPerformRequest) + + // Then still shows promo + switch sut.state { + case .promo: + break // Expected + case .campaign: + XCTFail("The card should show promo before the app fetches the data") + } + } +} + +private final class MockBlazeService: BlazeServiceProtocol { + var didPerformRequest = false + + func getRecentCampaigns(for blog: Blog, page: Int, completion: @escaping (Result) -> Void) { + didPerformRequest = true + DispatchQueue.main.async { + completion(Result(catching: getMockResponse)) + } + } +} + +private final class MockDashboardBlazeStore: DashboardBlazeStoreProtocol { + private var campaigns: [Int: BlazeCampaign] = [:] + + func getBlazeCampaign(forBlogID blogID: Int) -> BlazeCampaign? { + campaigns[blogID] + } + + func setBlazeCampaign(_ campaign: BlazeCampaign?, forBlogID blogID: Int) { + campaigns[blogID] = campaign + } +} + +private func getMockResponse() throws -> BlazeCampaignsSearchResponse { + let data = try Bundle.test.json(named: "blaze-search-response") + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .iso8601 + return try decoder.decode(BlazeCampaignsSearchResponse.self, from: data) +} diff --git a/WordPress/WordPressTest/Dashboard/FreeToPaidPlansDashboardCardHelperTests.swift b/WordPress/WordPressTest/Dashboard/FreeToPaidPlansDashboardCardHelperTests.swift index db0ae114461f..82b6bdc80c5f 100644 --- a/WordPress/WordPressTest/Dashboard/FreeToPaidPlansDashboardCardHelperTests.swift +++ b/WordPress/WordPressTest/Dashboard/FreeToPaidPlansDashboardCardHelperTests.swift @@ -28,7 +28,7 @@ final class FreeToPaidPlansDashboardCardHelperTests: CoreDataTestCase { XCTAssertFalse(result, "Card should not show for blogs without a free plan") } - func testShouldNotShowCardWithMappedDomain() { + func testShouldShowCardWithMappedDomain() { let blog = BlogBuilder(mainContext) .with(supportsDomains: true) .with(domainCount: 1, of: .wpCom) @@ -39,20 +39,7 @@ final class FreeToPaidPlansDashboardCardHelperTests: CoreDataTestCase { let result = FreeToPaidPlansDashboardCardHelper.shouldShowCard(for: blog, isJetpack: true, featureFlagEnabled: true) - XCTAssertFalse(result, "Card should not show for blogs with a mapped domain") - } - - func testShouldNotShowCardWhenDomainInformationIsNotLoaded() { - let blog = BlogBuilder(mainContext) - .with(supportsDomains: true) - .with(domainCount: 0, of: .wpCom) - .with(hasMappedDomain: false) - .with(hasPaidPlan: false) - .build() - - let result = FreeToPaidPlansDashboardCardHelper.shouldShowCard(for: blog, isJetpack: true, featureFlagEnabled: true) - - XCTAssertFalse(result, "Card should not show until domain information is loaded") + XCTAssertTrue(result, "Card should still be shown for blogs with a mapped domain and with a free plan") } func testShouldNotShowCardFeatureFlagDisabled() { diff --git a/WordPress/WordPressTest/GutenbergSettingsTests.swift b/WordPress/WordPressTest/GutenbergSettingsTests.swift index daf8e81d283e..c6ea339c3e3d 100644 --- a/WordPress/WordPressTest/GutenbergSettingsTests.swift +++ b/WordPress/WordPressTest/GutenbergSettingsTests.swift @@ -294,7 +294,7 @@ class GutenbergSettingsTests: CoreDataTestCase { blog.mobileEditor = editor if gutenbergEnabledFlag != nil { - let perSiteEnabledKey = GutenbergSettings.Key.enabledOnce(for: blog) + let perSiteEnabledKey = GutenbergSettings.Key.enabledOnce(forBlogURL: blog.url) database.set(true, forKey: perSiteEnabledKey) } } @@ -308,7 +308,7 @@ class GutenbergSettingsTests: CoreDataTestCase { settings.performGutenbergPhase2MigrationIfNeeded() - XCTAssertTrue(database.bool(forKey: GutenbergSettings.Key.showPhase2Dialog(for: blog))) + XCTAssertTrue(database.bool(forKey: GutenbergSettings.Key.showPhase2Dialog(forBlogURL: blog.url))) XCTAssertTrue(GutenbergRollout(database: database).isUserInRolloutGroup) } diff --git a/WordPress/WordPressTest/JetpackSocialServiceTests.swift b/WordPress/WordPressTest/JetpackSocialServiceTests.swift new file mode 100644 index 000000000000..5054b11b1187 --- /dev/null +++ b/WordPress/WordPressTest/JetpackSocialServiceTests.swift @@ -0,0 +1,189 @@ +import Foundation +import XCTest +import OHHTTPStubs + +@testable import WordPress + +class JetpackSocialServiceTests: CoreDataTestCase { + + private let timeout: TimeInterval = 1.0 + private let blogID = 1001 + + private var jetpackSocialPath: String { + "/wpcom/v2/sites/\(blogID)/jetpack-social" + } + + private lazy var service: JetpackSocialService = { + .init(coreDataStack: contextManager) + }() + + override func setUp() { + super.setUp() + + BlogBuilder(mainContext).with(blogID: blogID).build() + contextManager.saveContextAndWait(mainContext) + } + + override func tearDown() { + HTTPStubs.removeAllStubs() + super.tearDown() + } + + // MARK: syncSharingLimit + + // non-existing PublicizeInfo + some RemotePublicizeInfo -> insert + func testSyncSharingLimitWithNewPublicizeInfo() throws { + stub(condition: isPath(jetpackSocialPath)) { _ in + HTTPStubsResponse(jsonObject: ["share_limit": 30, + "to_be_publicized_count": 15, + "shared_posts_count": 15, + "shares_remaining": 14] as [String: Any], + statusCode: 200, + headers: nil) + } + + let expectation = expectation(description: "syncSharingLimit should succeed") + service.syncSharingLimit(for: blogID) { result in + guard case .success(let sharingLimit) = result else { + XCTFail("syncSharingLimit unexpectedly failed") + return expectation.fulfill() + } + + XCTAssertNotNil(sharingLimit) + XCTAssertEqual(sharingLimit?.remaining, 14) + XCTAssertEqual(sharingLimit?.limit, 30) + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + } + + // non-existing PublicizeInfo + nil RemotePublicizeInfo -> nothing changes + func testSyncSharingLimitWithNilPublicizeInfo() { + stub(condition: isPath(jetpackSocialPath)) { _ in + HTTPStubsResponse(jsonObject: [String: Any](), + statusCode: 200, + headers: nil) + } + + let expectation = expectation(description: "syncSharingLimit should succeed") + service.syncSharingLimit(for: blogID) { result in + guard case .success(let sharingLimit) = result else { + XCTFail("syncSharingLimit unexpectedly failed") + return expectation.fulfill() + } + + XCTAssertNil(sharingLimit) + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + } + + // pre-existing PublicizeInfo + some RemotePublicizeInfo -> update + func testSyncSharingLimitWithNewPublicizeInfoGivenPreExistingData() throws { + try addPreExistingPublicizeInfo() + stub(condition: isPath(jetpackSocialPath)) { _ in + HTTPStubsResponse(jsonObject: ["share_limit": 30, + "to_be_publicized_count": 15, + "shared_posts_count": 15, + "shares_remaining": 14] as [String: Any], + statusCode: 200, + headers: nil) + } + + let expectation = expectation(description: "syncSharingLimit should succeed") + service.syncSharingLimit(for: blogID) { result in + guard case .success(let sharingLimit) = result else { + XCTFail("syncSharingLimit unexpectedly failed") + return expectation.fulfill() + } + + // the sharing limit fields should be updated according to the newest data. + XCTAssertNotNil(sharingLimit) + XCTAssertEqual(sharingLimit?.remaining, 14) + XCTAssertEqual(sharingLimit?.limit, 30) + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + } + + // pre-existing PublicizeInfo + nil RemotePublicizeInfo -> delete + func testSyncSharingLimitWithNilPublicizeInfoGivenPreExistingData() throws { + try addPreExistingPublicizeInfo() + stub(condition: isPath(jetpackSocialPath)) { _ in + HTTPStubsResponse(jsonObject: [String: Any](), + statusCode: 200, + headers: nil) + } + + let expectation = expectation(description: "syncSharingLimit should succeed") + service.syncSharingLimit(for: blogID) { result in + guard case .success(let sharingLimit) = result else { + XCTFail("syncSharingLimit unexpectedly failed") + return expectation.fulfill() + } + + // the pre-existing sharing limit should've been deleted. + XCTAssertNil(sharingLimit) + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + } + + // non-existing blog ID + some RemotePublicizeInfo + func testSyncSharingLimitWithNewPublicizeInfoGivenInvalidBlogID() { + let invalidBlogID = 1002 + stub(condition: isPath("/wpcom/v2/sites/\(invalidBlogID)/jetpack-social")) { _ in + HTTPStubsResponse(jsonObject: [String: Any](), + statusCode: 200, + headers: nil) + } + + let expectation = expectation(description: "syncSharingLimit should fail") + service.syncSharingLimit(for: invalidBlogID) { result in + guard case .failure(let error) = result, + case .blogNotFound(let id) = error as? JetpackSocialService.ServiceError else { + XCTFail("Expected JetpackSocialService.ServiceError to occur") + return expectation.fulfill() + } + + XCTAssertEqual(id, invalidBlogID) + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + } + + func testSyncSharingLimitRemoteFetchFailure() { + stub(condition: isPath(jetpackSocialPath)) { _ in + HTTPStubsResponse(jsonObject: [String: Any](), + statusCode: 500, + headers: nil) + } + + let expectation = expectation(description: "syncSharingLimit should fail") + service.syncSharingLimit(for: blogID) { result in + guard case .failure = result else { + XCTFail("syncSharingLimit unexpectedly succeeded") + return expectation.fulfill() + } + + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + } + +} + +// MARK: - Helpers + +private extension JetpackSocialServiceTests { + + func addPreExistingPublicizeInfo() throws { + let blog = try Blog.lookup(withID: blogID, in: mainContext) + let info = PublicizeInfo(context: mainContext) + info.sharesRemaining = 550 + info.shareLimit = 1000 + blog?.publicizeInfo = info + contextManager.saveContextAndWait(mainContext) + } + +} diff --git a/WordPress/WordPressTest/LikeUserHelperTests.swift b/WordPress/WordPressTest/LikeUserHelperTests.swift index f5a789a5cf47..dedd508a8f1a 100644 --- a/WordPress/WordPressTest/LikeUserHelperTests.swift +++ b/WordPress/WordPressTest/LikeUserHelperTests.swift @@ -3,15 +3,21 @@ import XCTest class LikeUserHelperTests: CoreDataTestCase { - func createTestRemoteUserDictionary(withPreferredBlog hasPreferredBlog: Bool) -> [String: Any] { + var siteID: NSNumber = 20 + + func createTestRemoteUserDictionary( + withPreferredBlog hasPreferredBlog: Bool, + siteID: Int? = nil, + year: Int = 2021 + ) -> [String: Any] { var remoteUserDictionary: [String: Any] = [ - "ID": 15, + "ID": Int.random(in: 0...Int.max), "login": "testlogin", "name": "testname", - "site_ID": 20, + "site_ID": siteID ?? self.siteID.intValue, "avatar_URL": "wordpress.org/test2", "bio": "testbio", - "date_liked": "2021-11-24T04:02:42+0000", + "date_liked": "\(year)-11-24T04:02:42+0000", ] if hasPreferredBlog { @@ -77,4 +83,39 @@ class LikeUserHelperTests: CoreDataTestCase { waitForExpectations(timeout: 5) } + + func testFetchingLikedUser() { + XCTAssertEqual(mainContext.countObjects(ofType: LikeUser.self), 0) + + let commentID: NSNumber = 1 + let otherCommentID: NSNumber = 2 + // Insert likes with a recent date + for _ in 1...10 { + let dict = createTestRemoteUserDictionary(withPreferredBlog: false, year: 2010) + let user = RemoteLikeUser(dictionary: dict, commentID: commentID, siteID: siteID) + _ = LikeUserHelper.createOrUpdateFrom(remoteUser: user, context: mainContext) + } + // Insert likes with an older date + for _ in 1...5 { + let dict = createTestRemoteUserDictionary(withPreferredBlog: false, year: 1990) + let user = RemoteLikeUser(dictionary: dict, commentID: commentID, siteID: siteID) + _ = LikeUserHelper.createOrUpdateFrom(remoteUser: user, context: mainContext) + } + // Insert likes on another comment + for _ in 1...3 { + let dict = createTestRemoteUserDictionary(withPreferredBlog: false) + let user = RemoteLikeUser(dictionary: dict, commentID: otherCommentID, siteID: siteID) + _ = LikeUserHelper.createOrUpdateFrom(remoteUser: user, context: mainContext) + } + + // There are 18 like saved in the database in total + XCTAssertEqual(mainContext.countObjects(ofType: LikeUser.self), 18) + + // There are 15 likes on the comment with `commentID` + XCTAssertEqual(LikeUserHelper.likeUsersFor(commentID: commentID, siteID: siteID, in: mainContext).count, 15) + + // There are 10 likes since 2001 and 5 likes before. + // How the `after` argument should behave might be confusing. See https://github.com/wordpress-mobile/WordPress-iOS/pull/21028#issuecomment-1624661943 + XCTAssertEqual(LikeUserHelper.likeUsersFor(commentID: commentID, siteID: siteID, after: Date(timeIntervalSinceReferenceDate: 0), in: mainContext).count, 5) + } } diff --git a/WordPress/WordPressTest/PeopleServiceTests.swift b/WordPress/WordPressTest/PeopleServiceTests.swift index 128a27306586..48d46b3bee5c 100644 --- a/WordPress/WordPressTest/PeopleServiceTests.swift +++ b/WordPress/WordPressTest/PeopleServiceTests.swift @@ -18,9 +18,7 @@ class PeopleServiceTests: CoreDataTestCase { } contextManager.performAndSave { context in - let account = WPAccount(context: context) - account.username = "username" - account.authToken = "token" + let account = WPAccount.fixture(context: context) let blog = Blog(context: context) blog.dotComID = NSNumber(value: self.siteID) diff --git a/WordPress/WordPressTest/PostListTableViewHandlerTests.swift b/WordPress/WordPressTest/PostListTableViewHandlerTests.swift index b5a08b61a5cd..90d812ebd94f 100644 --- a/WordPress/WordPressTest/PostListTableViewHandlerTests.swift +++ b/WordPress/WordPressTest/PostListTableViewHandlerTests.swift @@ -22,7 +22,7 @@ class PostListHandlerMock: NSObject, WPTableViewHandlerDelegate { return setUpInMemoryManagedObjectContext() } - func fetchRequest() -> NSFetchRequest { + func fetchRequest() -> NSFetchRequest? { let a = NSFetchRequest(entityName: String(describing: Post.self)) a.sortDescriptors = [NSSortDescriptor(key: BasePost.statusKeyPath, ascending: true)] return a diff --git a/WordPress/WordPressTest/Test Data/blaze-search-page-1.json b/WordPress/WordPressTest/Test Data/blaze-search-page-1.json new file mode 100644 index 000000000000..60be82986557 --- /dev/null +++ b/WordPress/WordPressTest/Test Data/blaze-search-page-1.json @@ -0,0 +1,26 @@ +{ + "total_pages": 2, + "campaigns": [ + { + "campaign_id": 1, + "name": "Campaign 01", + "start_date": "2023-06-13T00:00:00Z", + "end_date": "2023-06-01T19:15:45Z", + "status": "finished", + "ui_status": "finished", + "avatar_url": "https://example.com/avatar.jpg", + "budget_cents": 500, + "target_url": "https://example.com/campaign-01/target-url", + "content_config": { + "title": "Test Post - don't approve", + "snippet": "Test Post Empty Empty", + "clickUrl": "https://example.com/campaign-01/click-url", + "imageUrl": "https://example.com/campaign-01/image.jpg" + }, + "campaign_stats": { + "impressions_total": 1000, + "clicks_total": 235 + } + } + ] +} diff --git a/WordPress/WordPressTest/Test Data/blaze-search-page-2.json b/WordPress/WordPressTest/Test Data/blaze-search-page-2.json new file mode 100644 index 000000000000..d39c1a3fdd1b --- /dev/null +++ b/WordPress/WordPressTest/Test Data/blaze-search-page-2.json @@ -0,0 +1,47 @@ +{ + "total_pages": 2, + "campaigns": [ + { + "campaign_id": 1, + "name": "Campaign 01", + "start_date": "2023-06-13T00:00:00Z", + "end_date": "2023-06-01T19:15:45Z", + "status": "finished", + "ui_status": "finished", + "avatar_url": "https://example.com/avatar.jpg", + "budget_cents": 500, + "target_url": "https://example.com/campaign-01/target-url", + "content_config": { + "title": "Test Post - don't approve", + "snippet": "Test Post Empty Empty", + "clickUrl": "https://example.com/campaign-01/click-url", + "imageUrl": "https://example.com/campaign-01/image.jpg" + }, + "campaign_stats": { + "impressions_total": 1000, + "clicks_total": 235 + } + }, + { + "campaign_id": 2, + "name": "Campaign 02", + "start_date": "2023-06-13T00:00:00Z", + "end_date": "2023-06-01T19:15:45Z", + "status": "finished", + "ui_status": "finished", + "avatar_url": "https://example.com/avatar.jpg", + "budget_cents": 500, + "target_url": "https://example.com/campaign-02/target-url", + "content_config": { + "title": "Test Post - don't approve", + "snippet": "Test Post Empty Empty", + "clickUrl": "https://example.com/campaign-02/click-url", + "imageUrl": "https://example.com/campaign-02/image.jpg" + }, + "campaign_stats": { + "impressions_total": 1000, + "clicks_total": 235 + } + } + ] +} diff --git a/WordPress/WordPressTest/Test Data/blaze-search-response.json b/WordPress/WordPressTest/Test Data/blaze-search-response.json new file mode 100644 index 000000000000..a4a848897540 --- /dev/null +++ b/WordPress/WordPressTest/Test Data/blaze-search-response.json @@ -0,0 +1,26 @@ +{ + "totalItems": 3, + "campaigns": [ + { + "campaign_id": 26916, + "name": "Test Post - don't approve", + "start_date": "2023-06-13T00:00:00Z", + "end_date": "2023-06-01T19:15:45Z", + "status": "finished", + "ui_status": "finished", + "avatar_url": "https://example.com/avatar.jpg", + "budget_cents": 500, + "target_url": "https://example.com/campaign-01/target-url", + "content_config": { + "title": "Test Post - don't approve", + "snippet": "Test Post Empty Empty", + "clickUrl": "https://example.com/campaign-01/click-url", + "imageUrl": "https://example.com/campaign-01/image.jpg" + }, + "campaign_stats": { + "impressions_total": 1000, + "clicks_total": 235 + } + } + ] +} diff --git a/WordPress/WordPressTest/TestError.swift b/WordPress/WordPressTest/TestError.swift new file mode 100644 index 000000000000..417e1b1162d7 --- /dev/null +++ b/WordPress/WordPressTest/TestError.swift @@ -0,0 +1,8 @@ +struct TestError: Error { + + let id: Int + + init(id: Int = 1) { + self.id = id + } +} diff --git a/WordPress/WordPressTest/WPAccount+Fixture.swift b/WordPress/WordPressTest/WPAccount+Fixture.swift new file mode 100644 index 000000000000..c56b8c08df0e --- /dev/null +++ b/WordPress/WordPressTest/WPAccount+Fixture.swift @@ -0,0 +1,20 @@ +/// Centralized utility to generate preconfigured WPAccount instances +extension WPAccount { + + static func fixture( + context: NSManagedObjectContext, + // Using a constant UUID by default to keep the tests deterministic. + // There's nothing special in the value itself. It's just a UUID() value copied over. + uuid: UUID = UUID(uuidString: "D0D0298F-D7EF-4F32-A1F8-DDDBB8ADB8DF")!, + userID: Int = 1, + username: String = "username", + authToken: String = "authToken" + ) -> WPAccount { + let account = WPAccount(context: context) + account.userID = NSNumber(value: userID) + account.username = username + account.authToken = authToken + account.uuid = uuid.uuidString + return account + } +} diff --git a/config/Common.xcconfig b/config/Common.xcconfig index b1dedffcad96..83f13cefeb4b 100644 --- a/config/Common.xcconfig +++ b/config/Common.xcconfig @@ -1,3 +1,3 @@ GCC_WARN_UNUSED_PARAMETER = YES WARNING_CFLAGS = -Wno-nullability-completeness -IPHONEOS_DEPLOYMENT_TARGET = 14.0 +IPHONEOS_DEPLOYMENT_TARGET = 15.0 diff --git a/config/Version.internal.xcconfig b/config/Version.internal.xcconfig index fa13abaf8b2c..3f4183673d42 100644 --- a/config/Version.internal.xcconfig +++ b/config/Version.internal.xcconfig @@ -1,4 +1,4 @@ -VERSION_SHORT=22.7 +VERSION_SHORT=22.8 // Internal long version example: VERSION_LONG=9.9.0.20180423 -VERSION_LONG=22.7.0.20230630 +VERSION_LONG=22.8.0.20230710 diff --git a/config/Version.public.xcconfig b/config/Version.public.xcconfig index 042eddd2a355..0441a4bd8f3c 100644 --- a/config/Version.public.xcconfig +++ b/config/Version.public.xcconfig @@ -1,4 +1,4 @@ -VERSION_SHORT=22.7 +VERSION_SHORT=22.8 // Public long version example: VERSION_LONG=9.9.0.0 -VERSION_LONG=22.7.0.1 +VERSION_LONG=22.8.0.0 diff --git a/fastlane/jetpack_metadata/ar-SA/release_notes.txt b/fastlane/jetpack_metadata/ar-SA/release_notes.txt new file mode 100644 index 000000000000..165c71a28dd5 --- /dev/null +++ b/fastlane/jetpack_metadata/ar-SA/release_notes.txt @@ -0,0 +1,6 @@ +أصلحنا مشكلة في بطاقة "العمل على مسودة تدوينة" في الشاشة الرئيسية. لن يتعطل التطبيق بعد الآن عند الوصول إلى المسودات في أثناء الوجود في منتصف عملية الرفع. + +قمنا بحل مشكلتين في محرر المكوّنات. + +- ناحية المين، تعرض مكوّنات الصور الآن نسبة العرض إلى الارتفاع الصحيحة، سواء أكانت الصورة تحتوي على عرض وارتفاع محدَدين أم لا. +- عندما تكتب نصًا، سيظل مرضع المؤشر في المكان المفترض أن يكون فيه؛ ولن يتحرك. تحلَّ بالهدوء وواصل الكتابة. diff --git a/fastlane/jetpack_metadata/de-DE/release_notes.txt b/fastlane/jetpack_metadata/de-DE/release_notes.txt new file mode 100644 index 000000000000..6738dbabed4e --- /dev/null +++ b/fastlane/jetpack_metadata/de-DE/release_notes.txt @@ -0,0 +1,6 @@ +Wir haben ein Problem mit der Karte „An einem Beitragsentwurf arbeiten“ der Startseite behoben. Die App stürzt nun nicht mehr ab, wenn du auf Entwürfe zugreifst, während sie hochgeladen werden. + +Außerdem haben wir einige Probleme im Block-Editor behoben. + +- Bei Bildblöcken wird jetzt das richtige Bildformat angezeigt, egal ob Höhe und Breite des Bildes definiert sind oder nicht. +- Beim Diktieren bleibt der Cursor an der gewünschten Stelle und springt nicht mehr herum. So kannst du ganz in Ruhe weiterdiktieren. diff --git a/fastlane/jetpack_metadata/default/release_notes.txt b/fastlane/jetpack_metadata/default/release_notes.txt index 637ca793c29c..3c0fb643df2b 100644 --- a/fastlane/jetpack_metadata/default/release_notes.txt +++ b/fastlane/jetpack_metadata/default/release_notes.txt @@ -1,7 +1,6 @@ -* [**] [internal] Blaze: Switch to using new canBlaze property to determine Blaze eligiblity. [#20916] -* [**] Fixed crash issue when accessing drafts that are mid-upload from the Home 'Work on a Draft Post' card. [#20872] -* [**] [internal] Make sure media-related features function correctly. [#20889], [20887] -* [*] [internal] Posts list: Disable action bar/menu button when a post is being uploaded [#20885] -* [*] Block editor: Image block - Fix issue where in some cases the image doesn't display the right aspect ratio [https://github.com/wordpress-mobile/gutenberg-mobile/pull/5869] -* [*] Block editor: Fix cursor positioning when dictating text on iOS [https://github.com/WordPress/gutenberg/issues/51227] +We fixed an issue with the home screen’s “Work on a draft post” card. The app will no longer crash when you access drafts while they’re in the middle of uploading. +We also solved a couple of problems in the block editor. + +- Right on—image blocks now display the correct aspect ratio, whether or not the image has a set width and height. +- When you’re dictating text, the cursor’s position will stay where it’s supposed to—no more jumping around. Keep calm and dictate on. diff --git a/fastlane/jetpack_metadata/es-ES/release_notes.txt b/fastlane/jetpack_metadata/es-ES/release_notes.txt new file mode 100644 index 000000000000..7601ed233ddd --- /dev/null +++ b/fastlane/jetpack_metadata/es-ES/release_notes.txt @@ -0,0 +1,6 @@ +Hemos corregido un problema que se producía en la tarjeta “Trabajar en un borrador de entrada” de la pantalla de inicio. La aplicación ya no se bloqueará si accedes a los borradores mientras se cargan. + +También hemos resuelto un par de problemas en el editor de bloques. + +- Ahora, los bloques de imagen muestran la relación de aspecto correcta, tanto si la imagen tiene una anchura y una altura determinadas como si no. +- Al dictar un texto, la posición del cursor se mantendrá en su sitio y no se producirán saltos. De esta manera, podrás dictar con tranquilidad. diff --git a/fastlane/jetpack_metadata/fr-FR/release_notes.txt b/fastlane/jetpack_metadata/fr-FR/release_notes.txt new file mode 100644 index 000000000000..86cc0dc7247c --- /dev/null +++ b/fastlane/jetpack_metadata/fr-FR/release_notes.txt @@ -0,0 +1,6 @@ +Nous avons corrigé un problème avec la carte « Travailler sur un brouillon d’article » de l’écran d’accueil. L’application ne rencontre plus d’incident lorsque vous accédez à des brouillons en cours de chargement. + +Nous avons également résolu quelques problèmes dans l’éditeur de blocs. + +- Les blocs d’images de droite affichent désormais le bon rapport hauteur/largeur, que l’image soit ou non définie en largeur et en hauteur. +- Lorsque vous dictez du texte, la position du curseur reste à l’endroit prévu, il n’y a plus de sauts. Restez calme et continuez à dicter. diff --git a/fastlane/jetpack_metadata/he/release_notes.txt b/fastlane/jetpack_metadata/he/release_notes.txt new file mode 100644 index 000000000000..2646a7b35fc9 --- /dev/null +++ b/fastlane/jetpack_metadata/he/release_notes.txt @@ -0,0 +1,6 @@ +תיקנו בעיה עם הכרטיס "לערוך פוסט טיוטה" שהופיע במסך הבית. האפליקציה לא קורסת עוד כאשר ניגשים לטיוטות שנמצאות בתהליך העלאה. + +בנוסף, פתרנו כמה בעיות בעורך הבלוקים. + +- ישר ולעניין – בלוקים של תמונה כעת מוצגים ביחס גובה-רוחב מתאים, לא משנה אם הרוחב והגובה של התמונה הוגדרו מראש. +- אם מכתיבים טקסט, המיקום של הסמן יישאר במקום הנכון – ולא יקפוץ ברחבי העמוד. אפשר להכתיב בשקט. diff --git a/fastlane/jetpack_metadata/id/release_notes.txt b/fastlane/jetpack_metadata/id/release_notes.txt new file mode 100644 index 000000000000..7b97938cba29 --- /dev/null +++ b/fastlane/jetpack_metadata/id/release_notes.txt @@ -0,0 +1,6 @@ +Kami sudah membereskan masalah kartu “Buat draft pos” di layar beranda. Aplikasi kini tidak akan mengalami crash lagi ketika Anda mengakses draft saat sedang diunggah. + +Kami juga telah mengatasi beberapa masalah di editor blok. + +- Betul sekali. Blok gambar kini menampilkan rasio aspek yang tepat, terlepas dari apakah lebar dan tinggi gambar sudah ditentukan. +- Ketika Anda mendiktekan teks, kursor tetap berada di posisinya dan tidak lagi melompat-lompat. Tetaplah tenang dan teruslah mendikte. diff --git a/fastlane/jetpack_metadata/it/release_notes.txt b/fastlane/jetpack_metadata/it/release_notes.txt new file mode 100644 index 000000000000..d389c5a30905 --- /dev/null +++ b/fastlane/jetpack_metadata/it/release_notes.txt @@ -0,0 +1,6 @@ +Abbiamo risolto un problema con la scheda "Lavora su un articolo bozza" nella schermata iniziale. L'app non si arresta più in modo anomalo quando si accede alle bozze mentre sono in fase di caricamento. + +Abbiamo anche risolto un paio di problemi nell'Editor a blocchi. + +- Miglioramento immediato: i blocchi di immagini ora mostrano le proporzioni corrette, indipendentemente dal fatto che l'immagine abbia o meno larghezza e altezza impostate. +- Durante la dettatura di un testo, la posizione del cursore rimarrà dove deve, non dovrai muoverti nel testo. Keep calm and dictate on. diff --git a/fastlane/jetpack_metadata/ja/release_notes.txt b/fastlane/jetpack_metadata/ja/release_notes.txt new file mode 100644 index 000000000000..f7169a5a66a9 --- /dev/null +++ b/fastlane/jetpack_metadata/ja/release_notes.txt @@ -0,0 +1,6 @@ +ホーム画面の「下書き投稿を作成」カードの問題を修正しました。 今後はアップロード中に下書きにアクセスしてもアプリがクラッシュすることはありません。 + +ブロックエディターの問題もいくつか解決しました。 + +- 修正完了 - 画像の幅と高さが設定されているかどうかにかかわらず、画像ブロックでは正しい縦横比が表示されるようになりました。 +- テキストを音声入力する際に、カーソルが所定の位置に留まり、飛び回ることがなくなります。 落ち着いて音声で入力することができます。 diff --git a/fastlane/jetpack_metadata/ko/release_notes.txt b/fastlane/jetpack_metadata/ko/release_notes.txt new file mode 100644 index 000000000000..6ccdb644a4d9 --- /dev/null +++ b/fastlane/jetpack_metadata/ko/release_notes.txt @@ -0,0 +1,6 @@ +홈 화면의 "임시글로 글 작업" 카드와 관련한 문제를 해결했습니다. 이제는 임시글을 업로드하는 도중에 접근할 때 앱이 충돌하지 않습니다. + +블록 편집기의 몇 가지 문제도 해결했습니다. + +- 정말입니다. 설정된 너비와 높이가 이미지에 있는지 여부와 관계없이 이제는 이미지 블록이 올바른 화면 비율로 표시됩니다. +- 텍스트를 받아쓰기할 때 커서의 위치가 이리저리 돌아다니지 않고 유지됩니다. 계속 차분하게 받아쓰세요. diff --git a/fastlane/jetpack_metadata/nl-NL/release_notes.txt b/fastlane/jetpack_metadata/nl-NL/release_notes.txt new file mode 100644 index 000000000000..cffee33237c0 --- /dev/null +++ b/fastlane/jetpack_metadata/nl-NL/release_notes.txt @@ -0,0 +1,6 @@ +We hebben een probleem opgelost met de kaart ‘Aan een conceptbericht werken’ op het startscherm. De app crasht niet meer wanneer je concepten opent terwijl ze nog worden geüpload. + +We hebben ook een aantal problemen opgelost met de blokeditor. + +- Geweldig, afbeeldingblokken worden nu in de juiste beeldverhouding weergegeven, of er nou wel of niet een breedte en hoogte zijn ingesteld voor het beeld. +- Wanneer je een tekst dicteert, blijft de cursor waar die zou moeten zijn en springt deze niet meer over het scherm. Blijf kalm en dicteer verder. diff --git a/fastlane/jetpack_metadata/pt-BR/release_notes.txt b/fastlane/jetpack_metadata/pt-BR/release_notes.txt new file mode 100644 index 000000000000..950261413c2e --- /dev/null +++ b/fastlane/jetpack_metadata/pt-BR/release_notes.txt @@ -0,0 +1,6 @@ +Corrigimos um problema no cartão "Trabalhar em um post em rascunho" da tela inicial. O aplicativo não vai mais travar quando você acessar os rascunhos durante o upload deles. + +Também resolvemos alguns problemas no editor de blocos. + +- Sem defeitos: agora os blocos de imagem vão exibir a proporção correta mesmo se a largura e altura da imagem não estiverem definidas. +- Quando você ditar o texto, a posição do cursor vai ficar no lugar certo e não pulando para lá e para cá. Continue a ditar, continue a ditar... diff --git a/fastlane/jetpack_metadata/ru/release_notes.txt b/fastlane/jetpack_metadata/ru/release_notes.txt new file mode 100644 index 000000000000..b0efc98c6e3d --- /dev/null +++ b/fastlane/jetpack_metadata/ru/release_notes.txt @@ -0,0 +1,6 @@ +Исправлена проблема с карточкой «Работа над черновиком» на главном экране. Приложение прекратило аварийно закрываться при попытке открыть черновики в процессе загрузки на сервер. + +Также решена пара проблем в редакторе блоков. + +— У блоков изображений сохраняется верное соотношение сторон независимо от заданной ширины и высоты изображения. +— Курсор больше не скачет при диктовке текста. Вы можете спокойно продолжать диктовать, ни на что не отвлекаясь. diff --git a/fastlane/jetpack_metadata/sv/release_notes.txt b/fastlane/jetpack_metadata/sv/release_notes.txt new file mode 100644 index 000000000000..a5ae9b99bc1a --- /dev/null +++ b/fastlane/jetpack_metadata/sv/release_notes.txt @@ -0,0 +1,6 @@ +Vi har åtgärdat ett problem med startskärmskortet "Arbeta med ett inläggsutkast". Appen kommer inte längre att krascha om du öppnar utkast medan de fortfarande håller på att laddas upp. + +Vi har även löst ett antal problem i blockredigeraren. + +- Fixat – bildblock visas nu med rätt bildförhållande, oavsett om bilden har en angiven bredd och höjd eller inte. +- När du dikterar text kommer markörens position att förbli där den ska vara – inget mer omkringhoppande. Ta det lugnt och diktera vidare. diff --git a/fastlane/jetpack_metadata/tr/release_notes.txt b/fastlane/jetpack_metadata/tr/release_notes.txt new file mode 100644 index 000000000000..0fae38e25b8d --- /dev/null +++ b/fastlane/jetpack_metadata/tr/release_notes.txt @@ -0,0 +1,6 @@ +Ana ekranın "Bir taslak yazı üzerinde çalışın" kartıyla ilgili bir sorunu düzelttik. Yüklenmekte olan taslaklara eriştiğinizde artık uygulama kilitlenmeyecek. + +Ayrıca, blok düzenleyicisiyle ilgili birkaç problemi de giderdik. + +- Tam isabet—Görsel blokları, görsel için belirlenmiş bir genişlik ve yükseklik olsun ya da olmasın artık doğru en boy oranını gösteriyor. +- Metni dikte ederken imlecin konumu olması gerektiği yerde kalır ve sağa sola hareket etmez. Sakin olun ve dikte etmeye devam edin. diff --git a/fastlane/jetpack_metadata/zh-Hans/release_notes.txt b/fastlane/jetpack_metadata/zh-Hans/release_notes.txt new file mode 100644 index 000000000000..1dce585107b9 --- /dev/null +++ b/fastlane/jetpack_metadata/zh-Hans/release_notes.txt @@ -0,0 +1,6 @@ +我们修复了主屏幕上“继续撰写文章草稿”卡片的问题。 访问正在上传的草稿时,应用不会再崩溃。 + +我们还解决了区块编辑器中的几个问题。 + +- 现在,无论否设置了图片宽度和高度,图片编辑器上的图片都可以显示正确的宽高比。 +- 口述文字时,光标的位置会停留在正确位置,不会再出现跳跃的情况。 保持冷静,继续口述。 diff --git a/fastlane/jetpack_metadata/zh-Hant/release_notes.txt b/fastlane/jetpack_metadata/zh-Hant/release_notes.txt new file mode 100644 index 000000000000..d02f16c8380f --- /dev/null +++ b/fastlane/jetpack_metadata/zh-Hant/release_notes.txt @@ -0,0 +1,6 @@ +我們修正了主畫面「編輯草稿文章」資訊卡的問題。 當你存取正在上傳的草稿時,應用程式不會再當機。 + +我們也解決了區塊編輯器的幾個問題。 + +- 不論圖片是否設定寬度和高度,圖片區塊現在會以正確的長寬比顯示。 +- 聽寫文字時,游標位置會停留在應該顯示的位置,不會再跳來跳去, 讓你安心使用聽寫功能。 diff --git a/fastlane/metadata/ar-SA/release_notes.txt b/fastlane/metadata/ar-SA/release_notes.txt new file mode 100644 index 000000000000..165c71a28dd5 --- /dev/null +++ b/fastlane/metadata/ar-SA/release_notes.txt @@ -0,0 +1,6 @@ +أصلحنا مشكلة في بطاقة "العمل على مسودة تدوينة" في الشاشة الرئيسية. لن يتعطل التطبيق بعد الآن عند الوصول إلى المسودات في أثناء الوجود في منتصف عملية الرفع. + +قمنا بحل مشكلتين في محرر المكوّنات. + +- ناحية المين، تعرض مكوّنات الصور الآن نسبة العرض إلى الارتفاع الصحيحة، سواء أكانت الصورة تحتوي على عرض وارتفاع محدَدين أم لا. +- عندما تكتب نصًا، سيظل مرضع المؤشر في المكان المفترض أن يكون فيه؛ ولن يتحرك. تحلَّ بالهدوء وواصل الكتابة. diff --git a/fastlane/metadata/de-DE/release_notes.txt b/fastlane/metadata/de-DE/release_notes.txt new file mode 100644 index 000000000000..6738dbabed4e --- /dev/null +++ b/fastlane/metadata/de-DE/release_notes.txt @@ -0,0 +1,6 @@ +Wir haben ein Problem mit der Karte „An einem Beitragsentwurf arbeiten“ der Startseite behoben. Die App stürzt nun nicht mehr ab, wenn du auf Entwürfe zugreifst, während sie hochgeladen werden. + +Außerdem haben wir einige Probleme im Block-Editor behoben. + +- Bei Bildblöcken wird jetzt das richtige Bildformat angezeigt, egal ob Höhe und Breite des Bildes definiert sind oder nicht. +- Beim Diktieren bleibt der Cursor an der gewünschten Stelle und springt nicht mehr herum. So kannst du ganz in Ruhe weiterdiktieren. diff --git a/fastlane/metadata/default/release_notes.txt b/fastlane/metadata/default/release_notes.txt index 637ca793c29c..3c0fb643df2b 100644 --- a/fastlane/metadata/default/release_notes.txt +++ b/fastlane/metadata/default/release_notes.txt @@ -1,7 +1,6 @@ -* [**] [internal] Blaze: Switch to using new canBlaze property to determine Blaze eligiblity. [#20916] -* [**] Fixed crash issue when accessing drafts that are mid-upload from the Home 'Work on a Draft Post' card. [#20872] -* [**] [internal] Make sure media-related features function correctly. [#20889], [20887] -* [*] [internal] Posts list: Disable action bar/menu button when a post is being uploaded [#20885] -* [*] Block editor: Image block - Fix issue where in some cases the image doesn't display the right aspect ratio [https://github.com/wordpress-mobile/gutenberg-mobile/pull/5869] -* [*] Block editor: Fix cursor positioning when dictating text on iOS [https://github.com/WordPress/gutenberg/issues/51227] +We fixed an issue with the home screen’s “Work on a draft post” card. The app will no longer crash when you access drafts while they’re in the middle of uploading. +We also solved a couple of problems in the block editor. + +- Right on—image blocks now display the correct aspect ratio, whether or not the image has a set width and height. +- When you’re dictating text, the cursor’s position will stay where it’s supposed to—no more jumping around. Keep calm and dictate on. diff --git a/fastlane/metadata/es-ES/release_notes.txt b/fastlane/metadata/es-ES/release_notes.txt new file mode 100644 index 000000000000..7601ed233ddd --- /dev/null +++ b/fastlane/metadata/es-ES/release_notes.txt @@ -0,0 +1,6 @@ +Hemos corregido un problema que se producía en la tarjeta “Trabajar en un borrador de entrada” de la pantalla de inicio. La aplicación ya no se bloqueará si accedes a los borradores mientras se cargan. + +También hemos resuelto un par de problemas en el editor de bloques. + +- Ahora, los bloques de imagen muestran la relación de aspecto correcta, tanto si la imagen tiene una anchura y una altura determinadas como si no. +- Al dictar un texto, la posición del cursor se mantendrá en su sitio y no se producirán saltos. De esta manera, podrás dictar con tranquilidad. diff --git a/fastlane/metadata/fr-FR/release_notes.txt b/fastlane/metadata/fr-FR/release_notes.txt new file mode 100644 index 000000000000..86cc0dc7247c --- /dev/null +++ b/fastlane/metadata/fr-FR/release_notes.txt @@ -0,0 +1,6 @@ +Nous avons corrigé un problème avec la carte « Travailler sur un brouillon d’article » de l’écran d’accueil. L’application ne rencontre plus d’incident lorsque vous accédez à des brouillons en cours de chargement. + +Nous avons également résolu quelques problèmes dans l’éditeur de blocs. + +- Les blocs d’images de droite affichent désormais le bon rapport hauteur/largeur, que l’image soit ou non définie en largeur et en hauteur. +- Lorsque vous dictez du texte, la position du curseur reste à l’endroit prévu, il n’y a plus de sauts. Restez calme et continuez à dicter. diff --git a/fastlane/metadata/he/release_notes.txt b/fastlane/metadata/he/release_notes.txt new file mode 100644 index 000000000000..2646a7b35fc9 --- /dev/null +++ b/fastlane/metadata/he/release_notes.txt @@ -0,0 +1,6 @@ +תיקנו בעיה עם הכרטיס "לערוך פוסט טיוטה" שהופיע במסך הבית. האפליקציה לא קורסת עוד כאשר ניגשים לטיוטות שנמצאות בתהליך העלאה. + +בנוסף, פתרנו כמה בעיות בעורך הבלוקים. + +- ישר ולעניין – בלוקים של תמונה כעת מוצגים ביחס גובה-רוחב מתאים, לא משנה אם הרוחב והגובה של התמונה הוגדרו מראש. +- אם מכתיבים טקסט, המיקום של הסמן יישאר במקום הנכון – ולא יקפוץ ברחבי העמוד. אפשר להכתיב בשקט. diff --git a/fastlane/metadata/id/release_notes.txt b/fastlane/metadata/id/release_notes.txt new file mode 100644 index 000000000000..7b97938cba29 --- /dev/null +++ b/fastlane/metadata/id/release_notes.txt @@ -0,0 +1,6 @@ +Kami sudah membereskan masalah kartu “Buat draft pos” di layar beranda. Aplikasi kini tidak akan mengalami crash lagi ketika Anda mengakses draft saat sedang diunggah. + +Kami juga telah mengatasi beberapa masalah di editor blok. + +- Betul sekali. Blok gambar kini menampilkan rasio aspek yang tepat, terlepas dari apakah lebar dan tinggi gambar sudah ditentukan. +- Ketika Anda mendiktekan teks, kursor tetap berada di posisinya dan tidak lagi melompat-lompat. Tetaplah tenang dan teruslah mendikte. diff --git a/fastlane/metadata/it/release_notes.txt b/fastlane/metadata/it/release_notes.txt new file mode 100644 index 000000000000..696c2a845eda --- /dev/null +++ b/fastlane/metadata/it/release_notes.txt @@ -0,0 +1,6 @@ +Abbiamo risolto un problema con la scheda "Lavora su un articolo bozza" nella schermata iniziale. L'app non si arresta più in modo anomalo quando si accede alle bozze mentre sono in fase di caricamento. + +Abbiamo anche risolto un paio di problemi nell'editor a blocchi. + +- Miglioramento immediato: i blocchi di immagini ora mostrano le proporzioni corrette, indipendentemente dal fatto che l'immagine abbia o meno larghezza e altezza impostate. +- Durante la dettatura di un testo, la posizione del cursore rimarrà dove deve, non dovrai muoverti nel testo. Calma e inizia a dettare. diff --git a/fastlane/metadata/ja/release_notes.txt b/fastlane/metadata/ja/release_notes.txt new file mode 100644 index 000000000000..f7169a5a66a9 --- /dev/null +++ b/fastlane/metadata/ja/release_notes.txt @@ -0,0 +1,6 @@ +ホーム画面の「下書き投稿を作成」カードの問題を修正しました。 今後はアップロード中に下書きにアクセスしてもアプリがクラッシュすることはありません。 + +ブロックエディターの問題もいくつか解決しました。 + +- 修正完了 - 画像の幅と高さが設定されているかどうかにかかわらず、画像ブロックでは正しい縦横比が表示されるようになりました。 +- テキストを音声入力する際に、カーソルが所定の位置に留まり、飛び回ることがなくなります。 落ち着いて音声で入力することができます。 diff --git a/fastlane/metadata/ko/release_notes.txt b/fastlane/metadata/ko/release_notes.txt new file mode 100644 index 000000000000..6ccdb644a4d9 --- /dev/null +++ b/fastlane/metadata/ko/release_notes.txt @@ -0,0 +1,6 @@ +홈 화면의 "임시글로 글 작업" 카드와 관련한 문제를 해결했습니다. 이제는 임시글을 업로드하는 도중에 접근할 때 앱이 충돌하지 않습니다. + +블록 편집기의 몇 가지 문제도 해결했습니다. + +- 정말입니다. 설정된 너비와 높이가 이미지에 있는지 여부와 관계없이 이제는 이미지 블록이 올바른 화면 비율로 표시됩니다. +- 텍스트를 받아쓰기할 때 커서의 위치가 이리저리 돌아다니지 않고 유지됩니다. 계속 차분하게 받아쓰세요. diff --git a/fastlane/metadata/nl-NL/release_notes.txt b/fastlane/metadata/nl-NL/release_notes.txt new file mode 100644 index 000000000000..cffee33237c0 --- /dev/null +++ b/fastlane/metadata/nl-NL/release_notes.txt @@ -0,0 +1,6 @@ +We hebben een probleem opgelost met de kaart ‘Aan een conceptbericht werken’ op het startscherm. De app crasht niet meer wanneer je concepten opent terwijl ze nog worden geüpload. + +We hebben ook een aantal problemen opgelost met de blokeditor. + +- Geweldig, afbeeldingblokken worden nu in de juiste beeldverhouding weergegeven, of er nou wel of niet een breedte en hoogte zijn ingesteld voor het beeld. +- Wanneer je een tekst dicteert, blijft de cursor waar die zou moeten zijn en springt deze niet meer over het scherm. Blijf kalm en dicteer verder. diff --git a/fastlane/metadata/ru/release_notes.txt b/fastlane/metadata/ru/release_notes.txt new file mode 100644 index 000000000000..f48a54c1082b --- /dev/null +++ b/fastlane/metadata/ru/release_notes.txt @@ -0,0 +1,6 @@ +Исправлена проблема с карточкой "Работа над черновиком" на главном экране. Приложение больше не будет аварийно закрываться, когда вы пытаетесь открыть черновики в процессе их загрузки на сервер. + +Также решена пара проблем в редакторе блоков. + +— Блоки изображений наконец-то будут иметь верное соотношение сторон независимо от заданной ширины и высоты изображения. +— Курсор больше не скачет при диктовке текста. Вы можете спокойно продолжать диктовку, ни на что не отвлекаясь. diff --git a/fastlane/metadata/sv/release_notes.txt b/fastlane/metadata/sv/release_notes.txt new file mode 100644 index 000000000000..a5ae9b99bc1a --- /dev/null +++ b/fastlane/metadata/sv/release_notes.txt @@ -0,0 +1,6 @@ +Vi har åtgärdat ett problem med startskärmskortet "Arbeta med ett inläggsutkast". Appen kommer inte längre att krascha om du öppnar utkast medan de fortfarande håller på att laddas upp. + +Vi har även löst ett antal problem i blockredigeraren. + +- Fixat – bildblock visas nu med rätt bildförhållande, oavsett om bilden har en angiven bredd och höjd eller inte. +- När du dikterar text kommer markörens position att förbli där den ska vara – inget mer omkringhoppande. Ta det lugnt och diktera vidare. diff --git a/fastlane/metadata/tr/release_notes.txt b/fastlane/metadata/tr/release_notes.txt new file mode 100644 index 000000000000..0fae38e25b8d --- /dev/null +++ b/fastlane/metadata/tr/release_notes.txt @@ -0,0 +1,6 @@ +Ana ekranın "Bir taslak yazı üzerinde çalışın" kartıyla ilgili bir sorunu düzelttik. Yüklenmekte olan taslaklara eriştiğinizde artık uygulama kilitlenmeyecek. + +Ayrıca, blok düzenleyicisiyle ilgili birkaç problemi de giderdik. + +- Tam isabet—Görsel blokları, görsel için belirlenmiş bir genişlik ve yükseklik olsun ya da olmasın artık doğru en boy oranını gösteriyor. +- Metni dikte ederken imlecin konumu olması gerektiği yerde kalır ve sağa sola hareket etmez. Sakin olun ve dikte etmeye devam edin. diff --git a/fastlane/metadata/zh-Hans/release_notes.txt b/fastlane/metadata/zh-Hans/release_notes.txt new file mode 100644 index 000000000000..1dce585107b9 --- /dev/null +++ b/fastlane/metadata/zh-Hans/release_notes.txt @@ -0,0 +1,6 @@ +我们修复了主屏幕上“继续撰写文章草稿”卡片的问题。 访问正在上传的草稿时,应用不会再崩溃。 + +我们还解决了区块编辑器中的几个问题。 + +- 现在,无论否设置了图片宽度和高度,图片编辑器上的图片都可以显示正确的宽高比。 +- 口述文字时,光标的位置会停留在正确位置,不会再出现跳跃的情况。 保持冷静,继续口述。 diff --git a/fastlane/metadata/zh-Hant/release_notes.txt b/fastlane/metadata/zh-Hant/release_notes.txt new file mode 100644 index 000000000000..d02f16c8380f --- /dev/null +++ b/fastlane/metadata/zh-Hant/release_notes.txt @@ -0,0 +1,6 @@ +我們修正了主畫面「編輯草稿文章」資訊卡的問題。 當你存取正在上傳的草稿時,應用程式不會再當機。 + +我們也解決了區塊編輯器的幾個問題。 + +- 不論圖片是否設定寬度和高度,圖片區塊現在會以正確的長寬比顯示。 +- 聽寫文字時,游標位置會停留在應該顯示的位置,不會再跳來跳去, 讓你安心使用聽寫功能。