diff --git a/.circleci/config.yml b/.circleci/config.yml index bd9b0ae96cbf..6e4101d54ba2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,44 +5,47 @@ orbs: bundle-install: toshimaru/bundle-install@0.3.1 slack: circleci/slack@3.4.2 +parameters: + translation_review_build: + type: boolean + default: false + translation_review_lang_id: + type: string + default: all-lang + commands: copy-gradle-properties: steps: - run: name: Setup gradle.properties command: cp gradle.properties-example gradle.properties - yarn-install: + npm-install: steps: - restore_cache: - name: Restore Yarn Cache + name: Restore NPM Cache keys: - - yarn-i18n-v4-cache-v{{ .Environment.CACHE_TRIGGER_VERSION }}-job-{{ .Environment.CIRCLE_JOB }}-{{ checksum "libs/gutenberg-mobile/yarn.lock" }} + - npm-i18n-v1-cache-v{{ .Environment.CACHE_TRIGGER_VERSION }}-job-{{ .Environment.CIRCLE_JOB }}-{{ checksum "libs/gutenberg-mobile/package-lock.json" }} - run: - name: Yarn Install + name: NPM Install working_directory: libs/gutenberg-mobile - command: yarn install --frozen-lockfile --prefer-offline --network-concurrency 1 + command: npm ci --prefer-offline - save_cache: - name: Save Yarn Cache - key: yarn-i18n-v4-cache-v{{ .Environment.CACHE_TRIGGER_VERSION }}-job-{{ .Environment.CIRCLE_JOB }}-{{ checksum "libs/gutenberg-mobile/yarn.lock" }} + name: Save NPM Cache + key: npm-i18n-v1-cache-v{{ .Environment.CACHE_TRIGGER_VERSION }}-job-{{ .Environment.CIRCLE_JOB }}-{{ checksum "libs/gutenberg-mobile/package-lock.json" }} paths: - - libs/gutenberg-mobile/node_modules + - ~/.npm - libs/gutenberg-mobile/i18n-cache/data checkout-submodules: steps: - run: name: Checkout submodules - command: git submodule update --init --recursive - checkout-gutenberg-mobile-submodule-only: - steps: - - run: - name: Checkout gutenberg-mobile submodule (no recursive) - command: git submodule update --init - yarn-bundle-android: + command: git submodule update --init --recursive --depth 1 + npm-bundle-android: steps: - run: - name: Yarn bundle Android + name: Npm bundle Android working_directory: libs/gutenberg-mobile - command: yarn bundle:android + command: npm run bundle:android save-gutenberg-bundle-cache: steps: - run: @@ -54,7 +57,7 @@ commands: name: Cache JS Bundle key: android-js-bundle-{{ checksum "gutenberg_submodule_hash" }} paths: - - libs/gutenberg-mobile/react-native-gutenberg-bridge/android/build/assets/index.android.bundle + - libs/gutenberg-mobile/gutenberg/packages/react-native-bridge/android/build/assets/index.android.bundle restore-gutenberg-bundle-cache: steps: - run: @@ -77,21 +80,21 @@ jobs: - run: name: Abort If JS Bundle Exists command: | - if [ -f "libs/gutenberg-mobile/react-native-gutenberg-bridge/android/build/assets/index.android.bundle" ]; then + if [ -f "libs/gutenberg-mobile/gutenberg/packages/react-native-bridge/android/build/assets/index.android.bundle" ]; then echo "Gutenberg-Mobile bundle already in cache, no need to create a new one." circleci-agent step halt else echo "Gutenberg-Mobile bundle not found in cache. Proceeding to generate new bundle" fi - checkout-submodules - - yarn-install - - yarn-bundle-android + - npm-install + - npm-bundle-android - run: name: Ensure assets folder exists - command: mkdir -p libs/gutenberg-mobile/react-native-gutenberg-bridge/android/build/assets + command: mkdir -p libs/gutenberg-mobile/gutenberg/packages/react-native-bridge/android/build/assets - run: name: Move bundle to assets folder - command: mv libs/gutenberg-mobile/bundle/android/App.js libs/gutenberg-mobile/react-native-gutenberg-bridge/android/build/assets/index.android.bundle + command: mv libs/gutenberg-mobile/bundle/android/App.js libs/gutenberg-mobile/gutenberg/packages/react-native-bridge/android/build/assets/index.android.bundle - save-gutenberg-bundle-cache test: executor: @@ -99,15 +102,15 @@ jobs: api-version: "28" steps: - git/shallow-checkout - - checkout-gutenberg-mobile-submodule-only + - checkout-submodules - android/restore-gradle-cache - copy-gradle-properties - restore-gutenberg-bundle-cache - run: name: Ensure assets folder exists - command: mkdir -p libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets + command: mkdir -p libs/gutenberg-mobile/gutenberg/packages/react-native-bridge/android/src/main/assets - attach_workspace: - at: libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets + at: libs/gutenberg-mobile/gutenberg/packages/react-native-bridge/android/src/main/assets - run: name: Test WordPress environment: @@ -126,15 +129,15 @@ jobs: api-version: "28" steps: - git/shallow-checkout - - checkout-gutenberg-mobile-submodule-only + - checkout-submodules - android/restore-gradle-cache - copy-gradle-properties - restore-gutenberg-bundle-cache - run: name: Ensure assets folder exists - command: mkdir -p libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets + command: mkdir -p libs/gutenberg-mobile/gutenberg/packages/react-native-bridge/android/src/main/assets - attach_workspace: - at: libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets + at: libs/gutenberg-mobile/gutenberg/packages/react-native-bridge/android/src/main/assets - run: name: Checkstyle environment: @@ -169,7 +172,7 @@ jobs: api-version: "28" steps: - git/shallow-checkout - - checkout-gutenberg-mobile-submodule-only + - checkout-submodules - bundle-install/bundle-install: cache_key_prefix: installable-build - run: @@ -179,9 +182,9 @@ jobs: - restore-gutenberg-bundle-cache - run: name: Ensure assets folder exists - command: mkdir -p libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets + command: mkdir -p libs/gutenberg-mobile/gutenberg/packages/react-native-bridge/android/src/main/assets - attach_workspace: - at: libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets + at: libs/gutenberg-mobile/gutenberg/packages/react-native-bridge/android/src/main/assets - run: name: Build APK environment: @@ -218,15 +221,15 @@ jobs: api-version: "28" steps: - git/shallow-checkout - - checkout-gutenberg-mobile-submodule-only + - checkout-submodules - android/restore-gradle-cache - copy-gradle-properties - restore-gutenberg-bundle-cache - run: name: Ensure assets folder exists - command: mkdir -p libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets + command: mkdir -p libs/gutenberg-mobile/gutenberg/packages/react-native-bridge/android/src/main/assets - attach_workspace: - at: libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets + at: libs/gutenberg-mobile/gutenberg/packages/react-native-bridge/android/src/main/assets - run: name: Build environment: @@ -261,15 +264,15 @@ jobs: api-version: "28" steps: - git/shallow-checkout - - checkout-gutenberg-mobile-submodule-only + - checkout-submodules - android/restore-gradle-cache - copy-gradle-properties - restore-gutenberg-bundle-cache - run: name: Ensure assets folder exists - command: mkdir -p libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets + command: mkdir -p libs/gutenberg-mobile/gutenberg/packages/react-native-bridge/android/src/main/assets - attach_workspace: - at: libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets + at: libs/gutenberg-mobile/gutenberg/packages/react-native-bridge/android/src/main/assets - run: name: Build environment: @@ -293,7 +296,7 @@ jobs: - image: circleci/ruby:2.6.4 steps: - git/shallow-checkout - - checkout-gutenberg-mobile-submodule-only + - checkout-submodules - run: name: Install bundler command: gem install bundler --version 2.0.2 @@ -302,9 +305,54 @@ jobs: - run: name: Validate login strings command: bundle exec fastlane validate_login_strings pr_url:$CIRCLE_PULL_REQUEST + translation-review-build: + executor: + name: android/default + api-version: "28" + environment: + APP_VERSION_PREFIX: << pipeline.parameters.translation_review_lang_id >> + steps: + - git/shallow-checkout + - checkout-submodules + - bundle-install/bundle-install: + cache_key_prefix: installable-build + - run: + name: Copy Secrets + command: bundle exec fastlane run configure_apply + - android/restore-gradle-cache + - restore-gutenberg-bundle-cache + - run: + name: Ensure assets folder exists + command: mkdir -p libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets + - attach_workspace: + at: libs/gutenberg-mobile/react-native-gutenberg-bridge/android/src/main/assets + - run: + name: Build APK + environment: + SUPPRESS_GUTENBERG_MOBILE_JS_BUNDLE_BUILD: 1 + command: | + TODAY_DATE=$(date +'%Y%m%d') + VERSION_NAME="${APP_VERSION_PREFIX}-build-${TODAY_DATE}-${CIRCLE_BUILD_NUM}" + echo "export VERSION_NAME=$VERSION_NAME" >> $BASH_ENV + + bundle exec fastlane build_for_translation_review custom_version:"$VERSION_NAME" + - android/save-gradle-cache + - run: + name: Prepare APK + command: | + mkdir -p Artifacts + mv WordPress/build/outputs/apk/jalapeno/debug/org.wordpress.android-jalapeno-debug.apk "Artifacts/WordPress-${VERSION_NAME}.apk" + - run: + name: Upload APK + command: | + curl --http1.1 https://${APPET_TOKEN}@api.appetize.io/v1/apps/${APPET_APPID} -F "file=@Artifacts/WordPress-${VERSION_NAME}.apk" -F "platform=android" + - store_artifacts: + path: Artifacts + destination: Artifacts workflows: wordpress_android: + unless: << pipeline.parameters.translation_review_build >> jobs: - gutenberg-bundle-build - strings-check @@ -329,7 +377,7 @@ workflows: - Connected Tests: requires: - gutenberg-bundle-build - post-to-slack: true + post-to-slack: false # Always run connected tests on develop and release branches filters: branches: @@ -337,6 +385,7 @@ workflows: - develop - /^release.*/ Optional Tests: + unless: << pipeline.parameters.translation_review_build >> #Optionally run connected tests on PRs jobs: - Hold: @@ -349,3 +398,7 @@ workflows: - /pull\/[0-9]+/ - Connected Tests: requires: [Hold] + Translation Review Build: + when: << pipeline.parameters.translation_review_build >> + jobs: + - translation-review-build diff --git a/Gemfile.lock b/Gemfile.lock index f8d70d02446e..c9872a9048ba 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,9 @@ GIT remote: https://github.com/wordpress-mobile/release-toolkit - revision: 6c1fa45f3beb216f4f5d03346f2981f1024b799e - tag: 0.9.6 + revision: b515c0b26b78bfffc3cbe5ceb3b51bb6eb979ab4 + tag: 0.9.8 specs: - fastlane-plugin-wpmreleasetoolkit (0.9.6) + fastlane-plugin-wpmreleasetoolkit (0.9.8) activesupport (~> 4) chroma (= 0.2.0) diffy (~> 3.3) @@ -169,7 +169,7 @@ GEM optimist (3.0.1) options (2.3.2) os (1.1.0) - parallel (1.19.1) + parallel (1.19.2) plist (3.5.0) progress_bar (1.3.1) highline (>= 1.6, < 3) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 62061087606b..1612e1e8c2e5 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,11 +1,19 @@ +15.3 +----- + 15.2 ----- +* [**] Fixes tons of rendering issues in Reader post detail by changing the technical solution (shared CSS file). +* [**] Block editor: Display content metrics information (blocks, words, characters count). +* [**] Block Editor: Adds editor support for theme defined colors and theme defined gradients on cover and button blocks. +* [**] Block Editor: Add support allowing Cover Block video uploads to complete after the editor has closed +* [*] Block Editor: Fix handling of upload completion while re-opening the editor +* [*] Fix crash when WordPress api response has an empty body 15.1 ----- * Fixes issue on Notifications tab when two screens were drawn on top of each other * [**] Fix video thumbnails, settings and preview in Media section for private sites -* [**] Block Editor: Adds editor support for theme defined colors and theme defined gradients on cover and button blocks. * [*] Support for breaking out of captions/citation authors by pressing enter on the following blocks: image, video, gallery, quote, and pullquote. 15.0 diff --git a/WordPress/FEATURE_ANNOUNCEMENTS.json-example b/WordPress/FEATURE_ANNOUNCEMENTS.json-example deleted file mode 100644 index cbabdcd67b86..000000000000 --- a/WordPress/FEATURE_ANNOUNCEMENTS.json-example +++ /dev/null @@ -1,61 +0,0 @@ -// FEATURE_ANNOUNCEMENT.json file should be located in WordPress/src/main/assets -// comments in this file are ignored by Gson, so you can either remove or leave them - -{ - "announcements": [ - { - "appVersionName": "14.7", - // app version name that announcement targets. Used for display purposes only. - "announcementVersion": 1, - // unique version of announcement. Used to track if the specific announcement was shown or not. - "detailsUrl": "http://wordpress.org", - // URL that "Find out more" button will open. If empty, "Find out more" button will not be shown. - "features": [ - { - "localizedContent": { - "en": { - "title": "Super Publishing", - "subtitle": "Super Publishing is here! Publish using the power of your mind." - }, - "it": { - "title": "Super Pubblicazione", - "subtitle": "Pubblicare articoli incredibili..." - } - }, - // icon data represented as base64 string without prefix (data:image/png;base64,). - "iconBase64": "iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAb1BMVEX///+ZzACTyQCRyQDi8MTk8cu+33f2+u3N5piczRSu2EnA4Hr+//rE4oTr9tXy+eOo1Dr1+unS6KOi0Sey2Fvt9tvd7ru322jb7bb6/PPE4oa73XHK5JLr9dfW6q32+uuo1DzQ6KC02mCv11LH4oz7JUSRAAAGUUlEQVR4nO2d63qiMBBASYK2eBcEReul6vs/4wJuEcJEg+Yy3Z3zc7fhyyEjl2TIBAFB/D/ELFmaOtZZrE0dyhy5YDwxdKytYGJj6Fjm+BCM8S8jh9oXh0JoGEx50a+5iSNFjDFT4WAUUx0zd6pMszcTp4YOYwUzJx9tjJaY6BzeGC0xEGCYY7Rk+/YAoI7Rknc7iDtGS6ogu77cfIY8RkuqOE07/5zPB+Hh+7qdlmyvn4dskH50W6OP0RKpk5PZYbtKuBBcpvin6DKNBw1R/DFakt7jdLkZJ6UaU1OasnW8r/6+itFPn53X42+chlMmHrm1PAVfb/KA/YYYLUmKjkbs4dCBlmU7/DFaEvZ0azL23XkN0vUbgowvMt8CT5ivxBt+JSLC7Jjv3vWrHBcz3yIq4r5XF6XjOvftArE8GfJj5W0S4TTNwUSA3hEr30Iya7OCxTAyVLfGPDEXoTUC0UV1bl6vUkTzjJpaGMCb4tS32g1rglgU5/YEC8XX5wuMkQP9Kt6GuPar0+MmIvYtWL0rSZ2NDsfiASDuJXhNi5OVLbqOYuBZsPsmweulv4v2MCY/Uxmf3dsqP3pSuwE8yTQeRka6hpO6yVf3rHh97V8Cp3zy8L8hRNg4JBD1Pi+op253W905axlGzSYH4Kfo720qBoaw9agFdBdg2GwyB8Y9CjyRA/3nrfOtNWXTnuSeAIbephiHkGHr4r7RMtw2m3xAv13h540YiifpdE+1ovTUbAIOO9+5FKuBLyOt34yOXzFCy6dHFcZSdXoADmFxuhuPWVfNW/7l3mSmuMH4GEToV1id7vrupj2xcb/jKd80BbBSZZlc2X0xra4Lxx5Ti3x0W5D7VjZ5Y1HyVWJ1BHIxGu4W/SZuRLIbXx7ORjo37D5d2YW7nrVJTc+tPcX19KLuZdIcYvK8VyaJXAsyx9PgipuhVdwm1T64ktrDqaHei59ZgCQWi3gQbD0PWsfHz9Dts2nm42fo9FUfmBFzgMs74tqHoNNLzcKLoctHUy+CLi+m0HyYC0N374hpN5fSBcLd7SIP/YA2k4ggCIL4L5mEAy+Yf/JOs/gzzro5gkc8zzT7TdnF11amZjt+ExFsLEmCq5gOnkuHUhfDc93Fa2/J+ahhwcW5tQiLw3CftLo47Pd+vJEceGv1GoWhnFzUL+UWWPlrZl9hMLwCeSD6intwhf6+RonAMAO7oG0IrpzxMyZD+A90s6cGsICoU+j8GypSkbimoSJHhB/wGCpWFXgICnW4KJrXMeDfULECzb/1DBVJk7z+dM6/oWL5Ujc9jAzJ0BZkSIZkSIb2IUMyJEMytA8ZkiEZkqF9yJAMyZAM7UOGZEiGZGgfMiRDMiRD+5AhGZIhGdqHDMmQDMnQPmRIhmRIhvYhQzIkQzK0DxmSIRmSoX3IUN9wpWhe79ns31D1ZZfmLkSKfS0xfZ2n2IpLt5CCYrfi+6ba/g3hj2S1v7CEtstnzT21/RsqvpLVLmEKbxp4/94dgSH4JW+PTfmAIOCNTSkQGEJVC1ifWh+htC1z+3N+DIbdTRGSflt+5+tGXRsuVSVCYRgsL80uvlBGeH6N/m6rkXxKpQlwGAZBWlZzLXsoTvFru0Z+DDabbNatvIDFsOBYdnFvfFNMRIaWIEMyJEMyJEMyJEMyJMPfYKiuhGTXcPy8a6bwsq1+j2mY9/Ei6LTSjKfKAQ5rkWvVNTSPO0E/P0SXP8Mg2HpQdFvIcuLe0HX1PHm20j7OC8ruHI9ij+1XTbFyquil6vHQXaBy5rReV03GHA2jWDmuDFgz+WLCuiQXI82VXTuE4xMXP6gHAUZ5dhqHXH37KAUsM7mhKgPFpz9/0SZIFYp8Vh8SGWpDGFWBOo62ZA4ZypAhPshQhgzxQYYyZIgPMpQhQ3yQoQwZ4oMMZcgQH2QoQ4b4IEMZMsQHGcqQIT7IUIYM8UGGMr/PUFXAVPWVvCoT12USYj++4FVroazwCgu6TULshSrqlA3gL/9dZjv3BcyTEuoStnACoOjzxbJjcmhETg8aQAmAQnPrBz/MO4PCk4cpI3IN+EJQt7i2J45J21GsnjTYSN/F4x7Bivie7cZF8rxQdr4TjQZrDBlQT8l2UZWxlUz1EtLy+FLlhbERigwvPSbHZc8dDpZLp2nc/xJ/AMM4nGaREA01AAAAAElFTkSuQmCC", - "iconUrl": "" - // If icon is missing, we will try to load content of iconBase64 - }, - { - "localizedContent": { - "en": { - "title": "Amazing Feature", - "subtitle": "That's right! They are right in the app! They require pets right now. Are you going to look for them or what?" - }, - "it": { // localized feature description for Italian language - "title": "Amazing Feature in Italian", - "subtitle": "Description in Italian 2" - } - }, - "iconBase64": "", - "iconUrl": "https://s0.wordpress.com/i/store/mobile/plans-personal.png" - }, - { - "localizedContent": { - "en": { - "title": "We like long feature announcements that why this one is going to be extra long and span multiple lines", - "subtitle": "Here we run out of budget." - }, - "it": { // localized feature description for Italian language - "title": "Long feature title in Italian", - "subtitle": "Description in Italian 3" - } - }, - "iconBase64": "", - "iconUrl": "https://s0.wordpress.com/i/store/mobile/plans-premium.png" - } - ] - } - ] -} diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 0020d47d80ea..a08ce1c55cfb 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -22,9 +22,9 @@ repositories { apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' -apply plugin: 'kotlin-kapt' apply plugin: 'se.bjurr.violations.violation-comments-to-github-gradle-plugin' apply plugin: 'kotlin-allopen' +apply plugin: 'kotlin-kapt' allOpen { // allows mocking for classes w/o directly opening them for release builds @@ -55,9 +55,9 @@ android { if (project.hasProperty("versionName")) { versionName project.property("versionName") } else { - versionName "alpha-230" + versionName "alpha-231" } - versionCode 884 + versionCode 887 minSdkVersion rootProject.minSdkVersion targetSdkVersion rootProject.targetSdkVersion @@ -69,6 +69,8 @@ android { buildConfigField "boolean", "OFFER_GUTENBERG", "true" buildConfigField "boolean", "TENOR_AVAILABLE", "true" buildConfigField "boolean", "READER_IMPROVEMENTS_PHASE_2", "true" + buildConfigField "long", "REMOTE_CONFIG_FETCH_INTERVAL", "10" + buildConfigField "boolean", "FEATURE_ANNOUNCEMENT_AVAILABLE", "false" } // Gutenberg's dependency - react-native-video is using @@ -85,12 +87,13 @@ android { dimension "buildType" // Only set the release version if one isn't provided if (!project.hasProperty("versionName")) { - versionName "15.1" + versionName "15.2-rc-1" } - versionCode 885 + versionCode 886 buildConfigField "boolean", "ME_ACTIVITY_AVAILABLE", "false" buildConfigField "boolean", "TENOR_AVAILABLE", "false" buildConfigField "boolean", "READER_IMPROVEMENTS_PHASE_2", "false" + buildConfigField "long", "REMOTE_CONFIG_FETCH_INTERVAL", "3600" } zalpha { // alpha version - enable experimental features @@ -147,6 +150,9 @@ android { exclude '**/libjscexecutor.so' exclude '**/libhermes-inspector.so' exclude '**/libhermes-executor-debug.so' + + pickFirst 'META-INF/-no-jdk.kotlin_module' + } bundle { @@ -202,9 +208,9 @@ dependencies { }) implementation 'com.android.volley:volley:1.1.1' + implementation 'com.google.firebase:firebase-messaging:20.1.5' implementation 'com.google.android.gms:play-services-auth:18.0.0' implementation 'com.google.android.gms:play-services-places:17.0.0' - implementation 'com.google.firebase:firebase-messaging:20.1.5' implementation 'com.android.installreferrer:installreferrer:1.0' implementation 'com.github.chrisbanes.photoview:library:1.2.4' implementation 'org.greenrobot:eventbus:3.1.1' @@ -233,7 +239,7 @@ dependencies { testImplementation "junit:junit:$jUnitVersion" testImplementation 'org.robolectric:robolectric:4.3' testImplementation 'org.robolectric:shadows-multidex:4.3' - testImplementation 'org.mockito:mockito-core:2.23.0' + testImplementation "org.mockito:mockito-core:$mockitoCoreVersion" testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$nhaarmanMockitoVersion" testImplementation "org.assertj:assertj-core:$assertJVersion" testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.1' @@ -265,7 +271,7 @@ dependencies { androidTestImplementation "androidx.test:runner:$androidxTestVersion" androidTestImplementation "androidx.test:rules:$androidxTestVersion" androidTestImplementation "androidx.test.ext:junit:$androidxTestVersion" - androidTestImplementation 'tools.fastlane:screengrab:1.2.0', { + androidTestImplementation 'tools.fastlane:screengrab:2.0.0', { exclude group: 'com.android.support.test.uiautomator', module: 'uiautomator-v18' } androidTestImplementation "androidx.work:work-testing:$androidxWorkVersion" @@ -310,6 +316,12 @@ dependencies { implementation 'io.sentry:sentry-android:2.1.3' implementation 'org.slf4j:slf4j-nop:1.7.25' + // Firebase + implementation 'com.google.firebase:firebase-config:19.1.3' + + compileOnly project(path:':libs:WordPressAnnotations') + kapt project(':libs:WordPressProcessors') + // Encrypted Logging implementation "com.goterl.lazycode:lazysodium-android:4.1.0@aar" implementation "net.java.dev.jna:jna:4.5.1@aar" diff --git a/WordPress/lint-baseline.xml b/WordPress/lint-baseline.xml index 42c2fd629297..023d9ff9aabc 100644 --- a/WordPress/lint-baseline.xml +++ b/WordPress/lint-baseline.xml @@ -57,7 +57,7 @@ errorLine1=" view.setPadding(" errorLine2=" ^"> @@ -3927,7 +3927,7 @@ errorLine1=" Layout.BREAK_STRATEGY_SIMPLE : Layout.BREAK_STRATEGY_HIGH_QUALITY;" errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -3943,7 +3943,7 @@ errorLine1=" Layout.BREAK_STRATEGY_SIMPLE : Layout.BREAK_STRATEGY_HIGH_QUALITY;" errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -3959,7 +3959,7 @@ errorLine1=" view.setJustificationMode(Layout.JUSTIFICATION_MODE_INTER_WORD);" errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -3975,7 +3975,7 @@ errorLine1=" view.setJustificationMode(Layout.JUSTIFICATION_MODE_NONE);" errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -3991,7 +3991,7 @@ errorLine1=" classpath 'com.android.tools.build:gradle:3.4.2'" errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -4007,7 +4007,7 @@ errorLine1=" implementation 'androidx.appcompat:appcompat:1.0.0'" errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -4023,7 +4023,7 @@ errorLine1=" implementation 'androidx.appcompat:appcompat:1.0.0'" errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -4039,7 +4039,7 @@ errorLine1=" implementation 'androidx.recyclerview:recyclerview:1.0.0'" errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -4055,7 +4055,7 @@ errorLine1=" @ReactProp(name = PROP_TEXT)" errorLine2=" ~~~~~~~~~"> @@ -4071,7 +4071,7 @@ errorLine1=" private HashMap<Integer, Media> mMediaToAddAfterMounting = new HashMap<>();" errorLine2=" ~~~~~~~~~~~~~~~"> @@ -4087,7 +4087,7 @@ url="http://developer.android.com/design/style/iconography.html" urls="http://developer.android.com/design/style/iconography.html"> + file="../libs/gutenberg-mobile/gutenberg/packages/react-native-aztec/android/src/main/res/drawable-hdpi/ic_launcher.png"/> + file="../libs/gutenberg-mobile/gutenberg/packages/react-native-aztec/android/src/main/res/drawable-mdpi/ic_launcher.png"/> + file="../libs/gutenberg-mobile/gutenberg/packages/react-native-aztec/android/src/main/res/drawable-xhdpi/ic_launcher.png"/> + file="../libs/gutenberg-mobile/gutenberg/packages/react-native-aztec/android/src/main/res/drawable-xxhdpi/ic_launcher.png"/> @@ -4159,7 +4159,7 @@ errorLine1=" (int) update.getPaddingLeft()," errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> @@ -4175,7 +4175,7 @@ errorLine1=" (int) update.getPaddingRight()," errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> diff --git a/WordPress/metadata/full_description.txt b/WordPress/metadata/full_description.txt index 0203cc20e3f6..8f6b87705ee0 100644 --- a/WordPress/metadata/full_description.txt +++ b/WordPress/metadata/full_description.txt @@ -40,4 +40,6 @@ WordPress is an open source website creator, meaning anyone can see how it's mad Whether you need a website builder to create your website, or a simple blog maker, WordPress can help. It gives you beautiful designs, powerful features, and the freedom to build anything you want. -WordPress for Android supports self-hosted sites running WordPress 4.0 and later, and all sites at WordPress.com. Just like WordPress, WordPress for Android is open source. Learn more at https://apps.wordpress.com/contribute/. Need help with the app? Send us a tweet at @WPAndroid, or visit our forums at https://android.forums.wordpress.org/forum/troubleshooting. \ No newline at end of file +WordPress for Android supports self-hosted sites running WordPress 4.0 and later, and all sites at WordPress.com. Just like WordPress, WordPress for Android is open source. Learn more at https://wp.me/P7jrAc-tJ. Need help with the app? Send us a tweet at @WPAndroid, or visit our forums at https://android.forums.wordpress.org/forum/troubleshooting. + +California users privacy notice: https://wp.me/Pe4R-d/#california-consumer-privacy-act-ccpa. \ No newline at end of file diff --git a/WordPress/src/androidTest/java/org/wordpress/android/e2e/BlockEditorTests.java b/WordPress/src/androidTest/java/org/wordpress/android/e2e/BlockEditorTests.java index dd6e299d4636..6157274feaa6 100644 --- a/WordPress/src/androidTest/java/org/wordpress/android/e2e/BlockEditorTests.java +++ b/WordPress/src/androidTest/java/org/wordpress/android/e2e/BlockEditorTests.java @@ -1,72 +1,74 @@ package org.wordpress.android.e2e; -// -//import android.Manifest.permission; -// -//import androidx.test.rule.ActivityTestRule; -//import androidx.test.rule.GrantPermissionRule; -// -//import org.junit.Before; -//import org.junit.Rule; -//import org.junit.Test; -//import org.wordpress.android.e2e.components.MasterbarComponent; -//import org.wordpress.android.e2e.pages.BlockEditorPage; -//import org.wordpress.android.e2e.pages.EditorPage; -//import org.wordpress.android.e2e.pages.MySitesPage; -//import org.wordpress.android.e2e.pages.PostPreviewPage; -//import org.wordpress.android.e2e.pages.SiteSettingsPage; -//import org.wordpress.android.support.BaseTest; -//import org.wordpress.android.ui.WPLaunchActivity; -// -//import static androidx.test.espresso.Espresso.pressBack; -//import static org.wordpress.android.support.WPSupportUtils.sleep; -// -//public class BlockEditorTests extends BaseTest { -// @Rule -// public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(WPLaunchActivity.class); -// -// @Rule -// public GrantPermissionRule mRuntimeImageAccessRule = GrantPermissionRule.grant(permission.WRITE_EXTERNAL_STORAGE); -// -// @Before -// public void setUp() { -// logoutIfNecessary(); -// wpLogin(); -// } -// -// @Test -// public void testSwitchToClassicAndPreview() { -// String title = "Hello Espresso!"; -// -// MasterbarComponent mb = new MasterbarComponent().goToMySitesTab(); -// sleep(); -// -// MySitesPage mySitesPage = new MySitesPage(); -// mySitesPage.gotoSiteSettings(); -// -// // Set to Gutenberg. Apparently the site is defaulting to Aztec still. -// new SiteSettingsPage().toggleGutenbergSetting(); -// -// // exit the Settings page -// pressBack(); -// -// mb.clickBlogPosts(); -// -// new MySitesPage() -// .startNewPost(); -// -// BlockEditorPage blockEditorPage = new BlockEditorPage(); -// blockEditorPage.waitForTitleDisplayed(); -// -// blockEditorPage.enterTitle(title); -// -// blockEditorPage.switchToClassic(); -// -// EditorPage editorPage = new EditorPage(); -// editorPage.hasTitle(title); -// -// editorPage.previewPost(); -// sleep(); -// -// new PostPreviewPage(); -// } -//} + +import android.Manifest.permission; + +import androidx.test.rule.ActivityTestRule; +import androidx.test.rule.GrantPermissionRule; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.wordpress.android.e2e.components.MasterbarComponent; +import org.wordpress.android.e2e.pages.BlockEditorPage; +import org.wordpress.android.e2e.pages.EditorPage; +import org.wordpress.android.e2e.pages.MySitesPage; +import org.wordpress.android.e2e.pages.PostPreviewPage; +import org.wordpress.android.e2e.pages.SiteSettingsPage; +import org.wordpress.android.support.BaseTest; +import org.wordpress.android.ui.WPLaunchActivity; + +import static androidx.test.espresso.Espresso.pressBack; +import static org.wordpress.android.support.WPSupportUtils.sleep; + +public class BlockEditorTests extends BaseTest { + @Rule + public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(WPLaunchActivity.class); + + @Rule + public GrantPermissionRule mRuntimeImageAccessRule = GrantPermissionRule.grant(permission.WRITE_EXTERNAL_STORAGE); + + @Before + public void setUp() { + logoutIfNecessary(); + wpLogin(); + } + + @Ignore("until startup times are improved or idling resources are made more reliable") + @Test + public void testSwitchToClassicAndPreview() { + String title = "Hello Espresso!"; + + MasterbarComponent mb = new MasterbarComponent().goToMySitesTab(); + sleep(); + + MySitesPage mySitesPage = new MySitesPage(); + mySitesPage.gotoSiteSettings(); + + // Set to Gutenberg. Apparently the site is defaulting to Aztec still. + new SiteSettingsPage().setEditorToGutenberg(); + + // exit the Settings page + pressBack(); + + mb.clickBlogPosts(); + + new MySitesPage() + .startNewPost(); + + BlockEditorPage blockEditorPage = new BlockEditorPage(); + blockEditorPage.waitForTitleDisplayed(); + + blockEditorPage.enterTitle(title); + + blockEditorPage.switchToClassic(); + + EditorPage editorPage = new EditorPage(); + editorPage.hasTitle(title); + + editorPage.previewPost(); + sleep(); + + new PostPreviewPage(); + } +} diff --git a/WordPress/src/androidTest/java/org/wordpress/android/e2e/EditorTests.java b/WordPress/src/androidTest/java/org/wordpress/android/e2e/EditorTests.java index 56b298ef2dc8..cf76c61a3ac6 100644 --- a/WordPress/src/androidTest/java/org/wordpress/android/e2e/EditorTests.java +++ b/WordPress/src/androidTest/java/org/wordpress/android/e2e/EditorTests.java @@ -1,107 +1,111 @@ package org.wordpress.android.e2e; -// -//import android.Manifest.permission; -// -//import androidx.test.rule.ActivityTestRule; -//import androidx.test.rule.GrantPermissionRule; -// -//import org.junit.Before; -//import org.junit.Rule; -//import org.junit.Test; -//import org.wordpress.android.R; -//import org.wordpress.android.e2e.components.MasterbarComponent; -//import org.wordpress.android.e2e.pages.EditorPage; -//import org.wordpress.android.e2e.pages.MySitesPage; -//import org.wordpress.android.support.BaseTest; -//import org.wordpress.android.ui.WPLaunchActivity; -// -//import java.time.Instant; -// -//import static androidx.test.espresso.Espresso.onView; -//import static androidx.test.espresso.Espresso.pressBack; -//import static androidx.test.espresso.matcher.ViewMatchers.withId; -//import static androidx.test.espresso.matcher.ViewMatchers.withText; -//import static junit.framework.TestCase.assertTrue; -//import static org.wordpress.android.support.WPSupportUtils.checkViewHasText; -//import static org.wordpress.android.support.WPSupportUtils.sleep; -//import static org.wordpress.android.support.WPSupportUtils.waitForElementToNotBeDisplayed; -// -//public class EditorTests extends BaseTest { -// @Rule -// public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(WPLaunchActivity.class); -// -// @Rule -// public GrantPermissionRule mRuntimeImageAccessRule = GrantPermissionRule.grant(permission.WRITE_EXTERNAL_STORAGE); -// -// @Before -// public void setUp() { -// logoutIfNecessary(); -// wpLogin(); -// } -// -// @Test -// public void testPublishSimplePost() { -// String title = "Hello Espresso!"; -// String content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; -// -// MasterbarComponent mb = new MasterbarComponent().goToMySitesTab(); -// sleep(); -// mb.clickBlogPosts(); -// -// new MySitesPage() -// .startNewPost(); -// -// EditorPage editorPage = new EditorPage(); -// editorPage.enterTitle(title); -// editorPage.enterContent(content); -// boolean isPublished = editorPage.publishPost(); -// assertTrue(isPublished); -// } -// -// @Test -// public void testPublishFullPost() { -// String title = "Hello Espresso!"; -// String content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod " -// + "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis " -// + "exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."; -// String category = "Wedding"; -// long now = Instant.now().toEpochMilli(); -// String tag = "Tag " + now; -// -// MasterbarComponent mb = new MasterbarComponent().goToMySitesTab(); -// sleep(); -// mb.clickBlogPosts(); -// -// new MySitesPage() -// .startNewPost(); -// -// EditorPage editorPage = new EditorPage(); -// editorPage.enterTitle(title); -// editorPage.enterContent(content); -// editorPage.enterImage(); -// editorPage.openSettings(); -// -// editorPage.addACategory(category); -// editorPage.addATag(tag); -// editorPage.setFeaturedImage(); -// -// // ---------------------------- -// // Verify post settings data -// // ---------------------------- -// // Verify Category added -// checkViewHasText(onView(withId(R.id.post_categories)), category); -// -// // Verify tag added -// checkViewHasText(onView(withId(R.id.post_tags)), tag); -// -// // Verify the featured image added -// waitForElementToNotBeDisplayed(onView(withText(R.string.post_settings_set_featured_image))); -// -// // head back to the post -// pressBack(); -// -// // publish -// boolean isPublished = editorPage.publishPost(); -// assertTrue(isPublished); -// } -//} + +import android.Manifest.permission; + +import androidx.test.rule.ActivityTestRule; +import androidx.test.rule.GrantPermissionRule; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.wordpress.android.R; +import org.wordpress.android.e2e.components.MasterbarComponent; +import org.wordpress.android.e2e.pages.EditorPage; +import org.wordpress.android.e2e.pages.MySitesPage; +import org.wordpress.android.e2e.pages.SiteSettingsPage; +import org.wordpress.android.support.BaseTest; +import org.wordpress.android.ui.WPLaunchActivity; + +import java.time.Instant; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.Espresso.pressBack; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static junit.framework.TestCase.assertTrue; +import static org.wordpress.android.support.WPSupportUtils.checkViewHasText; +import static org.wordpress.android.support.WPSupportUtils.sleep; +import static org.wordpress.android.support.WPSupportUtils.waitForElementToNotBeDisplayed; + +public class EditorTests extends BaseTest { + @Rule + public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(WPLaunchActivity.class); + + @Rule + public GrantPermissionRule mRuntimeImageAccessRule = GrantPermissionRule.grant(permission.WRITE_EXTERNAL_STORAGE); + + @Before + public void setUp() { + logoutIfNecessary(); + wpLogin(); + + MasterbarComponent mb = new MasterbarComponent().goToMySitesTab(); + sleep(); + + MySitesPage mySitesPage = new MySitesPage(); + mySitesPage.gotoSiteSettings(); + + // Set to Classic. + new SiteSettingsPage().setEditorToClassic(); + + // exit the Settings page + pressBack(); + + mb.clickBlogPosts(); + + new MySitesPage() + .startNewPost(); + } + + @Test + public void testPublishSimplePost() { + String title = "Hello Espresso!"; + String content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; + + EditorPage editorPage = new EditorPage(); + editorPage.enterTitle(title); + editorPage.enterContent(content); + boolean isPublished = editorPage.publishPost(); + assertTrue(isPublished); + } + + @Test + public void testPublishFullPost() { + String title = "Hello Espresso!"; + String content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod " + + "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud " + + "exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."; + String category = "Wedding"; + long now = Instant.now().toEpochMilli(); + String tag = "Tag " + now; + + EditorPage editorPage = new EditorPage(); + editorPage.enterTitle(title); + editorPage.enterContent(content); + editorPage.enterImage(); + editorPage.openSettings(); + + editorPage.addACategory(category); + editorPage.addATag(tag); + editorPage.setFeaturedImage(); + + // ---------------------------- + // Verify post settings data + // ---------------------------- + // Verify Category added + checkViewHasText(onView(withId(R.id.post_categories)), category); + + // Verify tag added + checkViewHasText(onView(withId(R.id.post_tags)), tag); + + // Verify the featured image added + waitForElementToNotBeDisplayed(onView(withText(R.string.post_settings_set_featured_image))); + + // head back to the post + pressBack(); + + // publish + boolean isPublished = editorPage.publishPost(); + assertTrue(isPublished); + } +} diff --git a/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/SiteSettingsPage.java b/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/SiteSettingsPage.java index 05fb76c9f7e1..3bbf986c6902 100644 --- a/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/SiteSettingsPage.java +++ b/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/SiteSettingsPage.java @@ -4,12 +4,14 @@ import org.wordpress.android.R; +import static androidx.test.espresso.Espresso.onData; import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.scrollTo; import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.PreferenceMatchers.withTitle; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static org.wordpress.android.support.WPSupportUtils.ensureSwitchPreferenceIsChecked; public class SiteSettingsPage { private static ViewInteraction settings = onView(withText(R.string.site_settings_title_title)); @@ -18,9 +20,15 @@ public SiteSettingsPage() { settings.check(matches(isDisplayed())); } - public void toggleGutenbergSetting() { - onView(withText(R.string.site_settings_gutenberg_default_for_new_posts)) + public void setEditorToClassic() { + onData(withTitle(R.string.site_settings_gutenberg_default_for_new_posts)) .perform(scrollTo()) - .perform((click())); + .perform(ensureSwitchPreferenceIsChecked(false)); + } + + public void setEditorToGutenberg() { + onData(withTitle(R.string.site_settings_gutenberg_default_for_new_posts)) + .perform(scrollTo()) + .perform(ensureSwitchPreferenceIsChecked(true)); } } diff --git a/WordPress/src/androidTest/java/org/wordpress/android/support/WPSupportUtils.java b/WordPress/src/androidTest/java/org/wordpress/android/support/WPSupportUtils.java index cc866c4da881..8dc8e239135d 100644 --- a/WordPress/src/androidTest/java/org/wordpress/android/support/WPSupportUtils.java +++ b/WordPress/src/androidTest/java/org/wordpress/android/support/WPSupportUtils.java @@ -6,22 +6,26 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.widget.Checkable; import androidx.recyclerview.widget.RecyclerView; import androidx.test.espresso.AmbiguousViewMatcherException; import androidx.test.espresso.Espresso; +import androidx.test.espresso.UiController; import androidx.test.espresso.ViewAction; import androidx.test.espresso.ViewInteraction; import androidx.test.espresso.action.GeneralClickAction; import androidx.test.espresso.action.GeneralLocation; import androidx.test.espresso.action.Press; import androidx.test.espresso.action.Tap; +import androidx.test.espresso.action.ViewActions; import androidx.test.espresso.matcher.ViewMatchers.Visibility; import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; import androidx.test.uiautomator.UiDevice; import androidx.test.uiautomator.UiObjectNotFoundException; import androidx.test.uiautomator.UiSelector; +import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.Matchers; @@ -55,6 +59,7 @@ import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.isA; public class WPSupportUtils { @@ -134,6 +139,38 @@ public static ViewAction click(ViewAction rollbackAction) { rollbackAction); } + /** Ensures that a SwitchPreference isChecked is true or false regardless of the current value. This is useful to + * guarantee a particular resulting state where a previously toggled state may not be known. + * @param isChecked the value to set the preference to + * @return the ViewAction + */ + public static ViewAction ensureSwitchPreferenceIsChecked(final boolean isChecked) { + return new ViewAction() { + @Override public BaseMatcher getConstraints() { + return new BaseMatcher() { + @Override public boolean matches(Object item) { + return isA(Checkable.class).matches(((View) item).findViewById(android.R.id.switch_widget)); + } + + @Override public void describeTo(Description description) { + description.appendText("checkable"); + } + }; + } + + @Override public String getDescription() { + return "setChecked(" + isChecked + ")"; + } + + @Override public void perform(UiController uiController, View view) { + // perform click only if necessary + if (((Checkable) view.findViewById(android.R.id.switch_widget)).isChecked() != isChecked) { + ViewActions.click().perform(uiController, view); + } + } + }; + } + private static boolean isResourceId(String text) { diff --git a/WordPress/src/androidTest/java/org/wordpress/android/util/FeatureAnnouncementJsonValidityTest.kt b/WordPress/src/androidTest/java/org/wordpress/android/util/FeatureAnnouncementJsonValidityTest.kt deleted file mode 100644 index d70213c52ea0..000000000000 --- a/WordPress/src/androidTest/java/org/wordpress/android/util/FeatureAnnouncementJsonValidityTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.wordpress.android.util - -import android.content.Context -import androidx.test.platform.app.InstrumentationRegistry -import com.google.gson.Gson -import com.google.gson.JsonSyntaxException -import org.junit.Assert.fail -import org.junit.Test -import org.wordpress.android.ui.whatsnew.FeatureAnnouncements -import java.io.FileNotFoundException - -class FeatureAnnouncementJsonValidityTest { - @Test - fun testValidityOfFeatureAnnouncementJsonFile() { - var featureAnnouncementFileContent: String? = null - - val context: Context = InstrumentationRegistry.getInstrumentation().targetContext - try { - featureAnnouncementFileContent = context.assets.open("FEATURE_ANNOUNCEMENTS.json") - .bufferedReader().use { it.readText() } - } catch (fileNotFound: FileNotFoundException) { - fail("FEATURE_ANNOUNCEMENTS.json is missing") - } - - try { - Gson().fromJson( - featureAnnouncementFileContent, - FeatureAnnouncements::class.java - ) - } catch (jsonSyntaxException: JsonSyntaxException) { - fail( - "JsonSyntaxException when parsing FEATURE_ANNOUNCEMENTS.json:" + - " ${jsonSyntaxException.message}" - ) - } - } -} diff --git a/WordPress/src/main/assets/FEATURE_ANNOUNCEMENTS.json b/WordPress/src/main/assets/FEATURE_ANNOUNCEMENTS.json deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/WordPress/src/main/java/org/wordpress/android/WordPress.java b/WordPress/src/main/java/org/wordpress/android/WordPress.java index 1021996f6e03..ab2626db5fa6 100644 --- a/WordPress/src/main/java/org/wordpress/android/WordPress.java +++ b/WordPress/src/main/java/org/wordpress/android/WordPress.java @@ -104,6 +104,7 @@ import org.wordpress.android.util.UploadWorkerKt; import org.wordpress.android.util.VolleyUtils; import org.wordpress.android.util.analytics.AnalyticsUtils; +import org.wordpress.android.util.config.AppConfig; import org.wordpress.android.util.image.ImageManager; import org.wordpress.android.widgets.AppRatingDialog; @@ -162,6 +163,7 @@ public class WordPress extends MultiDexApplication implements HasServiceInjector @Inject PrivateAtomicCookie mPrivateAtomicCookie; @Inject ImageEditorTracker mImageEditorTracker; @Inject CrashLogging mCrashLogging; + @Inject AppConfig mAppConfig; // For development and production `AnalyticsTrackerNosara`, for testing a mocked `Tracker` will be injected. @Inject Tracker mTracker; @@ -790,6 +792,7 @@ public AndroidInjector serviceInjector() { @OnLifecycleEvent(Lifecycle.Event.ON_START) void onAppComesFromBackground() { mApplicationLifecycleMonitor.onAppComesFromBackground(); + mAppConfig.refresh(); } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderCardType.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderCardType.java index e9df3ca472dc..a35d83737512 100644 --- a/WordPress/src/main/java/org/wordpress/android/models/ReaderCardType.java +++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderCardType.java @@ -4,7 +4,6 @@ import org.wordpress.android.ui.reader.ReaderConstants; import org.wordpress.android.ui.reader.utils.ReaderImageScanner; -import org.wordpress.android.ui.reader.views.ReaderThumbnailStrip; import org.wordpress.android.util.HtmlUtils; /** @@ -39,7 +38,8 @@ public static ReaderCardType fromReaderPost(ReaderPost post) { if (!post.hasFeaturedImage() && post.hasImages() && new ReaderImageScanner(post.getText(), post.isPrivate) - .hasUsableImageCount(ReaderThumbnailStrip.IMAGE_COUNT, ReaderConstants.MIN_GALLERY_IMAGE_WIDTH)) { + .hasUsableImageCount(ReaderConstants.THUMBNAIL_STRIP_IMG_COUNT, + ReaderConstants.MIN_GALLERY_IMAGE_WIDTH)) { return GALLERY; } diff --git a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java index 1f3604038648..bf9ca9e3c944 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java @@ -75,6 +75,7 @@ import org.wordpress.android.ui.people.RoleChangeDialogFragment; import org.wordpress.android.ui.people.RoleSelectDialogFragment; import org.wordpress.android.ui.photopicker.PhotoPickerActivity; +import org.wordpress.android.ui.photopicker.PhotoPickerFragment; import org.wordpress.android.ui.plans.PlanDetailsFragment; import org.wordpress.android.ui.plans.PlansActivity; import org.wordpress.android.ui.plans.PlansListAdapter; @@ -542,6 +543,8 @@ public interface AppComponent extends AndroidInjector { void inject(AztecVideoLoader object); + void inject(PhotoPickerFragment object); + // Allows us to inject the application without having to instantiate any modules, and provides the Application // in the app graph @Component.Builder diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java index 5da4ff020d06..8f767aa2423c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java @@ -216,6 +216,7 @@ public static void showGifPickerForResult(Activity activity, @NonNull SiteModel Intent intent = new Intent(activity, GifPickerActivity.class); intent.putExtra(WordPress.SITE, site); + intent.putExtra(GifPickerActivity.KEY_REQUEST_CODE, requestCode); activity.startActivityForResult(intent, requestCode); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/FullScreenDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/FullScreenDialogFragment.java index 266beaa5fbfa..c44cf7388477 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/FullScreenDialogFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/FullScreenDialogFragment.java @@ -64,7 +64,7 @@ public interface FullScreenDialogContent { boolean onDismissClicked(FullScreenDialogController controller); - void onViewCreated(FullScreenDialogController controller); + void setController(FullScreenDialogController controller); } public interface FullScreenDialogController { @@ -181,7 +181,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - ((FullScreenDialogContent) getContent()).onViewCreated(mController); + ((FullScreenDialogContent) getContent()).setController(mController); } @Override diff --git a/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java b/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java index f1cbaf40e95d..246e40fb2f3f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/RequestCodes.java @@ -48,7 +48,8 @@ public class RequestCodes { // QuickStart public static final int QUICK_START_REMINDER_RECEIVER = 4000; - public static final int GIF_PICKER = 3200; + public static final int GIF_PICKER_SINGLE_SELECT = 3200; + public static final int GIF_PICKER_MULTI_SELECT = 3201; // Domain Registration public static final int DOMAIN_REGISTRATION = 5000; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/BaseUsernameChangerFullScreenDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/BaseUsernameChangerFullScreenDialogFragment.java index b144c03b91e2..7ffb56f2ab76 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/BaseUsernameChangerFullScreenDialogFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/BaseUsernameChangerFullScreenDialogFragment.java @@ -14,6 +14,7 @@ import android.widget.ProgressBar; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.recyclerview.widget.LinearLayoutManager; @@ -135,20 +136,26 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, } @Override - public void onViewCreated(final FullScreenDialogController controller) { + public void setController(final FullScreenDialogController controller) { mDialogController = controller; } @Override - public void onActivityCreated(@Nullable final Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); if (savedInstanceState != null) { mIsShowingDismissDialog = savedInstanceState.getBoolean(KEY_IS_SHOWING_DISMISS_DIALOG); mShouldWatchText = savedInstanceState.getBoolean(KEY_SHOULD_WATCH_TEXT); mUsernameSelected = savedInstanceState.getString(KEY_USERNAME_SELECTED); mUsernameSelectedIndex = savedInstanceState.getInt(KEY_USERNAME_SELECTED_INDEX); - setUsernameSuggestions(savedInstanceState.getStringArrayList(KEY_USERNAME_SUGGESTIONS)); + ArrayList suggestions = savedInstanceState.getStringArrayList(KEY_USERNAME_SUGGESTIONS); + if (suggestions != null) { + setUsernameSuggestions(suggestions); + } else { + mUsernameSuggestionInput = getUsernameQueryFromDisplayName(); + getUsernameSuggestions(mUsernameSuggestionInput); + } if (mIsShowingDismissDialog) { showDismissDialog(); @@ -209,6 +216,12 @@ public void onStop() { mDispatcher.unregister(this); } + @Override + public void onDestroy() { + mGetSuggestionsHandler.removeCallbacksAndMessages(null); + super.onDestroy(); + } + @Override public boolean onConfirmClicked(FullScreenDialogController controller) { ActivityUtils.hideKeyboard(getActivity()); @@ -244,7 +257,9 @@ public void onSaveInstanceState(@NotNull Bundle outState) { outState.putBoolean(KEY_SHOULD_WATCH_TEXT, false); outState.putString(KEY_USERNAME_SELECTED, mUsernameSelected); outState.putInt(KEY_USERNAME_SELECTED_INDEX, mUsernameSelectedIndex); - outState.putStringArrayList(KEY_USERNAME_SUGGESTIONS, new ArrayList<>(mUsernamesAdapter.mItems)); + if (mUsernamesAdapter != null) { + outState.putStringArrayList(KEY_USERNAME_SUGGESTIONS, new ArrayList<>(mUsernamesAdapter.mItems)); + } } @Override diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SettingsUsernameChangerFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SettingsUsernameChangerFragment.kt index 82693315b4ec..d0f5253391d5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SettingsUsernameChangerFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SettingsUsernameChangerFragment.kt @@ -46,8 +46,8 @@ class SettingsUsernameChangerFragment : BaseUsernameChangerFullScreenDialogFragm ), HtmlCompat.FROM_HTML_MODE_LEGACY ) - override fun onViewCreated(controller: FullScreenDialogController) { - super.onViewCreated(controller) + override fun setController(controller: FullScreenDialogController) { + super.setController(controller) dialogController = controller dialogController.setActionEnabled(false) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/gif/GifMediaViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/gif/GifMediaViewHolder.kt index 610256836843..d3fb031176e0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/gif/GifMediaViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/gif/GifMediaViewHolder.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.Observer import kotlinx.android.synthetic.main.media_picker_thumbnail.view.* import org.wordpress.android.R import org.wordpress.android.util.AniUtils +import org.wordpress.android.util.AniUtils.Duration.MEDIUM import org.wordpress.android.util.getDistinct import org.wordpress.android.util.image.ImageManager import org.wordpress.android.util.image.ImageType.PHOTO @@ -47,7 +48,8 @@ class GifMediaViewHolder( /** * The dimensions used for the ImageView */ - thumbnailViewDimensions: ThumbnailViewDimensions + thumbnailViewDimensions: ThumbnailViewDimensions, + private val isMultiSelectEnabled: Boolean ) : LifecycleOwnerViewHolder(itemView) { data class ThumbnailViewDimensions(val width: Int, val height: Int) @@ -85,14 +87,14 @@ class GifMediaViewHolder( // Immediately update the selection number and scale the thumbnail when a bind happens val isSelected = mediaViewModel?.isSelected?.value ?: false - updateNumberTextOnSelectionChange(isSelected = isSelected, animated = false) + updateSelectionIndicatorOnSelectionChange(isSelected = isSelected, animated = false) updateThumbnailOnSelectionChange(isSelected = isSelected, animated = false) // When the [isSelected] property changes later, update the selection number and scale the thumbnail mediaViewModel?.isSelected?.observe(this, Observer { val selected = it ?: false - updateNumberTextOnSelectionChange(isSelected = selected, animated = true) + updateSelectionIndicatorOnSelectionChange(isSelected = selected, animated = true) updateThumbnailOnSelectionChange(isSelected = selected, animated = true) }) @@ -106,12 +108,27 @@ class GifMediaViewHolder( imageManager.load(thumbnailView, PHOTO, mediaViewModel?.thumbnailUri.toString(), CENTER_CROP) } - private fun updateNumberTextOnSelectionChange(isSelected: Boolean, animated: Boolean) { + private fun updateSelectionIndicatorOnSelectionChange(isSelected: Boolean, animated: Boolean) { // The `isSelected` here changes the color of the text. It will be blue when selected. selectionNumberTextView.isSelected = isSelected + if (!isMultiSelectEnabled) { + selectionNumberTextView.visibility = if (isSelected) { + View.VISIBLE + } else { + View.GONE + } + } if (animated) { - AniUtils.startAnimation(selectionNumberTextView, R.anim.pop) + if (!isMultiSelectEnabled) { + if (isSelected) { + AniUtils.scaleIn(selectionNumberTextView, MEDIUM) + } else { + AniUtils.scaleOut(selectionNumberTextView, MEDIUM) + } + } else { + AniUtils.startAnimation(selectionNumberTextView, R.anim.pop) + } } } @@ -144,7 +161,8 @@ class GifMediaViewHolder( onClickListener: (GifMediaViewModel?) -> Unit, onLongClickListener: (GifMediaViewModel) -> Unit, parent: ViewGroup, - thumbnailViewDimensions: ThumbnailViewDimensions + thumbnailViewDimensions: ThumbnailViewDimensions, + isMultiSelectEnabled: Boolean ): GifMediaViewHolder { // We are intentionally reusing this layout since the UI is very similar. val view = LayoutInflater.from(parent.context) @@ -154,7 +172,8 @@ class GifMediaViewHolder( onClickListener = onClickListener, onLongClickListener = onLongClickListener, itemView = view, - thumbnailViewDimensions = thumbnailViewDimensions + thumbnailViewDimensions = thumbnailViewDimensions, + isMultiSelectEnabled = isMultiSelectEnabled ) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/gif/GifPickerActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/gif/GifPickerActivity.kt index babf2dc45000..1887d66270a9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/gif/GifPickerActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/gif/GifPickerActivity.kt @@ -19,8 +19,9 @@ import org.wordpress.android.WordPress import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.ui.ActionableEmptyView -import org.wordpress.android.ui.gif.GifMediaViewHolder.ThumbnailViewDimensions import org.wordpress.android.ui.LocaleAwareActivity +import org.wordpress.android.ui.RequestCodes +import org.wordpress.android.ui.gif.GifMediaViewHolder.ThumbnailViewDimensions import org.wordpress.android.ui.media.MediaPreviewActivity import org.wordpress.android.util.AniUtils import org.wordpress.android.util.DisplayUtils @@ -48,6 +49,8 @@ class GifPickerActivity : LocaleAwareActivity() { private lateinit var viewModel: GifPickerViewModel + private var isMultiSelectEnabled: Boolean = false + private val gridColumnCount: Int by lazy { if (DisplayUtils.isLandscape(this)) 4 else 3 } /** @@ -63,9 +66,11 @@ class GifPickerActivity : LocaleAwareActivity() { (application as WordPress).component().inject(this) val site = intent.getSerializableExtra(WordPress.SITE) as SiteModel + val requestCode = intent.getIntExtra(KEY_REQUEST_CODE, 0) + isMultiSelectEnabled = requestCode == RequestCodes.GIF_PICKER_MULTI_SELECT viewModel = ViewModelProviders.of(this, viewModelFactory).get(GifPickerViewModel::class.java) - viewModel.setup(site) + viewModel.start(site, isMultiSelectEnabled) // We are intentionally reusing this layout since the UI is very similar. setContentView(R.layout.media_picker_activity) @@ -106,7 +111,8 @@ class GifPickerActivity : LocaleAwareActivity() { viewModel.retryAllFailedRangeLoads() } }, - onMediaViewLongClickListener = { showPreview(listOf(it)) } + onMediaViewLongClickListener = { showPreview(listOf(it)) }, + isMultiSelectEnabled = isMultiSelectEnabled ) recycler.apply { @@ -153,10 +159,26 @@ class GifPickerActivity : LocaleAwareActivity() { * Configure the selection bar and its labels when the [GifPickerViewModel] selected items change */ private fun initializeSelectionBar() { - viewModel.selectionBarIsVisible.observe(this, Observer { - // Do nothing if the [viewModel.selectionBarIsVisible] has not been initialized with a value yet. The - // selection bar is hidden by default anyway so we don't need to worry in this case. - val isVisible = it ?: return@Observer + viewModel.selectionBarUiModel.observe(this, Observer { uiModel -> + if (uiModel.isMultiselectEnabled) { + // Update the "Add" and "Preview" labels to include the number of items. For example, "Add 7" and "Preview 7". + // + // We do not change to labels back to the original text if the number of items go back to zero because that + // causes a weird UX. The selection bar is animated to disappear at that time and it looks weird if the labels + // change to just "Add" and "Preview" too. + val selectedCount = uiModel.numberOfSelectedImages + if (selectedCount > 0) { + text_preview.text = getString(R.string.preview_count, selectedCount) + text_add.text = getString(R.string.add_count, selectedCount) + } + } else { + // When in single selection mode we only show ADD label + text_add.text = getString(R.string.photo_picker_use_gif) + text_add.visibility = View.VISIBLE + text_preview.visibility = View.GONE + } + + val isVisible = uiModel.isVisible val selectionBar: ViewGroup = container_selection_bar // Do nothing if the selection bar is already in the visibility state that we want it to be @@ -178,19 +200,6 @@ class GifPickerActivity : LocaleAwareActivity() { recyclerViewLayoutParams.addRule(RelativeLayout.ABOVE, 0) } }) - - // Update the "Add" and "Preview" labels to include the number of items. For example, "Add 7" and "Preview 7". - // - // We do not change to labels back to the original text if the number of items go back to zero because that - // causes a weird UX. The selection bar is animated to disappear at that time and it looks weird if the labels - // change to just "Add" and "Preview" too. - viewModel.selectedMediaViewModelList.observe(this, Observer { - val selectedCount = it?.size ?: 0 - if (selectedCount > 0) { - text_preview.text = getString(R.string.preview_count, selectedCount) - text_add.text = getString(R.string.add_count, selectedCount) - } - }) } /** @@ -366,5 +375,6 @@ class GifPickerActivity : LocaleAwareActivity() { * Added to this Activity's result as an Int array [org.wordpress.android.fluxc.model.MediaModel] `id` values. */ const val KEY_SAVED_MEDIA_MODEL_LOCAL_IDS = "saved_media_model_local_ids" + const val KEY_REQUEST_CODE = "gif_picker_key_request_code" } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/gif/GifPickerPagedListAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/gif/GifPickerPagedListAdapter.kt index 29350ba3c590..daf0ea175567 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/gif/GifPickerPagedListAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/gif/GifPickerPagedListAdapter.kt @@ -14,7 +14,8 @@ class GifPickerPagedListAdapter( private val imageManager: ImageManager, private val thumbnailViewDimensions: ThumbnailViewDimensions, private val onMediaViewClickListener: (GifMediaViewModel?) -> Unit, - private val onMediaViewLongClickListener: (GifMediaViewModel) -> Unit + private val onMediaViewLongClickListener: (GifMediaViewModel) -> Unit, + private val isMultiSelectEnabled: Boolean ) : PagedListAdapter(DIFF_CALLBACK) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GifMediaViewHolder { return GifMediaViewHolder.create( @@ -22,7 +23,8 @@ class GifPickerPagedListAdapter( onClickListener = onMediaViewClickListener, onLongClickListener = onMediaViewLongClickListener, parent = parent, - thumbnailViewDimensions = thumbnailViewDimensions + thumbnailViewDimensions = thumbnailViewDimensions, + isMultiSelectEnabled = isMultiSelectEnabled ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.kt index 8bcab019a61b..ed03b719f5ac 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.kt @@ -1213,16 +1213,25 @@ class MySiteFragment : Fragment(), val focusPointSize = resources.getDimensionPixelOffset(R.dimen.quick_start_focus_point_size) val horizontalOffset: Int val verticalOffset: Int - if (isTargetingBottomNavBar(activeTutorialPrompt!!.task)) { - horizontalOffset = quickStartTarget.width / 2 - focusPointSize + resources - .getDimensionPixelOffset(R.dimen.quick_start_focus_point_bottom_nav_offset) - verticalOffset = 0 - } else if (activeTutorialPrompt!!.task == UPLOAD_SITE_ICON) { - horizontalOffset = focusPointSize - verticalOffset = -focusPointSize / 2 - } else { - horizontalOffset = resources.getDimensionPixelOffset(R.dimen.quick_start_focus_point_my_site_right_offset) - verticalOffset = (quickStartTarget.height - focusPointSize) / 2 + when { + isTargetingBottomNavBar(activeTutorialPrompt!!.task) -> { + horizontalOffset = quickStartTarget.width / 2 - focusPointSize + resources + .getDimensionPixelOffset(R.dimen.quick_start_focus_point_bottom_nav_offset) + verticalOffset = 0 + } + activeTutorialPrompt!!.task == UPLOAD_SITE_ICON -> { + horizontalOffset = focusPointSize + verticalOffset = -focusPointSize / 2 + } + activeTutorialPrompt!!.task == VIEW_SITE -> { // focus point might be hidden behind FAB + horizontalOffset = (focusPointSize / 0.5).toInt() + verticalOffset = (quickStartTarget.height - focusPointSize) / 2 + } + else -> { + horizontalOffset = + resources.getDimensionPixelOffset(R.dimen.quick_start_focus_point_my_site_right_offset) + verticalOffset = (quickStartTarget.height - focusPointSize) / 2 + } } addQuickStartFocusPointAboveTheView( parentView, quickStartTarget, horizontalOffset, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java index 2e2ec3dd046e..8970e95bb8ae 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java @@ -182,6 +182,7 @@ public class WPMainActivity extends LocaleAwareActivity implements private FloatingActionButton mFloatingActionButton; private WPTooltipView mFabTooltip; private static final String MAIN_BOTTOM_SHEET_TAG = "MAIN_BOTTOM_SHEET_TAG"; + private final Handler mHandler = new Handler(); @Inject AccountStore mAccountStore; @Inject SiteStore mSiteStore; @@ -228,20 +229,14 @@ public void onCreate(Bundle savedInstanceState) { mBottomNav.init(getSupportFragmentManager(), this); mConnectionBar = findViewById(R.id.connection_bar); - mConnectionBar.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - // slide out the bar on click, then re-check connection after a brief delay - AniUtils.animateBottomBar(mConnectionBar, false); - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - if (!isFinishing()) { - checkConnection(); - } - } - }, 2000); - } + mConnectionBar.setOnClickListener(v -> { + // slide out the bar on click, then re-check connection after a brief delay + AniUtils.animateBottomBar(mConnectionBar, false); + mHandler.postDelayed(() -> { + if (!isFinishing()) { + checkConnection(); + } + }, 2000); }); mIsMagicLinkLogin = getIntent().getBooleanExtra(ARG_IS_MAGIC_LINK_LOGIN, false); @@ -693,6 +688,7 @@ private void launchWithPostId(int postId, boolean isPage) { protected void onDestroy() { EventBus.getDefault().unregister(this); mDispatcher.unregister(this); + mHandler.removeCallbacksAndMessages(null); super.onDestroy(); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainNavigationView.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainNavigationView.kt index f5b0b5374ebb..cb6d2606aaf3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainNavigationView.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainNavigationView.kt @@ -27,7 +27,6 @@ import org.wordpress.android.ui.main.WPMainNavigationView.PageType.READER import org.wordpress.android.ui.notifications.NotificationsListFragment import org.wordpress.android.ui.prefs.AppPrefs import org.wordpress.android.ui.reader.ReaderFragment -import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsFragment import org.wordpress.android.util.AniUtils import org.wordpress.android.util.AniUtils.Duration import org.wordpress.android.util.getColorStateListFromAttribute @@ -283,11 +282,7 @@ class WPMainNavigationView @JvmOverloads constructor( private fun createFragment(pageType: PageType): Fragment { val fragment = when (pageType) { MY_SITE -> MySiteFragment.newInstance() - READER -> if (AppPrefs.isReaderImprovementsPhase2Enabled()) { - ReaderInterestsFragment() // TODO: Temporary entry point - } else { - ReaderFragment() - } + READER -> ReaderFragment() NOTIFS -> NotificationsListFragment.newInstance() } fragmentManager.beginTransaction() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java index 98756519419e..8a36512a8550 100755 --- a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java @@ -84,6 +84,7 @@ import org.wordpress.android.util.WPMediaUtils; import org.wordpress.android.util.WPPermissionUtils; import org.wordpress.android.util.analytics.AnalyticsUtils; +import org.wordpress.android.util.config.TenorFeatureConfig; import org.wordpress.android.widgets.AppRatingDialog; import java.util.ArrayList; @@ -113,6 +114,7 @@ public class MediaBrowserActivity extends LocaleAwareActivity implements MediaGr @Inject MediaStore mMediaStore; @Inject SiteStore mSiteStore; @Inject UploadUtilsWrapper mUploadUtilsWrapper; + @Inject TenorFeatureConfig mTenorFeatureConfig; private SiteModel mSite; @@ -487,7 +489,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { reloadMediaGrid(); } break; - case RequestCodes.GIF_PICKER: + case RequestCodes.GIF_PICKER_MULTI_SELECT: if (resultCode == RESULT_OK && data.hasExtra(GifPickerActivity.KEY_SAVED_MEDIA_MODEL_LOCAL_IDS)) { int[] mediaLocalIds = data.getIntArrayExtra(GifPickerActivity.KEY_SAVED_MEDIA_MODEL_LOCAL_IDS); @@ -915,7 +917,7 @@ public void showAddMediaPopup() { }); } - if (mBrowserType.isBrowser() && BuildConfig.TENOR_AVAILABLE) { + if (mBrowserType.isBrowser() && mTenorFeatureConfig.isEnabled()) { popup.getMenu().add(R.string.photo_picker_gif).setOnMenuItemClickListener( item -> { doAddMediaItemClicked(AddMenuItem.ITEM_CHOOSE_GIF); @@ -961,7 +963,7 @@ private void doAddMediaItemClicked(@NonNull AddMenuItem item) { mSite, RequestCodes.STOCK_MEDIA_PICKER_MULTI_SELECT); break; case ITEM_CHOOSE_GIF: - ActivityLauncher.showGifPickerForResult(this, mSite, RequestCodes.GIF_PICKER); + ActivityLauncher.showGifPickerForResult(this, mSite, RequestCodes.GIF_PICKER_MULTI_SELECT); break; } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSettingsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSettingsActivity.java index a28ee1a1b72e..0c327bfbc988 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSettingsActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSettingsActivity.java @@ -773,7 +773,14 @@ public void onLoadFailed(@Nullable Exception e, @Nullable Object model) { AppLog.e(T.MEDIA, e); } showProgress(false); - delayedFinishWithError(); + if (isVideo()) { + // for videos it's ok if we fail to load the thumbnail - can happen. + // let's show a toast but let the user edit the media settings! + ToastUtils.showToast(MediaSettingsActivity.this, + R.string.error_media_thumbnail_not_loaded); + } else { + delayedFinishWithError(); + } } } }); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/pages/PageItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/pages/PageItem.kt index a8a99d8d3d4f..24052fd633fd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/pages/PageItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/pages/PageItem.kt @@ -26,7 +26,8 @@ sealed class PageItem(open val type: Type) { open var actionsEnabled: Boolean, open val tapActionEnabled: Boolean, open val progressBarUiState: ProgressBarUiState, - open val showOverlay: Boolean + open val showOverlay: Boolean, + open val author: String? ) : PageItem(PAGE) data class PublishedPage( @@ -42,7 +43,8 @@ sealed class PageItem(open val type: Type) { override val actions: Set, override var actionsEnabled: Boolean = true, override val progressBarUiState: ProgressBarUiState, - override val showOverlay: Boolean + override val showOverlay: Boolean, + override val author: String? = null ) : Page( remoteId = remoteId, localId = localId, @@ -56,7 +58,8 @@ sealed class PageItem(open val type: Type) { actionsEnabled = actionsEnabled, tapActionEnabled = true, progressBarUiState = progressBarUiState, - showOverlay = showOverlay + showOverlay = showOverlay, + author = author ) data class DraftPage( @@ -71,7 +74,8 @@ sealed class PageItem(open val type: Type) { override val actions: Set, override var actionsEnabled: Boolean = true, override val progressBarUiState: ProgressBarUiState, - override val showOverlay: Boolean + override val showOverlay: Boolean, + override val author: String? = null ) : Page( remoteId = remoteId, localId = localId, @@ -85,7 +89,8 @@ sealed class PageItem(open val type: Type) { actionsEnabled = actionsEnabled, tapActionEnabled = true, progressBarUiState = progressBarUiState, - showOverlay = showOverlay + showOverlay = showOverlay, + author = author ) data class ScheduledPage( @@ -100,7 +105,8 @@ sealed class PageItem(open val type: Type) { override val actions: Set, override var actionsEnabled: Boolean = true, override val progressBarUiState: ProgressBarUiState, - override val showOverlay: Boolean + override val showOverlay: Boolean, + override val author: String? = null ) : Page( remoteId = remoteId, localId = localId, @@ -114,7 +120,8 @@ sealed class PageItem(open val type: Type) { actionsEnabled = actionsEnabled, tapActionEnabled = true, progressBarUiState = progressBarUiState, - showOverlay = showOverlay + showOverlay = showOverlay, + author = author ) data class TrashedPage( @@ -129,7 +136,8 @@ sealed class PageItem(open val type: Type) { override val actions: Set, override var actionsEnabled: Boolean = true, override val progressBarUiState: ProgressBarUiState, - override val showOverlay: Boolean + override val showOverlay: Boolean, + override val author: String? = null ) : Page( remoteId = remoteId, localId = localId, @@ -143,7 +151,9 @@ sealed class PageItem(open val type: Type) { actionsEnabled = actionsEnabled, tapActionEnabled = false, progressBarUiState = progressBarUiState, - showOverlay = showOverlay + showOverlay = showOverlay, + author = author + ) data class ParentPage( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/pages/PageItemViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/pages/PageItemViewHolder.kt index 3e7063f8bffe..82367387ccd1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/pages/PageItemViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/pages/PageItemViewHolder.kt @@ -80,20 +80,7 @@ sealed class PageItemViewHolder(internal val parent: ViewGroup, @LayoutRes layou else page.title - val date = if (page.date == Date(0)) Date() else page.date - val stringDate = DateTimeUtils.javaDateToTimeSpan(date, parent.context) - .capitalizeWithLocaleWithoutLint(parent.context.currentLocale) - val subtitle = page.subtitle - pageSubtitle.text = if (subtitle == null) { - stringDate - } else { - String.format( - Locale.getDefault(), - parent.context.getString(R.string.pages_item_subtitle), - stringDate, - parent.context.getString(subtitle) - ) - } + showSubtitle(page.date, page.author, page.subtitle) labels.text = page.labels.map { uiHelper.getTextOfUiString(parent.context, it) }.sorted() .joinToString(separator = " · ") @@ -176,6 +163,44 @@ sealed class PageItemViewHolder(internal val parent: ViewGroup, @LayoutRes layou } } } + + @ExperimentalStdlibApi + private fun showSubtitle(inputDate: Date, author: String?, subtitle: Int?) { + val date = if (inputDate == Date(0)) Date() else inputDate + val stringDate = DateTimeUtils.javaDateToTimeSpan(date, parent.context) + .capitalizeWithLocaleWithoutLint(parent.context.currentLocale) + + /** The subtitle can use 2 or 3 placeholders + * Date - Only (author & subtitle are null) + * Date - Author (author != null && subtitle == null) + * Date - subtitle (author == null && subtitle != null) + * Date - Author - subtitle (all have values) + */ + pageSubtitle.text = if (author == null && subtitle == null) { + stringDate + } else if (author != null && subtitle == null) { + String.format( + Locale.getDefault(), + parent.context.getString(R.string.pages_item_subtitle), + stringDate, + author) + } else if (author == null && subtitle != null) { + String.format( + Locale.getDefault(), + parent.context.getString(R.string.pages_item_subtitle), + stringDate, + parent.context.getString(subtitle)) + } else { + subtitle?.let { + String.format( + Locale.getDefault(), + parent.context.getString(R.string.pages_item_subtitle_date_author), + stringDate, + author, + parent.context.getString(it)) + } + } + } } class PageDividerViewHolder(parentView: ViewGroup) : PageItemViewHolder(parentView, R.layout.page_divider_item) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesAuthorFilterUIState.kt b/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesAuthorFilterUIState.kt new file mode 100644 index 000000000000..aeeb266bd458 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesAuthorFilterUIState.kt @@ -0,0 +1,10 @@ +package org.wordpress.android.ui.pages + +import org.wordpress.android.ui.posts.AuthorFilterListItemUIState +import org.wordpress.android.ui.posts.AuthorFilterSelection + +data class PagesAuthorFilterUIState( + val isAuthorFilterVisible: Boolean, + val authorFilterSelection: AuthorFilterSelection, + val authorFilterItems: List +) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesFragment.kt index 3bc6a82ef505..668ccd53ef43 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesFragment.kt @@ -14,6 +14,7 @@ import android.view.MenuItem.OnActionExpandListener import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.widget.AdapterView import android.widget.LinearLayout import android.widget.Toast import androidx.appcompat.widget.SearchView @@ -47,6 +48,7 @@ import org.wordpress.android.ui.posts.PostListAction.PreviewPost import org.wordpress.android.ui.posts.PreviewStateHelper import org.wordpress.android.ui.posts.ProgressDialogHelper import org.wordpress.android.ui.posts.RemotePreviewLogicHelper +import org.wordpress.android.ui.posts.adapters.AuthorSelectionAdapter import org.wordpress.android.ui.quickstart.QuickStartEvent import org.wordpress.android.ui.uploads.UploadActionUseCase import org.wordpress.android.ui.uploads.UploadUtilsWrapper @@ -94,6 +96,8 @@ class PagesFragment : Fragment() { private var restorePreviousSearch = false + private lateinit var authorSelectionAdapter: AuthorSelectionAdapter + companion object { fun newInstance(): PagesFragment { return PagesFragment() @@ -204,6 +208,16 @@ class PagesFragment : Fragment() { } return@setOnTouchListener false } + + authorSelectionAdapter = AuthorSelectionAdapter(activity) + pages_author_selection.adapter = authorSelectionAdapter + pages_author_selection.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onNothingSelected(parent: AdapterView<*>) {} + + override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { + viewModel.updateAuthorFilterSelection(id) + } + } } private fun initializeSearchView() { @@ -264,6 +278,25 @@ class PagesFragment : Fragment() { savedInstanceState.getSerializable(WordPress.SITE) as SiteModel } + viewModel.authorUIState.observe(activity, Observer { state -> + state?.let { + uiHelpers.updateVisibility(pages_author_selection, state.isAuthorFilterVisible) + uiHelpers.updateVisibility(pages_tab_layout_fading_edge, state.isAuthorFilterVisible) + + val tabLayoutPaddingStart = + if (state.isAuthorFilterVisible) + resources.getDimensionPixelSize(R.dimen.posts_list_tab_layout_fading_edge_width) + else 0 + tabLayout.setPaddingRelative(tabLayoutPaddingStart, 0, 0, 0) + + authorSelectionAdapter.updateItems(state.authorFilterItems) + + authorSelectionAdapter.getIndexOfSelection(state.authorFilterSelection)?.let { selectionIndex -> + pages_author_selection.setSelection(selectionIndex) + } + } + }) + viewModel.start(site) } @@ -428,6 +461,7 @@ class PagesFragment : Fragment() { private fun hideSearchList(myActionMenuItem: MenuItem) { pagesPager.visibility = View.VISIBLE tabLayout.visibility = View.VISIBLE + tabContainer.visibility = View.VISIBLE searchFrame.visibility = View.GONE if (myActionMenuItem.isActionViewExpanded) { myActionMenuItem.collapseActionView() @@ -437,6 +471,7 @@ class PagesFragment : Fragment() { private fun showSearchList(myActionMenuItem: MenuItem) { pagesPager.visibility = View.GONE tabLayout.visibility = View.GONE + tabContainer.visibility = View.GONE searchFrame.visibility = View.VISIBLE if (!myActionMenuItem.isActionViewExpanded) { myActionMenuItem.expandActionView() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.java index f9c379762c2d..ca1c23500d92 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.java @@ -25,7 +25,6 @@ import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import org.wordpress.android.BuildConfig; import org.wordpress.android.R; import org.wordpress.android.WordPress; import org.wordpress.android.analytics.AnalyticsTracker; @@ -45,12 +44,15 @@ import org.wordpress.android.util.WPMediaUtils; import org.wordpress.android.util.WPPermissionUtils; import org.wordpress.android.util.analytics.AnalyticsUtils; +import org.wordpress.android.util.config.TenorFeatureConfig; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.inject.Inject; + public class PhotoPickerFragment extends Fragment { private static final String KEY_LAST_TAPPED_ICON = "last_tapped_icon"; private static final String KEY_SELECTED_POSITIONS = "selected_positions"; @@ -102,6 +104,8 @@ public interface PhotoPickerListener { private SiteModel mSite; private ArrayList mSelectedPositions; + @Inject TenorFeatureConfig mTenorFeatureConfig; + public static PhotoPickerFragment newInstance(@NonNull PhotoPickerListener listener, @NonNull MediaBrowserType browserType, @Nullable SiteModel site) { @@ -120,6 +124,7 @@ public static PhotoPickerFragment newInstance(@NonNull PhotoPickerListener liste @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + ((WordPress) getActivity().getApplication()).component().inject(this); mBrowserType = (MediaBrowserType) getArguments().getSerializable(ARG_BROWSER_TYPE); mSite = (SiteModel) getArguments().getSerializable(WordPress.SITE); @@ -334,7 +339,7 @@ public boolean onMenuItemClick(MenuItem item) { } }); - if (BuildConfig.TENOR_AVAILABLE) { + if (mTenorFeatureConfig.isEnabled()) { MenuItem itemGif = popup.getMenu().add(R.string.photo_picker_gif); itemGif.setOnMenuItemClickListener(item -> { doIconClicked(PhotoPickerIcon.GIF); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanDetailsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanDetailsFragment.kt index 49a87d2e1c7a..45a0cc87b1ae 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanDetailsFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanDetailsFragment.kt @@ -93,7 +93,7 @@ class PlanDetailsFragment : Fragment(), FullScreenDialogContent { return true } - override fun onViewCreated(controller: FullScreenDialogController) { + override fun setController(controller: FullScreenDialogController) { dialogController = controller } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java index 725e74bda2c2..70d6e916a498 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java @@ -63,17 +63,21 @@ import org.wordpress.android.editor.EditorImageSettingsListener; import org.wordpress.android.editor.EditorMediaUploadListener; import org.wordpress.android.editor.EditorMediaUtils; +import org.wordpress.android.editor.EditorThemeUpdateListener; import org.wordpress.android.editor.ExceptionLogger; import org.wordpress.android.editor.GutenbergEditorFragment; import org.wordpress.android.editor.ImageSettingsDialogFragment; import org.wordpress.android.fluxc.Dispatcher; import org.wordpress.android.fluxc.action.AccountAction; import org.wordpress.android.fluxc.generated.AccountActionBuilder; +import org.wordpress.android.fluxc.generated.EditorThemeActionBuilder; import org.wordpress.android.fluxc.generated.PostActionBuilder; import org.wordpress.android.fluxc.generated.SiteActionBuilder; import org.wordpress.android.fluxc.model.AccountModel; import org.wordpress.android.fluxc.model.CauseOfOnPostChanged; import org.wordpress.android.fluxc.model.CauseOfOnPostChanged.RemoteAutoSavePost; +import org.wordpress.android.fluxc.model.EditorTheme; +import org.wordpress.android.fluxc.model.EditorThemeSupport; import org.wordpress.android.fluxc.model.MediaModel; import org.wordpress.android.fluxc.model.MediaModel.MediaUploadState; import org.wordpress.android.fluxc.model.PostImmutableModel; @@ -83,6 +87,9 @@ import org.wordpress.android.fluxc.network.rest.wpcom.site.PrivateAtomicCookie; import org.wordpress.android.fluxc.store.AccountStore; import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged; +import org.wordpress.android.fluxc.store.EditorThemeStore; +import org.wordpress.android.fluxc.store.EditorThemeStore.FetchEditorThemePayload; +import org.wordpress.android.fluxc.store.EditorThemeStore.OnEditorThemeChanged; import org.wordpress.android.fluxc.store.MediaStore; import org.wordpress.android.fluxc.store.MediaStore.MediaError; import org.wordpress.android.fluxc.store.MediaStore.MediaErrorType; @@ -180,6 +187,7 @@ import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper; import org.wordpress.android.util.analytics.AnalyticsUtils; import org.wordpress.android.util.analytics.AnalyticsUtils.BlockEditorEnabledSource; +import org.wordpress.android.util.config.TenorFeatureConfig; import org.wordpress.android.util.helpers.MediaFile; import org.wordpress.android.util.helpers.MediaGallery; import org.wordpress.android.util.image.ImageManager; @@ -322,6 +330,9 @@ enum RestartEditorOptions { // For opening the context menu after permissions have been granted private View mMenuView = null; + private Handler mShowPrepublishingBottomSheetHandler; + private Runnable mShowPrepublishingBottomSheetRunnable; + private boolean mHtmlModeMenuStateOn = false; @Inject Dispatcher mDispatcher; @@ -330,6 +341,7 @@ enum RestartEditorOptions { @Inject PostStore mPostStore; @Inject MediaStore mMediaStore; @Inject UploadStore mUploadStore; + @Inject EditorThemeStore mEditorThemeStore; @Inject FluxCImageLoader mImageLoader; @Inject ShortcutUtils mShortcutUtils; @Inject QuickStartStore mQuickStartStore; @@ -354,6 +366,7 @@ enum RestartEditorOptions { @Inject ReblogUtils mReblogUtils; @Inject AnalyticsTrackerWrapper mAnalyticsTrackerWrapper; @Inject PublishPostImmediatelyUseCase mPublishPostImmediatelyUseCase; + @Inject TenorFeatureConfig mTenorFeatureConfig; private StorePostViewModel mViewModel; @@ -588,6 +601,8 @@ protected void onCreate(Bundle savedInstanceState) { setupViewPager(); } ActivityId.trackLastActivity(ActivityId.POST_EDITOR); + + setupPrepublishingBottomSheetRunnable(); } @SuppressWarnings("unused") @@ -690,7 +705,7 @@ private void startObserving() { return null; })); mEditPostRepository.getPostChanged().observe(this, postEvent -> postEvent.applyIfNotHandled(post -> { - mViewModel.savePostToDb(this, mEditPostRepository, mSite); + mViewModel.savePostToDb(mEditPostRepository, mSite); return null; })); } @@ -772,6 +787,10 @@ protected void onPause() { mAztecImageLoader.clearTargets(); mAztecImageLoader = null; } + + if (mShowPrepublishingBottomSheetHandler != null && mShowPrepublishingBottomSheetRunnable != null) { + mShowPrepublishingBottomSheetHandler.removeCallbacks(mShowPrepublishingBottomSheetRunnable); + } } @Override @@ -982,7 +1001,7 @@ public void onPhotoPickerIconClicked(@NonNull PhotoPickerIcon icon, boolean allo ActivityLauncher.showStockMediaPickerForResult(this, mSite, requestCode); break; case GIF: - ActivityLauncher.showGifPickerForResult(this, mSite, RequestCodes.GIF_PICKER); + ActivityLauncher.showGifPickerForResult(this, mSite, RequestCodes.GIF_PICKER_SINGLE_SELECT); break; } } else { @@ -1068,6 +1087,20 @@ public boolean onPrepareOptionsMenu(Menu menu) { } } + MenuItem contentInfo = menu.findItem(R.id.menu_content_info); + if (mEditorFragment instanceof GutenbergEditorFragment) { + contentInfo.setOnMenuItemClickListener((menuItem) -> { + try { + mEditorFragment.showContentInfo(); + } catch (EditorFragmentNotAddedException e) { + ToastUtils.showToast(WordPress.getContext(), R.string.toast_content_info_failed); + } + return true; + }); + } else { + contentInfo.setVisible(false); // only show the menu item when for Gutenberg + } + return super.onPrepareOptionsMenu(menu); } @@ -1775,17 +1808,24 @@ private void saveResult(boolean saved, boolean uploadNotStarted) { setResult(RESULT_OK, i); } + private void setupPrepublishingBottomSheetRunnable() { + mShowPrepublishingBottomSheetHandler = new Handler(); + mShowPrepublishingBottomSheetRunnable = () -> { + Fragment fragment = getSupportFragmentManager().findFragmentByTag( + PrepublishingBottomSheetFragment.TAG); + if (fragment == null) { + PrepublishingBottomSheetFragment prepublishingFragment = + PrepublishingBottomSheetFragment.newInstance(getSite(), mIsPage); + prepublishingFragment.show(getSupportFragmentManager(), PrepublishingBottomSheetFragment.TAG); + } + }; + } + private void showPrepublishingNudgeBottomSheet() { mViewPager.setCurrentItem(PAGE_CONTENT); ActivityUtils.hideKeyboard(this); - - Fragment fragment = getSupportFragmentManager().findFragmentByTag( - PrepublishingBottomSheetFragment.TAG); - if (fragment == null) { - PrepublishingBottomSheetFragment prepublishingFragment = - PrepublishingBottomSheetFragment.newInstance(getSite(), mIsPage); - prepublishingFragment.show(getSupportFragmentManager(), PrepublishingBottomSheetFragment.TAG); - } + long delayMs = 100; + mShowPrepublishingBottomSheetHandler.postDelayed(mShowPrepublishingBottomSheetRunnable, delayMs); } @Override public void onSubmitButtonClicked(boolean publishPost) { @@ -1967,8 +2007,12 @@ public Fragment getItem(int position) { String languageString = LocaleManager.getLanguage(EditPostActivity.this); String wpcomLocaleSlug = languageString.replace("_", "-").toLowerCase(Locale.ENGLISH); boolean supportsStockPhotos = mSite.isUsingWpComRestApi(); - boolean isWpCom = getSite().isWPCom(); + boolean isWpCom = getSite().isWPCom() || mSite.isPrivateWPComAtomic() || mSite.isWPComAtomic(); boolean isSiteUsingWpComRestApi = mSite.isUsingWpComRestApi(); + + EditorTheme editorTheme = mEditorThemeStore.getEditorThemeForSite(mSite); + Bundle themeBundle = (editorTheme != null) ? editorTheme.getThemeSupport().toBundle() : null; + return GutenbergEditorFragment.newInstance( "", "", @@ -1982,7 +2026,10 @@ public Fragment getItem(int position) { isWpCom ? mAccountStore.getAccount().getUserName() : mSite.getUsername(), isWpCom ? "" : mSite.getPassword(), mAccountStore.getAccessToken(), - isSiteUsingWpComRestApi); + isSiteUsingWpComRestApi, + themeBundle, + WordPress.getUserAgent(), + mTenorFeatureConfig.isEnabled()); } else { // If gutenberg editor is not selected, default to Aztec. return AztecEditorFragment.newInstance("", "", AppPrefs.isAztecEditorToolbarExpanded()); @@ -2322,7 +2369,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { .addExistingMediaToEditorAsync(AddExistingMediaSource.STOCK_PHOTO_LIBRARY, mediaIds); } break; - case RequestCodes.GIF_PICKER: + case RequestCodes.GIF_PICKER_SINGLE_SELECT: if (data.hasExtra(GifPickerActivity.KEY_SAVED_MEDIA_MODEL_LOCAL_IDS)) { int[] localIds = data.getIntArrayExtra(GifPickerActivity.KEY_SAVED_MEDIA_MODEL_LOCAL_IDS); mEditorMedia.addGifMediaToPostAsync(localIds); @@ -2579,6 +2626,11 @@ public void onAddStockMediaClicked(boolean allowMultipleSelection) { onPhotoPickerIconClicked(PhotoPickerIcon.STOCK_MEDIA, allowMultipleSelection); } + @Override + public void onAddGifClicked(boolean allowMultipleSelection) { + onPhotoPickerIconClicked(PhotoPickerIcon.GIF, allowMultipleSelection); + } + @Override public void onPerformFetch(String path, Consumer onResult, Consumer onError) { if (mSite != null) { @@ -2784,6 +2836,7 @@ private void onEditorFinalTouchesBeforeShowing() { refreshEditorContent(); // probably here is best for Gutenberg to start interacting with if (mShowGutenbergEditor && mEditorFragment instanceof GutenbergEditorFragment) { + refreshEditorTheme(); List failedMedia = mMediaStore.getMediaForPostWithState(mEditPostRepository.getPost(), MediaUploadState.FAILED); if (failedMedia != null && !failedMedia.isEmpty()) { @@ -3005,6 +3058,24 @@ public void onEventMainThread(UploadService.UploadMediaRetryEvent event) { } } + private void refreshEditorTheme() { + FetchEditorThemePayload payload = new FetchEditorThemePayload(mSite); + mDispatcher.dispatch(EditorThemeActionBuilder.newFetchEditorThemeAction(payload)); + } + + @SuppressWarnings("unused") + @Subscribe(threadMode = ThreadMode.MAIN_ORDERED) + public void onEditorThemeChanged(OnEditorThemeChanged event) { + if (!(mEditorFragment instanceof EditorThemeUpdateListener)) return; + + if (mSite.getId() != event.getSiteId()) return; + EditorTheme editorTheme = event.getEditorTheme(); + + if (editorTheme == null) return; + EditorThemeSupport editorThemeSupport = editorTheme.getThemeSupport(); + ((EditorThemeUpdateListener) mEditorFragment) + .onEditorThemeUpdated(editorThemeSupport.toBundle()); + } // EditPostActivityHook methods @Override diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt index 6c631d518810..24280fc38c03 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt @@ -1,11 +1,13 @@ package org.wordpress.android.ui.posts +import android.content.Context import android.content.Intent import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer import androidx.lifecycle.ViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -78,6 +80,7 @@ class PostListMainViewModel @Inject constructor( private val postListEventListenerFactory: PostListEventListener.Factory, private val previewStateHelper: PreviewStateHelper, private val analyticsTracker: AnalyticsTrackerWrapper, + private val savePostToDbUseCase: SavePostToDbUseCase, @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, private val uploadStarter: UploadStarter @@ -211,7 +214,8 @@ class PostListMainViewModel @Inject constructor( site: SiteModel, initPreviewState: PostListRemotePreviewState, currentBottomSheetPostId: LocalId, - editPostRepository: EditPostRepository + editPostRepository: EditPostRepository, + context: Context ) { this.site = site this.editPostRepository = editPostRepository @@ -273,6 +277,12 @@ class PostListMainViewModel @Inject constructor( lifecycleRegistry.markState(Lifecycle.State.STARTED) uploadStarter.queueUploadFromSite(site) + + editPostRepository.run { + postChanged.observe(this@PostListMainViewModel, Observer { + savePostToDbUseCase.savePostToDb(editPostRepository, site) + }) + } } override fun onCleared() { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewState.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewState.kt index caaa5d23a9c4..90054b25fb43 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewState.kt @@ -34,14 +34,14 @@ sealed class AuthorFilterListItemUIState( data class Everyone(override val isSelected: Boolean, @DrawableRes val imageRes: Int) : AuthorFilterListItemUIState( id = EVERYONE.id, - text = UiStringRes(R.string.post_list_author_everyone), + text = UiStringRes(R.string.everyone), isSelected = isSelected ) data class Me(val avatarUrl: String?, override val isSelected: Boolean) : AuthorFilterListItemUIState( id = ME.id, - text = UiStringRes(R.string.post_list_author_me), + text = UiStringRes(R.string.me), isSelected = isSelected ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt index 9446391fd1b4..1cc5493e7dfd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt @@ -247,7 +247,7 @@ class PostsListActivity : LocaleAwareActivity(), private fun initViewModel(initPreviewState: PostListRemotePreviewState, currentBottomSheetPostId: LocalId) { viewModel = ViewModelProviders.of(this, viewModelFactory).get(PostListMainViewModel::class.java) - viewModel.start(site, initPreviewState, currentBottomSheetPostId, editPostRepository) + viewModel.start(site, initPreviewState, currentBottomSheetPostId, editPostRepository, this) viewModel.viewState.observe(this, Observer { state -> state?.let { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/SavePostToDbUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/SavePostToDbUseCase.kt index dcd37c9955a6..eac150ca7c9e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/SavePostToDbUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/SavePostToDbUseCase.kt @@ -15,10 +15,10 @@ class SavePostToDbUseCase private val uploadUtils: UploadUtilsWrapper, private val dateTimeUtils: DateTimeUtilsWrapper, private val dispatcher: Dispatcher, - private val pendingDraftsNotificationsUtils: PendingDraftsNotificationsUtilsWrapper + private val pendingDraftsNotificationsUtils: PendingDraftsNotificationsUtilsWrapper, + private val context: Context ) { fun savePostToDb( - context: Context, postRepository: EditPostRepository, site: SiteModel ) { @@ -43,14 +43,13 @@ class SavePostToDbUseCase post.setIsLocallyChanged(true) } post.setDateLocallyChanged(dateTimeUtils.currentTimeInIso8601()) - handlePendingDraftNotifications(context, postRepository) + handlePendingDraftNotifications(postRepository) postRepository.savePostSnapshot() dispatcher.dispatch(PostActionBuilder.newUpdatePostAction(post)) } } private fun handlePendingDraftNotifications( - context: Context, editPostRepository: EditPostRepository ) { if (editPostRepository.status == PostStatus.DRAFT) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/adapters/AuthorSelectionAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/adapters/AuthorSelectionAdapter.kt index cf4f5a383eed..1118d2dac97f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/adapters/AuthorSelectionAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/adapters/AuthorSelectionAdapter.kt @@ -37,7 +37,7 @@ class AuthorSelectionAdapter(context: Context) : BaseAdapter() { if (view == null) { val inflater = LayoutInflater.from(parent.context) - view = inflater.inflate(R.layout.post_list_author_selection_dropdown, parent, false) + view = inflater.inflate(R.layout.author_selection_dropdown, parent, false) holder = DropdownViewHolder(view) view.tag = holder } else { @@ -67,7 +67,7 @@ class AuthorSelectionAdapter(context: Context) : BaseAdapter() { if (view == null) { val inflater = LayoutInflater.from(parent.context) - view = inflater.inflate(R.layout.post_list_author_selection, parent, false) + view = inflater.inflate(R.layout.author_selection, parent, false) holder = NormalViewHolder(view) view.tag = holder } else { @@ -92,7 +92,7 @@ class AuthorSelectionAdapter(context: Context) : BaseAdapter() { } private open class NormalViewHolder(protected val itemView: View) { - protected val image: AppCompatImageView = itemView.findViewById(R.id.post_list_author_selection_image) + protected val image: AppCompatImageView = itemView.findViewById(R.id.author_selection_image) @CallSuper open fun bind( @@ -126,7 +126,7 @@ class AuthorSelectionAdapter(context: Context) : BaseAdapter() { } private class DropdownViewHolder(itemView: View) : NormalViewHolder(itemView) { - private val text: AppCompatTextView = itemView.findViewById(R.id.post_list_author_selection_text) + private val text: AppCompatTextView = itemView.findViewById(R.id.author_selection_text) override fun bind( state: AuthorFilterListItemUIState, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/StorePostViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/StorePostViewModel.kt index 0ff0d22af4df..b9cac4e7af37 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/StorePostViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/StorePostViewModel.kt @@ -53,7 +53,7 @@ class StorePostViewModel editPostRepository: EditPostRepository, site: SiteModel ): ActivityFinishState { - savePostToDbUseCase.savePostToDb(context, editPostRepository, site) + savePostToDbUseCase.savePostToDb(editPostRepository, site) return if (networkUtils.isNetworkAvailable()) { postUtils.trackSavePostAnalytics( editPostRepository.getPost(), @@ -79,11 +79,10 @@ class StorePostViewModel } fun savePostToDb( - context: Context, postRepository: EditPostRepository, site: SiteModel ) { - savePostToDbUseCase.savePostToDb(context, postRepository, site) + savePostToDbUseCase.savePostToDb(postRepository, site) } fun updatePostObjectWithUIAsync( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/CoverBlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/CoverBlockProcessor.java index cdb63163ad13..6897fd910d60 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/CoverBlockProcessor.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/CoverBlockProcessor.java @@ -11,6 +11,8 @@ import java.util.regex.Pattern; public class CoverBlockProcessor extends BlockProcessor { + private boolean mHasVideoBackground = false; + /** * Template pattern used to match and splice cover inner blocks */ @@ -55,6 +57,10 @@ public CoverBlockProcessor(String localId, MediaFile mediaFile, if (id != null && id.getAsInt() == Integer.parseInt(mLocalId, 10)) { jsonAttributes.addProperty("id", Integer.parseInt(mRemoteId, 10)); jsonAttributes.addProperty("url", mRemoteUrl); + + // check if background type is video + JsonElement backgroundType = jsonAttributes.get("backgroundType"); + mHasVideoBackground = backgroundType != null && "video".equals(backgroundType.getAsString()); return true; } @@ -63,14 +69,23 @@ public CoverBlockProcessor(String localId, MediaFile mediaFile, @Override boolean processBlockContentDocument(Document document) { // select cover block div - Element targetDiv = document.select(".wp-block-cover").first(); + Element targetDiv = document.selectFirst(".wp-block-cover"); // if a match is found, proceed with replacement if (targetDiv != null) { - // replace background-image url in style attribute - String style = PATTERN_BACKGROUND_IMAGE_URL.matcher(targetDiv.attr("style")) - .replaceFirst(String.format("background-image:url(%1$s)", mRemoteUrl)); - targetDiv.attr("style", style); + if (mHasVideoBackground) { + Element videoElement = targetDiv.selectFirst("video"); + if (videoElement != null) { + videoElement.attr("src", mRemoteUrl); + } else { + return false; + } + } else { + // replace background-image url in style attribute + String style = PATTERN_BACKGROUND_IMAGE_URL.matcher(targetDiv.attr("style")).replaceFirst( + String.format("background-image:url(%1$s)", mRemoteUrl)); + targetDiv.attr("style", style); + } // return injected block return true; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java index bfe7ae8ffdce..7e13ce37a115 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java @@ -140,6 +140,9 @@ public enum DeletablePrefKey implements PrefKey { // Keep the local_blog_id + local_post_id values that have HW Acc. turned off AZTEC_EDITOR_DISABLE_HW_ACC_KEYS, + + // timestamp of the last update of the reader css styles + READER_CSS_UPDATED_TIMESTAMP } /** @@ -226,7 +229,7 @@ public enum UndeletablePrefKey implements PrefKey { LAST_FEATURE_ANNOUNCEMENT_APP_VERSION_CODE, // feature flag for Reader Improvements Phase 2 - FF_READER_IMPROVEMENTS_PHASE_2 + FF_READER_IMPROVEMENTS_PHASE_2, } private static SharedPreferences prefs() { @@ -1136,6 +1139,14 @@ public static void setReaderTagsUpdatedTimestamp(long timestamp) { setLong(DeletablePrefKey.READER_TAGS_UPDATE_TIMESTAMP, timestamp); } + public static long getReaderCssUpdatedTimestamp() { + return getLong(DeletablePrefKey.READER_CSS_UPDATED_TIMESTAMP, 0); + } + + public static void setReaderCssUpdatedTimestamp(long timestamp) { + setLong(DeletablePrefKey.READER_CSS_UPDATED_TIMESTAMP, timestamp); + } + /* * adds a local site ID to the top of list of recently chosen sites */ diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt index 70b618409f7a..5ed67d5e108f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt @@ -67,6 +67,10 @@ class AppPrefsWrapper @Inject constructor() { get() = AppPrefs.getReaderTagsUpdatedTimestamp() set(timestamp) = AppPrefs.setReaderTagsUpdatedTimestamp(timestamp) + var readerCssUpdatedTimestamp: Long + get() = AppPrefs.getReaderCssUpdatedTimestamp() + set(timestamp) = AppPrefs.setReaderCssUpdatedTimestamp(timestamp) + fun getAppWidgetSiteId(appWidgetId: Int) = AppPrefs.getStatsWidgetSelectedSiteId(appWidgetId) fun setAppWidgetSiteId(siteId: Long, appWidgetId: Int) = AppPrefs.setStatsWidgetSelectedSiteId(siteId, appWidgetId) fun removeAppWidgetSiteId(appWidgetId: Int) = AppPrefs.removeStatsWidgetSelectedSiteId(appWidgetId) @@ -142,6 +146,8 @@ class AppPrefsWrapper @Inject constructor() { fun setReaderTag(selectedTag: ReaderTag?) = AppPrefs.setReaderTag(selectedTag) fun getReaderTag(): ReaderTag? = AppPrefs.getReaderTag() + fun isReaderImprovementsPhase2Enabled(): Boolean = AppPrefs.isReaderImprovementsPhase2Enabled() + companion object { private const val LIGHT_MODE_ID = 0 private const val DARK_MODE_ID = 1 diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsFragment.java index 39182bd4dea8..91f2dee1c74e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsFragment.java @@ -28,16 +28,21 @@ import org.wordpress.android.fluxc.Dispatcher; import org.wordpress.android.fluxc.action.AccountAction; import org.wordpress.android.fluxc.generated.AccountActionBuilder; +import org.wordpress.android.fluxc.generated.WhatsNewActionBuilder; +import org.wordpress.android.fluxc.model.whatsnew.WhatsNewAnnouncementModel; import org.wordpress.android.fluxc.store.AccountStore; import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged; import org.wordpress.android.fluxc.store.SiteStore; +import org.wordpress.android.fluxc.store.WhatsNewStore.OnWhatsNewFetched; +import org.wordpress.android.fluxc.store.WhatsNewStore.WhatsNewAppId; +import org.wordpress.android.fluxc.store.WhatsNewStore.WhatsNewFetchPayload; import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic; import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarter; -import org.wordpress.android.ui.whatsnew.FeatureAnnouncement; import org.wordpress.android.ui.whatsnew.FeatureAnnouncementDialogFragment; import org.wordpress.android.ui.whatsnew.FeatureAnnouncementProvider; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppThemeUtils; +import org.wordpress.android.util.BuildConfigWrapper; import org.wordpress.android.util.LocaleManager; import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.ToastUtils; @@ -77,11 +82,13 @@ public class AppSettingsFragment extends PreferenceFragment @Inject Dispatcher mDispatcher; @Inject ContextProvider mContextProvider; @Inject FeatureAnnouncementProvider mFeatureAnnouncementProvider; + @Inject BuildConfigWrapper mBuildConfigWrapper; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ((WordPress) getActivity().getApplication()).component().inject(this); + mDispatcher.register(this); setRetainInstance(true); @@ -168,13 +175,8 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { mWhatsNew = findPreference(getString(R.string.pref_key_whats_new)); - FeatureAnnouncement featureAnnouncement = mFeatureAnnouncementProvider.getLatestFeatureAnnouncement(); - if (featureAnnouncement != null) { - mWhatsNew.setSummary(getString(R.string.version_with_name_param, featureAnnouncement.getAppVersionName())); - mWhatsNew.setOnPreferenceClickListener(this); - } else { - removeWhatsNewPreference(); - } + removeWhatsNewPreference(); + mDispatcher.dispatch(WhatsNewActionBuilder.newFetchCachedAnnouncementAction()); if (!BuildConfig.OFFER_GUTENBERG) { removeExperimentalCategory(); @@ -196,6 +198,12 @@ private void removeWhatsNewPreference() { aboutTheAppPreferenceCategory.removePreference(mWhatsNew); } + private void addWhatsNewPreference() { + PreferenceCategory aboutTheAppPreferenceCategory = + (PreferenceCategory) findPreference(getString(R.string.pref_key_about_section)); + aboutTheAppPreferenceCategory.addPreference(mWhatsNew); + } + @Override public void onResume() { super.onResume(); @@ -207,7 +215,6 @@ public void onResume() { @Override public void onStart() { super.onStart(); - mDispatcher.register(this); } @Override @@ -225,6 +232,26 @@ public void onActivityCreated(Bundle savedInstanceState) { AnalyticsTracker.flush(); } + @SuppressWarnings("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + public void onWhatsNewFetched(OnWhatsNewFetched event) { + if (event.isFromCache()) { + mDispatcher.dispatch(WhatsNewActionBuilder + .newFetchRemoteAnnouncementAction( + new WhatsNewFetchPayload(mBuildConfigWrapper.getAppVersionName(), + WhatsNewAppId.WP_ANDROID))); + } + + if (event.error != null || event.getWhatsNewItems() == null || event.getWhatsNewItems().isEmpty()) { + return; + } + + WhatsNewAnnouncementModel latestAnnouncement = event.getWhatsNewItems().get(0); + mWhatsNew.setSummary(getString(R.string.version_with_name_param, latestAnnouncement.getAppVersionName())); + mWhatsNew.setOnPreferenceClickListener(this); + addWhatsNewPreference(); + } + @SuppressWarnings("unused") @Subscribe(threadMode = ThreadMode.MAIN) public void onAccountChanged(OnAccountChanged event) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/quickstart/QuickStartFullScreenDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/quickstart/QuickStartFullScreenDialogFragment.java index 7cbba24c12ed..084ca5421d93 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/quickstart/QuickStartFullScreenDialogFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/quickstart/QuickStartFullScreenDialogFragment.java @@ -122,7 +122,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c } @Override - public void onViewCreated(final FullScreenDialogController controller) { + public void setController(final FullScreenDialogController controller) { mDialogController = controller; } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java index b4e47e2c9e58..ee618765ffac 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java @@ -26,6 +26,8 @@ public class ReaderConstants { // the Calypso web reader public static final int MIN_GALLERY_IMAGE_WIDTH = 144; + public static final int THUMBNAIL_STRIP_IMG_COUNT = 4; + // referrer url for reader posts opened in a browser public static final String HTTP_REFERER_URL = "https://wordpress.com"; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCssProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCssProvider.kt new file mode 100644 index 000000000000..a2d4dee19193 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCssProvider.kt @@ -0,0 +1,33 @@ +package org.wordpress.android.ui.reader + +import org.wordpress.android.ui.prefs.AppPrefsWrapper +import org.wordpress.android.ui.reader.utils.DateProvider +import org.wordpress.android.util.NetworkUtilsWrapper +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +const val EXPIRATION_IN_DAYS = 5L +private const val BASE_CSS_URL = "https://wordpress.com/calypso/reader-mobile.css" + +class ReaderCssProvider @Inject constructor( + private val networkUtilsWrapper: NetworkUtilsWrapper, + private val appPrefsWrapper: AppPrefsWrapper, + private val dateProvider: DateProvider +) { + fun getCssUrl(): String { + val lastUpdated = appPrefsWrapper.readerCssUpdatedTimestamp + val currentDate = dateProvider.getCurrentDate().time + + val urlSuffix = if (networkUtilsWrapper.isNetworkAvailable() && isExpired(lastUpdated, currentDate)) { + appPrefsWrapper.readerCssUpdatedTimestamp = currentDate + currentDate + } else { + lastUpdated + } + return "$BASE_CSS_URL?$urlSuffix" + } + + private fun isExpired(lastUpdated: Long, currentDate: Long): Boolean { + return lastUpdated < currentDate - TimeUnit.DAYS.toMillis(EXPIRATION_IN_DAYS) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt index e981eabdcfdb..d1e3822ad156 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt @@ -20,18 +20,24 @@ import org.wordpress.android.R.string import org.wordpress.android.WordPress import org.wordpress.android.models.ReaderTagList import org.wordpress.android.ui.WPWebViewActivity +import org.wordpress.android.ui.prefs.AppPrefs import org.wordpress.android.ui.reader.ReaderTypes.ReaderPostListType +import org.wordpress.android.ui.reader.discover.ReaderDiscoverFragment +import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsFragment import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic.UpdateTask.FOLLOWED_BLOGS import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic.UpdateTask.TAGS import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarter import org.wordpress.android.ui.reader.viewmodels.NewsCardViewModel import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel -import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState +import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState.ContentUiState +import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState.InitialUiState +import org.wordpress.android.ui.utils.UiHelpers import java.util.EnumSet import javax.inject.Inject class ReaderFragment : Fragment(R.layout.reader_fragment_layout) { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory + @Inject lateinit var uiHelpers: UiHelpers private lateinit var viewModel: ReaderViewModel private lateinit var newsCardViewModel: NewsCardViewModel @@ -41,7 +47,8 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout) { override fun onPageSelected(position: Int) { super.onPageSelected(position) viewModel.uiState.value?.let { - val selectedTag = it.readerTagList[position] + val currentUiState = it as ContentUiState + val selectedTag = currentUiState.readerTagList[position] newsCardViewModel.onTagChanged(selectedTag) viewModel.onTagChanged(selectedTag) } @@ -111,7 +118,15 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout) { private fun startObserving() { viewModel.uiState.observe(viewLifecycleOwner, Observer { uiState -> uiState?.let { - updateTabs(uiState) + when (it) { + is InitialUiState -> { + } + is ContentUiState -> { + updateTabs(it) + } + } + app_bar.setExpanded(uiState.appBarExpanded) + uiHelpers.updateVisibility(tab_layout, uiState.tabLayoutVisible) searchMenuItem?.isVisible = uiState.searchIconVisible } }) @@ -124,7 +139,7 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout) { viewModel.selectTab.observe(viewLifecycleOwner, Observer { selectTabAction -> selectTabAction.getContentIfNotHandled()?.let { tabPosition -> - view_pager.currentItem = tabPosition + view_pager.setCurrentItem(tabPosition, false) } }) @@ -134,6 +149,18 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout) { } }) + viewModel.showReaderInterests.observe(viewLifecycleOwner, Observer { event -> + event?.getContentIfNotHandled()?.let { + showReaderInterests() + } + }) + + viewModel.closeReaderInterests.observe(viewLifecycleOwner, Observer { event -> + event?.getContentIfNotHandled()?.let { + closeReaderInterests() + } + }) + newsCardViewModel.openUrlEvent.observe(viewLifecycleOwner, Observer { it?.getContentIfNotHandled()?.let { url -> val activity: Activity? = activity @@ -146,7 +173,7 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout) { viewModel.start() } - private fun updateTabs(uiState: ReaderUiState) { + private fun updateTabs(uiState: ContentUiState) { val adapter = TabsAdapter(this, uiState.readerTagList) view_pager.adapter = adapter @@ -159,7 +186,33 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout) { override fun getItemCount(): Int = tags.size override fun createFragment(position: Int): Fragment { - return ReaderPostListFragment.newInstanceForTag(tags[position], ReaderPostListType.TAG_FOLLOWED, true) + return if (AppPrefs.isReaderImprovementsPhase2Enabled() && tags[position].isDiscover) { + ReaderDiscoverFragment() + } else { + ReaderPostListFragment.newInstanceForTag(tags[position], ReaderPostListType.TAG_FOLLOWED, true) + } + } + } + + private fun showReaderInterests() { + val readerInterestsFragment = childFragmentManager.findFragmentByTag(ReaderInterestsFragment.TAG) + if (readerInterestsFragment == null) { + childFragmentManager.beginTransaction() + .replace( + R.id.interests_fragment_container, + ReaderInterestsFragment(), + ReaderInterestsFragment.TAG + ) + .commitNow() + } + } + + private fun closeReaderInterests() { + val readerInterestsFragment = childFragmentManager.findFragmentByTag(ReaderInterestsFragment.TAG) + if (readerInterestsFragment?.isAdded == true) { + childFragmentManager.beginTransaction() + .remove(readerInterestsFragment) + .commitNow() } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt index f49d2548e227..6b7437942fc4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt @@ -183,6 +183,7 @@ class ReaderPostDetailFragment : Fragment(), @Inject internal lateinit var readerFileDownloadManager: ReaderFileDownloadManager @Inject internal lateinit var featuredImageUtils: FeaturedImageUtils @Inject internal lateinit var privateAtomicCookie: PrivateAtomicCookie + @Inject internal lateinit var readerCssProvider: ReaderCssProvider private val mSignInClickListener = View.OnClickListener { EventBus.getDefault() @@ -1351,7 +1352,7 @@ class ReaderPostDetailFragment : Fragment(), scrollView.visibility = View.VISIBLE // render the post in the webView - renderer = ReaderPostRenderer(readerWebView, post, featuredImageUtils) + renderer = ReaderPostRenderer(readerWebView, post, featuredImageUtils, readerCssProvider) // if the post is from private atomic site postpone render until we have a special access cookie if (post!!.isPrivateAtomic && privateAtomicCookie.isCookieRefreshRequired()) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java index f8ed34053bb1..6d8626847fbf 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java @@ -751,6 +751,7 @@ public void onStart() { @Override public void onStop() { super.onStop(); + mNewPostsBar.clearAnimation(); mDispatcher.unregister(this); EventBus.getDefault().unregister(this); } @@ -1824,20 +1825,21 @@ private boolean isEmptyViewShowing() { } private void setCurrentTagFromEmptyViewButton(ActionableEmptyViewButtonType button) { - ReaderTag tag; + ReaderTag tag = null; - switch (button) { - case DISCOVER: - tag = ReaderUtils.getTagFromEndpoint(ReaderTag.DISCOVER_PATH); - break; - case FOLLOWED: - tag = ReaderUtils.getTagFromEndpoint(ReaderTag.FOLLOWING_PATH); - break; - default: + switch (button) { + case DISCOVER: + tag = ReaderUtils.getTagFromEndpoint(ReaderTag.DISCOVER_PATH); + break; + case FOLLOWED: + tag = ReaderUtils.getTagFromEndpoint(ReaderTag.FOLLOWING_PATH); + break; + } + if (tag == null) { tag = ReaderUtils.getDefaultTag(); - } + } - mViewModel.onEmptyStateButtonTapped(tag); + mViewModel.onEmptyStateButtonTapped(tag); } /* diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java index e65457d6af97..ca46eecda9a3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java @@ -50,9 +50,11 @@ public class ReaderPostRenderer { private String mRenderedHtml; private ImageSizeMap mAttachmentSizes; private FeaturedImageUtils mFeaturedImageUtils; + private ReaderCssProvider mCssProvider; @SuppressLint("SetJavaScriptEnabled") - public ReaderPostRenderer(ReaderWebView webView, ReaderPost post, FeaturedImageUtils featuredImageUtils) { + public ReaderPostRenderer(ReaderWebView webView, ReaderPost post, FeaturedImageUtils featuredImageUtils, + ReaderCssProvider cssProvider) { if (webView == null) { throw new IllegalArgumentException("ReaderPostRenderer requires a webView"); } @@ -64,6 +66,7 @@ public ReaderPostRenderer(ReaderWebView webView, ReaderPost post, FeaturedImageU mWeakWebView = new WeakReference<>(webView); mResourceVars = new ReaderResourceVars(webView.getContext()); mFeaturedImageUtils = featuredImageUtils; + mCssProvider = cssProvider; mMinFullSizeWidthDp = pxToDp(mResourceVars.mFullSizeImageWidthPx / 3); mMinMidSizeWidthDp = mMinFullSizeWidthDp / 2; @@ -361,41 +364,48 @@ private String formatPostContentForWebView(final String content, final SetReader Post") - // https://developers.google.com/chrome/mobile/docs/webview/pixelperfect - .append("") - .append(" diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 39edf29b6594..4c785a556a48 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -117,6 +117,9 @@ %1$f, %2$f \@%s + Me + Everyone + %d selected @@ -1415,6 +1418,7 @@ Debug Menu Switch to classic editor Switch to block editor + Content structure Title @@ -1576,6 +1580,7 @@ There was an error uploading the media in this page: %s. Read permission denied on device media Media could not be found + Media thumbnail could not be loaded You don\'t have permission to view or edit media Unexpected response from server An error occurred while uploading media @@ -2304,6 +2309,7 @@ Use this photo Use this video Use this media + Use this GIF Image Thumbnail Image selected ,Selected @@ -2671,8 +2677,10 @@ You don\'t have any trashed pages The selected page is not available Cancel upload + Page author We cannot open pages at the moment. Please try again later %1$s · %2$s + %1$s · %2$s · %3$s @string/post_status_pending_review @string/post_status_post_private @@ -2715,8 +2723,6 @@ Post author - Me - Everyone Published Drafts Scheduled @@ -2868,6 +2874,7 @@ Current value is %s CUSTOMIZE + Customize Gradient Dismiss Don’t cry because it’s over, smile because it happened. @@ -2890,6 +2897,8 @@ Double tap to select a video Double tap to select an image Double tap to select layout + + Double tap to select the option Double tap to toggle setting Double tap to undo last change @@ -2910,6 +2919,8 @@ translators: sample content for "Portfolio" page template translators: sample content for "Services" page template translators: sample content for "Team" page template --> Get in Touch + Go back + Gradient Type Help icon Here is the panel content! Hide keyboard @@ -2959,10 +2970,14 @@ translators: sample content for "Services" page template --> My post status info My pre publish panel Navigate Up + Navigates to custom color picker + Navigates to customize the gradient + Navigates to the previous content sheet No application can handle this request. Please install a Web browser. Number of columns Only show excerpt Open Block Actions Menu + Open link in a browser Open Settings Page break block. %s diff --git a/WordPress/src/main/res/values/styles.xml b/WordPress/src/main/res/values/styles.xml index d2abc9a9a315..3cc7a7a3a524 100644 --- a/WordPress/src/main/res/values/styles.xml +++ b/WordPress/src/main/res/values/styles.xml @@ -27,6 +27,7 @@ @style/WordPress.ToolBar @style/WordPress.TabLayout @style/WordPress.AppBarLayout + @style/Widget.MaterialComponents.Snackbar @style/Widget.MaterialComponents.CardView @@ -1124,7 +1125,7 @@ false @style/FullScreenDialogFragmentAnimation - +