From 73348a31b74a8d8e694dede30c4c8c66c0b44f6d Mon Sep 17 00:00:00 2001 From: winrid Date: Tue, 30 Jun 2026 16:57:01 -0700 Subject: [PATCH 1/2] Add CI (lint + compile) and Spotless formatting - GitHub Actions workflow (.github/workflows/ci.yml) running on PRs and pushes to main: Spotless formatting check, Android lint, and a debug compile (assembleDebug) for both modules. JDK 17 for AGP 8.9. The com.fastcomments dependencies are public-read on Repsy, so no secrets are required to build. - Spotless (com.diffplug.spotless) wired in the root build: - Java: palantir-java-format (4-space, IntelliJ-like) + unused-import removal, trailing-whitespace trim, final newline. - Kotlin: ktlint, configured via .editorconfig to the intellij_idea code style (with Compose-friendly naming rules). Run `./gradlew spotlessApply` to format, `spotlessCheck` to verify. - Android lint baselines (app + sdk) so CI fails only on newly introduced issues rather than the ~2300 pre-existing ones (mostly the SDK's resourcePrefix naming). --- .editorconfig | 21 + .github/workflows/ci.yml | 47 + app/build.gradle.kts | 9 +- app/lint-baseline.xml | 652 + build.gradle.kts | 33 +- gradle/libs.versions.toml | 4 +- libraries/sdk/build.gradle.kts | 11 +- libraries/sdk/lint-baseline.xml | 26412 ++++++++++++++++++++++++++++++ 8 files changed, 27182 insertions(+), 7 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/workflows/ci.yml create mode 100644 app/lint-baseline.xml create mode 100644 libraries/sdk/lint-baseline.xml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c3fbff2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{kt,kts}] +# Match IntelliJ IDEA's default Kotlin formatting rather than ktlint's stricter "official" style. +ktlint_code_style = intellij_idea +# Compose @Composable functions use PascalCase and components live in multi-declaration files, +# which the default naming/filename rules would otherwise flag. +ktlint_function_naming_ignore_when_annotated_with = Composable +ktlint_standard_function-naming = disabled +ktlint_standard_filename = disabled + +[*.{yml,yaml,json}] +indent_size = 2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cd329ce --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Lint & compile + runs-on: ubuntu-latest + env: + # The com.fastcomments artifacts are public-read on Repsy, so these are optional; + # they are forwarded if the secrets happen to be configured. + REPSY_USERNAME: ${{ secrets.REPSY_USERNAME }} + REPSY_PASSWORD: ${{ secrets.REPSY_PASSWORD }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + + - name: Spotless (formatting check) + run: ./gradlew spotlessCheck + + - name: Android Lint + run: ./gradlew lintDebug + + - name: Compile (assemble debug) + run: ./gradlew assembleDebug diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 90690a4..90f9ae7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,12 +18,17 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + lint { + // Snapshot of pre-existing issues so CI only fails on newly introduced ones. + baseline = file("lint-baseline.xml") + } + buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } @@ -79,4 +84,4 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) -} \ No newline at end of file +} diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml new file mode 100644 index 0000000..2bcc05a --- /dev/null +++ b/app/lint-baseline.xml @@ -0,0 +1,652 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build.gradle.kts b/build.gradle.kts index 48367a4..f1ce22f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,4 +2,35 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false -} \ No newline at end of file + alias(libs.plugins.spotless) +} + +spotless { + // Java sources: Palantir formatter (4-space indentation, conventional wrapping) - the + // closest mainstream auto-formatter to IntelliJ IDEA's Java defaults. + java { + target("app/src/**/*.java", "libraries/sdk/src/**/*.java") + targetExclude("**/build/**") + palantirJavaFormat() + removeUnusedImports() + trimTrailingWhitespace() + endWithNewline() + } + // Kotlin sources: ktlint configured (via .editorconfig) to the intellij_idea code style. + kotlin { + target("app/src/**/*.kt", "libraries/sdk/src/**/*.kt") + targetExclude("**/build/**") + ktlint() + trimTrailingWhitespace() + endWithNewline() + } + kotlinGradle { + target( + "build.gradle.kts", + "settings.gradle.kts", + "app/build.gradle.kts", + "libraries/sdk/build.gradle.kts", + ) + ktlint() + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0290c83..a8b68bd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ material = "1.12.0" fastcomments = "3.0.0" constraintlayout = "2.1.4" swiperefreshlayout = "1.1.0" +spotless = "8.8.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -41,4 +42,5 @@ fastcommentsPubsub = { group = "com.fastcomments", name = "pubsub", version.ref android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } \ No newline at end of file +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } \ No newline at end of file diff --git a/libraries/sdk/build.gradle.kts b/libraries/sdk/build.gradle.kts index 4ad9df6..8407b8e 100644 --- a/libraries/sdk/build.gradle.kts +++ b/libraries/sdk/build.gradle.kts @@ -23,12 +23,17 @@ android { } } + lint { + // Snapshot of pre-existing issues so CI only fails on newly introduced ones. + baseline = file("lint-baseline.xml") + } + buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } @@ -117,9 +122,9 @@ publishing { maven { name = "repsy" val releasesRepoUrl = "https://repo.repsy.io/mvn/winrid/fastcomments" - val snapshotsRepoUrl = "https://repo.repsy.io/mvn/winrid/fastcomments" + val snapshotsRepoUrl = "https://repo.repsy.io/mvn/winrid/fastcomments" url = uri(if (releaseVersion.endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) - + credentials { username = findProperty("repsyUsername") as String? ?: System.getenv("REPSY_USERNAME") password = findProperty("repsyPassword") as String? ?: System.getenv("REPSY_PASSWORD") diff --git a/libraries/sdk/lint-baseline.xml b/libraries/sdk/lint-baseline.xml new file mode 100644 index 0000000..c6ba3fc --- /dev/null +++ b/libraries/sdk/lint-baseline.xml @@ -0,0 +1,26412 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 8687bf811abd97d00a851131d3b7f693f33a4c2b Mon Sep 17 00:00:00 2001 From: winrid Date: Tue, 30 Jun 2026 16:57:07 -0700 Subject: [PATCH 2/2] Apply one-time Spotless reformat (palantir Java + ktlint Kotlin) Mechanical, no behavior changes: 4-space indentation, conventional wrapping, normalized imports, trailing-whitespace and final-newline fixes across all Java and Kotlin sources. Verified with assembleDebug and the non-integration unit tests. --- .../fastcomments/CommentActionsUITests.java | 29 +- .../com/fastcomments/CommentCRUDUITests.java | 30 +- .../com/fastcomments/FeedUserA_UITests.java | 18 +- .../com/fastcomments/FeedUserB_UITests.java | 15 +- .../LiveChatPaginationUITests.java | 74 +- .../fastcomments/LiveChatUserA_UITests.java | 24 +- .../fastcomments/LiveChatUserB_UITests.java | 21 +- .../fastcomments/LiveEventUserA_UITests.java | 127 ++- .../fastcomments/LiveEventUserB_UITests.java | 70 +- .../java/com/fastcomments/MentionUITests.java | 18 +- .../com/fastcomments/PaginationUITests.java | 3 - .../fastcomments/PresenceIntegrationTest.java | 46 +- .../java/com/fastcomments/SyncClient.java | 6 +- .../com/fastcomments/ThreadingUITests.java | 9 +- .../java/com/fastcomments/UITestBase.java | 90 ++- .../java/com/fastcomments/VoteUITests.java | 8 +- .../fastcomments/WebSocketConcurrentTest.java | 19 +- .../WebSocketFromCallbackTest.java | 20 +- .../fastcomments/WebSocketSdkFlowTest.java | 19 +- .../java/com/fastcomments/TestActivity.java | 2 - .../com/fastcomments/TestFeedActivity.java | 2 - .../fastcomments/TestLiveChatActivity.java | 3 - .../com/fastcomments/DemoBrowserActivity.kt | 62 +- .../com/fastcomments/FeedExampleActivity.java | 129 ++-- .../FeedExampleCustomButtonsActivity.java | 37 +- .../fastcomments/LiveChatExampleActivity.kt | 22 +- .../java/com/fastcomments/MainActivity.kt | 28 +- .../fastcomments/SecureSSOExampleActivity.kt | 8 +- .../fastcomments/SimpleSSOExampleActivity.kt | 6 +- .../fastcomments/ToolbarShowcaseActivity.kt | 8 +- .../java/com/fastcomments/ui/theme/Color.kt | 2 +- .../java/com/fastcomments/ui/theme/Theme.kt | 13 +- .../java/com/fastcomments/ui/theme/Type.kt | 8 +- .../java/com/fastcomments/ExampleUnitTest.kt | 5 +- .../sdk/ExampleInstrumentedTest.java | 10 +- .../com/fastcomments/sdk/AddLinkDialog.java | 55 +- .../com/fastcomments/sdk/AvatarFetcher.java | 11 +- .../fastcomments/sdk/BadgeAwardDialog.java | 29 +- .../java/com/fastcomments/sdk/BadgeView.java | 33 +- .../sdk/BottomCommentInputView.java | 132 ++-- .../com/fastcomments/sdk/CallbackWrapper.java | 2 - .../fastcomments/sdk/CommentEditDialog.java | 2 - .../com/fastcomments/sdk/CommentFormView.java | 74 +- .../fastcomments/sdk/CommentViewHolder.java | 88 +-- .../com/fastcomments/sdk/CommentsAdapter.java | 99 ++- .../com/fastcomments/sdk/CommentsDialog.java | 109 ++- .../com/fastcomments/sdk/CommentsTree.java | 175 +++-- .../fastcomments/sdk/CustomImageGetter.java | 91 +-- .../fastcomments/sdk/CustomToolbarButton.java | 2 +- .../fastcomments/sdk/DemoBannerHelper.java | 16 +- .../java/com/fastcomments/sdk/FCCallback.java | 1 - .../fastcomments/sdk/FastCommentsFeedSDK.java | 418 +++++----- .../sdk/FastCommentsFeedView.java | 306 ++++---- .../com/fastcomments/sdk/FastCommentsSDK.java | 526 +++++++------ .../fastcomments/sdk/FastCommentsTheme.java | 244 ++++-- .../fastcomments/sdk/FastCommentsView.java | 730 ++++++++++-------- .../sdk/FeedCustomToolbarButton.java | 2 +- .../fastcomments/sdk/FeedPostCreateView.java | 86 ++- .../com/fastcomments/sdk/FeedPostType.java | 8 +- .../fastcomments/sdk/FeedPostsAdapter.java | 168 ++-- .../fastcomments/sdk/FollowStateProvider.java | 4 +- .../com/fastcomments/sdk/FullImageDialog.java | 34 +- .../fastcomments/sdk/GalleryImageAdapter.java | 11 +- .../fastcomments/sdk/GetChildrenRequest.java | 10 +- .../com/fastcomments/sdk/HtmlLinkHandler.java | 20 +- .../com/fastcomments/sdk/LiveChatView.java | 723 +++++++++-------- .../sdk/MentionSuggestionsAdapter.java | 4 +- .../sdk/NestedScrollableHost.java | 13 +- .../fastcomments/sdk/OnUserClickListener.java | 6 +- .../fastcomments/sdk/PostImagesAdapter.java | 61 +- .../fastcomments/sdk/RenderableButton.java | 16 +- .../fastcomments/sdk/RenderableComment.java | 27 +- .../com/fastcomments/sdk/RenderableNode.java | 17 +- .../com/fastcomments/sdk/RichEditText.java | 1 - .../com/fastcomments/sdk/RichTextHelper.java | 260 ++++--- .../sdk/SelectedMediaAdapter.java | 11 +- .../com/fastcomments/sdk/TagSupplier.java | 5 +- .../fastcomments/sdk/ThemeColorResolver.java | 113 ++- .../com/fastcomments/sdk/URLDrawable.java | 2 +- .../fastcomments/sdk/UserClickContext.java | 22 +- .../com/fastcomments/sdk/UserClickSource.java | 4 +- .../java/com/fastcomments/sdk/UserInfo.java | 32 +- .../com/fastcomments/sdk/UserLoginDialog.java | 52 +- .../com/fastcomments/sdk/UserMention.java | 2 +- .../examples/CodeFormattingToolbarButton.java | 3 +- .../examples/EmojiPickerToolbarButton.java | 5 +- .../examples/GifPickerFeedToolbarButton.java | 4 +- .../sdk/examples/GifPickerToolbarButton.java | 10 +- .../examples/ImagePickerToolbarButton.java | 1 - .../sdk/examples/MentionToolbarButton.java | 3 +- .../sdk/examples/ToolbarUsageExample.java | 3 +- .../sdk/CommentCRUDIntegrationTests.java | 19 +- .../fastcomments/sdk/CommentsTreeTests.java | 116 +-- .../com/fastcomments/sdk/ExampleUnitTest.java | 6 +- .../sdk/FeedIntegrationTests.java | 30 +- .../sdk/FeedPostsAdapterFollowButtonTest.java | 71 +- .../sdk/FeedPostsAdapterTest.java | 39 +- .../com/fastcomments/sdk/FeedStateTests.java | 11 +- .../fastcomments/sdk/HtmlLinkHandlerTest.java | 7 +- .../fastcomments/sdk/IntegrationTestBase.java | 115 +-- .../com/fastcomments/sdk/MockComment.java | 76 +- .../sdk/ModerationIntegrationTests.java | 14 +- .../sdk/PresenceIntegrationTests.java | 6 +- .../fastcomments/sdk/RenderableNodeTests.java | 5 +- .../fastcomments/sdk/RichTextHelperTest.java | 30 +- .../sdk/SortingIntegrationTests.java | 32 +- .../java/com/fastcomments/sdk/ThemeTests.java | 27 +- .../sdk/ThreadingIntegrationTests.java | 20 +- .../sdk/VoteIntegrationTests.java | 11 +- 109 files changed, 3446 insertions(+), 3066 deletions(-) diff --git a/app/src/androidTest/java/com/fastcomments/CommentActionsUITests.java b/app/src/androidTest/java/com/fastcomments/CommentActionsUITests.java index 1114882..ed8e93b 100644 --- a/app/src/androidTest/java/com/fastcomments/CommentActionsUITests.java +++ b/app/src/androidTest/java/com/fastcomments/CommentActionsUITests.java @@ -12,10 +12,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; - import com.fastcomments.sdk.R; - -import org.json.JSONObject; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -210,8 +207,9 @@ public void testBlockShowsBlockedText() throws Exception { pollUntil(10000, () -> { try { onView(withId(R.id.recyclerViewComments)) - .check(matches(hasDescendant(withText(containsString( - InstrumentationRegistry.getInstrumentation().getTargetContext() + .check(matches( + hasDescendant(withText(containsString(InstrumentationRegistry.getInstrumentation() + .getTargetContext() .getString(R.string.you_blocked_this_user)))))); return true; } catch (Exception | AssertionError e) { @@ -221,14 +219,14 @@ public void testBlockShowsBlockedText() throws Exception { // Verify the blocked user placeholder name is shown onView(withId(R.id.recyclerViewComments)) - .check(matches(hasDescendant(withText(containsString( - InstrumentationRegistry.getInstrumentation().getTargetContext() - .getString(R.string.blocked_user_placeholder)))))); + .check(matches(hasDescendant(withText(containsString(InstrumentationRegistry.getInstrumentation() + .getTargetContext() + .getString(R.string.blocked_user_placeholder)))))); // Verify the original comment text is no longer visible onView(withId(R.id.recyclerViewComments)) - .check(matches(org.hamcrest.Matchers.not( - hasDescendant(withText(containsString("Block this comment")))))); + .check(matches( + org.hamcrest.Matchers.not(hasDescendant(withText(containsString("Block this comment")))))); } @Test @@ -263,8 +261,9 @@ public void testUnblockRestoresComment() throws Exception { pollUntil(10000, () -> { try { onView(withId(R.id.recyclerViewComments)) - .check(matches(hasDescendant(withText(containsString( - InstrumentationRegistry.getInstrumentation().getTargetContext() + .check(matches( + hasDescendant(withText(containsString(InstrumentationRegistry.getInstrumentation() + .getTargetContext() .getString(R.string.you_blocked_this_user)))))); return true; } catch (Exception | AssertionError e) { @@ -293,8 +292,8 @@ public void testUnblockRestoresComment() throws Exception { // Verify blocked placeholder text is gone onView(withId(R.id.recyclerViewComments)) .check(matches(org.hamcrest.Matchers.not( - hasDescendant(withText(containsString( - InstrumentationRegistry.getInstrumentation().getTargetContext() - .getString(R.string.you_blocked_this_user))))))); + hasDescendant(withText(containsString(InstrumentationRegistry.getInstrumentation() + .getTargetContext() + .getString(R.string.you_blocked_this_user))))))); } } diff --git a/app/src/androidTest/java/com/fastcomments/CommentCRUDUITests.java b/app/src/androidTest/java/com/fastcomments/CommentCRUDUITests.java index a8a94ee..2b6190c 100644 --- a/app/src/androidTest/java/com/fastcomments/CommentCRUDUITests.java +++ b/app/src/androidTest/java/com/fastcomments/CommentCRUDUITests.java @@ -1,10 +1,9 @@ package com.fastcomments; import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.clearText; +import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; -import static androidx.test.espresso.action.ViewActions.replaceText; import static androidx.test.espresso.action.ViewActions.typeText; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.RootMatchers.isDialog; @@ -16,11 +15,8 @@ import static org.junit.Assert.assertTrue; import android.util.Log; - import androidx.test.ext.junit.runners.AndroidJUnit4; - import com.fastcomments.sdk.R; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -76,10 +72,8 @@ public void testTypeAndSubmitComment() throws Exception { } }); - onView(withId(R.id.commentInput)) - .perform(click(), typeText("Hello from UI test"), closeSoftKeyboard()); - onView(withId(R.id.sendButton)) - .perform(click()); + onView(withId(R.id.commentInput)).perform(click(), typeText("Hello from UI test"), closeSoftKeyboard()); + onView(withId(R.id.sendButton)).perform(click()); boolean found = false; long deadline = System.currentTimeMillis() + 15000; @@ -111,8 +105,7 @@ public void testEditCommentViaMenu() throws Exception { } }); - onView(withId(R.id.commentInput)) - .perform(click(), typeText("Original text"), closeSoftKeyboard()); + onView(withId(R.id.commentInput)).perform(click(), typeText("Original text"), closeSoftKeyboard()); onView(withId(R.id.sendButton)).perform(click()); // Wait for comment to appear @@ -134,9 +127,7 @@ public void testEditCommentViaMenu() throws Exception { onView(withId(R.id.editCommentText)) .inRoot(isDialog()) .perform(clearText(), typeText("Edited text"), closeSoftKeyboard()); - onView(withId(R.id.saveEditButton)) - .inRoot(isDialog()) - .perform(click()); + onView(withId(R.id.saveEditButton)).inRoot(isDialog()).perform(click()); // Verify edited text appears boolean found = false; @@ -169,8 +160,7 @@ public void testDeleteCommentViaMenu() throws Exception { } }); - onView(withId(R.id.commentInput)) - .perform(click(), typeText("Delete me"), closeSoftKeyboard()); + onView(withId(R.id.commentInput)).perform(click(), typeText("Delete me"), closeSoftKeyboard()); onView(withId(R.id.sendButton)).perform(click()); // Wait for comment to appear @@ -189,9 +179,7 @@ public void testDeleteCommentViaMenu() throws Exception { onView(withText(R.string.delete_comment)).perform(click()); // Confirm in AlertDialog — positive button is R.string.delete ("Delete"), not R.string.delete_comment - onView(withText(R.string.delete)) - .inRoot(isDialog()) - .perform(click()); + onView(withText(R.string.delete)).inRoot(isDialog()).perform(click()); Thread.sleep(1000); // Let dialog dismiss and delete API complete // Verify comment disappeared — check that the empty state reappears @@ -260,8 +248,8 @@ public void testPaginationLoadsMore() throws Exception { try { // Try scrolling to the last adapter position to find older comments onView(withId(R.id.recyclerViewComments)) - .perform(androidx.test.espresso.contrib.RecyclerViewActions - .scrollToPosition(34)); // 0-indexed, position 34 = 35th item + .perform(androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition( + 34)); // 0-indexed, position 34 = 35th item Thread.sleep(500); onView(withId(R.id.recyclerViewComments)) .check(matches(hasDescendant(withText(containsString("Comment 1"))))); diff --git a/app/src/androidTest/java/com/fastcomments/FeedUserA_UITests.java b/app/src/androidTest/java/com/fastcomments/FeedUserA_UITests.java index 90d7bf9..e42aee3 100644 --- a/app/src/androidTest/java/com/fastcomments/FeedUserA_UITests.java +++ b/app/src/androidTest/java/com/fastcomments/FeedUserA_UITests.java @@ -13,15 +13,13 @@ import static org.junit.Assert.assertTrue; import android.util.Log; +import androidx.test.ext.junit.runners.AndroidJUnit4; import com.fastcomments.sdk.R; - import org.json.JSONObject; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import androidx.test.ext.junit.runners.AndroidJUnit4; - /** * Observer role for feed — runs on Emulator A. * Phase 1: UserB creates a post via SDK, UserA sees the live banner via WebSocket and taps it. @@ -94,7 +92,9 @@ public void testFeed_UserA() throws Exception { onView(withId(R.id.newPostsBanner)).check(matches(isDisplayed())); bannerVisible = true; break; - } catch (Exception | AssertionError e) { Thread.sleep(250); } + } catch (Exception | AssertionError e) { + Thread.sleep(250); + } } Log.d(TAG, "Banner visible: " + bannerVisible); assertTrue("New posts banner should appear when UserB posts", bannerVisible); @@ -109,11 +109,12 @@ public void testFeed_UserA() throws Exception { deadline = System.currentTimeMillis() + 15000; while (System.currentTimeMillis() < deadline) { try { - onView(withId(R.id.recyclerViewFeed)) - .check(matches(hasDescendant(withText(containsString(postText))))); + onView(withId(R.id.recyclerViewFeed)).check(matches(hasDescendant(withText(containsString(postText))))); found = true; break; - } catch (Exception | AssertionError e) { Thread.sleep(250); } + } catch (Exception | AssertionError e) { + Thread.sleep(250); + } } Log.d(TAG, "Phase 1 result: " + found); assertTrue("UserB's post should appear after tapping new posts banner", found); @@ -122,8 +123,7 @@ public void testFeed_UserA() throws Exception { // --- Phase 2: UserA posts via UI, UserB sees it --- Log.d(TAG, "=== Phase 2: Create post for UserB ==="); String myPostText = "Feed post from A " + System.currentTimeMillis(); - onView(withId(R.id.postContentEditText)) - .perform(click(), typeText(myPostText), closeSoftKeyboard()); + onView(withId(R.id.postContentEditText)).perform(click(), typeText(myPostText), closeSoftKeyboard()); onView(withId(R.id.submitPostButton)).perform(click()); Log.d(TAG, "Submitted post via UI"); diff --git a/app/src/androidTest/java/com/fastcomments/FeedUserB_UITests.java b/app/src/androidTest/java/com/fastcomments/FeedUserB_UITests.java index fe742fb..626982f 100644 --- a/app/src/androidTest/java/com/fastcomments/FeedUserB_UITests.java +++ b/app/src/androidTest/java/com/fastcomments/FeedUserB_UITests.java @@ -13,16 +13,14 @@ import static org.junit.Assert.assertTrue; import android.util.Log; +import androidx.test.ext.junit.runners.AndroidJUnit4; import com.fastcomments.sdk.R; - import org.json.JSONObject; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import androidx.test.ext.junit.runners.AndroidJUnit4; - /** * Actor role for feed — runs on Emulator B. * Phase 1: UserB creates a post via SDK, UserA verifies receipt. @@ -79,8 +77,7 @@ public void testFeed_UserB() throws Exception { }); String postText = "Feed post from B " + System.currentTimeMillis(); - onView(withId(R.id.postContentEditText)) - .perform(click(), typeText(postText), closeSoftKeyboard()); + onView(withId(R.id.postContentEditText)).perform(click(), typeText(postText), closeSoftKeyboard()); onView(withId(R.id.submitPostButton)).perform(click()); Log.d(TAG, "Submitted post via UI"); @@ -107,7 +104,9 @@ public void testFeed_UserB() throws Exception { onView(withId(R.id.newPostsBanner)).check(matches(isDisplayed())); bannerVisible = true; break; - } catch (Exception | AssertionError e) { Thread.sleep(250); } + } catch (Exception | AssertionError e) { + Thread.sleep(250); + } } Log.d(TAG, "Banner visible: " + bannerVisible); assertTrue("New posts banner should appear when UserA posts", bannerVisible); @@ -126,7 +125,9 @@ public void testFeed_UserB() throws Exception { .check(matches(hasDescendant(withText(containsString(userAPostText))))); found = true; break; - } catch (Exception | AssertionError e) { Thread.sleep(250); } + } catch (Exception | AssertionError e) { + Thread.sleep(250); + } } Log.d(TAG, "Phase 2 result: " + found); assertTrue("UserA's post should appear in UserB's feed", found); diff --git a/app/src/androidTest/java/com/fastcomments/LiveChatPaginationUITests.java b/app/src/androidTest/java/com/fastcomments/LiveChatPaginationUITests.java index 75768b7..68a60bf 100644 --- a/app/src/androidTest/java/com/fastcomments/LiveChatPaginationUITests.java +++ b/app/src/androidTest/java/com/fastcomments/LiveChatPaginationUITests.java @@ -13,14 +13,11 @@ import android.util.Log; import android.view.View; import android.widget.TextView; - import androidx.recyclerview.widget.RecyclerView; import androidx.test.espresso.UiController; import androidx.test.espresso.ViewAction; import androidx.test.ext.junit.runners.AndroidJUnit4; - import com.fastcomments.sdk.R; - import org.hamcrest.Matcher; import org.junit.Before; import org.junit.Test; @@ -70,23 +67,33 @@ public void testLoadOlderMessages() throws Exception { onView(withId(R.id.recyclerViewComments)) .check(matches(hasDescendant(withText(containsString("msg-" + TOTAL_MESSAGES))))); return true; - } catch (Exception | AssertionError e) { return false; } + } catch (Exception | AssertionError e) { + return false; + } }); Log.d(TAG, "Latest message visible"); // Check that not all messages loaded — pagination should be needed final int[] adapterCount = {0}; onView(withId(R.id.recyclerViewComments)).perform(new ViewAction() { - @Override public Matcher getConstraints() { return org.hamcrest.Matchers.any(View.class); } - @Override public String getDescription() { return "read adapter count"; } - @Override public void perform(UiController uc, View v) { + @Override + public Matcher getConstraints() { + return org.hamcrest.Matchers.any(View.class); + } + + @Override + public String getDescription() { + return "read adapter count"; + } + + @Override + public void perform(UiController uc, View v) { RecyclerView rv = (RecyclerView) v; adapterCount[0] = rv.getAdapter() != null ? rv.getAdapter().getItemCount() : 0; } }); Log.d(TAG, "Initial adapter count: " + adapterCount[0]); - assertTrue("Should have fewer items than total seeded (pagination needed)", - adapterCount[0] < TOTAL_MESSAGES); + assertTrue("Should have fewer items than total seeded (pagination needed)", adapterCount[0] < TOTAL_MESSAGES); int countBefore = adapterCount[0]; @@ -95,12 +102,9 @@ public void testLoadOlderMessages() throws Exception { Log.d(TAG, "Pagination buttons correctly hidden"); // Scroll to the top to trigger infinite scroll loading of older messages - onView(withId(R.id.recyclerViewComments)).perform( - androidx.test.espresso.action.ViewActions.swipeDown()); - onView(withId(R.id.recyclerViewComments)).perform( - androidx.test.espresso.action.ViewActions.swipeDown()); - onView(withId(R.id.recyclerViewComments)).perform( - androidx.test.espresso.action.ViewActions.swipeDown()); + onView(withId(R.id.recyclerViewComments)).perform(androidx.test.espresso.action.ViewActions.swipeDown()); + onView(withId(R.id.recyclerViewComments)).perform(androidx.test.espresso.action.ViewActions.swipeDown()); + onView(withId(R.id.recyclerViewComments)).perform(androidx.test.espresso.action.ViewActions.swipeDown()); Log.d(TAG, "Swiped up to trigger infinite scroll"); // Poll for adapter count to increase @@ -109,9 +113,18 @@ public void testLoadOlderMessages() throws Exception { long deadline = System.currentTimeMillis() + 15000; while (System.currentTimeMillis() < deadline) { onView(withId(R.id.recyclerViewComments)).perform(new ViewAction() { - @Override public Matcher getConstraints() { return org.hamcrest.Matchers.any(View.class); } - @Override public String getDescription() { return "read adapter count after scroll"; } - @Override public void perform(UiController uc, View v) { + @Override + public Matcher getConstraints() { + return org.hamcrest.Matchers.any(View.class); + } + + @Override + public String getDescription() { + return "read adapter count after scroll"; + } + + @Override + public void perform(UiController uc, View v) { RecyclerView rv = (RecyclerView) v; countAfter[0] = rv.getAdapter() != null ? rv.getAdapter().getItemCount() : 0; } @@ -146,7 +159,9 @@ public void testMessagesInOldestFirstOrder() throws Exception { onView(withId(R.id.recyclerViewComments)) .check(matches(hasDescendant(withText(containsString("message"))))); return true; - } catch (Exception | AssertionError e) { return false; } + } catch (Exception | AssertionError e) { + return false; + } }); // In live chat (oldest-first), position 0 should be the oldest message. @@ -155,9 +170,18 @@ public void testMessagesInOldestFirstOrder() throws Exception { final String[] firstCommentText = {""}; final String[] lastCommentText = {""}; onView(withId(R.id.recyclerViewComments)).perform(new ViewAction() { - @Override public Matcher getConstraints() { return org.hamcrest.Matchers.any(View.class); } - @Override public String getDescription() { return "Read first and last comment text"; } - @Override public void perform(UiController uiController, View view) { + @Override + public Matcher getConstraints() { + return org.hamcrest.Matchers.any(View.class); + } + + @Override + public String getDescription() { + return "Read first and last comment text"; + } + + @Override + public void perform(UiController uiController, View view) { RecyclerView rv = (RecyclerView) view; int count = rv.getAdapter() != null ? rv.getAdapter().getItemCount() : 0; // Find first comment @@ -186,9 +210,11 @@ public void testMessagesInOldestFirstOrder() throws Exception { }); Log.d(TAG, "First comment: " + firstCommentText[0] + ", Last comment: " + lastCommentText[0]); - assertTrue("First comment should be 'First message' (oldest-first chat order)", + assertTrue( + "First comment should be 'First message' (oldest-first chat order)", firstCommentText[0].contains("First message")); - assertTrue("Last comment should be 'Third message' (oldest-first chat order)", + assertTrue( + "Last comment should be 'Third message' (oldest-first chat order)", lastCommentText[0].contains("Third message")); } } diff --git a/app/src/androidTest/java/com/fastcomments/LiveChatUserA_UITests.java b/app/src/androidTest/java/com/fastcomments/LiveChatUserA_UITests.java index f6d9bc4..0cbfe00 100644 --- a/app/src/androidTest/java/com/fastcomments/LiveChatUserA_UITests.java +++ b/app/src/androidTest/java/com/fastcomments/LiveChatUserA_UITests.java @@ -13,16 +13,13 @@ import static org.junit.Assert.assertTrue; import android.util.Log; - +import androidx.test.ext.junit.runners.AndroidJUnit4; import com.fastcomments.sdk.R; - import org.json.JSONObject; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import androidx.test.ext.junit.runners.AndroidJUnit4; - /** * Observer role for live chat — runs on Emulator A. * Phase 1: UserB sends a message, UserA verifies it appears live. @@ -66,7 +63,9 @@ public void testLiveChat_UserA() throws Exception { try { onView(withId(R.id.connectionStatusText)).check(matches(withText(R.string.live_chat_live))); return true; - } catch (Exception | AssertionError e) { return false; } + } catch (Exception | AssertionError e) { + return false; + } }); Log.d(TAG, "Connected"); @@ -87,7 +86,9 @@ public void testLiveChat_UserA() throws Exception { .check(matches(hasDescendant(withText(containsString(messageText))))); found = true; break; - } catch (Exception | AssertionError e) { Thread.sleep(250); } + } catch (Exception | AssertionError e) { + Thread.sleep(250); + } } Log.d(TAG, "Phase 1 result: " + found); assertTrue("UserB's message should appear in UserA's chat", found); @@ -100,7 +101,9 @@ public void testLiveChat_UserA() throws Exception { onView(withId(R.id.userCountText)).check(matches(isDisplayed())); userCountVisible = true; break; - } catch (Exception | AssertionError e) { Thread.sleep(250); } + } catch (Exception | AssertionError e) { + Thread.sleep(250); + } } Log.d(TAG, "User count visible: " + userCountVisible); assertTrue("User count should be visible in header", userCountVisible); @@ -108,8 +111,7 @@ public void testLiveChat_UserA() throws Exception { // --- Phase 2: UserA sends, UserB receives --- Log.d(TAG, "=== Phase 2: Send message to UserB ==="); String myMessage = "Hello from A " + System.currentTimeMillis(); - onView(withId(R.id.commentInput)) - .perform(click(), typeText(myMessage), closeSoftKeyboard()); + onView(withId(R.id.commentInput)).perform(click(), typeText(myMessage), closeSoftKeyboard()); onView(withId(R.id.sendButton)).perform(click()); // Verify own message appears @@ -121,7 +123,9 @@ public void testLiveChat_UserA() throws Exception { .check(matches(hasDescendant(withText(containsString(myMessage))))); ownAppeared = true; break; - } catch (Exception | AssertionError e) { Thread.sleep(250); } + } catch (Exception | AssertionError e) { + Thread.sleep(250); + } } assertTrue("Own message should appear after sending", ownAppeared); diff --git a/app/src/androidTest/java/com/fastcomments/LiveChatUserB_UITests.java b/app/src/androidTest/java/com/fastcomments/LiveChatUserB_UITests.java index c753771..bd53aa2 100644 --- a/app/src/androidTest/java/com/fastcomments/LiveChatUserB_UITests.java +++ b/app/src/androidTest/java/com/fastcomments/LiveChatUserB_UITests.java @@ -6,24 +6,20 @@ import static androidx.test.espresso.action.ViewActions.typeText; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant; -import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertTrue; import android.util.Log; - +import androidx.test.ext.junit.runners.AndroidJUnit4; import com.fastcomments.sdk.R; - import org.json.JSONObject; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import androidx.test.ext.junit.runners.AndroidJUnit4; - /** * Actor role for live chat — runs on Emulator B. * Phase 1: UserB sends a message, UserA verifies receipt. @@ -69,12 +65,13 @@ public void testLiveChat_UserB() throws Exception { try { onView(withId(R.id.connectionStatusText)).check(matches(withText(R.string.live_chat_live))); return true; - } catch (Exception | AssertionError e) { return false; } + } catch (Exception | AssertionError e) { + return false; + } }); String messageText = "Chat from B " + System.currentTimeMillis(); - onView(withId(R.id.commentInput)) - .perform(click(), typeText(messageText), closeSoftKeyboard()); + onView(withId(R.id.commentInput)).perform(click(), typeText(messageText), closeSoftKeyboard()); onView(withId(R.id.sendButton)).perform(click()); // Verify own message appears @@ -86,7 +83,9 @@ public void testLiveChat_UserB() throws Exception { .check(matches(hasDescendant(withText(containsString(messageText))))); ownAppeared = true; break; - } catch (Exception | AssertionError e) { Thread.sleep(250); } + } catch (Exception | AssertionError e) { + Thread.sleep(250); + } } assertTrue("Own message should appear after sending", ownAppeared); @@ -112,7 +111,9 @@ public void testLiveChat_UserB() throws Exception { .check(matches(hasDescendant(withText(containsString(userAMessage))))); found = true; break; - } catch (Exception | AssertionError e) { Thread.sleep(250); } + } catch (Exception | AssertionError e) { + Thread.sleep(250); + } } Log.d(TAG, "Phase 2 result: " + found); assertTrue("UserA's message should appear live in UserB's chat", found); diff --git a/app/src/androidTest/java/com/fastcomments/LiveEventUserA_UITests.java b/app/src/androidTest/java/com/fastcomments/LiveEventUserA_UITests.java index 81c61a9..91fd596 100644 --- a/app/src/androidTest/java/com/fastcomments/LiveEventUserA_UITests.java +++ b/app/src/androidTest/java/com/fastcomments/LiveEventUserA_UITests.java @@ -7,21 +7,17 @@ import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import android.util.Log; - +import androidx.test.ext.junit.runners.AndroidJUnit4; import com.fastcomments.sdk.R; - import org.json.JSONObject; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import androidx.test.ext.junit.runners.AndroidJUnit4; - /** * Observer role — runs on Emulator A. * Tests 6 phases of live event behavior via WebSocket. @@ -90,7 +86,9 @@ public void testLiveEvents_UserA() throws Exception { .check(matches(hasDescendant(withText(containsString(commentText))))); found = true; break; - } catch (Exception | AssertionError e) { Thread.sleep(250); } + } catch (Exception | AssertionError e) { + Thread.sleep(250); + } } Log.d(TAG, "Phase 1 result: " + found); assertTrue("Live comment should appear via WebSocket", found); @@ -114,7 +112,9 @@ public void testLiveEvents_UserA() throws Exception { onView(withId(R.id.recyclerViewComments)) .check(matches(hasDescendant(withText(containsString("Vote target from A"))))); return true; - } catch (Exception | AssertionError e) { return false; } + } catch (Exception | AssertionError e) { + return false; + } }); // NOW signal UserB to vote (WS should be connected by now) @@ -130,19 +130,32 @@ public void testLiveEvents_UserA() throws Exception { .perform(androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem( hasDescendant(withText(containsString("Vote target from A"))), new androidx.test.espresso.ViewAction() { - @Override public org.hamcrest.Matcher getConstraints() { + @Override + public org.hamcrest.Matcher getConstraints() { return org.hamcrest.Matchers.any(android.view.View.class); } - @Override public String getDescription() { return "check vote count"; } - @Override public void perform(androidx.test.espresso.UiController uc, android.view.View v) { + + @Override + public String getDescription() { + return "check vote count"; + } + + @Override + public void perform(androidx.test.espresso.UiController uc, android.view.View v) { android.widget.TextView voteCount = v.findViewById(R.id.upVoteCount); - if (voteCount != null && !"0".equals(voteCount.getText().toString())) { + if (voteCount != null + && !"0" + .equals(voteCount + .getText() + .toString())) { voteChanged[0] = true; } } })); if (voteChanged[0]) break; - } catch (Exception | AssertionError e) { /* retry */ } + } catch (Exception | AssertionError e) { + /* retry */ + } Thread.sleep(250); } Log.d(TAG, "Phase 2 result: " + voteChanged[0]); @@ -164,7 +177,9 @@ public void testLiveEvents_UserA() throws Exception { onView(withId(R.id.recyclerViewComments)) .check(matches(hasDescendant(withText(containsString("Offline user C comment"))))); return true; - } catch (Exception | AssertionError e) { return false; } + } catch (Exception | AssertionError e) { + return false; + } }); // Signal UserB to join (UserA's WS should be connected by now) @@ -180,19 +195,29 @@ public void testLiveEvents_UserA() throws Exception { .perform(androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem( hasDescendant(withText(containsString(commentText))), new androidx.test.espresso.ViewAction() { - @Override public org.hamcrest.Matcher getConstraints() { + @Override + public org.hamcrest.Matcher getConstraints() { return org.hamcrest.Matchers.any(android.view.View.class); } - @Override public String getDescription() { return "check online indicator on UserB comment"; } - @Override public void perform(androidx.test.espresso.UiController uc, android.view.View v) { + + @Override + public String getDescription() { + return "check online indicator on UserB comment"; + } + + @Override + public void perform(androidx.test.espresso.UiController uc, android.view.View v) { android.view.View indicator = v.findViewById(R.id.onlineIndicator); - if (indicator != null && indicator.getVisibility() == android.view.View.VISIBLE) { + if (indicator != null + && indicator.getVisibility() == android.view.View.VISIBLE) { userBOnline[0] = true; } } })); if (userBOnline[0]) break; - } catch (Exception | AssertionError e) { /* retry */ } + } catch (Exception | AssertionError e) { + /* retry */ + } Thread.sleep(250); } Log.d(TAG, "Phase 3 UserB online: " + userBOnline[0]); @@ -204,13 +229,21 @@ public void testLiveEvents_UserA() throws Exception { .perform(androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem( hasDescendant(withText(containsString("Offline user C comment"))), new androidx.test.espresso.ViewAction() { - @Override public org.hamcrest.Matcher getConstraints() { + @Override + public org.hamcrest.Matcher getConstraints() { return org.hamcrest.Matchers.any(android.view.View.class); } - @Override public String getDescription() { return "check online indicator on UserC comment"; } - @Override public void perform(androidx.test.espresso.UiController uc, android.view.View v) { + + @Override + public String getDescription() { + return "check online indicator on UserC comment"; + } + + @Override + public void perform(androidx.test.espresso.UiController uc, android.view.View v) { android.view.View indicator = v.findViewById(R.id.onlineIndicator); - userCIndicatorOff[0] = indicator == null || indicator.getVisibility() != android.view.View.VISIBLE; + userCIndicatorOff[0] = + indicator == null || indicator.getVisibility() != android.view.View.VISIBLE; } })); Log.d(TAG, "Phase 3 UserC offline: " + userCIndicatorOff[0]); @@ -234,7 +267,9 @@ public void testLiveEvents_UserA() throws Exception { .check(matches(hasDescendant(withText(containsString(deleteText))))); deleteVisible = true; break; - } catch (Exception | AssertionError e) { Thread.sleep(250); } + } catch (Exception | AssertionError e) { + Thread.sleep(250); + } } assertTrue("Comment to delete should be visible", deleteVisible); @@ -272,7 +307,9 @@ public void testLiveEvents_UserA() throws Exception { onView(withId(R.id.recyclerViewComments)) .check(matches(hasDescendant(withText(containsString("Pin target from A"))))); return true; - } catch (Exception | AssertionError e) { return false; } + } catch (Exception | AssertionError e) { + return false; + } }); JSONObject phase5Setup = new JSONObject(); @@ -290,9 +327,18 @@ public void testLiveEvents_UserA() throws Exception { .perform(androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem( hasDescendant(withText(containsString("Pin target from A"))), new androidx.test.espresso.ViewAction() { - @Override public org.hamcrest.Matcher getConstraints() { return org.hamcrest.Matchers.any(android.view.View.class); } - @Override public String getDescription() { return "check pin icon"; } - @Override public void perform(androidx.test.espresso.UiController uc, android.view.View v) { + @Override + public org.hamcrest.Matcher getConstraints() { + return org.hamcrest.Matchers.any(android.view.View.class); + } + + @Override + public String getDescription() { + return "check pin icon"; + } + + @Override + public void perform(androidx.test.espresso.UiController uc, android.view.View v) { android.view.View icon = v.findViewById(R.id.pinIcon); if (icon != null && icon.getVisibility() == android.view.View.VISIBLE) { pinVisible[0] = true; @@ -300,7 +346,9 @@ public void testLiveEvents_UserA() throws Exception { } })); if (pinVisible[0]) break; - } catch (Exception | AssertionError e) { /* retry */ } + } catch (Exception | AssertionError e) { + /* retry */ + } Thread.sleep(250); } Log.d(TAG, "Phase 5 result: " + pinVisible[0]); @@ -321,7 +369,9 @@ public void testLiveEvents_UserA() throws Exception { onView(withId(R.id.recyclerViewComments)) .check(matches(hasDescendant(withText(containsString("Lock target from A"))))); return true; - } catch (Exception | AssertionError e) { return false; } + } catch (Exception | AssertionError e) { + return false; + } }); JSONObject phase6Setup = new JSONObject(); @@ -339,9 +389,18 @@ public void testLiveEvents_UserA() throws Exception { .perform(androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem( hasDescendant(withText(containsString("Lock target from A"))), new androidx.test.espresso.ViewAction() { - @Override public org.hamcrest.Matcher getConstraints() { return org.hamcrest.Matchers.any(android.view.View.class); } - @Override public String getDescription() { return "check lock icon"; } - @Override public void perform(androidx.test.espresso.UiController uc, android.view.View v) { + @Override + public org.hamcrest.Matcher getConstraints() { + return org.hamcrest.Matchers.any(android.view.View.class); + } + + @Override + public String getDescription() { + return "check lock icon"; + } + + @Override + public void perform(androidx.test.espresso.UiController uc, android.view.View v) { android.view.View icon = v.findViewById(R.id.lockIcon); if (icon != null && icon.getVisibility() == android.view.View.VISIBLE) { lockVisible[0] = true; @@ -349,7 +408,9 @@ public void testLiveEvents_UserA() throws Exception { } })); if (lockVisible[0]) break; - } catch (Exception | AssertionError e) { /* retry */ } + } catch (Exception | AssertionError e) { + /* retry */ + } Thread.sleep(250); } Log.d(TAG, "Phase 6 result: " + lockVisible[0]); diff --git a/app/src/androidTest/java/com/fastcomments/LiveEventUserB_UITests.java b/app/src/androidTest/java/com/fastcomments/LiveEventUserB_UITests.java index ea00147..244d006 100644 --- a/app/src/androidTest/java/com/fastcomments/LiveEventUserB_UITests.java +++ b/app/src/androidTest/java/com/fastcomments/LiveEventUserB_UITests.java @@ -7,27 +7,21 @@ import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.RootMatchers.isDialog; import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant; -import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; -import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertTrue; import android.util.Log; - +import androidx.test.ext.junit.runners.AndroidJUnit4; import com.fastcomments.sdk.R; - - import org.json.JSONObject; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import androidx.test.ext.junit.runners.AndroidJUnit4; - /** * Actor role — runs on Emulator B. * Performs actions in 6 phases that are observed by UserA. @@ -76,12 +70,13 @@ public void testLiveEvents_UserB() throws Exception { try { onView(withId(R.id.commentInput)).check(matches(isDisplayed())); return true; - } catch (Exception | AssertionError e) { return false; } + } catch (Exception | AssertionError e) { + return false; + } }); String commentText = "Live from B " + System.currentTimeMillis(); - onView(withId(R.id.commentInput)) - .perform(click(), typeText(commentText), closeSoftKeyboard()); + onView(withId(R.id.commentInput)).perform(click(), typeText(commentText), closeSoftKeyboard()); onView(withId(R.id.sendButton)).perform(click()); boolean ownCommentAppeared = false; @@ -92,7 +87,9 @@ public void testLiveEvents_UserB() throws Exception { .check(matches(hasDescendant(withText(containsString(commentText))))); ownCommentAppeared = true; break; - } catch (Exception | AssertionError e) { Thread.sleep(250); } + } catch (Exception | AssertionError e) { + Thread.sleep(250); + } } assertTrue("Own comment should appear after posting", ownCommentAppeared); @@ -114,7 +111,9 @@ public void testLiveEvents_UserB() throws Exception { onView(withId(R.id.recyclerViewComments)) .check(matches(hasDescendant(withText(containsString("Vote target from A"))))); return true; - } catch (Exception | AssertionError e) { return false; } + } catch (Exception | AssertionError e) { + return false; + } }); // Click the upvote button within the "Vote target" item using RecyclerViewActions @@ -122,11 +121,18 @@ public void testLiveEvents_UserB() throws Exception { .perform(androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem( hasDescendant(withText(containsString("Vote target from A"))), new androidx.test.espresso.ViewAction() { - @Override public org.hamcrest.Matcher getConstraints() { + @Override + public org.hamcrest.Matcher getConstraints() { return org.hamcrest.Matchers.any(android.view.View.class); } - @Override public String getDescription() { return "click upVoteButton in item"; } - @Override public void perform(androidx.test.espresso.UiController uc, android.view.View v) { + + @Override + public String getDescription() { + return "click upVoteButton in item"; + } + + @Override + public void perform(androidx.test.espresso.UiController uc, android.view.View v) { v.findViewById(R.id.upVoteButton).performClick(); } })); @@ -142,7 +148,9 @@ public void testLiveEvents_UserB() throws Exception { try { onView(withId(R.id.commentInput)).check(matches(isDisplayed())); return true; - } catch (Exception | AssertionError e) { return false; } + } catch (Exception | AssertionError e) { + return false; + } }); sync.signalReady("phase3"); @@ -170,26 +178,34 @@ public void testLiveEvents_UserB() throws Exception { onView(withId(R.id.recyclerViewComments)) .check(matches(hasDescendant(withText(containsString(deleteText))))); return true; - } catch (Exception | AssertionError e) { return false; } + } catch (Exception | AssertionError e) { + return false; + } }); // Click menu on the specific comment using RecyclerViewActions onView(withId(R.id.recyclerViewComments)) .perform(androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem( - hasDescendant(withText(containsString(deleteText))), - new androidx.test.espresso.ViewAction() { - @Override public org.hamcrest.Matcher getConstraints() { return org.hamcrest.Matchers.any(android.view.View.class); } - @Override public String getDescription() { return "click menu in item"; } - @Override public void perform(androidx.test.espresso.UiController uc, android.view.View v) { + hasDescendant(withText(containsString(deleteText))), new androidx.test.espresso.ViewAction() { + @Override + public org.hamcrest.Matcher getConstraints() { + return org.hamcrest.Matchers.any(android.view.View.class); + } + + @Override + public String getDescription() { + return "click menu in item"; + } + + @Override + public void perform(androidx.test.espresso.UiController uc, android.view.View v) { v.findViewById(R.id.commentMenuButton).performClick(); } })); onView(withText(R.string.delete_comment)).perform(click()); // Confirm in AlertDialog - onView(withText(R.string.delete)) - .inRoot(isDialog()) - .perform(click()); + onView(withText(R.string.delete)).inRoot(isDialog()).perform(click()); // Wait for comment to disappear from UserB's own view pollUntil(10000, () -> { @@ -217,7 +233,9 @@ public void testLiveEvents_UserB() throws Exception { try { onView(withId(R.id.recyclerViewComments)).check(matches(isDisplayed())); return true; - } catch (Exception | AssertionError e) { return false; } + } catch (Exception | AssertionError e) { + return false; + } }); pinComment(pinCommentId, ssoTokenBAdmin); diff --git a/app/src/androidTest/java/com/fastcomments/MentionUITests.java b/app/src/androidTest/java/com/fastcomments/MentionUITests.java index 07f2e75..2167e95 100644 --- a/app/src/androidTest/java/com/fastcomments/MentionUITests.java +++ b/app/src/androidTest/java/com/fastcomments/MentionUITests.java @@ -14,11 +14,8 @@ import static org.junit.Assert.assertTrue; import android.util.Log; - import androidx.test.ext.junit.runners.AndroidJUnit4; - import com.fastcomments.sdk.R; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -71,8 +68,7 @@ public void testMentionSearchAndSelect() throws Exception { }); // Type @Tester a to trigger mention search (should match "Tester alice1") - onView(withId(R.id.commentInput)) - .perform(click(), typeText("@Tester a"), closeSoftKeyboard()); + onView(withId(R.id.commentInput)).perform(click(), typeText("@Tester a"), closeSoftKeyboard()); // Poll until the mention popup appears with "Tester alice1" boolean popupFound = false; @@ -96,10 +92,8 @@ public void testMentionSearchAndSelect() throws Exception { .perform(click()); // Type additional text and submit - onView(withId(R.id.commentInput)) - .perform(typeText("hello!"), closeSoftKeyboard()); - onView(withId(R.id.sendButton)) - .perform(click()); + onView(withId(R.id.commentInput)).perform(typeText("hello!"), closeSoftKeyboard()); + onView(withId(R.id.sendButton)).perform(click()); // Verify the posted comment appears with the mentioned username boolean commentFound = false; @@ -138,8 +132,7 @@ public void testMentionPopupDismissesOnSpace() throws Exception { }); // Type @Tester a to trigger mention search - onView(withId(R.id.commentInput)) - .perform(click(), typeText("@Tester a"), closeSoftKeyboard()); + onView(withId(R.id.commentInput)).perform(click(), typeText("@Tester a"), closeSoftKeyboard()); // Wait for popup to appear boolean popupFound = false; @@ -158,8 +151,7 @@ public void testMentionPopupDismissesOnSpace() throws Exception { assertTrue("Mention popup should appear before dismissal test", popupFound); // Type a space to dismiss the mention (triggers cancelMention) - onView(withId(R.id.commentInput)) - .perform(typeText(" ")); + onView(withId(R.id.commentInput)).perform(typeText(" ")); // Verify popup is gone boolean popupDismissed = false; diff --git a/app/src/androidTest/java/com/fastcomments/PaginationUITests.java b/app/src/androidTest/java/com/fastcomments/PaginationUITests.java index 98f908a..74058d0 100644 --- a/app/src/androidTest/java/com/fastcomments/PaginationUITests.java +++ b/app/src/androidTest/java/com/fastcomments/PaginationUITests.java @@ -10,14 +10,11 @@ import android.view.View; import android.widget.TextView; - import androidx.recyclerview.widget.RecyclerView; import androidx.test.espresso.UiController; import androidx.test.espresso.ViewAction; import androidx.test.ext.junit.runners.AndroidJUnit4; - import com.fastcomments.sdk.R; - import org.hamcrest.Matcher; import org.junit.Before; import org.junit.Test; diff --git a/app/src/androidTest/java/com/fastcomments/PresenceIntegrationTest.java b/app/src/androidTest/java/com/fastcomments/PresenceIntegrationTest.java index 0aa8bc9..6238263 100644 --- a/app/src/androidTest/java/com/fastcomments/PresenceIntegrationTest.java +++ b/app/src/androidTest/java/com/fastcomments/PresenceIntegrationTest.java @@ -1,28 +1,23 @@ package com.fastcomments; +import static org.junit.Assert.*; + import android.util.Log; import androidx.test.ext.junit.runners.AndroidJUnit4; - import com.fastcomments.core.CommentWidgetConfig; -import com.fastcomments.model.LiveEvent; import com.fastcomments.pubsub.LiveEventSubscriber; import com.fastcomments.pubsub.SubscribeToChangesResult; - -import org.json.JSONObject; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; - import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; - -import static org.junit.Assert.*; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; /** * Integration test for the presence update system. @@ -59,12 +54,11 @@ public void testUserBJoinVisibleToUserA() throws Exception { List usersJoinedSeen = new CopyOnWriteArrayList<>(); CountDownLatch joinLatch = new CountDownLatch(1); - subscriberA.setOnConnectionStatusChange((connected, lastEventTime) -> - Log.d(TAG, "UserA connection: " + connected)); + subscriberA.setOnConnectionStatusChange( + (connected, lastEventTime) -> Log.d(TAG, "UserA connection: " + connected)); SubscribeToChangesResult resultA = subscriberA.subscribeToChanges( - configA, wsParamsA[0], urlId, wsParamsA[1], wsParamsA[2], - null, event -> { + configA, wsParamsA[0], urlId, wsParamsA[1], wsParamsA[2], null, event -> { if (event.getType() != null && "p-u".equals(event.getType().getValue())) { List uj = event.getUj(); if (uj != null) { @@ -89,8 +83,7 @@ public void testUserBJoinVisibleToUserA() throws Exception { LiveEventSubscriber subscriberB = LiveEventSubscriber.createTesting(); CommentWidgetConfig configB = new CommentWidgetConfig(testTenantId, urlId); SubscribeToChangesResult resultB = subscriberB.subscribeToChanges( - configB, wsParamsB[0], urlId, wsParamsB[1], wsParamsB[2], - null, event -> {}); + configB, wsParamsB[0], urlId, wsParamsB[1], wsParamsB[2], null, event -> {}); assertNotNull("UserB should subscribe", resultB); Log.d(TAG, "UserB subscribed, waiting for UserA to see the join..."); @@ -116,12 +109,11 @@ public void testUserBReconnectVisibleToUserA() throws Exception { List allJoins = new CopyOnWriteArrayList<>(); CountDownLatch secondJoinLatch = new CountDownLatch(1); - subscriberA.setOnConnectionStatusChange((connected, lastEventTime) -> - Log.d(TAG, "UserA connection: " + connected)); + subscriberA.setOnConnectionStatusChange( + (connected, lastEventTime) -> Log.d(TAG, "UserA connection: " + connected)); SubscribeToChangesResult resultA = subscriberA.subscribeToChanges( - configA, wsParamsA[0], urlId, wsParamsA[1], wsParamsA[2], - null, event -> { + configA, wsParamsA[0], urlId, wsParamsA[1], wsParamsA[2], null, event -> { if (event.getType() != null && "p-u".equals(event.getType().getValue())) { List uj = event.getUj(); if (uj != null) { @@ -145,8 +137,7 @@ public void testUserBReconnectVisibleToUserA() throws Exception { LiveEventSubscriber subscriberB1 = LiveEventSubscriber.createTesting(); CommentWidgetConfig configB = new CommentWidgetConfig(testTenantId, urlId); SubscribeToChangesResult resultB1 = subscriberB1.subscribeToChanges( - configB, wsParamsB[0], urlId, wsParamsB[1], wsParamsB[2], - null, event -> {}); + configB, wsParamsB[0], urlId, wsParamsB[1], wsParamsB[2], null, event -> {}); Log.d(TAG, "UserB first connection established"); Thread.sleep(3000); // Let the join propagate @@ -159,8 +150,7 @@ public void testUserBReconnectVisibleToUserA() throws Exception { String[] wsParamsB2 = getWsParams(urlId, ssoB); LiveEventSubscriber subscriberB2 = LiveEventSubscriber.createTesting(); SubscribeToChangesResult resultB2 = subscriberB2.subscribeToChanges( - configB, wsParamsB2[0], urlId, wsParamsB2[1], wsParamsB2[2], - null, event -> {}); + configB, wsParamsB2[0], urlId, wsParamsB2[1], wsParamsB2[2], null, event -> {}); Log.d(TAG, "UserB reconnected, waiting for UserA to see second join..."); boolean sawSecondJoin = secondJoinLatch.await(10, TimeUnit.SECONDS); @@ -184,10 +174,8 @@ private String[] getWsParams(String urlId, String sso) throws Exception { String body = resp.body().string(); JSONObject json = new JSONObject(body); client.dispatcher().executorService().shutdown(); - return new String[]{ - json.optString("tenantIdWS", ""), - json.optString("urlIdWS", ""), - json.optString("userIdWS", "") + return new String[] { + json.optString("tenantIdWS", ""), json.optString("urlIdWS", ""), json.optString("userIdWS", "") }; } } diff --git a/app/src/androidTest/java/com/fastcomments/SyncClient.java b/app/src/androidTest/java/com/fastcomments/SyncClient.java index ad7ab7b..ec61123 100644 --- a/app/src/androidTest/java/com/fastcomments/SyncClient.java +++ b/app/src/androidTest/java/com/fastcomments/SyncClient.java @@ -1,15 +1,13 @@ package com.fastcomments; -import org.json.JSONObject; - import java.io.IOException; import java.util.concurrent.TimeUnit; - import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; +import org.json.JSONObject; /** * HTTP client for coordinating dual-emulator tests via the sync server. @@ -30,7 +28,7 @@ public SyncClient(String syncUrl, String role) { this.role = role; this.client = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(180, TimeUnit.SECONDS) // /wait can block up to 120s + .readTimeout(180, TimeUnit.SECONDS) // /wait can block up to 120s .writeTimeout(30, TimeUnit.SECONDS) .build(); } diff --git a/app/src/androidTest/java/com/fastcomments/ThreadingUITests.java b/app/src/androidTest/java/com/fastcomments/ThreadingUITests.java index 23ae8f3..e02a619 100644 --- a/app/src/androidTest/java/com/fastcomments/ThreadingUITests.java +++ b/app/src/androidTest/java/com/fastcomments/ThreadingUITests.java @@ -13,11 +13,8 @@ import static org.junit.Assert.assertTrue; import android.util.Log; - import androidx.test.ext.junit.runners.AndroidJUnit4; - import com.fastcomments.sdk.R; - import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -56,8 +53,7 @@ public void testReplyToComment() throws Exception { } }); - onView(withId(R.id.commentInput)) - .perform(click(), typeText("Parent comment"), closeSoftKeyboard()); + onView(withId(R.id.commentInput)).perform(click(), typeText("Parent comment"), closeSoftKeyboard()); onView(withId(R.id.sendButton)).perform(click()); // Wait for parent to appear @@ -86,8 +82,7 @@ public void testReplyToComment() throws Exception { // Type and submit reply Log.d(TAG, "Typing reply..."); - onView(withId(R.id.commentInput)) - .perform(click(), typeText("This is a reply"), closeSoftKeyboard()); + onView(withId(R.id.commentInput)).perform(click(), typeText("This is a reply"), closeSoftKeyboard()); Thread.sleep(500); // Let keyboard dismiss Log.d(TAG, "Clicking send for reply..."); onView(withId(R.id.sendButton)).perform(click()); diff --git a/app/src/androidTest/java/com/fastcomments/UITestBase.java b/app/src/androidTest/java/com/fastcomments/UITestBase.java index 024d27b..d1afb6a 100644 --- a/app/src/androidTest/java/com/fastcomments/UITestBase.java +++ b/app/src/androidTest/java/com/fastcomments/UITestBase.java @@ -1,34 +1,29 @@ package com.fastcomments; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import android.content.Intent; import android.os.Bundle; - import androidx.test.core.app.ActivityScenario; import androidx.test.platform.app.InstrumentationRegistry; - import com.fastcomments.core.sso.FastCommentsSSO; import com.fastcomments.core.sso.SecureSSOUserData; import com.fastcomments.model.APIError; import com.fastcomments.model.CreateFeedPostParams; import com.fastcomments.model.FeedPost; import com.fastcomments.sdk.FCCallback; - -import org.json.JSONArray; -import org.json.JSONObject; -import org.junit.After; -import org.junit.Before; - import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; - import okhttp3.Cookie; import okhttp3.CookieJar; import okhttp3.FormBody; @@ -38,10 +33,10 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; /** * Base class for UI tests. @@ -132,8 +127,7 @@ protected void createTestTenant(String email) throws Exception { // 2. Get tenant ID via e2e test API Request tenantRequest = new Request.Builder() - .url(HOST + "/test-e2e/api/tenant/by-email/" + testTenantEmail - + "?API_KEY=" + e2eApiKey) + .url(HOST + "/test-e2e/api/tenant/by-email/" + testTenantEmail + "?API_KEY=" + e2eApiKey) .get() .build(); @@ -177,14 +171,14 @@ public void tearDown() throws Exception { if (testTenantEmail != null && e2eApiKey != null && !e2eApiKey.isEmpty()) { try { Request deleteRequest = new Request.Builder() - .url(HOST + "/test-e2e/api/tenant/by-email/" + testTenantEmail - + "?API_KEY=" + e2eApiKey) + .url(HOST + "/test-e2e/api/tenant/by-email/" + testTenantEmail + "?API_KEY=" + e2eApiKey) .delete() .build(); try (Response ignored = httpClient.newCall(deleteRequest).execute()) { // Best effort } - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } } @@ -199,7 +193,8 @@ private void deleteTenantByEmail(String email) { try (Response ignored = httpClient.newCall(request).execute()) { // Best effort — tenant may not exist yet } - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } // ---- SSO ---- @@ -214,8 +209,7 @@ protected String makeSecureSSOToken(String userId, boolean isAdmin) { userId, "tester-" + userId.substring(0, Math.min(8, userId.length())) + "@fctest.com", "Tester " + userId.substring(0, Math.min(6, userId.length())), - "" - ); + ""); if (isAdmin) { userData.isAdmin = true; } @@ -232,10 +226,7 @@ protected void launchActivity(String urlId, String ssoToken) { if (scenario != null) { scenario.close(); } - Intent intent = new Intent( - InstrumentationRegistry.getInstrumentation().getTargetContext(), - TestActivity.class - ); + Intent intent = new Intent(InstrumentationRegistry.getInstrumentation().getTargetContext(), TestActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra("tenantId", testTenantId); intent.putExtra("urlId", urlId); @@ -247,10 +238,8 @@ protected void launchLiveChatActivity(String urlId, String ssoToken) { if (liveChatScenario != null) { liveChatScenario.close(); } - Intent intent = new Intent( - InstrumentationRegistry.getInstrumentation().getTargetContext(), - TestLiveChatActivity.class - ); + Intent intent = + new Intent(InstrumentationRegistry.getInstrumentation().getTargetContext(), TestLiveChatActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra("tenantId", testTenantId); intent.putExtra("urlId", urlId); @@ -262,10 +251,8 @@ protected void launchFeedActivity(String urlId, String ssoToken) { if (feedScenario != null) { feedScenario.close(); } - Intent intent = new Intent( - InstrumentationRegistry.getInstrumentation().getTargetContext(), - TestFeedActivity.class - ); + Intent intent = + new Intent(InstrumentationRegistry.getInstrumentation().getTargetContext(), TestFeedActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra("tenantId", testTenantId); intent.putExtra("urlId", urlId); @@ -376,8 +363,7 @@ protected String fetchLatestCommentId(String urlId) { for (int attempt = 1; attempt <= 3; attempt++) { try { Request request = new Request.Builder() - .url(HOST + "/api/v1/comments?tenantId=" + testTenantId - + "&urlId=" + urlId + "&limit=1") + .url(HOST + "/api/v1/comments?tenantId=" + testTenantId + "&urlId=" + urlId + "&limit=1") .addHeader("x-api-key", testTenantApiKey) .get() .build(); @@ -399,7 +385,10 @@ protected String fetchLatestCommentId(String urlId) { } if (attempt < 3) { - try { Thread.sleep(500); } catch (InterruptedException ignored) {} + try { + Thread.sleep(500); + } catch (InterruptedException ignored) { + } } } return null; @@ -420,7 +409,8 @@ protected boolean adminUpdateComment(String commentId, JSONObject params) { try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { String body = response.body() != null ? response.body().string() : ""; - fail("adminUpdateComment failed: status=" + response.code() + " body=" + body.substring(0, Math.min(200, body.length()))); + fail("adminUpdateComment failed: status=" + response.code() + " body=" + + body.substring(0, Math.min(200, body.length()))); return false; } } @@ -437,16 +427,20 @@ protected void pinComment(String commentId, String adminSsoToken) { String broadcastId = UUID.randomUUID().toString(); String encodedSso = java.net.URLEncoder.encode(adminSsoToken, "UTF-8"); Request request = new Request.Builder() - .url(HOST + "/comments/" + testTenantId + "/" + commentId + "/pin" - + "?broadcastId=" + broadcastId + "&sso=" + encodedSso) + .url(HOST + "/comments/" + testTenantId + "/" + commentId + "/pin" + "?broadcastId=" + broadcastId + + "&sso=" + encodedSso) .post(RequestBody.create("", null)) .build(); try (Response response = httpClient.newCall(request).execute()) { String body = response.body() != null ? response.body().string() : ""; - android.util.Log.d("UITestBase", "pinComment response: " + response.code() + " body=" + body.substring(0, Math.min(200, body.length()))); + android.util.Log.d( + "UITestBase", + "pinComment response: " + response.code() + " body=" + + body.substring(0, Math.min(200, body.length()))); if (!response.isSuccessful()) { - fail("pinComment failed: status=" + response.code() + " body=" + body.substring(0, Math.min(200, body.length()))); + fail("pinComment failed: status=" + response.code() + " body=" + + body.substring(0, Math.min(200, body.length()))); } } } catch (Exception e) { @@ -460,15 +454,16 @@ protected void lockComment(String commentId, String adminSsoToken) { String broadcastId = UUID.randomUUID().toString(); String encodedSso = java.net.URLEncoder.encode(adminSsoToken, "UTF-8"); Request request = new Request.Builder() - .url(HOST + "/comments/" + testTenantId + "/" + commentId + "/lock" - + "?broadcastId=" + broadcastId + "&sso=" + encodedSso) + .url(HOST + "/comments/" + testTenantId + "/" + commentId + "/lock" + "?broadcastId=" + broadcastId + + "&sso=" + encodedSso) .post(RequestBody.create("", null)) .build(); try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { String body = response.body() != null ? response.body().string() : ""; - fail("lockComment failed: status=" + response.code() + " body=" + body.substring(0, Math.min(200, body.length()))); + fail("lockComment failed: status=" + response.code() + " body=" + + body.substring(0, Math.min(200, body.length()))); } } } catch (Exception e) { @@ -492,7 +487,10 @@ protected void pollUntil(long timeoutMs, PollCondition condition) { if (System.currentTimeMillis() > deadline) { return; } - try { Thread.sleep(50); } catch (InterruptedException ignored) {} + try { + Thread.sleep(50); + } catch (InterruptedException ignored) { + } } } diff --git a/app/src/androidTest/java/com/fastcomments/VoteUITests.java b/app/src/androidTest/java/com/fastcomments/VoteUITests.java index b258a64..fa64fcb 100644 --- a/app/src/androidTest/java/com/fastcomments/VoteUITests.java +++ b/app/src/androidTest/java/com/fastcomments/VoteUITests.java @@ -13,9 +13,7 @@ import static org.junit.Assert.assertTrue; import androidx.test.ext.junit.runners.AndroidJUnit4; - import com.fastcomments.sdk.R; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -47,14 +45,12 @@ private void postCommentAndWait(String text) throws Exception { } }); - onView(withId(R.id.commentInput)) - .perform(click(), typeText(text), closeSoftKeyboard()); + onView(withId(R.id.commentInput)).perform(click(), typeText(text), closeSoftKeyboard()); onView(withId(R.id.sendButton)).perform(click()); pollUntil(10000, () -> { try { - onView(withId(R.id.recyclerViewComments)) - .check(matches(hasDescendant(withText(containsString(text))))); + onView(withId(R.id.recyclerViewComments)).check(matches(hasDescendant(withText(containsString(text))))); return true; } catch (Exception | AssertionError e) { return false; diff --git a/app/src/androidTest/java/com/fastcomments/WebSocketConcurrentTest.java b/app/src/androidTest/java/com/fastcomments/WebSocketConcurrentTest.java index 7a5c760..0d3b66f 100644 --- a/app/src/androidTest/java/com/fastcomments/WebSocketConcurrentTest.java +++ b/app/src/androidTest/java/com/fastcomments/WebSocketConcurrentTest.java @@ -1,14 +1,15 @@ package com.fastcomments; +import static org.junit.Assert.*; + import android.util.Log; import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; import java.util.Collections; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import okhttp3.*; -import static org.junit.Assert.*; +import org.junit.Test; +import org.junit.runner.RunWith; /** * Test whether concurrent HTTP API calls kill the WebSocket connection. @@ -20,8 +21,10 @@ public class WebSocketConcurrentTest { @Test public void testWsSurvivesConcurrentHttpCalls() throws Exception { - String wsUrl = "wss://ws.fastcomments.com/sub?urlId=demo%3Ahttps%3A%2F%2Ffastcomments.com%2F&userIdWS=conctest&tenantIdWS=demo"; - String apiUrl = "https://fastcomments.com/comments/demo/?urlId=https%3A%2F%2Ffastcomments.com%2F&direction=NF&count=5"; + String wsUrl = + "wss://ws.fastcomments.com/sub?urlId=demo%3Ahttps%3A%2F%2Ffastcomments.com%2F&userIdWS=conctest&tenantIdWS=demo"; + String apiUrl = + "https://fastcomments.com/comments/demo/?urlId=https%3A%2F%2Ffastcomments.com%2F&direction=NF&count=5"; // WS client with its own dispatcher and pool (same as SDK's LiveEventSubscriber) OkHttpClient wsClient = new OkHttpClient.Builder() @@ -45,13 +48,17 @@ public void testWsSurvivesConcurrentHttpCalls() throws Exception { public void onOpen(WebSocket ws, Response r) { Log.d(TAG, "WS OPEN"); } + public void onMessage(WebSocket ws, String t) { double elapsed = (System.currentTimeMillis() - start) / 1000.0; Log.d(TAG, String.format("[%.1fs] WS MSG: %s", elapsed, t.substring(0, Math.min(60, t.length())))); } + public void onFailure(WebSocket ws, Throwable t, Response r) { diedAt[0] = System.currentTimeMillis() - start; - Log.d(TAG, "WS DIED after " + diedAt[0] + "ms: " + t.getClass().getSimpleName() + ": " + t.getMessage()); + Log.d( + TAG, + "WS DIED after " + diedAt[0] + "ms: " + t.getClass().getSimpleName() + ": " + t.getMessage()); latch.countDown(); } }); diff --git a/app/src/androidTest/java/com/fastcomments/WebSocketFromCallbackTest.java b/app/src/androidTest/java/com/fastcomments/WebSocketFromCallbackTest.java index 1b322c5..dde4ac0 100644 --- a/app/src/androidTest/java/com/fastcomments/WebSocketFromCallbackTest.java +++ b/app/src/androidTest/java/com/fastcomments/WebSocketFromCallbackTest.java @@ -1,21 +1,22 @@ package com.fastcomments; +import static org.junit.Assert.*; + import android.util.Log; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.fastcomments.core.CommentWidgetConfig; import com.fastcomments.pubsub.LiveEventSubscriber; import com.fastcomments.pubsub.SubscribeToChangesResult; -import org.json.JSONObject; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; import java.io.IOException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import okhttp3.*; -import static org.junit.Assert.*; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; /** * Calls subscribeToChanges from WITHIN an OkHttp async callback, @@ -70,7 +71,9 @@ public void onResponse(Call call, Response response) throws IOException { String tenantIdWS = json.optString("tenantIdWS", ""); String urlIdWS = json.optString("urlIdWS", ""); String userIdWS = json.optString("userIdWS", ""); - Log.d(TAG, "API response on thread: " + Thread.currentThread().getName()); + Log.d( + TAG, + "API response on thread: " + Thread.currentThread().getName()); Log.d(TAG, "WS params: " + tenantIdWS + " / " + urlIdWS + " / " + userIdWS); // Create WS from within THIS callback (OkHttp dispatcher thread) @@ -86,9 +89,8 @@ public void onResponse(Call call, Response response) throws IOException { } }); - SubscribeToChangesResult result = subscriber.subscribeToChanges( - config, tenantIdWS, urlId, urlIdWS, userIdWS, - null, event -> { + SubscribeToChangesResult result = + subscriber.subscribeToChanges(config, tenantIdWS, urlId, urlIdWS, userIdWS, null, event -> { double elapsed = (System.currentTimeMillis() - start) / 1000.0; Log.d(TAG, String.format("[%.1fs] Event: %s", elapsed, event.getType())); }); diff --git a/app/src/androidTest/java/com/fastcomments/WebSocketSdkFlowTest.java b/app/src/androidTest/java/com/fastcomments/WebSocketSdkFlowTest.java index f019553..bb909da 100644 --- a/app/src/androidTest/java/com/fastcomments/WebSocketSdkFlowTest.java +++ b/app/src/androidTest/java/com/fastcomments/WebSocketSdkFlowTest.java @@ -1,25 +1,21 @@ package com.fastcomments; +import static org.junit.Assert.*; + import android.util.Log; import androidx.test.ext.junit.runners.AndroidJUnit4; - import com.fastcomments.core.CommentWidgetConfig; import com.fastcomments.pubsub.LiveEventSubscriber; import com.fastcomments.pubsub.SubscribeToChangesResult; - -import org.json.JSONObject; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; - import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; - -import static org.junit.Assert.*; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; /** * Uses the actual LiveEventSubscriber.createTesting() and subscribeToChanges() @@ -91,8 +87,7 @@ public void testSubscribeToChangesDirectly() throws Exception { event -> { double elapsed = (System.currentTimeMillis() - start) / 1000.0; Log.d(TAG, String.format("[%.1fs] Event: type=%s", elapsed, event.getType())); - } - ); + }); assertNotNull("subscribeToChanges should return a result", result); Log.d(TAG, "subscribeToChanges returned, waiting 30s..."); diff --git a/app/src/debug/java/com/fastcomments/TestActivity.java b/app/src/debug/java/com/fastcomments/TestActivity.java index fc04c5c..eee964b 100644 --- a/app/src/debug/java/com/fastcomments/TestActivity.java +++ b/app/src/debug/java/com/fastcomments/TestActivity.java @@ -1,9 +1,7 @@ package com.fastcomments; import android.os.Bundle; - import androidx.appcompat.app.AppCompatActivity; - import com.fastcomments.core.CommentWidgetConfig; import com.fastcomments.model.SortDirections; import com.fastcomments.sdk.FastCommentsSDK; diff --git a/app/src/debug/java/com/fastcomments/TestFeedActivity.java b/app/src/debug/java/com/fastcomments/TestFeedActivity.java index 8d3d3ff..6bdc038 100644 --- a/app/src/debug/java/com/fastcomments/TestFeedActivity.java +++ b/app/src/debug/java/com/fastcomments/TestFeedActivity.java @@ -2,9 +2,7 @@ import android.os.Bundle; import android.util.Log; - import androidx.appcompat.app.AppCompatActivity; - import com.fastcomments.core.CommentWidgetConfig; import com.fastcomments.model.FeedPost; import com.fastcomments.sdk.FastCommentsFeedSDK; diff --git a/app/src/debug/java/com/fastcomments/TestLiveChatActivity.java b/app/src/debug/java/com/fastcomments/TestLiveChatActivity.java index 47b52b5..7db88c6 100644 --- a/app/src/debug/java/com/fastcomments/TestLiveChatActivity.java +++ b/app/src/debug/java/com/fastcomments/TestLiveChatActivity.java @@ -1,11 +1,8 @@ package com.fastcomments; import android.os.Bundle; - import androidx.appcompat.app.AppCompatActivity; - import com.fastcomments.core.CommentWidgetConfig; -import com.fastcomments.model.SortDirections; import com.fastcomments.sdk.FastCommentsSDK; import com.fastcomments.sdk.LiveChatView; diff --git a/app/src/main/java/com/fastcomments/DemoBrowserActivity.kt b/app/src/main/java/com/fastcomments/DemoBrowserActivity.kt index ee11c01..7e80d84 100644 --- a/app/src/main/java/com/fastcomments/DemoBrowserActivity.kt +++ b/app/src/main/java/com/fastcomments/DemoBrowserActivity.kt @@ -19,8 +19,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Chat import androidx.compose.material.icons.automirrored.filled.Article +import androidx.compose.material.icons.automirrored.filled.Chat import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Forum import androidx.compose.material.icons.filled.GridView @@ -50,12 +50,12 @@ private data class DemoItem( val iconColor: Color, val title: String, val description: String, - val activityClass: Class + val activityClass: Class, ) private data class DemoSection( val title: String, - val items: List + val items: List, ) private val demoSections = listOf( @@ -67,23 +67,23 @@ private val demoSections = listOf( iconColor = Color(0xFF2196F3), title = "Threaded Comments", description = "Live threaded commenting with SSO.", - activityClass = MainActivity::class.java + activityClass = MainActivity::class.java, ), DemoItem( icon = Icons.AutoMirrored.Filled.Chat, iconColor = Color(0xFF4CAF50), title = "Live Chat", description = "A streaming chat UI optimized for high volume discussions.", - activityClass = LiveChatExampleActivity::class.java + activityClass = LiveChatExampleActivity::class.java, ), DemoItem( icon = Icons.Filled.Edit, iconColor = Color(0xFF3F51B5), title = "Custom Toolbar", description = "Global and per-instance custom toolbar buttons", - activityClass = ToolbarShowcaseActivity::class.java - ) - ) + activityClass = ToolbarShowcaseActivity::class.java, + ), + ), ), DemoSection( title = "Feed", @@ -93,16 +93,16 @@ private val demoSections = listOf( iconColor = Color(0xFFFF9800), title = "Social Feed", description = "Social Feed with SSO Configured", - activityClass = FeedExampleActivity::class.java + activityClass = FeedExampleActivity::class.java, ), DemoItem( icon = Icons.Filled.GridView, iconColor = Color(0xFF9C27B0), title = "Feed Custom Buttons", description = "Custom toolbar buttons on the post creation form", - activityClass = FeedExampleCustomButtonsActivity::class.java - ) - ) + activityClass = FeedExampleCustomButtonsActivity::class.java, + ), + ), ), DemoSection( title = "Authentication", @@ -112,17 +112,17 @@ private val demoSections = listOf( iconColor = Color(0xFF009688), title = "Simple SSO", description = "Client-side SSO for demos and testing", - activityClass = SimpleSSOExampleActivity::class.java + activityClass = SimpleSSOExampleActivity::class.java, ), DemoItem( icon = Icons.Filled.Lock, iconColor = Color(0xFFF44336), title = "Secure SSO", description = "Production SSO with server-side token generation", - activityClass = SecureSSOExampleActivity::class.java - ) - ) - ) + activityClass = SecureSSOExampleActivity::class.java, + ), + ), + ), ) class DemoBrowserActivity : ComponentActivity() { @@ -133,7 +133,7 @@ class DemoBrowserActivity : ComponentActivity() { DemoBrowserScreen( onDemoSelected = { item -> startActivity(Intent(this, item.activityClass)) - } + }, ) } } @@ -146,15 +146,15 @@ private fun DemoBrowserScreen(onDemoSelected: (DemoItem) -> Unit) { Scaffold( topBar = { TopAppBar( - title = { Text("FastComments") } + title = { Text("FastComments") }, ) - } + }, ) { padding -> LazyColumn( modifier = Modifier .fillMaxSize() .padding(padding), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), ) { demoSections.forEach { section -> item { @@ -162,13 +162,13 @@ private fun DemoBrowserScreen(onDemoSelected: (DemoItem) -> Unit) { text = section.title.uppercase(), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = 24.dp, bottom = 8.dp) + modifier = Modifier.padding(top = 24.dp, bottom = 8.dp), ) } items(section.items) { demoItem -> DemoItemRow( item = demoItem, - onClick = { onDemoSelected(demoItem) } + onClick = { onDemoSelected(demoItem) }, ) } } @@ -181,14 +181,14 @@ private fun DemoItemRow(item: DemoItem, onClick: () -> Unit) { Surface( onClick = onClick, shape = MaterialTheme.shapes.medium, - color = MaterialTheme.colorScheme.surface + color = MaterialTheme.colorScheme.surface, ) { Row( modifier = Modifier .fillMaxWidth() .padding(vertical = 12.dp, horizontal = 4.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(14.dp) + horizontalArrangement = Arrangement.spacedBy(14.dp), ) { Box( modifier = Modifier @@ -196,30 +196,30 @@ private fun DemoItemRow(item: DemoItem, onClick: () -> Unit) { .clip(RoundedCornerShape(8.dp)) .background( Brush.verticalGradient( - colors = listOf(item.iconColor, item.iconColor.copy(alpha = 0.8f)) - ) + colors = listOf(item.iconColor, item.iconColor.copy(alpha = 0.8f)), + ), ), - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, ) { Icon( imageVector = item.icon, contentDescription = null, tint = Color.White, - modifier = Modifier.size(18.dp) + modifier = Modifier.size(18.dp), ) } Column { Text( text = item.title, style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.SemiBold + fontWeight = FontWeight.SemiBold, ) Text( text = item.description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 2, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) } } diff --git a/app/src/main/java/com/fastcomments/FeedExampleActivity.java b/app/src/main/java/com/fastcomments/FeedExampleActivity.java index 5412bf3..b988d4b 100644 --- a/app/src/main/java/com/fastcomments/FeedExampleActivity.java +++ b/app/src/main/java/com/fastcomments/FeedExampleActivity.java @@ -5,13 +5,11 @@ import android.os.Looper; import android.view.View; import android.widget.Toast; - import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.AppCompatActivity; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; - import com.fastcomments.core.CommentWidgetConfig; import com.fastcomments.core.sso.FastCommentsSSO; import com.fastcomments.core.sso.SimpleSSOUserData; @@ -22,13 +20,9 @@ import com.fastcomments.sdk.FeedPostCreateView; import com.fastcomments.sdk.FollowStateProvider; import com.fastcomments.sdk.OnUserClickListener; -import com.fastcomments.sdk.TagSupplier; -import com.fastcomments.sdk.UserClickContext; import com.fastcomments.sdk.UserClickSource; import com.fastcomments.sdk.UserInfo; import com.google.android.material.floatingactionbutton.FloatingActionButton; - -import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -46,7 +40,7 @@ public class FeedExampleActivity extends AppCompatActivity { private final Handler mainHandler = new Handler(Looper.getMainLooper()); // Image picker launcher - private final ActivityResultLauncher pickImageLauncher = + private final ActivityResultLauncher pickImageLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), uri -> { if (uri != null && postCreateView != null) { postCreateView.handleImageResult(uri); @@ -63,13 +57,12 @@ protected void onCreate(Bundle savedInstanceState) { config.tenantId = "demo"; // Use your tenant ID here config.urlId = "https://example.com/page1"; // Use your URL ID here config.pageTitle = "Feed Example"; - - // Set up Simple SSO for user authentication. In production you probably want to use SecureSSO and create a token from your server. + + // Set up Simple SSO for user authentication. In production you probably want to use SecureSSO and create a + // token from your server. SimpleSSOUserData userData = new SimpleSSOUserData( - "Example User", - "user@example.com", - "https://staticm.fastcomments.com/1639362726066-DSC_0841.JPG"); - + "Example User", "user@example.com", "https://staticm.fastcomments.com/1639362726066-DSC_0841.JPG"); + FastCommentsSSO sso = new FastCommentsSSO(userData); config.sso = sso.prepareToSend(); @@ -86,29 +79,33 @@ public boolean isFollowing(UserInfo user) { } @Override - public void onFollowStateChangeRequested(UserInfo user, boolean desiredFollowing, - FollowStateCallback resultCallback) { + public void onFollowStateChangeRequested( + UserInfo user, boolean desiredFollowing, FollowStateCallback resultCallback) { // Simulate a 2s network round-trip. - mainHandler.postDelayed(() -> { - if (desiredFollowing) { - followedUserIds.add(user.getUserId()); - } else { - followedUserIds.remove(user.getUserId()); - } - Toast.makeText(FeedExampleActivity.this, - (desiredFollowing ? "Followed " : "Unfollowed ") + user.getDisplayName(), - Toast.LENGTH_SHORT).show(); - resultCallback.onResult(desiredFollowing); - }, 2000L); + mainHandler.postDelayed( + () -> { + if (desiredFollowing) { + followedUserIds.add(user.getUserId()); + } else { + followedUserIds.remove(user.getUserId()); + } + Toast.makeText( + FeedExampleActivity.this, + (desiredFollowing ? "Followed " : "Unfollowed ") + user.getDisplayName(), + Toast.LENGTH_SHORT) + .show(); + resultCallback.onResult(desiredFollowing); + }, + 2000L); } }); // Find the feed view in the layout feedView = findViewById(R.id.feedView); - + // Set the SDK instance for the view feedView.setSDK(feedSDK); - + feedView.setTagSupplier(currentUser -> { // You can customize the user's experience. Only feed items with the same tags will be returned. // return null or don't set a supplier to get a "global" feed. @@ -131,15 +128,16 @@ public void onFeedLoaded(List posts) { public void onFeedError(String errorMessage) { // Error loading feed // Consumer can handle this error as needed - Toast.makeText(FeedExampleActivity.this, errorMessage, Toast.LENGTH_SHORT).show(); + Toast.makeText(FeedExampleActivity.this, errorMessage, Toast.LENGTH_SHORT) + .show(); } @Override public void onPostSelected(FeedPost post) { // User selected a post - // Here you would typically navigate to a detail view + // Here you would typically navigate to a detail view // or show comments for this post - + // In a real app, you might do: // navigateToPostDetail(post); // or @@ -150,7 +148,7 @@ public void onPostSelected(FeedPost post) { public void onCommentsRequested(FeedPost post) { // Show comments dialog for the post from the SDK CommentsDialog dialog = new CommentsDialog(FeedExampleActivity.this, post, feedSDK); - + // Set comment added listener to update the post in the feed dialog.setOnCommentAddedListener(postId -> { // Post stats already updated in feedSDK, just need to refresh UI @@ -159,13 +157,13 @@ public void onCommentsRequested(FeedPost post) { feedView.refreshPost(postId); }); }); - + dialog.setOnUserClickListener(userClickListener); - + dialog.show(); } }); - + feedView.setOnUserClickListener(userClickListener); // Load the feed @@ -176,9 +174,11 @@ private void setupUserClickListener() { userClickListener = (context, userInfo, source) -> { String sourceText = source == UserClickSource.NAME ? "name" : "avatar"; String contextText = context.isComment() ? "comment" : "feed post"; - Toast.makeText(FeedExampleActivity.this, - "Clicked " + userInfo.getDisplayName() + "'s " + sourceText + " in " + contextText, - Toast.LENGTH_SHORT).show(); + Toast.makeText( + FeedExampleActivity.this, + "Clicked " + userInfo.getDisplayName() + "'s " + sourceText + " in " + contextText, + Toast.LENGTH_SHORT) + .show(); }; } @@ -197,20 +197,22 @@ private void setupPostCreationView() { // Create constraints for post creation view (full width at top, floating above content) ConstraintLayout.LayoutParams postCreateParams = new ConstraintLayout.LayoutParams( - ConstraintLayout.LayoutParams.MATCH_PARENT, - ConstraintLayout.LayoutParams.WRAP_CONTENT); + ConstraintLayout.LayoutParams.MATCH_PARENT, ConstraintLayout.LayoutParams.WRAP_CONTENT); postCreateView.setLayoutParams(postCreateParams); postCreateView.setElevation(16f); // Elevate above other content - + // Add view to parent parentLayout.addView(postCreateView); // Update constraints to position the view at the top ConstraintSet initialConstraintSet = new ConstraintSet(); initialConstraintSet.clone(parentLayout); - initialConstraintSet.connect(postCreateView.getId(), ConstraintSet.TOP, parentLayout.getId(), ConstraintSet.TOP, 0); - initialConstraintSet.connect(postCreateView.getId(), ConstraintSet.START, parentLayout.getId(), ConstraintSet.START, 0); - initialConstraintSet.connect(postCreateView.getId(), ConstraintSet.END, parentLayout.getId(), ConstraintSet.END, 0); + initialConstraintSet.connect( + postCreateView.getId(), ConstraintSet.TOP, parentLayout.getId(), ConstraintSet.TOP, 0); + initialConstraintSet.connect( + postCreateView.getId(), ConstraintSet.START, parentLayout.getId(), ConstraintSet.START, 0); + initialConstraintSet.connect( + postCreateView.getId(), ConstraintSet.END, parentLayout.getId(), ConstraintSet.END, 0); initialConstraintSet.applyTo(parentLayout); // Create FAB @@ -221,8 +223,7 @@ private void setupPostCreationView() { // Create params for FAB ConstraintLayout.LayoutParams fabParams = new ConstraintLayout.LayoutParams( - ConstraintLayout.LayoutParams.WRAP_CONTENT, - ConstraintLayout.LayoutParams.WRAP_CONTENT); + ConstraintLayout.LayoutParams.WRAP_CONTENT, ConstraintLayout.LayoutParams.WRAP_CONTENT); createPostFab.setLayoutParams(fabParams); // Add FAB to parent @@ -231,7 +232,8 @@ private void setupPostCreationView() { // Update constraints to position the FAB at bottom-end ConstraintSet fabConstraintSet = new ConstraintSet(); fabConstraintSet.clone(parentLayout); - fabConstraintSet.connect(createPostFab.getId(), ConstraintSet.BOTTOM, parentLayout.getId(), ConstraintSet.BOTTOM, 32); + fabConstraintSet.connect( + createPostFab.getId(), ConstraintSet.BOTTOM, parentLayout.getId(), ConstraintSet.BOTTOM, 32); fabConstraintSet.connect(createPostFab.getId(), ConstraintSet.END, parentLayout.getId(), ConstraintSet.END, 32); fabConstraintSet.applyTo(parentLayout); @@ -240,7 +242,7 @@ private void setupPostCreationView() { // Configure post creation view and prepare it for showing postCreateView.show(); createPostFab.setVisibility(View.GONE); - + // Apply animation to slide it down postCreateView.startAnimation(android.view.animation.AnimationUtils.loadAnimation( FeedExampleActivity.this, com.fastcomments.sdk.R.anim.slide_down_from_top)); @@ -253,13 +255,13 @@ public void onPostCreated(FeedPost post) { // Use our new slide up and fade animation android.view.animation.Animation slideUpFade = android.view.animation.AnimationUtils.loadAnimation( FeedExampleActivity.this, com.fastcomments.sdk.R.anim.slide_up_and_fade); - + slideUpFade.setAnimationListener(new android.view.animation.Animation.AnimationListener() { @Override public void onAnimationStart(android.view.animation.Animation animation) { // No need to change visibility yet } - + @Override public void onAnimationEnd(android.view.animation.Animation animation) { // Make form completely gone and unclickable @@ -267,27 +269,28 @@ public void onAnimationEnd(android.view.animation.Animation animation) { postCreateView.setVisibility(View.GONE); postCreateView.setClickable(false); postCreateView.setEnabled(false); - + // Show FAB button createPostFab.setVisibility(View.VISIBLE); - + // Refresh the feed to show the new post feedView.refresh(); } - + @Override public void onAnimationRepeat(android.view.animation.Animation animation) {} }); - + postCreateView.startAnimation(slideUpFade); - + // Refresh the feed to show the new post feedView.refresh(); } @Override public void onPostCreateError(String errorMessage) { - Toast.makeText(FeedExampleActivity.this, errorMessage, Toast.LENGTH_SHORT).show(); + Toast.makeText(FeedExampleActivity.this, errorMessage, Toast.LENGTH_SHORT) + .show(); } @Override @@ -295,13 +298,13 @@ public void onPostCreateCancelled() { // Use our new slide up and fade animation android.view.animation.Animation slideUpFade = android.view.animation.AnimationUtils.loadAnimation( FeedExampleActivity.this, com.fastcomments.sdk.R.anim.slide_up_and_fade); - + slideUpFade.setAnimationListener(new android.view.animation.Animation.AnimationListener() { @Override public void onAnimationStart(android.view.animation.Animation animation) { // No need to change visibility yet } - + @Override public void onAnimationEnd(android.view.animation.Animation animation) { // Make form completely gone and unclickable @@ -309,15 +312,15 @@ public void onAnimationEnd(android.view.animation.Animation animation) { postCreateView.setVisibility(View.GONE); postCreateView.setClickable(false); postCreateView.setEnabled(false); - + // Show FAB button createPostFab.setVisibility(View.VISIBLE); } - + @Override public void onAnimationRepeat(android.view.animation.Animation animation) {} }); - + postCreateView.startAnimation(slideUpFade); } @@ -328,7 +331,7 @@ public void onImagePickerRequested() { } }); } - + @Override protected void onDestroy() { super.onDestroy(); @@ -341,4 +344,4 @@ protected void onDestroy() { feedView.cleanup(); } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/fastcomments/FeedExampleCustomButtonsActivity.java b/app/src/main/java/com/fastcomments/FeedExampleCustomButtonsActivity.java index e38214e..9bb923c 100644 --- a/app/src/main/java/com/fastcomments/FeedExampleCustomButtonsActivity.java +++ b/app/src/main/java/com/fastcomments/FeedExampleCustomButtonsActivity.java @@ -3,13 +3,11 @@ import android.os.Bundle; import android.view.View; import android.widget.Toast; - import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.AppCompatActivity; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; - import com.fastcomments.core.CommentWidgetConfig; import com.fastcomments.core.sso.FastCommentsSSO; import com.fastcomments.core.sso.SimpleSSOUserData; @@ -22,7 +20,6 @@ import com.fastcomments.sdk.UserClickSource; import com.fastcomments.sdk.examples.GifPickerFeedToolbarButton; import com.google.android.material.floatingactionbutton.FloatingActionButton; - import java.util.List; /** @@ -105,7 +102,8 @@ public void onFeedLoaded(List posts) { @Override public void onFeedError(String errorMessage) { // Error loading feed - Toast.makeText(FeedExampleCustomButtonsActivity.this, errorMessage, Toast.LENGTH_SHORT).show(); + Toast.makeText(FeedExampleCustomButtonsActivity.this, errorMessage, Toast.LENGTH_SHORT) + .show(); } @Override @@ -158,9 +156,11 @@ private void setupUserClickListener() { userClickListener = (context, userInfo, source) -> { String sourceText = source == UserClickSource.NAME ? "name" : "avatar"; String contextText = context.isComment() ? "comment" : "feed post"; - Toast.makeText(FeedExampleCustomButtonsActivity.this, - "Clicked " + userInfo.getDisplayName() + "'s " + sourceText + " in " + contextText, - Toast.LENGTH_SHORT).show(); + Toast.makeText( + FeedExampleCustomButtonsActivity.this, + "Clicked " + userInfo.getDisplayName() + "'s " + sourceText + " in " + contextText, + Toast.LENGTH_SHORT) + .show(); }; } @@ -183,8 +183,7 @@ private void setupPostCreationView() { // Create constraints for post creation view (full width at top, floating above content) ConstraintLayout.LayoutParams postCreateParams = new ConstraintLayout.LayoutParams( - ConstraintLayout.LayoutParams.MATCH_PARENT, - ConstraintLayout.LayoutParams.WRAP_CONTENT); + ConstraintLayout.LayoutParams.MATCH_PARENT, ConstraintLayout.LayoutParams.WRAP_CONTENT); postCreateView.setLayoutParams(postCreateParams); postCreateView.setElevation(16f); // Elevate above other content @@ -194,9 +193,12 @@ private void setupPostCreationView() { // Update constraints to position the view at the top ConstraintSet initialConstraintSet = new ConstraintSet(); initialConstraintSet.clone(parentLayout); - initialConstraintSet.connect(postCreateView.getId(), ConstraintSet.TOP, parentLayout.getId(), ConstraintSet.TOP, 0); - initialConstraintSet.connect(postCreateView.getId(), ConstraintSet.START, parentLayout.getId(), ConstraintSet.START, 0); - initialConstraintSet.connect(postCreateView.getId(), ConstraintSet.END, parentLayout.getId(), ConstraintSet.END, 0); + initialConstraintSet.connect( + postCreateView.getId(), ConstraintSet.TOP, parentLayout.getId(), ConstraintSet.TOP, 0); + initialConstraintSet.connect( + postCreateView.getId(), ConstraintSet.START, parentLayout.getId(), ConstraintSet.START, 0); + initialConstraintSet.connect( + postCreateView.getId(), ConstraintSet.END, parentLayout.getId(), ConstraintSet.END, 0); initialConstraintSet.applyTo(parentLayout); // Create FAB @@ -207,8 +209,7 @@ private void setupPostCreationView() { // Create params for FAB ConstraintLayout.LayoutParams fabParams = new ConstraintLayout.LayoutParams( - ConstraintLayout.LayoutParams.WRAP_CONTENT, - ConstraintLayout.LayoutParams.WRAP_CONTENT); + ConstraintLayout.LayoutParams.WRAP_CONTENT, ConstraintLayout.LayoutParams.WRAP_CONTENT); createPostFab.setLayoutParams(fabParams); // Add FAB to parent @@ -217,7 +218,8 @@ private void setupPostCreationView() { // Update constraints to position the FAB at bottom-end ConstraintSet fabConstraintSet = new ConstraintSet(); fabConstraintSet.clone(parentLayout); - fabConstraintSet.connect(createPostFab.getId(), ConstraintSet.BOTTOM, parentLayout.getId(), ConstraintSet.BOTTOM, 32); + fabConstraintSet.connect( + createPostFab.getId(), ConstraintSet.BOTTOM, parentLayout.getId(), ConstraintSet.BOTTOM, 32); fabConstraintSet.connect(createPostFab.getId(), ConstraintSet.END, parentLayout.getId(), ConstraintSet.END, 32); fabConstraintSet.applyTo(parentLayout); @@ -273,7 +275,8 @@ public void onAnimationRepeat(android.view.animation.Animation animation) {} @Override public void onPostCreateError(String errorMessage) { - Toast.makeText(FeedExampleCustomButtonsActivity.this, errorMessage, Toast.LENGTH_SHORT).show(); + Toast.makeText(FeedExampleCustomButtonsActivity.this, errorMessage, Toast.LENGTH_SHORT) + .show(); } @Override @@ -323,4 +326,4 @@ protected void onDestroy() { feedView.cleanup(); } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/fastcomments/LiveChatExampleActivity.kt b/app/src/main/java/com/fastcomments/LiveChatExampleActivity.kt index 451da3a..f94b38c 100644 --- a/app/src/main/java/com/fastcomments/LiveChatExampleActivity.kt +++ b/app/src/main/java/com/fastcomments/LiveChatExampleActivity.kt @@ -6,12 +6,12 @@ import androidx.appcompat.app.AppCompatActivity import com.fastcomments.core.CommentWidgetConfig import com.fastcomments.core.sso.FastCommentsSSO import com.fastcomments.core.sso.SimpleSSOUserData -import com.fastcomments.sdk.LiveChatView import com.fastcomments.sdk.FastCommentsSDK +import com.fastcomments.sdk.LiveChatView import com.fastcomments.sdk.OnUserClickListener +import com.fastcomments.sdk.UserClickContext import com.fastcomments.sdk.UserClickSource import com.fastcomments.sdk.UserInfo -import com.fastcomments.sdk.UserClickContext /** * Example activity showing how to use the LiveChatView @@ -38,8 +38,8 @@ class LiveChatExampleActivity : AppCompatActivity() { val userData = SimpleSSOUserData( "Example User", "user@example.com", - "https://staticm.fastcomments.com/1639362726066-DSC_0841.JPG" - ); + "https://staticm.fastcomments.com/1639362726066-DSC_0841.JPG", + ) val sso = FastCommentsSSO(userData) config.sso = sso.prepareToSend() sdk = FastCommentsSDK(config) @@ -49,12 +49,14 @@ class LiveChatExampleActivity : AppCompatActivity() { // Set the SDK instance for the view liveChatView.setSDK(sdk) - + liveChatView.setOnUserClickListener { context, userInfo, source -> val sourceText = if (source == UserClickSource.NAME) "name" else "avatar" - Toast.makeText(this@LiveChatExampleActivity, - "Clicked ${userInfo.displayName}'s $sourceText in live chat", - Toast.LENGTH_SHORT).show() + Toast.makeText( + this@LiveChatExampleActivity, + "Clicked ${userInfo.displayName}'s $sourceText in live chat", + Toast.LENGTH_SHORT, + ).show() } // Load the chat @@ -66,7 +68,7 @@ class LiveChatExampleActivity : AppCompatActivity() { sdk.refreshLiveEvents() liveChatView.onResume() } - + override fun onPause() { super.onPause() liveChatView.onPause() @@ -77,4 +79,4 @@ class LiveChatExampleActivity : AppCompatActivity() { liveChatView.cleanup() sdk.cleanup() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/fastcomments/MainActivity.kt b/app/src/main/java/com/fastcomments/MainActivity.kt index f65bcab..cf74549 100644 --- a/app/src/main/java/com/fastcomments/MainActivity.kt +++ b/app/src/main/java/com/fastcomments/MainActivity.kt @@ -9,14 +9,14 @@ import com.fastcomments.sdk.FastCommentsSDK import com.fastcomments.sdk.FastCommentsTheme import com.fastcomments.sdk.FastCommentsView import com.fastcomments.sdk.OnUserClickListener +import com.fastcomments.sdk.UserClickContext import com.fastcomments.sdk.UserClickSource import com.fastcomments.sdk.UserInfo -import com.fastcomments.sdk.UserClickContext class MainActivity : AppCompatActivity() { - + private lateinit var commentsView: FastCommentsView - + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) @@ -27,29 +27,27 @@ class MainActivity : AppCompatActivity() { "https://example.com/page1", // Use your URL ID "Example Page", // Page title "example.com", // Domain - "Example Demo" // Site name + "Example Demo", // Site name ) - + // Optional configuration // config.voteStyle = VoteStyle.Heart // config.enableInfiniteScrolling = true - + // Initialize the SDK val sdk = FastCommentsSDK(config) - + // Optional: Set a custom theme programmatically val resources = resources val theme = FastCommentsTheme.Builder() .setPrimaryColor(resources.getColor(com.fastcomments.sdk.R.color.primary, null)) .setPrimaryLightColor(resources.getColor(com.fastcomments.sdk.R.color.primary_light, null)) .setPrimaryDarkColor(resources.getColor(com.fastcomments.sdk.R.color.primary_dark, null)) - // Button theming .setActionButtonColor(android.graphics.Color.parseColor("#FF1976D2")) .setReplyButtonColor(android.graphics.Color.parseColor("#FF4CAF50")) .setToggleRepliesButtonColor(android.graphics.Color.parseColor("#FFFF5722")) .setLoadMoreButtonTextColor(android.graphics.Color.parseColor("#FF9C27B0")) - // Other UI colors .setLinkColor(resources.getColor(com.fastcomments.sdk.R.color.fastcomments_link_color, null)) .setLinkColorPressed(resources.getColor(com.fastcomments.sdk.R.color.fastcomments_link_color_pressed, null)) @@ -60,26 +58,26 @@ class MainActivity : AppCompatActivity() { .setDialogHeaderTextColor(resources.getColor(com.fastcomments.sdk.R.color.fastcomments_dialog_header_text_color, null)) .setOnlineIndicatorColor(resources.getColor(com.fastcomments.sdk.R.color.fastcomments_online_indicator_color, null)) .build() - + // Apply the theme to the SDK sdk.setTheme(theme) // Find the comments view in the layout commentsView = findViewById(R.id.commentsView) - + // Set the SDK instance for the view commentsView.setSDK(sdk) - + commentsView.setOnUserClickListener { context, userInfo, source -> val sourceText = if (source == UserClickSource.NAME) "name" else "avatar" Toast.makeText(this@MainActivity, "Clicked ${userInfo.displayName}'s $sourceText", Toast.LENGTH_SHORT).show() } - + // Load comments commentsView.load() } - + companion object { private const val TAG = "FastCommentsExample" } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/fastcomments/SecureSSOExampleActivity.kt b/app/src/main/java/com/fastcomments/SecureSSOExampleActivity.kt index e935bc9..7f4c276 100644 --- a/app/src/main/java/com/fastcomments/SecureSSOExampleActivity.kt +++ b/app/src/main/java/com/fastcomments/SecureSSOExampleActivity.kt @@ -22,14 +22,14 @@ class SecureSSOExampleActivity : AppCompatActivity() { "ssotest", "https://fastcomments.com/demo", "fastcomments.com", - "Demo" + "Demo", ) config.sso = getSSOTokenFromServer() val sdk = FastCommentsSDK(config) // Find the comments view in the layout commentsView = findViewById(R.id.commentsView) - + // Set the SDK instance for the view commentsView.setSDK(sdk) commentsView.load() @@ -37,8 +37,8 @@ class SecureSSOExampleActivity : AppCompatActivity() { private fun getSSOTokenFromServer(): String { // DO THIS ON THE SERVER. THIS IS ONLY IN THE APP AS AN EXAMPLE! - val userData = SecureSSOUserData("user-123", "user@example.com", "Example User", "https://staticm.fastcomments.com/1639362726066-DSC_0841.JPG"); - userData.displayName = "Fancy Name"; + val userData = SecureSSOUserData("user-123", "user@example.com", "Example User", "https://staticm.fastcomments.com/1639362726066-DSC_0841.JPG") + userData.displayName = "Fancy Name" val sso = FastCommentsSSO.createSecure("DEMO_API_SECRET", userData) return sso.prepareToSend(); // send to client } diff --git a/app/src/main/java/com/fastcomments/SimpleSSOExampleActivity.kt b/app/src/main/java/com/fastcomments/SimpleSSOExampleActivity.kt index 149f0ba..3ddec86 100644 --- a/app/src/main/java/com/fastcomments/SimpleSSOExampleActivity.kt +++ b/app/src/main/java/com/fastcomments/SimpleSSOExampleActivity.kt @@ -22,14 +22,14 @@ class SimpleSSOExampleActivity : AppCompatActivity() { "ssotest", "https://fastcomments.com/demo", "fastcomments.com", - "Demo" + "Demo", ) // Optional configuration // config.voteStyle = VoteStyle.Heart // config.enableInfiniteScrolling = true - val userData = SimpleSSOUserData("Example User", "user@example.com", "https://staticm.fastcomments.com/1639362726066-DSC_0841.JPG"); + val userData = SimpleSSOUserData("Example User", "user@example.com", "https://staticm.fastcomments.com/1639362726066-DSC_0841.JPG") val sso = FastCommentsSSO(userData) config.sso = sso.prepareToSend() val sdk = FastCommentsSDK(config) @@ -45,4 +45,4 @@ class SimpleSSOExampleActivity : AppCompatActivity() { companion object { private const val TAG = "FastCommentsExample" } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/fastcomments/ToolbarShowcaseActivity.kt b/app/src/main/java/com/fastcomments/ToolbarShowcaseActivity.kt index a2a5c09..5c6eae0 100644 --- a/app/src/main/java/com/fastcomments/ToolbarShowcaseActivity.kt +++ b/app/src/main/java/com/fastcomments/ToolbarShowcaseActivity.kt @@ -36,7 +36,7 @@ class ToolbarShowcaseActivity : AppCompatActivity() { "toolbar-showcase", // Different URL slug to avoid comment conflicts "https://fastcomments.com/toolbar-demo", "fastcomments.com", - "Toolbar Demo" + "Toolbar Demo", ) // Optional: Configure vote style @@ -46,7 +46,7 @@ class ToolbarShowcaseActivity : AppCompatActivity() { val userData = SimpleSSOUserData( "Toolbar Demo User", "toolbar-demo@example.com", - "https://staticm.fastcomments.com/1639362726066-DSC_0841.JPG" + "https://staticm.fastcomments.com/1639362726066-DSC_0841.JPG", ) val sso = FastCommentsSSO(userData) config.sso = sso.prepareToSend() @@ -118,10 +118,10 @@ class ToolbarShowcaseActivity : AppCompatActivity() { // Or hide the toolbar entirely for this instance inputView.setToolbarVisible(false) } - */ + */ } companion object { private const val TAG = "ToolbarShowcase" } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/fastcomments/ui/theme/Color.kt b/app/src/main/java/com/fastcomments/ui/theme/Color.kt index e801c26..bbe8ddd 100644 --- a/app/src/main/java/com/fastcomments/ui/theme/Color.kt +++ b/app/src/main/java/com/fastcomments/ui/theme/Color.kt @@ -8,4 +8,4 @@ val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file +val Pink40 = Color(0xFF7D5260) diff --git a/app/src/main/java/com/fastcomments/ui/theme/Theme.kt b/app/src/main/java/com/fastcomments/ui/theme/Theme.kt index 3709871..380bbec 100644 --- a/app/src/main/java/com/fastcomments/ui/theme/Theme.kt +++ b/app/src/main/java/com/fastcomments/ui/theme/Theme.kt @@ -14,13 +14,13 @@ import androidx.compose.ui.platform.LocalContext private val DarkColorScheme = darkColorScheme( primary = Purple80, secondary = PurpleGrey80, - tertiary = Pink80 + tertiary = Pink80, ) private val LightColorScheme = lightColorScheme( primary = Purple40, secondary = PurpleGrey40, - tertiary = Pink40 + tertiary = Pink40, /* Other default colors to override background = Color(0xFFFFFBFE), @@ -30,7 +30,7 @@ private val LightColorScheme = lightColorScheme( onTertiary = Color.White, onBackground = Color(0xFF1C1B1F), onSurface = Color(0xFF1C1B1F), - */ + */ ) @Composable @@ -38,7 +38,7 @@ fun FastcommentsexamplesimpleTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { @@ -47,12 +47,13 @@ fun FastcommentsexamplesimpleTheme( } darkTheme -> DarkColorScheme + else -> LightColorScheme } MaterialTheme( colorScheme = colorScheme, typography = Typography, - content = content + content = content, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/fastcomments/ui/theme/Type.kt b/app/src/main/java/com/fastcomments/ui/theme/Type.kt index 6a531df..8cdcec4 100644 --- a/app/src/main/java/com/fastcomments/ui/theme/Type.kt +++ b/app/src/main/java/com/fastcomments/ui/theme/Type.kt @@ -13,8 +13,8 @@ val Typography = Typography( fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) + letterSpacing = 0.5.sp, + ), /* Other default text styles to override titleLarge = TextStyle( fontFamily = FontFamily.Default, @@ -30,5 +30,5 @@ val Typography = Typography( lineHeight = 16.sp, letterSpacing = 0.5.sp ) - */ -) \ No newline at end of file + */ +) diff --git a/app/src/test/java/com/fastcomments/ExampleUnitTest.kt b/app/src/test/java/com/fastcomments/ExampleUnitTest.kt index 2c155b7..de4fb94 100644 --- a/app/src/test/java/com/fastcomments/ExampleUnitTest.kt +++ b/app/src/test/java/com/fastcomments/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package com.fastcomments +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * @@ -14,4 +13,4 @@ class ExampleUnitTest { fun addition_isCorrect() { assertEquals(4, 2 + 2) } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/androidTest/java/com/fastcomments/sdk/ExampleInstrumentedTest.java b/libraries/sdk/src/androidTest/java/com/fastcomments/sdk/ExampleInstrumentedTest.java index 8178e33..cf49ba7 100644 --- a/libraries/sdk/src/androidTest/java/com/fastcomments/sdk/ExampleInstrumentedTest.java +++ b/libraries/sdk/src/androidTest/java/com/fastcomments/sdk/ExampleInstrumentedTest.java @@ -1,15 +1,13 @@ package com.fastcomments.sdk; -import android.content.Context; +import static org.junit.Assert.*; -import androidx.test.platform.app.InstrumentationRegistry; +import android.content.Context; import androidx.test.ext.junit.runners.AndroidJUnit4; - +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Test; import org.junit.runner.RunWith; -import static org.junit.Assert.*; - /** * Instrumented test, which will execute on an Android device. * @@ -23,4 +21,4 @@ public void useAppContext() { Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); assertEquals("com.fastcomments.client", appContext.getPackageName()); } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/AddLinkDialog.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/AddLinkDialog.java index 1e7f8a4..28c10b6 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/AddLinkDialog.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/AddLinkDialog.java @@ -9,12 +9,9 @@ import android.webkit.URLUtil; import android.widget.Button; import android.widget.TextView; - import androidx.annotation.NonNull; - -import com.google.android.material.textfield.TextInputEditText; - import com.fastcomments.model.FeedPostLink; +import com.google.android.material.textfield.TextInputEditText; /** * Dialog for adding a link to a post @@ -45,23 +42,23 @@ public AddLinkDialog(@NonNull Context context, OnLinkAddedListener listener) { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); - + setContentView(R.layout.dialog_add_link); - + // Find views linkUrlEditText = findViewById(R.id.linkUrlEditText); linkTitleEditText = findViewById(R.id.linkTitleEditText); linkDescriptionEditText = findViewById(R.id.linkDescriptionEditText); linkErrorTextView = findViewById(R.id.linkErrorTextView); - + // Find the buttons in the layout - cancelButton = findViewById(android.R.id.button2); // Negative button - addButton = findViewById(android.R.id.button1); // Positive button - + cancelButton = findViewById(android.R.id.button2); // Negative button + addButton = findViewById(android.R.id.button1); // Positive button + // Set button click listeners cancelButton.setOnClickListener(v -> dismiss()); addButton.setOnClickListener(v -> validateAndAddLink()); - + // Set dialog width if (getWindow() != null) { getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT); @@ -74,57 +71,63 @@ protected void onCreate(Bundle savedInstanceState) { private void validateAndAddLink() { // Reset error state linkErrorTextView.setVisibility(View.GONE); - + // Get input values - String url = linkUrlEditText.getText() != null ? linkUrlEditText.getText().toString().trim() : ""; - String title = linkTitleEditText.getText() != null ? linkTitleEditText.getText().toString().trim() : ""; - String description = linkDescriptionEditText.getText() != null ? linkDescriptionEditText.getText().toString().trim() : ""; - + String url = linkUrlEditText.getText() != null + ? linkUrlEditText.getText().toString().trim() + : ""; + String title = linkTitleEditText.getText() != null + ? linkTitleEditText.getText().toString().trim() + : ""; + String description = linkDescriptionEditText.getText() != null + ? linkDescriptionEditText.getText().toString().trim() + : ""; + // Validate URL if (url.isEmpty()) { showError(getContext().getString(R.string.link_url_required)); return; } - + // Ensure URL is valid if (!URLUtil.isValidUrl(url) && !url.startsWith("http://") && !url.startsWith("https://")) { // Try adding https:// prefix if missing url = "https://" + url; - + // Check if it's valid now if (!URLUtil.isValidUrl(url)) { showError(getContext().getString(R.string.invalid_url)); return; } } - + // Create the link object FeedPostLink link = new FeedPostLink(); link.setUrl(url); - + if (!title.isEmpty()) { link.setTitle(title); } - + if (!description.isEmpty()) { link.setDescription(description); } - + // Notify listener and close dialog if (listener != null) { listener.onLinkAdded(link); } - + dismiss(); } - + /** * Show an error message - * + * * @param errorMessage The error message to display */ private void showError(String errorMessage) { linkErrorTextView.setText(errorMessage); linkErrorTextView.setVisibility(View.VISIBLE); } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/AvatarFetcher.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/AvatarFetcher.java index 18a15bc..93038b0 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/AvatarFetcher.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/AvatarFetcher.java @@ -2,27 +2,28 @@ import android.content.Context; import android.widget.ImageView; - import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; - import java.net.URI; public class AvatarFetcher { public static void fetchTransformInto(Context context, String avatarSrc, ImageView imageView) { - Glide.with(context).load(avatarSrc) + Glide.with(context) + .load(avatarSrc) .apply(RequestOptions.circleCropTransform()) .into(imageView); } public static void fetchTransformInto(Context context, URI uri, ImageView imageView) { - Glide.with(context).load(uri) + Glide.with(context) + .load(uri) .apply(RequestOptions.circleCropTransform()) .into(imageView); } public static void fetchTransformInto(Context context, int resourceId, ImageView imageView) { - Glide.with(context).load(resourceId) + Glide.with(context) + .load(resourceId) .apply(RequestOptions.circleCropTransform()) .into(imageView); } diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/BadgeAwardDialog.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/BadgeAwardDialog.java index 569ec8d..6b26eab 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/BadgeAwardDialog.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/BadgeAwardDialog.java @@ -9,7 +9,6 @@ import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; - import com.bumptech.glide.Glide; import com.fastcomments.model.CommentUserBadgeInfo; @@ -34,23 +33,23 @@ public void show(CommentUserBadgeInfo badge) { dialog = new Dialog(context); dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); dialog.setCancelable(true); - + View view = LayoutInflater.from(context).inflate(R.layout.badge_award_dialog, null); dialog.setContentView(view); - + // Set up dialog views TextView badgeText = view.findViewById(R.id.badgeDialogText); ImageView badgeIcon = view.findViewById(R.id.badgeDialogIcon); TextView badgeDescription = view.findViewById(R.id.badgeDialogDescription); Button closeButton = view.findViewById(R.id.badgeDialogCloseButton); - + // Set badge text (display label or fallback to description) String displayText = badge.getDisplayLabel() != null ? badge.getDisplayLabel() : badge.getDescription(); badgeText.setText(displayText); - - // Set badge description + + // Set badge description badgeDescription.setText(badge.getDescription()); - + // Apply custom colors if provided if (badge.getBackgroundColor() != null) { try { @@ -59,7 +58,7 @@ public void show(CommentUserBadgeInfo badge) { // Use default background if color is invalid } } - + if (badge.getTextColor() != null) { try { badgeText.setTextColor(Color.parseColor(badge.getTextColor())); @@ -67,23 +66,21 @@ public void show(CommentUserBadgeInfo badge) { // Use default text color if color is invalid } } - + // Load badge icon if available if (badge.getDisplaySrc() != null && !badge.getDisplaySrc().isEmpty()) { badgeIcon.setVisibility(View.VISIBLE); - Glide.with(context) - .load(badge.getDisplaySrc()) - .into(badgeIcon); + Glide.with(context).load(badge.getDisplaySrc()).into(badgeIcon); } else { badgeIcon.setVisibility(View.GONE); } - + // Set close button action closeButton.setOnClickListener(v -> dismiss()); - + dialog.show(); } - + /** * Dismiss the dialog */ @@ -92,4 +89,4 @@ public void dismiss() { dialog.dismiss(); } } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/BadgeView.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/BadgeView.java index f594b1a..be7e31a 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/BadgeView.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/BadgeView.java @@ -7,7 +7,6 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; - import com.bumptech.glide.Glide; import com.fastcomments.model.CommentUserBadgeInfo; @@ -18,23 +17,23 @@ public class BadgeView { /** * Create a badge view based on badge information - * + * * @param context The context * @param badge The badge information * @return The badge view */ public static View createBadgeView(Context context, CommentUserBadgeInfo badge) { View badgeView = LayoutInflater.from(context).inflate(R.layout.badge_item, null); - + // Get references to the badge view components ImageView badgeIcon = badgeView.findViewById(R.id.badgeIcon); TextView badgeText = badgeView.findViewById(R.id.badgeText); - LinearLayout badgeContainer = (LinearLayout)badgeView; - + LinearLayout badgeContainer = (LinearLayout) badgeView; + // Set badge text String displayText = badge.getDisplayLabel() != null ? badge.getDisplayLabel() : badge.getDescription(); badgeText.setText(displayText); - + // Apply colors if provided if (badge.getBackgroundColor() != null) { try { @@ -43,23 +42,23 @@ public static View createBadgeView(Context context, CommentUserBadgeInfo badge) // Use default background if color is invalid } } - + if (badge.getBorderColor() != null) { try { LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT); + LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); params.setMargins(1, 1, 1, 1); - badgeContainer.setPadding(badgeContainer.getPaddingLeft() + 1, + badgeContainer.setPadding( + badgeContainer.getPaddingLeft() + 1, badgeContainer.getPaddingTop() + 1, - badgeContainer.getPaddingRight() + 1, + badgeContainer.getPaddingRight() + 1, badgeContainer.getPaddingBottom() + 1); badgeContainer.setBackgroundColor(Color.parseColor(badge.getBorderColor())); } catch (IllegalArgumentException e) { // Ignore invalid border color } } - + if (badge.getTextColor() != null) { try { badgeText.setTextColor(Color.parseColor(badge.getTextColor())); @@ -67,17 +66,15 @@ public static View createBadgeView(Context context, CommentUserBadgeInfo badge) // Use default text color if color is invalid } } - + // Load badge icon if available if (badge.getDisplaySrc() != null && !badge.getDisplaySrc().isEmpty()) { badgeIcon.setVisibility(View.VISIBLE); - Glide.with(context) - .load(badge.getDisplaySrc()) - .into(badgeIcon); + Glide.with(context).load(badge.getDisplaySrc()).into(badgeIcon); } else { badgeIcon.setVisibility(View.GONE); } - + return badgeView; } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/BottomCommentInputView.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/BottomCommentInputView.java index 99e9f68..8c1de92 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/BottomCommentInputView.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/BottomCommentInputView.java @@ -9,7 +9,6 @@ import android.text.style.BackgroundColorSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; -import android.text.style.URLSpan; import android.util.AttributeSet; import android.util.TypedValue; import android.view.LayoutInflater; @@ -24,13 +23,10 @@ import android.widget.PopupWindow; import android.widget.ProgressBar; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.fastcomments.model.APIError; import com.fastcomments.model.UserSessionInfo; - import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashMap; @@ -167,21 +163,33 @@ public void afterTextChanged(Editable s) { switch (format) { case "bold": if (!hasMatchingStyleSpan(s, Typeface.BOLD, applyStart, applyEnd)) { - s.setSpan(new StyleSpan(Typeface.BOLD), applyStart, applyEnd, + s.setSpan( + new StyleSpan(Typeface.BOLD), + applyStart, + applyEnd, Spanned.SPAN_EXCLUSIVE_INCLUSIVE); } break; case "italic": if (!hasMatchingStyleSpan(s, Typeface.ITALIC, applyStart, applyEnd)) { - s.setSpan(new StyleSpan(Typeface.ITALIC), applyStart, applyEnd, + s.setSpan( + new StyleSpan(Typeface.ITALIC), + applyStart, + applyEnd, Spanned.SPAN_EXCLUSIVE_INCLUSIVE); } break; case "code": if (!hasMatchingSpan(s, TypefaceSpan.class, applyStart, applyEnd)) { - s.setSpan(new TypefaceSpan("monospace"), applyStart, applyEnd, + s.setSpan( + new TypefaceSpan("monospace"), + applyStart, + applyEnd, Spanned.SPAN_EXCLUSIVE_INCLUSIVE); - s.setSpan(new BackgroundColorSpan(0x20808080), applyStart, applyEnd, + s.setSpan( + new BackgroundColorSpan(0x20808080), + applyStart, + applyEnd, Spanned.SPAN_EXCLUSIVE_INCLUSIVE); } break; @@ -198,7 +206,8 @@ public void afterTextChanged(Editable s) { String plainText = commentInput.getText().toString().trim(); if (!plainText.isEmpty() && submitListener != null) { String html = getText(); - String parentId = parentComment != null ? parentComment.getComment().getId() : null; + String parentId = + parentComment != null ? parentComment.getComment().getId() : null; submitListener.onCommentSubmit(html, parentId); } }); @@ -290,7 +299,8 @@ public void setSubmitting(boolean submitting) { sendButton.setVisibility(submitting ? View.GONE : View.VISIBLE); sendProgress.setVisibility(submitting ? View.VISIBLE : View.GONE); commentInput.setEnabled(!submitting); - sendButton.setEnabled(!submitting && !commentInput.getText().toString().trim().isEmpty()); + sendButton.setEnabled( + !submitting && !commentInput.getText().toString().trim().isEmpty()); } public void showError(String error) { @@ -367,11 +377,11 @@ public void setSDK(FastCommentsSDK sdk) { private void setupMentions() { // Initialize mention suggestions adapter mentionsAdapter = new MentionSuggestionsAdapter(getContext(), mentionSuggestions); - + // Create the popup window for mentions createMentionPopup(); } - + /** * Create the popup window for mention suggestions */ @@ -383,18 +393,17 @@ private void createMentionPopup() { popupListView.setDivider(getContext().getResources().getDrawable(android.R.color.darker_gray)); popupListView.setDividerHeight(1); popupListView.setPadding(8, 8, 8, 8); - + // Handle mention selection popupListView.setOnItemClickListener((parent, view, position, id) -> { if (position < mentionSuggestions.size()) { selectUserMention(mentionSuggestions.get(position)); } }); - + // Create the popup window - mentionPopup = new PopupWindow(popupListView, - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT); + mentionPopup = new PopupWindow( + popupListView, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); mentionPopup.setOutsideTouchable(true); mentionPopup.setFocusable(false); mentionPopup.setElevation(12); @@ -417,7 +426,7 @@ private void handleMentionInput(CharSequence s, int start, int before, int count cancelMention(); return; } - + // We're in the middle of typing a mention if (start < mentionStartPosition) { // Cursor moved before the @ symbol, cancel mention @@ -426,12 +435,13 @@ private void handleMentionInput(CharSequence s, int start, int before, int count // Extract the text between @ and cursor int mentionLength = start + count - mentionStartPosition; if (mentionLength > 0 && start + count <= s.length()) { - String newMentionText = s.subSequence(mentionStartPosition + 1, start + count).toString(); - + String newMentionText = s.subSequence(mentionStartPosition + 1, start + count) + .toString(); + // Check if mention text changed if (!newMentionText.equals(currentMentionText)) { currentMentionText = newMentionText; - + // Cancel mention if space is added (end of mention) if (currentMentionText.contains(" ")) { cancelMention(); @@ -453,26 +463,26 @@ private void showMentionSuggestions() { // Calculate height based on number of items (max 3 visible) int maxItems = Math.min(mentionSuggestions.size(), 3); float density = getContext().getResources().getDisplayMetrics().density; - int itemHeight = (int)(50 * density); // 50dp per item - int totalHeight = maxItems * itemHeight + (int)(16 * density); // Add padding - + int itemHeight = (int) (50 * density); // 50dp per item + int totalHeight = maxItems * itemHeight + (int) (16 * density); // Add padding + // Set the popup height mentionPopup.setHeight(totalHeight); - + // Calculate position and size int[] location = new int[2]; commentInput.getLocationOnScreen(location); int inputWidth = commentInput.getWidth(); - int popupWidth = (int)(inputWidth * 0.9f); // 90% of input width + int popupWidth = (int) (inputWidth * 0.9f); // 90% of input width mentionPopup.setWidth(popupWidth); - + // Position popup above the entire bottom input view int[] thisViewLocation = new int[2]; this.getLocationOnScreen(thisViewLocation); - - int xPos = location[0] + (int)(12 * density); // Small margin from left of input - int yPos = thisViewLocation[1] - totalHeight - (int)(8 * density); // Above the entire view with margin - + + int xPos = location[0] + (int) (12 * density); // Small margin from left of input + int yPos = thisViewLocation[1] - totalHeight - (int) (8 * density); // Above the entire view with margin + if (!mentionPopup.isShowing()) { mentionPopup.showAtLocation(commentInput, android.view.Gravity.NO_GRAVITY, xPos, yPos); } @@ -480,7 +490,7 @@ private void showMentionSuggestions() { hideMentionSuggestions(); } } - + /** * Hide the mention suggestions list */ @@ -489,7 +499,7 @@ private void hideMentionSuggestions() { mentionPopup.dismiss(); } } - + /** * Cancel the current mention being typed */ @@ -506,21 +516,21 @@ private void searchUsers(String searchTerm) { if (sdk == null) { return; } - + // Don't search if the term is empty if (searchTerm == null || searchTerm.trim().isEmpty()) { mentionSuggestions.clear(); mentionsAdapter.notifyDataSetChanged(); return; } - + // Don't search if already searching if (isSearchingUsers) { return; } isSearchingUsers = true; - + sdk.searchUsers(searchTerm, new FCCallback>() { @Override public boolean onFailure(APIError error) { @@ -530,13 +540,13 @@ public boolean onFailure(APIError error) { // Clear any previous results mentionSuggestions.clear(); mentionsAdapter.notifyDataSetChanged(); - + // Hide the list on error hideMentionSuggestions(); }); return FCCallback.CONSUME; } - + @Override public boolean onSuccess(List users) { isSearchingUsers = false; @@ -544,12 +554,12 @@ public boolean onSuccess(List users) { post(() -> { // Clear previous suggestions mentionSuggestions.clear(); - + if (users != null && !users.isEmpty()) { // Add new suggestions mentionSuggestions.addAll(users); mentionsAdapter.notifyDataSetChanged(); - + // Show the suggestions list showMentionSuggestions(); } else { @@ -589,17 +599,17 @@ private void selectUserMention(UserMention mention) { // Reset the mention state cancelMention(); } - + /** * Apply theme colors to the UI elements */ private void applyTheme() { FastCommentsTheme theme = sdk != null ? sdk.getTheme() : null; - + // Apply action button color to the send button int actionButtonColor = ThemeColorResolver.getActionButtonColor(getContext(), theme); sendButton.setImageTintList(ColorStateList.valueOf(actionButtonColor)); - + // Also apply to cancel reply button if (cancelReplyButton != null) { cancelReplyButton.setImageTintList(ColorStateList.valueOf(actionButtonColor)); @@ -732,23 +742,22 @@ private void addDefaultFormattingButtons() { defaultToolbarButtons.clear(); // Bold button - ImageButton boldBtn = addDefaultToolbarButton(R.drawable.ic_format_bold, R.string.format_bold, - v -> toggleFormat("bold")); + ImageButton boldBtn = + addDefaultToolbarButton(R.drawable.ic_format_bold, R.string.format_bold, v -> toggleFormat("bold")); defaultToolbarButtons.put("bold", boldBtn); // Italic button - ImageButton italicBtn = addDefaultToolbarButton(R.drawable.ic_format_italic, R.string.format_italic, - v -> toggleFormat("italic")); + ImageButton italicBtn = addDefaultToolbarButton( + R.drawable.ic_format_italic, R.string.format_italic, v -> toggleFormat("italic")); defaultToolbarButtons.put("italic", italicBtn); // Link button - ImageButton linkBtn = addDefaultToolbarButton(R.drawable.link_icon, R.string.add_link, - v -> showLinkDialog()); + ImageButton linkBtn = addDefaultToolbarButton(R.drawable.link_icon, R.string.add_link, v -> showLinkDialog()); defaultToolbarButtons.put("link", linkBtn); // Code button - ImageButton codeBtn = addDefaultToolbarButton(R.drawable.ic_code, R.string.format_code, - v -> toggleFormat("code")); + ImageButton codeBtn = + addDefaultToolbarButton(R.drawable.ic_code, R.string.format_code, v -> toggleFormat("code")); codeBtn.setOnLongClickListener(v -> { toggleFormat("codeblock"); return true; @@ -761,7 +770,8 @@ private void addDefaultFormattingButtons() { * * @return The created ImageButton, for tracking active states */ - private ImageButton addDefaultToolbarButton(int iconRes, int contentDescriptionRes, View.OnClickListener clickListener) { + private ImageButton addDefaultToolbarButton( + int iconRes, int contentDescriptionRes, View.OnClickListener clickListener) { ImageButton button = new ImageButton(getContext()); button.setImageResource(iconRes); button.setContentDescription(getContext().getString(contentDescriptionRes)); @@ -999,8 +1009,10 @@ public String getSelectedText() { return ""; } - return commentInput.getText().subSequence( - Math.min(start, end), Math.max(start, end)).toString(); + return commentInput + .getText() + .subSequence(Math.min(start, end), Math.max(start, end)) + .toString(); } /** @@ -1150,10 +1162,9 @@ private void updateToolbarActiveStates() { Editable editable = commentInput.getText(); int pos = Math.max(commentInput.getSelectionStart(), 0); - boolean boldActive = RichTextHelper.isBoldActive(editable, pos) - || activeFormatsForNextChar.contains("bold"); - boolean italicActive = RichTextHelper.isItalicActive(editable, pos) - || activeFormatsForNextChar.contains("italic"); + boolean boldActive = RichTextHelper.isBoldActive(editable, pos) || activeFormatsForNextChar.contains("bold"); + boolean italicActive = + RichTextHelper.isItalicActive(editable, pos) || activeFormatsForNextChar.contains("italic"); boolean codeActive = RichTextHelper.isCodeActive(editable, pos) || activeFormatsForNextChar.contains("code") || activeFormatsForNextChar.contains("codeblock"); @@ -1171,8 +1182,7 @@ private void setButtonActiveState(ImageButton button, boolean active) { button.setBackgroundColor(0x33000000); } else { TypedValue outValue = new TypedValue(); - getContext().getTheme().resolveAttribute( - android.R.attr.selectableItemBackgroundBorderless, outValue, true); + getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, outValue, true); button.setBackgroundResource(outValue.resourceId); } } @@ -1192,4 +1202,4 @@ protected void onDetachedFromWindow() { editableImageGetter.clearTargets(); } } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/CallbackWrapper.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/CallbackWrapper.java index ce4be89..0cafe06 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/CallbackWrapper.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/CallbackWrapper.java @@ -2,12 +2,10 @@ import android.os.Handler; import android.util.Log; - import com.fastcomments.invoker.ApiCallback; import com.fastcomments.invoker.ApiException; import com.fastcomments.model.APIError; import com.google.gson.Gson; - import java.util.List; import java.util.Map; diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentEditDialog.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentEditDialog.java index f495d9e..efd8aa1 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentEditDialog.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentEditDialog.java @@ -2,8 +2,6 @@ import android.app.Dialog; import android.content.Context; -import android.content.res.ColorStateList; -import android.text.Spanned; import android.view.LayoutInflater; import android.view.View; import android.view.Window; diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentFormView.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentFormView.java index 3703e5d..4da80aa 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentFormView.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentFormView.java @@ -7,7 +7,6 @@ import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; -import android.widget.AdapterView; import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; @@ -15,13 +14,10 @@ import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; - import androidx.annotation.NonNull; - import com.fastcomments.model.APIError; import com.fastcomments.model.PublicComment; import com.fastcomments.model.UserSessionInfo; - import java.util.ArrayList; import java.util.List; @@ -40,7 +36,7 @@ public class CommentFormView extends LinearLayout { private OnCancelReplyListener cancelListener; private String parentId; private RenderableComment parentComment; - + // For @mentions functionality private FastCommentsSDK sdk; private MentionSuggestionsAdapter mentionsAdapter; @@ -125,7 +121,7 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { cancelMention(); return; } - + // We're in the middle of typing a mention if (start < mentionStartPosition) { // Cursor moved before the @ symbol, cancel mention @@ -134,12 +130,13 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { // Extract the text between @ and cursor int mentionLength = start + count - mentionStartPosition; if (mentionLength > 0 && start + count <= s.length()) { - String newMentionText = s.subSequence(mentionStartPosition + 1, start + count).toString(); - + String newMentionText = s.subSequence(mentionStartPosition + 1, start + count) + .toString(); + // Check if mention text changed if (!newMentionText.equals(currentMentionText)) { currentMentionText = newMentionText; - + // Cancel mention if space is added (end of mention) if (currentMentionText.contains(" ")) { cancelMention(); @@ -162,8 +159,9 @@ public void afterTextChanged(Editable s) { // Set up the submit button — serialize spans to HTML submitButton.setOnClickListener(v -> { String plainText = commentEditText.getText().toString().trim(); - String commentText = TextUtils.isEmpty(plainText) ? "" : - RichTextHelper.toHtml(commentEditText.getText()).trim(); + String commentText = TextUtils.isEmpty(plainText) + ? "" + : RichTextHelper.toHtml(commentEditText.getText()).trim(); if (TextUtils.isEmpty(plainText)) { errorTextView.setText(R.string.empty_comment_error); errorTextView.setVisibility(View.VISIBLE); @@ -225,7 +223,7 @@ public void setCurrentUser(@NonNull UserSessionInfo userInfo) { } } userNameTextView.setText(nameToShow); - + if (userInfo.getAvatarSrc() != null) { AvatarFetcher.fetchTransformInto(getContext(), userInfo.getAvatarSrc(), avatarImageView); } else { @@ -255,10 +253,10 @@ public void clearText() { selectedMentions.clear(); } } - + /** * Check if the comment text input is empty - * + * * @return true if the comment text is empty, false otherwise */ public boolean isTextEmpty() { @@ -310,44 +308,44 @@ public void resetReplyState() { commentEditText.setHint(R.string.comment_hint); clearText(); // This will also clear selected mentions } - + /** * Get the parent comment that's being replied to - * + * * @return The parent RenderableComment or null if not replying */ public RenderableComment getParentComment() { return parentComment; } - + /** * Set the SDK instance for API access - * + * * @param sdk FastCommentsSDK instance */ public void setSDK(FastCommentsSDK sdk) { this.sdk = sdk; applyTheme(); } - + /** * Apply theme colors to buttons and UI elements */ private void applyTheme() { FastCommentsTheme theme = sdk != null ? sdk.getTheme() : null; - + // Apply action button color to submit button int actionButtonColor = ThemeColorResolver.getActionButtonColor(getContext(), theme); if (submitButton != null) { submitButton.setTextColor(actionButtonColor); } - + // Apply action button color to cancel button as well if (cancelButton != null) { cancelButton.setTextColor(actionButtonColor); } } - + /** * Show the mention suggestions list */ @@ -358,7 +356,7 @@ private void showMentionSuggestions() { hideMentionSuggestions(); } } - + /** * Hide the mention suggestions list */ @@ -367,7 +365,7 @@ private void hideMentionSuggestions() { mentionSuggestionsList.setVisibility(View.GONE); } } - + /** * Cancel the current mention being typed */ @@ -376,31 +374,31 @@ private void cancelMention() { currentMentionText = ""; hideMentionSuggestions(); } - + /** * Search for users by partial name - * + * * @param searchTerm The search term (text after @ symbol) */ private void searchUsers(String searchTerm) { if (sdk == null) { return; } - + // Don't search if the term is empty if (searchTerm == null || searchTerm.trim().isEmpty()) { mentionSuggestions.clear(); mentionsAdapter.notifyDataSetChanged(); return; } - + // Don't search if already searching if (isSearchingUsers) { return; } - + isSearchingUsers = true; - + // Call the API to search for users sdk.searchUsers(searchTerm, new FCCallback>() { @Override @@ -411,7 +409,7 @@ public boolean onFailure(APIError error) { // Clear any previous results mentionSuggestions.clear(); mentionsAdapter.notifyDataSetChanged(); - + // Hide the list on error hideMentionSuggestions(); }); @@ -421,12 +419,12 @@ public boolean onFailure(APIError error) { @Override public boolean onSuccess(List users) { isSearchingUsers = false; - + // Use the main thread to update UI post(() -> { // Update the suggestions list mentionSuggestions.clear(); - + // Only show the list if we have actual results if (users != null && !users.isEmpty()) { mentionSuggestions.addAll(users); @@ -441,10 +439,10 @@ public boolean onSuccess(List users) { } }); } - + /** * Select a user mention from the suggestions list - * + * * @param mention The user mention to select */ private void selectUserMention(UserMention mention) { @@ -471,13 +469,13 @@ private void selectUserMention(UserMention mention) { // Reset the mention state cancelMention(); } - + /** * Get the selected user mentions for the current comment - * + * * @return List of selected user mentions */ public List getSelectedMentions() { return selectedMentions; } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentViewHolder.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentViewHolder.java index 2db7875..b286f21 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentViewHolder.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentViewHolder.java @@ -2,36 +2,22 @@ import android.content.Context; import android.content.res.ColorStateList; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.text.Html; import android.text.format.DateUtils; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageButton; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.PopupMenu; import android.widget.ProgressBar; import android.widget.TextView; - import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; -import com.bumptech.glide.request.target.CustomTarget; -import com.bumptech.glide.request.transition.Transition; import com.fastcomments.core.VoteStyle; import com.fastcomments.model.CommentUserBadgeInfo; - import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; -import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Objects; @@ -102,7 +88,7 @@ public CommentViewHolder(Context context, FastCommentsSDK sdk, @NonNull View ite childPaginationControls = itemView.findViewById(R.id.childPaginationControls); btnLoadMoreReplies = itemView.findViewById(R.id.btnLoadMoreReplies); childPaginationProgressBar = itemView.findViewById(R.id.childPaginationProgressBar); - + // Apply theme colors applyTheme(); } @@ -112,7 +98,7 @@ public CommentViewHolder(Context context, FastCommentsSDK sdk, @NonNull View ite */ private void applyTheme() { FastCommentsTheme theme = sdk != null ? sdk.getTheme() : null; - + // Apply action button colors to ImageButtons int actionButtonColor = ThemeColorResolver.getActionButtonColor(context, theme); if (upVoteButton != null) { @@ -127,19 +113,19 @@ private void applyTheme() { if (commentMenuButton != null) { commentMenuButton.setImageTintList(ColorStateList.valueOf(actionButtonColor)); } - + // Apply reply button color int replyButtonColor = ThemeColorResolver.getReplyButtonColor(context, theme); if (replyButton != null) { replyButton.setTextColor(replyButtonColor); } - + // Apply toggle replies button color int toggleRepliesButtonColor = ThemeColorResolver.getToggleRepliesButtonColor(context, theme); if (toggleRepliesButton != null) { toggleRepliesButton.setTextColor(toggleRepliesButtonColor); } - + // Apply load more button text color int loadMoreButtonTextColor = ThemeColorResolver.getLoadMoreButtonTextColor(context, theme); if (btnLoadMoreReplies != null) { @@ -148,19 +134,22 @@ private void applyTheme() { } private boolean liveChatStyle = false; - + /** * Set whether this view is using live chat styling * @param liveChatStyle True for live chat style */ public void setLiveChatStyle(boolean liveChatStyle) { this.liveChatStyle = liveChatStyle; - + // No need to modify the view since we're using different layouts // Just store the flag for other behavior adjustments } - - public void setComment(final RenderableComment comment, boolean disableUnverifiedLabel, final CommentsAdapter.OnToggleRepliesListener listener) { + + public void setComment( + final RenderableComment comment, + boolean disableUnverifiedLabel, + final CommentsAdapter.OnToggleRepliesListener listener) { Boolean isBlocked = comment.getComment().getIsBlocked(); boolean blocked = isBlocked != null && isBlocked; @@ -264,7 +253,7 @@ public void setComment(final RenderableComment comment, boolean disableUnverifie String htmlContent = comment.getComment().getCommentHTML(); contentTextView.setText(HtmlLinkHandler.parseHtml(context, htmlContent, contentTextView)); } - + // No need for special handling for live chat - we're using a dedicated layout // Indent child comments to reflect hierarchy @@ -453,23 +442,20 @@ public void updateDateDisplay() { if (useAbsoluteDates) { // Use system's locale-aware date formatting - Locale currentLocale = context.getResources().getConfiguration().getLocales().get(0); - DateTimeFormatter formatter = DateTimeFormatter - .ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT) + Locale currentLocale = + context.getResources().getConfiguration().getLocales().get(0); + DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT) .withLocale(currentLocale); dateTextView.setText(date.format(formatter)); } else { // Format as relative date: 2 minutes ago, 1 hour ago, etc. CharSequence relativeTime = DateUtils.getRelativeTimeSpanString( - date.toInstant().toEpochMilli(), - System.currentTimeMillis(), - DateUtils.MINUTE_IN_MILLIS - ); + date.toInstant().toEpochMilli(), System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS); dateTextView.setText(relativeTime); } } - + /** * Update badges display for a comment * @@ -477,27 +463,27 @@ public void updateDateDisplay() { */ public void updateBadges(List badges) { badgesContainer.removeAllViews(); - + if (badges == null || badges.isEmpty()) { badgesContainer.setVisibility(View.GONE); return; } - + badgesContainer.setVisibility(View.VISIBLE); - + for (com.fastcomments.model.CommentUserBadgeInfo badge : badges) { View badgeView = BadgeView.createBadgeView(context, badge); badgesContainer.addView(badgeView); } } - + /** * Format a count number to an abbreviated format * Examples: * - 0-999: Shows as is (123) * - 1,000-999,999: Shows as #K (1.2K, 45K, 999K) * - 1,000,000+: Shows as #M (1.2M, 45M) - * + * * @param count The count to format * @return Formatted string */ @@ -525,59 +511,59 @@ private String formatAbbreviatedCount(int count) { } } } - + /** * Set the click listener for the comment menu button - * + * * @param commentMenuListener The listener to handle menu items */ public void setCommentMenuClickListener(final CommentsAdapter.OnCommentMenuItemListener commentMenuListener) { commentMenuButton.setOnClickListener(v -> showCommentMenu(commentMenuListener)); } - + /** * Set the click listener for the user avatar - * + * * @param clickListener The click listener to set */ public void setAvatarClickListener(View.OnClickListener clickListener) { avatarImageView.setOnClickListener(clickListener); } - + /** * Set the click listener for the user name - * + * * @param clickListener The click listener to set */ public void setUserNameClickListener(View.OnClickListener clickListener) { nameTextView.setOnClickListener(clickListener); } - + /** * Show the comment menu - * + * * @param commentMenuListener The listener to handle menu items */ private void showCommentMenu(final CommentsAdapter.OnCommentMenuItemListener commentMenuListener) { // Create popup menu PopupMenu popupMenu = new PopupMenu(context, commentMenuButton); popupMenu.inflate(R.menu.comment_menu); - + // Get menu for adjustments android.view.Menu menu = popupMenu.getMenu(); - + // Only show edit option for user's own comments boolean isCurrentUserComment = false; - + if (currentComment != null && sdk.getCurrentUser() != null) { String commentUserId = currentComment.getComment().getUserId(); String currentUserId = sdk.getCurrentUser().getId(); - + if (commentUserId != null && currentUserId != null) { isCurrentUserComment = commentUserId.equals(currentUserId); } } - + // Show/hide edit and delete options based on ownership menu.findItem(R.id.menu_edit_comment).setVisible(isCurrentUserComment); menu.findItem(R.id.menu_delete_comment).setVisible(isCurrentUserComment); @@ -636,7 +622,7 @@ private void showCommentMenu(final CommentsAdapter.OnCommentMenuItemListener com return false; }); - + // Show the popup menu popupMenu.show(); } diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentsAdapter.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentsAdapter.java index a718b96..20cf4f2 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentsAdapter.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentsAdapter.java @@ -1,25 +1,15 @@ package com.fastcomments.sdk; import android.content.Context; -import android.content.res.ColorStateList; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; - import com.fastcomments.model.PublicComment; - -import java.time.LocalDate; -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.FormatStyle; -import java.util.ArrayList; import java.util.List; -import java.util.Locale; public class CommentsAdapter extends RecyclerView.Adapter { @@ -44,7 +34,7 @@ public CommentsAdapter(Context context, FastCommentsSDK sdk) { this.sdk = sdk; commentsTree.setAdapter(this); } - + public Context getContext() { return context; } @@ -52,23 +42,23 @@ public Context getContext() { public void setRequestingReplyListener(Callback listener) { this.replyListener = listener; } - + public void setUpVoteListener(Callback listener) { this.upVoteListener = listener; } - + public void setDownVoteListener(Callback listener) { this.downVoteListener = listener; } - + public void setNewChildCommentsListener(Callback listener) { this.newChildCommentsListener = listener; } - + public void setCommentMenuListener(OnCommentMenuItemListener listener) { this.commentMenuListener = listener; } - + public void setUserClickListener(OnUserClickListener listener) { this.userClickListener = listener; } @@ -81,7 +71,7 @@ public void setGetChildrenProducer(Producer { commentsTree.setRepliesVisible(updatedComment, !updatedComment.isRepliesShown, (request, producer) -> { // Create a new request with the button @@ -158,30 +148,30 @@ private void bindCommentViewHolder(CommentViewHolder holder, int position) { } }); } - + // Set up vote button click listeners holder.setUpVoteClickListener(v -> { if (upVoteListener != null) { upVoteListener.call(comment); } }); - + holder.setDownVoteClickListener(v -> { if (downVoteListener != null) { downVoteListener.call(comment); } }); - + // Set up heart button click listener (uses same upvote handler) holder.setHeartClickListener(v -> { if (upVoteListener != null) { upVoteListener.call(comment); } }); - + // Set up comment menu click listener holder.setCommentMenuClickListener(commentMenuListener); - + // Set up user click listeners if (userClickListener != null) { holder.setUserNameClickListener(v -> { @@ -189,30 +179,25 @@ private void bindCommentViewHolder(CommentViewHolder holder, int position) { UserClickContext context = UserClickContext.fromComment(comment.getComment()); userClickListener.onUserClicked(context, userInfo, UserClickSource.NAME); }); - + holder.setAvatarClickListener(v -> { UserInfo userInfo = UserInfo.fromComment(comment.getComment()); UserClickContext context = UserClickContext.fromComment(comment.getComment()); userClickListener.onUserClicked(context, userInfo, UserClickSource.AVATAR); }); } - + // Set up load more children click listener holder.setLoadMoreChildrenClickListener(v -> { if (getChildren != null && comment.isRepliesShown && comment.hasMoreChildren) { // Mark as loading to update UI comment.isLoadingChildren = true; notifyItemChanged(position); - + // Create a request for the next page of child comments GetChildrenRequest paginationRequest = new GetChildrenRequest( - comment.getComment().getId(), - null, - comment.childSkip, - comment.childPageSize, - true - ); - + comment.getComment().getId(), null, comment.childSkip, comment.childPageSize, true); + getChildren.get(paginationRequest, childComments -> { // Update has more flag based on response getHandler().post(() -> { @@ -223,10 +208,10 @@ private void bindCommentViewHolder(CommentViewHolder holder, int position) { } }); } - + private void bindButtonViewHolder(ButtonViewHolder holder, int position) { final RenderableButton button = (RenderableButton) commentsTree.visibleNodes.get(position); - + if (button.getButtonType() == RenderableButton.TYPE_NEW_ROOT_COMMENTS) { // New root comments button holder.setButtonText(context.getString(R.string.show_new_comments, button.getCommentCount())); @@ -247,7 +232,7 @@ private void bindButtonViewHolder(ButtonViewHolder holder, int position) { }); } } - + private android.os.Handler getHandler() { return new android.os.Handler(android.os.Looper.getMainLooper()); } @@ -269,18 +254,22 @@ public int getPositionForComment(RenderableComment comment) { public interface OnToggleRepliesListener { void onToggle(RenderableComment comment, Button toggleButton); } - + /** * Listener for comment menu items */ public interface OnCommentMenuItemListener { void onEdit(String commentId, String commentText); + void onDelete(String commentId); + void onFlag(String commentId); + void onBlock(String commentId, String userName); + void onUnblock(String commentId, String userName); } - + /** * ViewHolder for button items that prompt the user to load new comments */ @@ -288,7 +277,7 @@ static class ButtonViewHolder extends RecyclerView.ViewHolder { private final Button button; private final FastCommentsSDK sdk; private final Context context; - + public ButtonViewHolder(@NonNull View itemView, FastCommentsSDK sdk, Context context) { super(itemView); this.sdk = sdk; @@ -296,42 +285,42 @@ public ButtonViewHolder(@NonNull View itemView, FastCommentsSDK sdk, Context con button = itemView.findViewById(R.id.btnNewComments); applyTheme(); } - + /** * Apply theme colors to the button */ private void applyTheme() { FastCommentsTheme theme = sdk != null ? sdk.getTheme() : null; - + // Apply load more button text color int loadMoreButtonTextColor = ThemeColorResolver.getLoadMoreButtonTextColor(context, theme); if (button != null) { button.setTextColor(loadMoreButtonTextColor); } } - + public void setButtonText(String text) { button.setText(text); } - + public void setButtonClickListener(View.OnClickListener listener) { button.setOnClickListener(listener); } } - + /** * Date separator view holder for the live chat view */ static class DateSeparatorViewHolder extends RecyclerView.ViewHolder { private final TextView dateText; - + public DateSeparatorViewHolder(@NonNull View itemView) { super(itemView); dateText = itemView.findViewById(R.id.dateSeparatorText); } - + public void setDate(RenderableNode.DateSeparator separator) { dateText.setText(separator.getFormattedDate()); } } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentsDialog.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentsDialog.java index bd0b42d..3a7430c 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentsDialog.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentsDialog.java @@ -11,16 +11,12 @@ import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.TextView; - import androidx.annotation.NonNull; - -import java.util.ArrayList; -import java.util.List; - import com.fastcomments.model.APIError; import com.fastcomments.model.FeedPost; import com.fastcomments.model.FeedPostsStatsResponse; -import com.fastcomments.model.PublicComment; +import java.util.ArrayList; +import java.util.List; /** * Dialog for displaying comments for a post @@ -32,7 +28,7 @@ public class CommentsDialog extends Dialog { private FastCommentsView commentsView; private OnCommentAddedListener commentAddedListener; private OnUserClickListener userClickListener; - + /** * Interface for notifying when a comment is added */ @@ -45,14 +41,14 @@ public CommentsDialog(@NonNull Context context, FeedPost post, FastCommentsFeedS this.post = post; this.feedSDK = feedSDK; } - + /** * Set a listener to be notified when a comment is added */ public void setOnCommentAddedListener(OnCommentAddedListener listener) { this.commentAddedListener = listener; } - + /** * Get the current comment added listener * @return The current listener @@ -60,7 +56,7 @@ public void setOnCommentAddedListener(OnCommentAddedListener listener) { public OnCommentAddedListener getOnCommentAddedListener() { return commentAddedListener; } - + /** * Set a listener to be notified when a user (name or avatar) is clicked * @param listener The listener to set @@ -72,7 +68,7 @@ public void setOnUserClickListener(OnUserClickListener listener) { commentsView.setOnUserClickListener(listener); } } - + /** * Get the current user click listener * @return The current listener @@ -80,7 +76,7 @@ public void setOnUserClickListener(OnUserClickListener listener) { public OnUserClickListener getOnUserClickListener() { return userClickListener; } - + /** * Get the post ID for this dialog * @return The post ID @@ -88,19 +84,18 @@ public OnUserClickListener getOnUserClickListener() { public String getPostId() { return post != null ? post.getId() : null; } - + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - + // Remove default dialog title requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.dialog_comments); - + // Make dialog fill most of the screen if (getWindow() != null) { - getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT); + getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); // Resize the dialog window when the soft keyboard appears so the bottom-anchored // comment/reply input stays visible above the keyboard instead of behind it. @@ -108,41 +103,38 @@ protected void onCreate(Bundle savedInstanceState) { // set it explicitly here. getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); } - + // Set up close button ImageButton closeButton = findViewById(R.id.closeButton); closeButton.setOnClickListener(v -> { // Use the same logic as onBackPressed onBackPressed(); }); - + // Set title if available TextView titleTextView = findViewById(R.id.titleTextView); if (post.getTitle() != null && !post.getTitle().isEmpty()) { titleTextView.setText(post.getTitle()); } - + // Create the comments SDK and view FastCommentsSDK commentsSDK = feedSDK.createCommentsSDKForPost(post); commentsView = new FastCommentsView(getContext(), commentsSDK); - + // Apply theme colors to dialog header if theme is available if (feedSDK.getTheme() != null) { FrameLayout headerContainer = findViewById(R.id.headerContainer); headerContainer.setBackgroundColor( - ThemeColorResolver.getDialogHeaderBackgroundColor(getContext(), feedSDK.getTheme()) - ); - - titleTextView.setTextColor( - ThemeColorResolver.getDialogHeaderTextColor(getContext(), feedSDK.getTheme()) - ); + ThemeColorResolver.getDialogHeaderBackgroundColor(getContext(), feedSDK.getTheme())); + + titleTextView.setTextColor(ThemeColorResolver.getDialogHeaderTextColor(getContext(), feedSDK.getTheme())); } - + // Set user click listener if one was provided if (userClickListener != null) { commentsView.setOnUserClickListener(userClickListener); } - + // Set up comment form listener to know when a comment is posted commentsView.setCommentPostListener((comment) -> { // Update post stats to refresh comment count @@ -155,16 +147,16 @@ public boolean onFailure(APIError error) { // Silently fail - not critical return CONSUME; } - + @Override public boolean onSuccess(FeedPostsStatsResponse response) { // Stats updated in the SDK's cache - + // Notify the listener that a comment was added if (commentAddedListener != null) { commentAddedListener.onCommentAdded(post.getId()); } - + return CONSUME; } }); @@ -173,20 +165,21 @@ public boolean onSuccess(FeedPostsStatsResponse response) { commentAddedListener.onCommentAdded(post.getId()); } }); - + // Add comments view to container FrameLayout container = findViewById(R.id.commentsContainer); - container.addView(commentsView, new FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT)); - + container.addView( + commentsView, + new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); + // Load comments commentsView.load(); } - + /** * Check if the back press should be intercepted to handle comment form state - * + * * @return true if the back press should be intercepted, false otherwise */ private boolean shouldInterceptBackPress() { @@ -194,26 +187,26 @@ private boolean shouldInterceptBackPress() { if (commentsView == null) { return false; } - + // Get the bottom comment input from CommentsView BottomCommentInputView bottomInput = commentsView.getBottomCommentInput(); if (bottomInput == null) { return false; } - + // Check if user has typed any text (either in reply or new comment) boolean hasText = !bottomInput.isTextEmpty(); - + return hasText; } - + @Override public void onBackPressed() { if (shouldInterceptBackPress()) { // Get the bottom input and check if it has a parent comment (reply) or text BottomCommentInputView bottomInput = commentsView.getBottomCommentInput(); RenderableComment parentComment = bottomInput.getParentComment(); - + // Show confirmation dialog String title, message; if (parentComment != null) { @@ -223,25 +216,25 @@ public void onBackPressed() { title = getContext().getString(R.string.cancel_comment_title); message = getContext().getString(R.string.cancel_comment_confirm); } - + new android.app.AlertDialog.Builder(getContext()) - .setTitle(title) - .setMessage(message) - .setPositiveButton(android.R.string.yes, (dialog, which) -> { - // Proceed with cancellation - bottomInput.clearReplyState(); - bottomInput.clearText(); - - // Don't dismiss the dialog yet - let the user continue with comments - }) - .setNegativeButton(android.R.string.no, null) - .show(); + .setTitle(title) + .setMessage(message) + .setPositiveButton(android.R.string.yes, (dialog, which) -> { + // Proceed with cancellation + bottomInput.clearReplyState(); + bottomInput.clearText(); + + // Don't dismiss the dialog yet - let the user continue with comments + }) + .setNegativeButton(android.R.string.no, null) + .show(); } else { // No need to intercept, just dismiss the dialog super.onBackPressed(); } } - + @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); @@ -251,4 +244,4 @@ public void onDetachedFromWindow() { commentsView = null; } } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentsTree.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentsTree.java index 6c3b5ba..db49ecf 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentsTree.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/CommentsTree.java @@ -1,11 +1,8 @@ package com.fastcomments.sdk; import android.content.Context; -import android.util.Log; - import com.fastcomments.model.PublicComment; import com.fastcomments.model.SortDirections; - import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -13,7 +10,6 @@ import java.util.Map; import java.util.Set; - /** * The way the RecyclerView works is that when it needs to load an element at an index it calls onBindViewHolder(index) * To keep this as an n-time lookup without hashmaps it means we need to maintain an array of visible items. @@ -49,15 +45,15 @@ public CommentsTree() { public void setAdapter(CommentsAdapter adapter) { this.adapter = adapter; } - + public CommentsAdapter getAdapter() { return this.adapter; } - + public Context getContext() { return adapter != null ? adapter.getContext() : null; } - + /** * Set whether this tree should use live chat style rendering (with date separators) * @param liveChatStyle true for live chat style @@ -100,20 +96,20 @@ public void build(List comments) { final RenderableComment renderableComment = new RenderableComment(comment); addToMapAndRelated(renderableComment); allComments.add(renderableComment); - + // Check if we need a date separator if (comment.getDate() != null) { java.time.LocalDate commentDate = comment.getDate().toLocalDate(); - + if (currentDate == null || !currentDate.equals(commentDate)) { // Add date separator for this new date currentDate = commentDate; visibleNodes.add(new RenderableNode.DateSeparator(currentDate)); } } - + visibleNodes.add(renderableComment); - + // In live chat, we typically don't show children/replies // But process them anyway in case this changes if (comment.getChildren() != null) { @@ -155,8 +151,8 @@ public void appendComments(List comments) { } // Notify the adapter of exactly what changed (the newly added comments) TODO doesn't work right -// int itemCount = visibleNodes.size() - initialSize; -// adapter.notifyItemRangeInserted(initialSize, itemCount); + // int itemCount = visibleNodes.size() - initialSize; + // adapter.notifyItemRangeInserted(initialSize, itemCount); adapter.notifyDataSetChanged(); } @@ -195,7 +191,11 @@ public void addForParent(String parentId, List comments) { } } - private void handleChildren(List allComments, List visibleNodes, List comments, boolean visible) { + private void handleChildren( + List allComments, + List visibleNodes, + List comments, + boolean visible) { for (PublicComment child : comments) { final RenderableComment childRenderable = new RenderableComment(child); addToMapAndRelated(childRenderable); @@ -235,7 +235,8 @@ public void removeChildren(List publicComments, List ind final RenderableComment childRenderable = commentsById.get(child.getId()); // see explanation at top of class allComments.remove(childRenderable); - int index = visibleNodes.indexOf(childRenderable); // TODO optimize away lookup if parent does not have children visible + int index = visibleNodes.indexOf( + childRenderable); // TODO optimize away lookup if parent does not have children visible if (index >= 0) { indexesRemoved.add(index); visibleNodes.remove(index); @@ -246,7 +247,10 @@ public void removeChildren(List publicComments, List ind } } - public void setRepliesVisible(RenderableComment renderableComment, boolean areRepliesVisible, Producer> getChildren) { + public void setRepliesVisible( + RenderableComment renderableComment, + boolean areRepliesVisible, + Producer> getChildren) { final boolean wasRepliesVisible = renderableComment.isRepliesShown; if (wasRepliesVisible == areRepliesVisible) { return; @@ -274,8 +278,7 @@ public void setRepliesVisible(RenderableComment renderableComment, boolean areRe RenderableButton newRepliesButton = new RenderableButton( RenderableButton.TYPE_NEW_CHILD_COMMENTS, renderableComment.getNewChildCommentsCount(), - renderableComment.getComment().getId() - ); + renderableComment.getComment().getId()); // Add the button after the last child and track it String parentId = renderableComment.getComment().getId(); @@ -294,12 +297,7 @@ public void setRepliesVisible(RenderableComment renderableComment, boolean areRe // Create GetChildrenRequest with the comment ID and the toggle button // Note: We can't directly get the button here, so we'll handle button reference via the adapter GetChildrenRequest request = new GetChildrenRequest( - renderableComment.getComment().getId(), - null, - 0, - renderableComment.childPageSize, - false - ); + renderableComment.getComment().getId(), null, 0, renderableComment.childPageSize, false); final String parentId = renderableComment.getComment().getId(); getChildren.get(request, (asyncFetchedChildren) -> { @@ -309,8 +307,9 @@ public void setRepliesVisible(RenderableComment renderableComment, boolean areRe // Set hasMoreChildren based on child count vs. loaded children count if (renderableComment.getComment().getChildCount() != null) { int totalChildCount = renderableComment.getComment().getChildCount(); - int loadedChildCount = renderableComment.getComment().getChildren() != null ? - renderableComment.getComment().getChildren().size() : 0; + int loadedChildCount = renderableComment.getComment().getChildren() != null + ? renderableComment.getComment().getChildren().size() + : 0; renderableComment.hasMoreChildren = (loadedChildCount < totalChildCount); } @@ -323,8 +322,7 @@ public void setRepliesVisible(RenderableComment renderableComment, boolean areRe RenderableButton newRepliesButton = new RenderableButton( RenderableButton.TYPE_NEW_CHILD_COMMENTS, renderableComment.getNewChildCommentsCount(), - parentId - ); + parentId); // Add the button after the last child and track it visibleNodes.add(lastChildIndex + 1, newRepliesButton); @@ -386,8 +384,8 @@ private int findLastChildIndex(RenderableComment parentComment) { lastChildIndex = i; } else if (node instanceof RenderableButton) { RenderableButton button = (RenderableButton) node; - if (button.getButtonType() == RenderableButton.TYPE_NEW_CHILD_COMMENTS && - parentId.equals(button.getParentId())) { + if (button.getButtonType() == RenderableButton.TYPE_NEW_CHILD_COMMENTS + && parentId.equals(button.getParentId())) { lastChildIndex = i; } else { break; @@ -409,8 +407,9 @@ public void insertChildrenAfter(RenderableComment renderableComment, List= 0; i--) { RenderableNode node = visibleNodes.get(i); @@ -508,15 +507,19 @@ public void addComment(PublicComment comment, boolean displayNow, SortDirections break; // Exit after finding the most recent date separator } else if (node instanceof RenderableComment) { RenderableComment lastComment = (RenderableComment) node; - if (lastComment.getComment().getDate() != null && - lastComment.getComment().getDate().toLocalDate().equals(commentDate)) { + if (lastComment.getComment().getDate() != null + && lastComment + .getComment() + .getDate() + .toLocalDate() + .equals(commentDate)) { // The previous comment is from the same date needDateSeparator = false; break; } } } - + if (needDateSeparator) { RenderableNode.DateSeparator separator = new RenderableNode.DateSeparator(commentDate); visibleNodes.add(separator); @@ -524,11 +527,11 @@ public void addComment(PublicComment comment, boolean displayNow, SortDirections position++; } } - + visibleNodes.add(renderableComment); } adapter.notifyItemInserted(position); - + // Check for new user presence (optimized for single comment) checkAndRequestUserPresenceStatus(renderableComment); } else { @@ -537,10 +540,8 @@ public void addComment(PublicComment comment, boolean displayNow, SortDirections // If this is the first new comment, add a "New Comments" button at the top if (newRootCommentsButton == null) { - newRootCommentsButton = new RenderableButton( - RenderableButton.TYPE_NEW_ROOT_COMMENTS, - newRootComments.size() - ); + newRootCommentsButton = + new RenderableButton(RenderableButton.TYPE_NEW_ROOT_COMMENTS, newRootComments.size()); visibleNodes.add(0, newRootCommentsButton); adapter.notifyItemInserted(0); } else { @@ -548,10 +549,8 @@ public void addComment(PublicComment comment, boolean displayNow, SortDirections int buttonIndex = visibleNodes.indexOf(newRootCommentsButton); if (buttonIndex >= 0) { // Replace with updated button - newRootCommentsButton = new RenderableButton( - RenderableButton.TYPE_NEW_ROOT_COMMENTS, - newRootComments.size() - ); + newRootCommentsButton = + new RenderableButton(RenderableButton.TYPE_NEW_ROOT_COMMENTS, newRootComments.size()); visibleNodes.set(buttonIndex, newRootCommentsButton); adapter.notifyItemChanged(buttonIndex); } @@ -594,7 +593,7 @@ public void addComment(PublicComment comment, boolean displayNow, SortDirections int insertionIndex = findLastChildIndex(parent) + 1; visibleNodes.add(insertionIndex, renderableComment); adapter.notifyItemInserted(insertionIndex); - + // Check for new user presence (optimized for single comment) checkAndRequestUserPresenceStatus(renderableComment); } else if (parent.isRepliesShown) { @@ -613,8 +612,7 @@ public void addComment(PublicComment comment, boolean displayNow, SortDirections RenderableButton updatedButton = new RenderableButton( RenderableButton.TYPE_NEW_CHILD_COMMENTS, parent.getNewChildCommentsCount(), - parentId - ); + parentId); visibleNodes.set(buttonIndex, updatedButton); newChildCommentsButtons.put(parentId, updatedButton); adapter.notifyItemChanged(buttonIndex); @@ -625,8 +623,7 @@ public void addComment(PublicComment comment, boolean displayNow, SortDirections RenderableButton newRepliesButton = new RenderableButton( RenderableButton.TYPE_NEW_CHILD_COMMENTS, parent.getNewChildCommentsCount(), - parentId - ); + parentId); visibleNodes.add(insertionIndex, newRepliesButton); newChildCommentsButtons.put(parentId, newRepliesButton); adapter.notifyItemInserted(insertionIndex); @@ -659,7 +656,7 @@ public void showNewRootComments() { // Add all buffered comments at the top of the list in chronological order (oldest first) // Convert PublicComment list to RenderableComment list as we add them List addedComments = new ArrayList<>(); - + for (int i = 0; i < newRootComments.size(); i++) { PublicComment comment = newRootComments.get(i); RenderableComment renderableComment = commentsById.get(comment.getId()); @@ -669,7 +666,7 @@ public void showNewRootComments() { addedComments.add(comment); } } - + // Check for new user presence (optimized for the specific comments) if (!addedComments.isEmpty()) { checkAndRequestUserPresenceStatuses(addedComments); @@ -726,7 +723,7 @@ public void showNewChildComments(String parentId) { // the starting position by subtracting the number of inserted items int startPosition = insertionIndex - newChildComments.size(); adapter.notifyItemRangeInserted(startPosition, newChildComments.size()); - + // Check for new user presence (optimized for specific comments) checkAndRequestUserPresenceStatuses(newChildComments); } @@ -811,26 +808,26 @@ public void updateUserPresence(String userId, boolean isOnline) { notifyItemChanged(comment); } } - + /** * Check for newly visible comments and return any user IDs we need to fetch presence for - * + * * @return A set of user IDs needing presence status updates */ public Set checkForNewlyVisibleCommentUsers() { Set userIdsToFetch = new HashSet<>(); - + // Check all visible comments for (RenderableNode node : visibleNodes) { if (node instanceof RenderableComment) { RenderableComment comment = (RenderableComment) node; - + // Check regular user ID String userId = comment.getComment().getUserId(); if (userId != null && !userId.isEmpty() && !userPresenceCache.containsKey(userId)) { userIdsToFetch.add(userId); } - + // Check anonymous user ID String anonUserId = comment.getComment().getAnonUserId(); if (anonUserId != null && !anonUserId.isEmpty() && !userPresenceCache.containsKey(anonUserId)) { @@ -838,10 +835,10 @@ public Set checkForNewlyVisibleCommentUsers() { } } } - + return userIdsToFetch; } - + /** * Check for newly visible comments and request presence status updates if needed */ @@ -849,62 +846,62 @@ public void checkAndRequestUserPresenceStatuses() { Set userIdsToFetch = checkForNewlyVisibleCommentUsers(); requestPresenceStatusesIfNeeded(userIdsToFetch); } - + /** * Check for newly visible comment and request presence status updates if needed - * + * * @param comment The specific comment that became visible */ public void checkAndRequestUserPresenceStatus(RenderableComment comment) { Set userIdsToFetch = new HashSet<>(); - + // Check regular user ID String userId = comment.getComment().getUserId(); if (userId != null && !userId.isEmpty() && !userPresenceCache.containsKey(userId)) { userIdsToFetch.add(userId); } - + // Check anonymous user ID String anonUserId = comment.getComment().getAnonUserId(); if (anonUserId != null && !anonUserId.isEmpty() && !userPresenceCache.containsKey(anonUserId)) { userIdsToFetch.add(anonUserId); } - + requestPresenceStatusesIfNeeded(userIdsToFetch); } - + /** * Check for newly visible comments and request presence status updates if needed - * + * * @param comments The specific comments that became visible */ public void checkAndRequestUserPresenceStatuses(List comments) { if (comments == null || comments.isEmpty()) { return; } - + Set userIdsToFetch = new HashSet<>(); - + for (PublicComment comment : comments) { // Check regular user ID String userId = comment.getUserId(); if (userId != null && !userId.isEmpty() && !userPresenceCache.containsKey(userId)) { userIdsToFetch.add(userId); } - + // Check anonymous user ID String anonUserId = comment.getAnonUserId(); if (anonUserId != null && !anonUserId.isEmpty() && !userPresenceCache.containsKey(anonUserId)) { userIdsToFetch.add(anonUserId); } } - + requestPresenceStatusesIfNeeded(userIdsToFetch); } - + /** * Request presence status updates for a set of user IDs - * + * * @param userIdsToFetch The set of user IDs to fetch status for */ private void requestPresenceStatusesIfNeeded(Set userIdsToFetch) { @@ -917,22 +914,22 @@ private void requestPresenceStatusesIfNeeded(Set userIdsToFetch) { } userIdsCSV.append(userId); } - + // Request presence status updates presenceStatusListener.onPresenceStatusNeeded(userIdsCSV.toString()); } } - + // Interface for requesting presence status updates public interface PresenceStatusListener { void onPresenceStatusNeeded(String userIdsCSV); } - + private PresenceStatusListener presenceStatusListener; - + /** * Set the listener for presence status update requests - * + * * @param listener The listener to set */ public void setPresenceStatusListener(PresenceStatusListener listener) { @@ -960,19 +957,20 @@ public boolean removeComment(String commentId) { // Remove this from the cached list of user's comments. if (comment.getComment().getUserId() != null) { - final List usersComments = commentsByUserId.get(comment.getComment().getUserId()); + final List usersComments = + commentsByUserId.get(comment.getComment().getUserId()); if (usersComments != null) { usersComments.remove(comment); } } if (comment.getComment().getAnonUserId() != null) { - final List usersComments = commentsByUserId.get(comment.getComment().getAnonUserId()); + final List usersComments = + commentsByUserId.get(comment.getComment().getAnonUserId()); if (usersComments != null) { usersComments.remove(comment); } } - // Handle parent's child count if this is a reply final String parentId = comment.getComment().getParentId(); if (parentId != null) { @@ -1010,7 +1008,8 @@ public boolean removeComment(String commentId) { adapter.notifyItemRemoved(visibleIndex); if (comment.isRepliesShown && comment.getComment().getChildren() != null) { - final List indexesToRemove = new ArrayList<>(comment.getComment().getChildren().size()); + final List indexesToRemove = + new ArrayList<>(comment.getComment().getChildren().size()); removeChildren(comment.getComment().getChildren(), indexesToRemove); for (Integer i : indexesToRemove) { adapter.notifyItemRemoved(i); // TODO worth to optimize into one notification? @@ -1020,7 +1019,7 @@ public boolean removeComment(String commentId) { return true; } - + /** * Updates the isBlocked status of comments based on the commentStatuses map * returned from the block/unblock API response, and notifies the adapter. diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/CustomImageGetter.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/CustomImageGetter.java index 6349944..cf538f3 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/CustomImageGetter.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/CustomImageGetter.java @@ -2,15 +2,12 @@ import android.content.Context; import android.graphics.Bitmap; -import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.text.Html; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.bumptech.glide.Glide; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.request.target.CustomTarget; @@ -33,7 +30,8 @@ public Drawable getDrawable(String source) { final URLDrawable urlDrawable = new URLDrawable(); urlDrawable.setBounds(0, 0, textView.getWidth(), 100); - boolean isGif = source != null && (source.endsWith(".gif") || source.contains(".gif?") || source.contains("/giphy.gif")); + boolean isGif = source != null + && (source.endsWith(".gif") || source.contains(".gif?") || source.contains("/giphy.gif")); if (isGif) { loadGif(source, urlDrawable); @@ -45,61 +43,54 @@ public Drawable getDrawable(String source) { } private void loadGif(String source, URLDrawable urlDrawable) { - Glide.with(context) - .asGif() - .load(source) - .into(new CustomTarget() { + Glide.with(context).asGif().load(source).into(new CustomTarget() { + @Override + public void onResourceReady( + @NonNull GifDrawable resource, @Nullable Transition transition) { + resource.setBounds(0, 0, resource.getIntrinsicWidth(), resource.getIntrinsicHeight()); + resource.setLoopCount(GifDrawable.LOOP_FOREVER); + + resource.setCallback(new Drawable.Callback() { @Override - public void onResourceReady(@NonNull GifDrawable resource, @Nullable Transition transition) { - resource.setBounds(0, 0, resource.getIntrinsicWidth(), resource.getIntrinsicHeight()); - resource.setLoopCount(GifDrawable.LOOP_FOREVER); - - resource.setCallback(new Drawable.Callback() { - @Override - public void invalidateDrawable(@NonNull Drawable who) { - textView.invalidate(); - } - - @Override - public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { - textView.postDelayed(what, when - android.os.SystemClock.uptimeMillis()); - } - - @Override - public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { - textView.removeCallbacks(what); - } - }); + public void invalidateDrawable(@NonNull Drawable who) { + textView.invalidate(); + } - urlDrawable.setDrawable(resource); - urlDrawable.setBounds(0, 0, resource.getIntrinsicWidth(), resource.getIntrinsicHeight()); - resource.start(); - textView.setText(textView.getText()); + @Override + public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { + textView.postDelayed(what, when - android.os.SystemClock.uptimeMillis()); } @Override - public void onLoadCleared(@Nullable Drawable placeholder) { + public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { + textView.removeCallbacks(what); } }); + + urlDrawable.setDrawable(resource); + urlDrawable.setBounds(0, 0, resource.getIntrinsicWidth(), resource.getIntrinsicHeight()); + resource.start(); + textView.setText(textView.getText()); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) {} + }); } private void loadBitmap(String source, URLDrawable urlDrawable) { - Glide.with(context) - .asBitmap() - .load(source) - .into(new CustomTarget() { - @Override - public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { - BitmapDrawable drawable = new BitmapDrawable(context.getResources(), resource); - drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); - urlDrawable.setDrawable(drawable); - urlDrawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); - textView.setText(textView.getText()); - } - - @Override - public void onLoadCleared(@Nullable Drawable placeholder) { - } - }); + Glide.with(context).asBitmap().load(source).into(new CustomTarget() { + @Override + public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { + BitmapDrawable drawable = new BitmapDrawable(context.getResources(), resource); + drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + urlDrawable.setDrawable(drawable); + urlDrawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + textView.setText(textView.getText()); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) {} + }); } } diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/CustomToolbarButton.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/CustomToolbarButton.java index 5bd1858..11afa48 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/CustomToolbarButton.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/CustomToolbarButton.java @@ -101,4 +101,4 @@ default void onAttached(BottomCommentInputView view, View buttonView) { default void onDetached(BottomCommentInputView view, View buttonView) { // Default implementation does nothing } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/DemoBannerHelper.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/DemoBannerHelper.java index d2361db..2b2de4a 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/DemoBannerHelper.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/DemoBannerHelper.java @@ -9,10 +9,10 @@ * Helper class for setting up demo banners across SDK views */ public class DemoBannerHelper { - + /** * Sets up the demo banner if tenant ID is "demo" - * + * * @param containerView The view containing the demo banner * @param sdk The SDK instance to check tenant ID */ @@ -20,10 +20,10 @@ public static void setupDemoBanner(View containerView, FastCommentsSDK sdk) { String tenantId = sdk != null ? sdk.getConfig().tenantId : null; setupDemoBannerInternal(containerView, tenantId); } - + /** * Sets up the demo banner for feed views with feed SDK - * + * * @param containerView The view containing the demo banner * @param feedSdk The feed SDK instance to check tenant ID */ @@ -31,10 +31,10 @@ public static void setupDemoBanner(View containerView, FastCommentsFeedSDK feedS String tenantId = feedSdk != null ? feedSdk.getConfig().tenantId : null; setupDemoBannerInternal(containerView, tenantId); } - + /** * Internal method to handle the common demo banner setup logic - * + * * @param containerView The view containing the demo banner * @param tenantId The tenant ID to check */ @@ -44,7 +44,7 @@ private static void setupDemoBannerInternal(View containerView, String tenantId) // Show banner only if tenant ID is "demo" if ("demo".equals(tenantId)) { demoBanner.setVisibility(View.VISIBLE); - + // Set up click listener for "Create an account" link TextView createAccountLink = demoBanner.findViewById(R.id.createAccountLink); if (createAccountLink != null) { @@ -58,4 +58,4 @@ private static void setupDemoBannerInternal(View containerView, String tenantId) } } } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/FCCallback.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/FCCallback.java index 567eb8d..d8b6ca7 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/FCCallback.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/FCCallback.java @@ -14,7 +14,6 @@ public interface FCCallback { /** * @return true to stop the callback chain */ - boolean onSuccess(ResponseType response); default void onUploadProgress(long bytesWritten, long contentLength, boolean done) { diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsFeedSDK.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsFeedSDK.java index e4f92a7..9945c15 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsFeedSDK.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsFeedSDK.java @@ -2,10 +2,13 @@ import static com.fastcomments.model.LiveEventType.NEW_FEED_POST; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; import android.os.Handler; import android.os.Looper; +import android.provider.OpenableColumns; import android.util.Log; - import com.fastcomments.api.PublicApi; import com.fastcomments.core.CommentWidgetConfig; import com.fastcomments.invoker.ApiCallback; @@ -20,16 +23,20 @@ import com.fastcomments.model.FeedPostMediaItemAsset; import com.fastcomments.model.FeedPostStats; import com.fastcomments.model.FeedPostsStatsResponse; -import com.fastcomments.model.MediaAsset; -import com.fastcomments.model.SizePreset; -import com.fastcomments.model.UploadImageResponse; -import com.fastcomments.model.PublicFeedPostsResponse; import com.fastcomments.model.LiveEvent; import com.fastcomments.model.LiveEventType; +import com.fastcomments.model.MediaAsset; +import com.fastcomments.model.PublicFeedPostsResponse; import com.fastcomments.model.ReactBodyParams; import com.fastcomments.model.ReactFeedPostResponse; +import com.fastcomments.model.SizePreset; +import com.fastcomments.model.UploadImageResponse; import com.fastcomments.model.UserSessionInfo; - +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; @@ -42,18 +49,6 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicReference; -import android.net.Uri; -import android.content.Context; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.FileOutputStream; -import java.io.OutputStream; - -import android.provider.OpenableColumns; -import android.database.Cursor; - /** * SDK class for handling FastComments Feed functionality */ @@ -119,7 +114,7 @@ public FastCommentsFeedSDK(CommentWidgetConfig config) { public CommentWidgetConfig getConfig() { return config; } - + /** * Get the current theme * @@ -128,7 +123,7 @@ public CommentWidgetConfig getConfig() { public FastCommentsTheme getTheme() { return theme; } - + /** * Set a TagSupplier to provide tags for filtering feed posts. * The tags returned by the supplier will be used when fetching posts from the API. @@ -138,7 +133,7 @@ public FastCommentsTheme getTheme() { public void setTagSupplier(TagSupplier tagSupplier) { this.tagSupplier = tagSupplier; } - + /** * Get the current TagSupplier * @@ -425,7 +420,7 @@ public void restorePaginationState(FeedState state) { this.myReacts.putAll(state.getMyReacts()); } - // Restore like counts if available + // Restore like counts if available if (state.getLikeCounts() != null) { this.likeCounts.clear(); this.likeCounts.putAll(state.getLikeCounts()); @@ -439,7 +434,7 @@ public void restorePaginationState(FeedState state) { */ public void load(FCCallback callback) { // Reset pagination for initial load - lastPostId = null; // Reset the cursor for pagination + lastPostId = null; // Reset the cursor for pagination // Reset any existing error message blockingErrorMessage = null; @@ -462,7 +457,7 @@ private void loadFeedPosts(FCCallback callback) { if (tagSupplier != null) { tags = tagSupplier.getTags(currentUser); } - + api.getFeedPostsPublic(config.tenantId) .afterId(lastPostId) .limit(pageSize) @@ -471,18 +466,23 @@ private void loadFeedPosts(FCCallback callback) { .tags(tags) .executeAsync(new ApiCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { APIError error = CallbackWrapper.createErrorFromException(e); - if (error.getTranslatedError() != null && !error.getTranslatedError().isEmpty()) { + if (error.getTranslatedError() != null + && !error.getTranslatedError().isEmpty()) { blockingErrorMessage = error.getTranslatedError(); - } else if (error.getReason() != null && !error.getReason().isEmpty()) { + } else if (error.getReason() != null + && !error.getReason().isEmpty()) { blockingErrorMessage = "Feed could not load! Details: " + error.getReason(); } // Log the error, particularly for JsonSyntaxException - String errorMessage = "API Error: " + (e.getMessage() != null ? e.getMessage() : "Unknown error"); + String errorMessage = + "API Error: " + (e.getMessage() != null ? e.getMessage() : "Unknown error"); if (e.getCause() != null) { - errorMessage += " Cause: " + e.getCause().getClass().getSimpleName(); + errorMessage += + " Cause: " + e.getCause().getClass().getSimpleName(); if (e.getCause().getMessage() != null) { errorMessage += " (" + e.getCause().getMessage() + ")"; } @@ -493,99 +493,104 @@ public void onFailure(ApiException e, int statusCode, Map> } @Override - public void onSuccess(PublicFeedPostsResponse response, int statusCode, Map> responseHeaders) { - mainHandler.post(() -> { - final PublicFeedPostsResponse publicResponse = response; - - boolean needsWebsocketReconnect = false; + public void onSuccess( + PublicFeedPostsResponse response, + int statusCode, + Map> responseHeaders) { + mainHandler.post(() -> { + final PublicFeedPostsResponse publicResponse = response; - if (publicResponse.getUser() != null) { - currentUser = publicResponse.getUser(); - } + boolean needsWebsocketReconnect = false; - if (publicResponse.getTenantIdWS() != null) { - tenantIdWS = publicResponse.getTenantIdWS(); - } + if (publicResponse.getUser() != null) { + currentUser = publicResponse.getUser(); + } - if (publicResponse.getUrlIdWS() != null) { - urlIdWS = publicResponse.getUrlIdWS(); - } + if (publicResponse.getTenantIdWS() != null) { + tenantIdWS = publicResponse.getTenantIdWS(); + } - if (publicResponse.getUserIdWS() != null) { - // Check if userIdWS has changed, which requires WebSocket reconnection - if (userIdWS != null && !Objects.equals(publicResponse.getUserIdWS(), userIdWS)) { - needsWebsocketReconnect = true; - } - userIdWS = publicResponse.getUserIdWS(); - } + if (publicResponse.getUrlIdWS() != null) { + urlIdWS = publicResponse.getUrlIdWS(); + } - // Subscribe to live events if we have all required parameters - // or if we need to reconnect due to userIdWS change - if ((tenantIdWS != null && urlIdWS != null && userIdWS != null) && - (liveEventSubscription == null || needsWebsocketReconnect)) { - subscribeToLiveEvents(); + if (publicResponse.getUserIdWS() != null) { + // Check if userIdWS has changed, which requires WebSocket reconnection + if (userIdWS != null && !Objects.equals(publicResponse.getUserIdWS(), userIdWS)) { + needsWebsocketReconnect = true; } + userIdWS = publicResponse.getUserIdWS(); + } - final List posts = publicResponse.getFeedPosts(); + // Subscribe to live events if we have all required parameters + // or if we need to reconnect due to userIdWS change + if ((tenantIdWS != null && urlIdWS != null && userIdWS != null) + && (liveEventSubscription == null || needsWebsocketReconnect)) { + subscribeToLiveEvents(); + } - // Process the myReacts from the response if available - if (publicResponse.getMyReacts() != null) { - // Only clear reactions if this is an initial load - if (lastPostId == null) { - myReacts.clear(); - } - // Add all the myReacts for the posts - myReacts.putAll(publicResponse.getMyReacts()); - } + final List posts = publicResponse.getFeedPosts(); - // Only clear the list if this is an initial load (no lastPostId) - // This ensures we don't clear when paginating or loading more + // Process the myReacts from the response if available + if (publicResponse.getMyReacts() != null) { + // Only clear reactions if this is an initial load if (lastPostId == null) { - feedPosts.clear(); - postsById.clear(); - likeCounts.clear(); + myReacts.clear(); } + // Add all the myReacts for the posts + myReacts.putAll(publicResponse.getMyReacts()); + } - if (!posts.isEmpty()) { - // Add posts to list and maps - for (FeedPost post : posts) { - if (post.getId() != null) { - // Store post by ID for quick lookup - postsById.put(post.getId(), post); - - // Calculate initial like count from post's reacts - if (post.getReacts() != null && post.getReacts().containsKey("l")) { - likeCounts.put(post.getId(), post.getReacts().get("l").intValue()); - } else { - likeCounts.put(post.getId(), 0); - } + // Only clear the list if this is an initial load (no lastPostId) + // This ensures we don't clear when paginating or loading more + if (lastPostId == null) { + feedPosts.clear(); + postsById.clear(); + likeCounts.clear(); + } + + if (!posts.isEmpty()) { + // Add posts to list and maps + for (FeedPost post : posts) { + if (post.getId() != null) { + // Store post by ID for quick lookup + postsById.put(post.getId(), post); + + // Calculate initial like count from post's reacts + if (post.getReacts() != null + && post.getReacts().containsKey("l")) { + likeCounts.put( + post.getId(), + post.getReacts() + .get("l") + .intValue()); + } else { + likeCounts.put(post.getId(), 0); } } + } - // Add to main post list - feedPosts.addAll(posts); + // Add to main post list + feedPosts.addAll(posts); - // Update lastPostId for pagination if we have posts - FeedPost lastPost = posts.get(posts.size() - 1); - lastPostId = lastPost.getId(); - } + // Update lastPostId for pagination if we have posts + FeedPost lastPost = posts.get(posts.size() - 1); + lastPostId = lastPost.getId(); + } - // Check if there are more posts to load - // If we got posts back and size equals page size, assume more posts are available - hasMore = !posts.isEmpty() && posts.size() >= pageSize; + // Check if there are more posts to load + // If we got posts back and size equals page size, assume more posts are available + hasMore = !posts.isEmpty() && posts.size() >= pageSize; - callback.onSuccess(publicResponse); - }); - + callback.onSuccess(publicResponse); + }); } @Override - public void onUploadProgress(long bytesWritten, long contentLength, boolean done) { - } + public void onUploadProgress(long bytesWritten, long contentLength, boolean done) {} @Override - public void onDownloadProgress(long bytesRead, long contentLength, boolean done) { - } + public void onDownloadProgress(long bytesRead, long contentLength, boolean done) {} }); } catch (ApiException e) { CallbackWrapper.handleAPIException(mainHandler, callback, e); @@ -627,7 +632,7 @@ public boolean onSuccess(PublicFeedPostsResponse response) { * @param callback Callback to receive the response */ public void refresh(FCCallback callback) { - lastPostId = null; // Reset cursor-based pagination + lastPostId = null; // Reset cursor-based pagination loadFeedPosts(callback); } @@ -716,7 +721,8 @@ public void likePost(String postId, FCCallback callback) { .isUndo(isUndo) .executeAsync(new ApiCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { // API call failed - revert the optimistic update if (isUndo) { // Revert back to liked state @@ -741,7 +747,10 @@ public void onFailure(ApiException e, int statusCode, Map> } @Override - public void onSuccess(ReactFeedPostResponse result, int statusCode, Map> responseHeaders) { + public void onSuccess( + ReactFeedPostResponse result, + int statusCode, + Map> responseHeaders) { // API call succeeded, our optimistic update was correct mainHandler.post(() -> { callback.onSuccess(updatedPost); @@ -792,7 +801,7 @@ public void cleanup() { } // Clear listener references to prevent memory leaks this.onPostDeletedListener = null; - + // Clear collections to help GC if (feedPosts != null) { feedPosts.clear(); @@ -830,8 +839,9 @@ public FastCommentsSDK createCommentsSDKForPost(FeedPost post) { config.pageTitle = post.getTitle(); } else if (post.getContentHTML() != null) { // Use start of content as title if no title is available - String contentText = android.text.Html.fromHtml(post.getContentHTML(), - android.text.Html.FROM_HTML_MODE_COMPACT).toString(); + String contentText = android.text.Html.fromHtml( + post.getContentHTML(), android.text.Html.FROM_HTML_MODE_COMPACT) + .toString(); // Limit to 100 characters if (contentText.length() > 100) { contentText = contentText.substring(0, 97) + "..."; @@ -846,16 +856,15 @@ public FastCommentsSDK createCommentsSDKForPost(FeedPost post) { // Create a new FastCommentsSDK with this config FastCommentsSDK commentsSDK = new FastCommentsSDK(config); - + // Pass the theme from the feed SDK to the comments SDK if (this.theme != null) { commentsSDK.setTheme(this.theme); } - + return commentsSDK; } - /** * Uploads an image to the server with CrossPlatform preset * @@ -920,7 +929,8 @@ public void uploadImage(Context context, Uri imageUri, FCCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { // Clean up the temp file imageFile.delete(); @@ -929,7 +939,10 @@ public void onFailure(ApiException e, int statusCode, Map> } @Override - public void onSuccess(UploadImageResponse result, int statusCode, Map> responseHeaders) { + public void onSuccess( + UploadImageResponse result, + int statusCode, + Map> responseHeaders) { // Clean up the temp file imageFile.delete(); @@ -946,20 +959,16 @@ public void onSuccess(UploadImageResponse result, int statusCode, Map assets = new ArrayList<>(); if (result.getMedia() != null) { for (MediaAsset media : result.getMedia()) { - assets.add( - new FeedPostMediaItemAsset() - .h(media.getH()) - .w(media.getW()) - .src(media.getSrc()) - ); + assets.add(new FeedPostMediaItemAsset() + .h(media.getH()) + .w(media.getW()) + .src(media.getSrc())); } } else if (result.getUrl() != null) { - assets.add( - new FeedPostMediaItemAsset() - .h(1000) - .w(1000) - .src(result.getUrl()) - ); + assets.add(new FeedPostMediaItemAsset() + .h(1000) + .w(1000) + .src(result.getUrl())); } mediaItem.setSizes(assets); @@ -1013,7 +1022,7 @@ public void uploadImages(Context context, List imageUris, FCCallback uploadedItems = new ArrayList<>(); final AtomicReference uploadError = new AtomicReference<>(); - int[] countRemaining = new int[]{imageUris.size()}; + int[] countRemaining = new int[] {imageUris.size()}; for (Uri uri : imageUris) { uploadImage(context, uri, new FCCallback() { @@ -1088,7 +1097,7 @@ private String getFileExtension(String fileName) { */ private void copyUriToFile(Context context, Uri uri, File destFile) throws IOException { try (InputStream inputStream = context.getContentResolver().openInputStream(uri); - OutputStream outputStream = new FileOutputStream(destFile)) { + OutputStream outputStream = new FileOutputStream(destFile)) { if (inputStream == null) { throw new IOException("Failed to open input stream from URI"); } @@ -1120,36 +1129,37 @@ public void createPost(CreateFeedPostParams params, FCCallback callbac .sso(config.getSSOToken()) .executeAsync(new ApiCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { APIError error = CallbackWrapper.createErrorFromException(e); callback.onFailure(error); } @Override - public void onSuccess(CreateFeedPostResponse response, int statusCode, Map> responseHeaders) { + public void onSuccess( + CreateFeedPostResponse response, + int statusCode, + Map> responseHeaders) { mainHandler.post(() -> { - CreateFeedPostResponse feedResponse = response; - FeedPost createdPost = feedResponse.getFeedPost(); - - // Add post to the local list at the beginning - if (feedPosts.isEmpty()) { - feedPosts.add(createdPost); - } else { - feedPosts.add(0, createdPost); // Add at the beginning - } + CreateFeedPostResponse feedResponse = response; + FeedPost createdPost = feedResponse.getFeedPost(); + + // Add post to the local list at the beginning + if (feedPosts.isEmpty()) { + feedPosts.add(createdPost); + } else { + feedPosts.add(0, createdPost); // Add at the beginning + } - callback.onSuccess(createdPost); - + callback.onSuccess(createdPost); }); } @Override - public void onUploadProgress(long bytesWritten, long contentLength, boolean done) { - } + public void onUploadProgress(long bytesWritten, long contentLength, boolean done) {} @Override - public void onDownloadProgress(long bytesRead, long contentLength, boolean done) { - } + public void onDownloadProgress(long bytesRead, long contentLength, boolean done) {} }); } catch (ApiException e) { CallbackWrapper.handleAPIException(mainHandler, callback, e); @@ -1180,14 +1190,7 @@ private void subscribeToLiveEvents() { // Subscribe to live events liveEventSubscription = liveEventSubscriber.subscribeToChanges( - config, - tenantIdWS, - config.urlId, - urlIdWS, - userIdWS, - this::checkPostVisibility, - this::handleLiveEvent - ); + config, tenantIdWS, config.urlId, urlIdWS, userIdWS, this::checkPostVisibility, this::handleLiveEvent); } /** @@ -1201,7 +1204,8 @@ private void handleConnectionStatusChange(boolean isConnected, Long lastEventTim /** * Check if posts can be seen based on filtering/blocking logic */ - private void checkPostVisibility(List postIds, java.util.function.Consumer> resultCallback) { + private void checkPostVisibility( + List postIds, java.util.function.Consumer> resultCallback) { // For now, we'll assume all posts are visible // This can be enhanced later with visibility checking logic if needed resultCallback.accept(null); @@ -1295,41 +1299,50 @@ public void getFeedPostsStats(List postIds, FCCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { callback.onFailure(CallbackWrapper.createErrorFromException(e)); } @Override - public void onSuccess(FeedPostsStatsResponse result, int statusCode, Map> responseHeaders) { - // Update cached posts with the new stats - final FeedPostsStatsResponse statsResponse = result; - Map statsMap = statsResponse.getStats(); - - // Update posts in our cache - for (Map.Entry entry : statsMap.entrySet()) { - String postId = entry.getKey(); - FeedPostStats updatedStats = entry.getValue(); - - // Find the post in our cache - FeedPost cachedPost = postsById.get(postId); - if (cachedPost != null) { - // Update comment count - cachedPost.setCommentCount(updatedStats.getCommentCount()); - - // Update reactions - cachedPost.setReacts(updatedStats.getReacts()); - - // Update like count in our tracking map - if (updatedStats.getReacts() != null && updatedStats.getReacts().containsKey("l")) { - likeCounts.put(postId, updatedStats.getReacts().get("l").intValue()); - } else { - likeCounts.put(postId, 0); - } + public void onSuccess( + FeedPostsStatsResponse result, + int statusCode, + Map> responseHeaders) { + // Update cached posts with the new stats + final FeedPostsStatsResponse statsResponse = result; + Map statsMap = statsResponse.getStats(); + + // Update posts in our cache + for (Map.Entry entry : statsMap.entrySet()) { + String postId = entry.getKey(); + FeedPostStats updatedStats = entry.getValue(); + + // Find the post in our cache + FeedPost cachedPost = postsById.get(postId); + if (cachedPost != null) { + // Update comment count + cachedPost.setCommentCount(updatedStats.getCommentCount()); + + // Update reactions + cachedPost.setReacts(updatedStats.getReacts()); + + // Update like count in our tracking map + if (updatedStats.getReacts() != null + && updatedStats.getReacts().containsKey("l")) { + likeCounts.put( + postId, + updatedStats + .getReacts() + .get("l") + .intValue()); + } else { + likeCounts.put(postId, 0); } } + } - callback.onSuccess(result); - + callback.onSuccess(result); } @Override @@ -1372,11 +1385,11 @@ private void handleDeletedFeedPost(LiveEvent eventData) { break; } } - + if (wasRemoved) { // Log deletion for debugging Log.d("FastCommentsFeedSDK", "Post with ID " + postId + " was deleted via live event"); - + // Notify any callback listeners about the post deletion // This allows the UI to update when a post is deleted by someone else if (onPostDeletedListener != null) { @@ -1384,19 +1397,19 @@ private void handleDeletedFeedPost(LiveEvent eventData) { } } } - + /** * Interface for notifying when a post is deleted via live event */ public interface OnPostDeletedListener { void onPostDeleted(String postId); } - + private OnPostDeletedListener onPostDeletedListener; - + /** * Set a listener to be notified when posts are deleted via live events - * + * * @param listener The listener to notify */ public void setOnPostDeletedListener(OnPostDeletedListener listener) { @@ -1456,28 +1469,33 @@ public void deleteFeedPost(String postId, FCCallback callback) .sso(config.getSSOToken()) .executeAsync(new ApiCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { APIError error = CallbackWrapper.createErrorFromException(e); callback.onFailure(error); } @Override - public void onSuccess(DeleteFeedPostPublicResponse result, int statusCode, Map> responseHeaders) { - mainHandler.post(() -> { - // Remove the post from our local list - for (int i = 0; i < feedPosts.size(); i++) { - FeedPost post = feedPosts.get(i); - if (post != null && post.getId() != null && post.getId().equals(postId)) { - feedPosts.remove(i); - postsById.remove(postId); - likeCounts.remove(postId); - break; - } + public void onSuccess( + DeleteFeedPostPublicResponse result, + int statusCode, + Map> responseHeaders) { + mainHandler.post(() -> { + // Remove the post from our local list + for (int i = 0; i < feedPosts.size(); i++) { + FeedPost post = feedPosts.get(i); + if (post != null + && post.getId() != null + && post.getId().equals(postId)) { + feedPosts.remove(i); + postsById.remove(postId); + likeCounts.remove(postId); + break; } + } - callback.onSuccess(new APIEmptyResponse()); - }); - + callback.onSuccess(new APIEmptyResponse()); + }); } @Override @@ -1534,4 +1552,4 @@ public void clearGlobalFeedToolbarButtons() { public List getGlobalFeedToolbarButtons() { return new ArrayList<>(globalFeedToolbarButtons); } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsFeedView.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsFeedView.java index cb18b0a..6ab7acb 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsFeedView.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsFeedView.java @@ -12,51 +12,55 @@ import android.widget.FrameLayout; import android.widget.ProgressBar; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - import com.fastcomments.model.APIEmptyResponse; import com.fastcomments.model.APIError; import com.fastcomments.model.FeedPost; import com.fastcomments.model.FeedPostMediaItem; -import com.fastcomments.model.GetFeedPostsResponse; import com.fastcomments.model.FeedPostsStatsResponse; import com.fastcomments.model.PublicFeedPostsResponse; - import java.io.Serializable; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; /** * FastCommentsFeedView displays a feed of posts from FastComments with infinite scrolling * Includes support for scroll position retention and state restoration */ public class FastCommentsFeedView extends FrameLayout { - + /** * Class to store view state information */ public static class ViewState implements Serializable { private static final long serialVersionUID = 1L; - + private int scrollPosition; private FastCommentsFeedSDK.FeedState feedState; - + public ViewState() { // Default constructor } - - public int getScrollPosition() { return scrollPosition; } - public void setScrollPosition(int position) { this.scrollPosition = position; } - - public FastCommentsFeedSDK.FeedState getFeedState() { return feedState; } - public void setFeedState(FastCommentsFeedSDK.FeedState state) { this.feedState = state; } + + public int getScrollPosition() { + return scrollPosition; + } + + public void setScrollPosition(int position) { + this.scrollPosition = position; + } + + public FastCommentsFeedSDK.FeedState getFeedState() { + return feedState; + } + + public void setFeedState(FastCommentsFeedSDK.FeedState state) { + this.feedState = state; + } } private SwipeRefreshLayout swipeRefreshLayout; @@ -66,14 +70,14 @@ public ViewState() { private TextView emptyStateView; private TextView errorStateView; private TextView newPostsBanner; - + private FeedPostsAdapter adapter; private FastCommentsFeedSDK sdk; private Handler handler; private List feedPosts = new ArrayList<>(); private OnFeedViewInteractionListener listener; private OnUserClickListener userClickListener; - + // Polling for post stats private static final long POLLING_INTERVAL_MS = 30 * 1000; // 30 seconds private boolean isPollingEnabled = true; @@ -84,11 +88,13 @@ public ViewState() { */ public interface OnFeedViewInteractionListener { void onFeedLoaded(List posts); + void onFeedError(String errorMessage); + void onPostSelected(FeedPost post); /** * Called when the user wants to view or add comments for a post - * + * * @param post The post to show comments for */ void onCommentsRequested(FeedPost post); @@ -121,7 +127,8 @@ public FastCommentsFeedView(@NonNull Context context, @Nullable AttributeSet att init(context, attrs, sdk); } - public FastCommentsFeedView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, FastCommentsFeedSDK sdk) { + public FastCommentsFeedView( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, FastCommentsFeedSDK sdk) { super(context, attrs, defStyleAttr); init(context, attrs, sdk); } @@ -140,24 +147,24 @@ private void init(Context context, AttributeSet attrs, FastCommentsFeedSDK sdk) emptyStateView = findViewById(R.id.emptyStateView); errorStateView = findViewById(R.id.errorStateView); newPostsBanner = findViewById(R.id.newPostsBanner); - + // Initialize demo banner setupDemoBanner(); // Set up RecyclerView LinearLayoutManager layoutManager = new LinearLayoutManager(context); recyclerView.setLayoutManager(layoutManager); - + // Disable item animations to prevent flicker when clicking items recyclerView.setItemAnimator(null); - + // Skip initializing the adapter if SDK is not provided yet // It will be initialized when setSDK is called if (sdk != null) { initAdapter(context); } } - + /** * Initialize the adapter with the SDK */ @@ -166,13 +173,13 @@ private void initAdapter(Context context) { recyclerView.setItemViewCacheSize(20); // Cache more items recyclerView.setDrawingCacheEnabled(true); recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH); - + // Set larger prefetch to load images ahead of time LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); if (layoutManager != null) { layoutManager.setInitialPrefetchItemCount(5); // Prefetch 5 items } - + // Initialize adapter adapter = new FeedPostsAdapter(context, feedPosts, sdk, new FeedPostsAdapter.OnFeedPostInteractionListener() { @Override @@ -216,7 +223,7 @@ public void onMediaClick(FeedPostMediaItem mediaItem) { openUrl(mediaItem.getLinkUrl()); } } - + @Override public void onDeletePost(FeedPost post) { // Confirm before deleting @@ -232,7 +239,7 @@ public void onDeletePost(FeedPost post) { }) .show(); } - + @Override public void onUserClick(FeedPost post, UserInfo userInfo, UserClickSource source) { // Notify the user click listener if set @@ -242,7 +249,7 @@ public void onUserClick(FeedPost post, UserInfo userInfo, UserClickSource source } } }); - + recyclerView.setAdapter(adapter); // Set up scroll-to-top listener for when new posts are added @@ -257,11 +264,11 @@ public void onUserClick(FeedPost post, UserInfo userInfo, UserClickSource source // Set up pull-to-refresh swipeRefreshLayout.setOnRefreshListener(this::refresh); - + // Set up infinite scrolling setupInfiniteScrolling(); } - + /** * Set up infinite scrolling for the RecyclerView */ @@ -291,10 +298,10 @@ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { } }); } - + /** * Set the SDK instance to use with this view (for use when inflating from XML) - * + * * @param sdk The FastCommentsFeedSDK instance */ public void setSDK(FastCommentsFeedSDK sdk) { @@ -313,8 +320,7 @@ public void setSDK(FastCommentsFeedSDK sdk) { // Show "Show X New Posts" banner when new posts arrive via WebSocket sdk.setNewPostsAvailableListener(count -> { if (newPostsBanner != null) { - newPostsBanner.setText(getResources().getQuantityString( - R.plurals.show_new_posts, count, count)); + newPostsBanner.setText(getResources().getQuantityString(R.plurals.show_new_posts, count, count)); newPostsBanner.setVisibility(View.VISIBLE); } }); @@ -356,32 +362,32 @@ public boolean onSuccess(PublicFeedPostsResponse response) { } }); } - + // Setup demo banner setupDemoBanner(); } - + /** * Save the complete view state including scroll position and feed data * @return A ViewState object containing all state information */ public ViewState saveViewState() { ViewState state = new ViewState(); - + // Save scroll position LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); if (layoutManager != null) { state.setScrollPosition(layoutManager.findFirstVisibleItemPosition()); } - + // Save SDK state if (sdk != null) { state.setFeedState(sdk.savePaginationState()); } - + return state; } - + /** * Restore the complete view state * @param state The ViewState object to restore from @@ -390,11 +396,11 @@ public void restoreViewState(ViewState state) { if (state == null) { return; } - + // Restore SDK state first if (sdk != null && state.getFeedState() != null) { sdk.restorePaginationState(state.getFeedState()); - + // Update adapter with restored posts if (adapter != null && sdk != null) { List posts = sdk.getFeedPosts(); @@ -403,7 +409,7 @@ public void restoreViewState(ViewState state) { } } } - + // Restore scroll position final int position = state.getScrollPosition(); if (position >= 0 && recyclerView != null) { @@ -411,7 +417,7 @@ public void restoreViewState(ViewState state) { recyclerView.post(() -> recyclerView.scrollToPosition(position)); } } - + /** * Save just the scroll position (use saveViewState for complete state) * @return The first visible item position @@ -423,7 +429,7 @@ public int saveScrollPosition() { } return 0; } - + /** * Restore just the scroll position (use restoreViewState for complete state) * @param position The position to scroll to @@ -436,22 +442,22 @@ public void restoreScrollPosition(int position) { /** * Set a listener for feed interactions - * + * * @param listener The listener to set */ public void setFeedViewInteractionListener(OnFeedViewInteractionListener listener) { this.listener = listener; } - + /** * Set a listener to be notified when a user (name or avatar) is clicked - * + * * @param listener The listener to set */ public void setOnUserClickListener(OnUserClickListener listener) { this.userClickListener = listener; } - + /** * Set a TagSupplier to provide tags for filtering feed posts. * The tags returned by the supplier will be used when fetching posts from the API. @@ -491,12 +497,16 @@ public boolean onFailure(APIError error) { } else if (error != null && error.getReason() != null) { errorMessage = error.getReason(); } - + // Log the error for debugging Log.e("FastCommentsFeedView", "Feed loading error: " + errorMessage); - if (error != null && error.getReason() != null && error.getReason().contains("JsonSyntax")) { - Log.e("FastCommentsFeedView", "JsonSyntaxException detected in API response", - new Exception("JSON parsing error occurred in API response")); + if (error != null + && error.getReason() != null + && error.getReason().contains("JsonSyntax")) { + Log.e( + "FastCommentsFeedView", + "JsonSyntaxException detected in API response", + new Exception("JSON parsing error occurred in API response")); } showError(errorMessage); @@ -519,18 +529,19 @@ public boolean onSuccess(PublicFeedPostsResponse response) { return; } List posts = sdk.getFeedPosts(); - + if (posts.isEmpty()) { showEmptyState(true); } else { showEmptyState(false); int firstVisiblePosition = -1; if (recyclerView != null && recyclerView.getLayoutManager() != null) { - firstVisiblePosition = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition(); + firstVisiblePosition = ((LinearLayoutManager) recyclerView.getLayoutManager()) + .findFirstVisibleItemPosition(); } - + adapter.updatePosts(posts); - + // Restore position if we had one if (firstVisiblePosition >= 0 && firstVisiblePosition < posts.size()) { recyclerView.scrollToPosition(firstVisiblePosition); @@ -540,7 +551,7 @@ public boolean onSuccess(PublicFeedPostsResponse response) { if (listener != null) { listener.onFeedLoaded(posts); } - + // Start polling for stats updates startPolling(); }); @@ -554,7 +565,7 @@ public boolean onSuccess(PublicFeedPostsResponse response) { */ public void refresh() { hideError(); - + sdk.refresh(new FCCallback() { @Override public boolean onFailure(APIError error) { @@ -569,12 +580,16 @@ public boolean onFailure(APIError error) { } else if (error != null && error.getReason() != null) { errorMessage = error.getReason(); } - + // Log the error for debugging Log.e("FastCommentsFeedView", "Feed refresh error: " + errorMessage); - if (error != null && error.getReason() != null && error.getReason().contains("JsonSyntax")) { - Log.e("FastCommentsFeedView", "JsonSyntaxException detected in API response during refresh", - new Exception("JSON parsing error occurred in API response")); + if (error != null + && error.getReason() != null + && error.getReason().contains("JsonSyntax")) { + Log.e( + "FastCommentsFeedView", + "JsonSyntaxException detected in API response during refresh", + new Exception("JSON parsing error occurred in API response")); } showError(errorMessage); @@ -596,23 +611,24 @@ public boolean onSuccess(PublicFeedPostsResponse response) { return; } List posts = sdk.getFeedPosts(); - + if (posts.isEmpty()) { showEmptyState(true); } else { showEmptyState(false); int firstVisiblePosition = -1; if (recyclerView != null && recyclerView.getLayoutManager() != null) { - firstVisiblePosition = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition(); + firstVisiblePosition = ((LinearLayoutManager) recyclerView.getLayoutManager()) + .findFirstVisibleItemPosition(); } - + adapter.updatePosts(posts, true); } if (listener != null) { listener.onFeedLoaded(posts); } - + // Start polling for stats updates startPolling(); }); @@ -645,12 +661,16 @@ public boolean onFailure(APIError error) { } else if (error != null && error.getReason() != null) { errorMessage = error.getReason(); } - + // Log the error for debugging Log.e("FastCommentsFeedView", "Error loading more posts: " + errorMessage); - if (error != null && error.getReason() != null && error.getReason().contains("JsonSyntax")) { - Log.e("FastCommentsFeedView", "JsonSyntaxException detected when loading more posts", - new Exception("JSON parsing error occurred in API response")); + if (error != null + && error.getReason() != null + && error.getReason().contains("JsonSyntax")) { + Log.e( + "FastCommentsFeedView", + "JsonSyntaxException detected when loading more posts", + new Exception("JSON parsing error occurred in API response")); } // Notify listener of error (if set) @@ -676,7 +696,7 @@ public boolean onSuccess(PublicFeedPostsResponse response) { /** * Toggle like status for a post - * + * * @param post The post to like/unlike * @param position The position of the post in the adapter */ @@ -684,14 +704,15 @@ private void toggleLike(FeedPost post, int position) { if (sdk == null || post == null || post.getId() == null) { return; } - + // Call the SDK to toggle the like status sdk.likePost(post.getId(), new FCCallback() { @Override public boolean onFailure(APIError error) { handler.post(() -> { String errorMessage; - if (error.getTranslatedError() != null && !error.getTranslatedError().isEmpty()) { + if (error.getTranslatedError() != null + && !error.getTranslatedError().isEmpty()) { errorMessage = error.getTranslatedError(); } else if (error.getReason() != null && !error.getReason().isEmpty()) { errorMessage = error.getReason(); @@ -699,11 +720,8 @@ public boolean onFailure(APIError error) { errorMessage = getContext().getString(R.string.error_liking_post); } - android.widget.Toast.makeText( - getContext(), - errorMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); + android.widget.Toast.makeText(getContext(), errorMessage, android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } @@ -721,44 +739,46 @@ public boolean onSuccess(FeedPost updatedPost) { /** * Share a post - * + * * @param post The post to share */ private void sharePost(FeedPost post) { // Create a share intent Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); - + // Create a share message with the content and link to the post StringBuilder shareMessage = new StringBuilder(); - + // Add post content if available if (post.getContentHTML() != null) { // Strip HTML tags for sharing - String plainText = android.text.Html.fromHtml(post.getContentHTML(), - android.text.Html.FROM_HTML_MODE_COMPACT).toString(); + String plainText = android.text.Html.fromHtml( + post.getContentHTML(), android.text.Html.FROM_HTML_MODE_COMPACT) + .toString(); shareMessage.append(plainText); } - + // Get the first link if available - if (post.getLinks() != null && !post.getLinks().isEmpty() + if (post.getLinks() != null + && !post.getLinks().isEmpty() && post.getLinks().get(0).getUrl() != null) { if (shareMessage.length() > 0) { shareMessage.append("\n\n"); } shareMessage.append(post.getLinks().get(0).getUrl()); } - + shareIntent.putExtra(Intent.EXTRA_TEXT, shareMessage.toString()); - + // Start the share activity - getContext().startActivity(Intent.createChooser(shareIntent, - getContext().getString(R.string.share))); + getContext() + .startActivity(Intent.createChooser(shareIntent, getContext().getString(R.string.share))); } /** * Open a URL in the browser - * + * * @param url The URL to open */ private void openUrl(String url) { @@ -770,12 +790,12 @@ private void openUrl(String url) { /** * Show or hide the loading indicator - * + * * @param isLoading Whether loading is in progress */ private void showLoading(boolean isLoading) { feedProgressBar.setVisibility(isLoading ? View.VISIBLE : View.GONE); - + if (isLoading) { recyclerView.setVisibility(View.GONE); emptyStateView.setVisibility(View.GONE); @@ -786,7 +806,7 @@ private void showLoading(boolean isLoading) { /** * Show or hide the empty state view - * + * * @param isEmpty Whether the feed is empty */ private void showEmptyState(boolean isEmpty) { @@ -796,13 +816,13 @@ private void showEmptyState(boolean isEmpty) { /** * Show an error message - * + * * @param errorMessage The error message to display */ private void showError(String errorMessage) { errorStateView.setText(errorMessage); errorStateView.setVisibility(View.VISIBLE); - + // Log the error to help with debugging Log.e("FastCommentsFeedView", "Displaying error: " + errorMessage); } @@ -827,21 +847,21 @@ public FastCommentsView createCommentsViewForPost(FeedPost post) { // Create a FastCommentsSDK instance for this post's comments FastCommentsSDK commentsSDK = sdk.createCommentsSDKForPost(post); - + // Create and return a FastCommentsView with the new SDK return new FastCommentsView(getContext(), commentsSDK); } /** * Refreshes a specific post in the feed after comment is added - * + * * @param postId The ID of the post to refresh */ public void refreshPost(String postId) { if (sdk == null || postId == null || adapter == null) { return; } - + // Get the updated posts list from SDK if (sdk == null) { return; @@ -850,11 +870,11 @@ public void refreshPost(String postId) { if (posts == null || posts.isEmpty()) { return; } - + // Find the post in the posts list int position = -1; FeedPost updatedPost = null; - + for (int i = 0; i < posts.size(); i++) { FeedPost post = posts.get(i); if (post != null && postId.equals(post.getId())) { @@ -863,13 +883,13 @@ public void refreshPost(String postId) { break; } } - + // If post is found, update it in the adapter with the refreshed data if (position >= 0 && updatedPost != null) { adapter.updatePost(position, updatedPost); } } - + /** * Initialize polling for post stats */ @@ -880,7 +900,7 @@ public void run() { if (isPollingEnabled && sdk != null && !feedPosts.isEmpty()) { refreshVisiblePostStats(); } - + // Schedule next run if (isPollingEnabled) { handler.postDelayed(this, POLLING_INTERVAL_MS); @@ -888,7 +908,7 @@ public void run() { } }; } - + /** * Start polling for post stats */ @@ -896,14 +916,14 @@ public void startPolling() { if (pollStatsRunnable == null) { initPolling(); } - + isPollingEnabled = true; // Remove any existing callbacks to prevent duplicates handler.removeCallbacks(pollStatsRunnable); // Start polling handler.postDelayed(pollStatsRunnable, POLLING_INTERVAL_MS); } - + /** * Stop polling for post stats */ @@ -913,7 +933,7 @@ public void stopPolling() { handler.removeCallbacks(pollStatsRunnable); } } - + /** * Refresh stats for currently visible posts */ @@ -921,29 +941,29 @@ private void refreshVisiblePostStats() { if (sdk == null || feedPosts.isEmpty() || recyclerView == null) { return; } - + // Get visible post IDs LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); if (layoutManager == null) { return; } - + // Find visible items range int firstVisible = layoutManager.findFirstVisibleItemPosition(); int lastVisible = layoutManager.findLastVisibleItemPosition(); - + // Safety check if (firstVisible < 0 || lastVisible < 0 || firstVisible > lastVisible) { return; } - + // Get post IDs for visible posts List visiblePostIds = new ArrayList<>(); - + // Limit the range to valid indices firstVisible = Math.max(0, firstVisible); lastVisible = Math.min(feedPosts.size() - 1, lastVisible); - + // Get post IDs for visible posts for (int i = firstVisible; i <= lastVisible; i++) { if (i < feedPosts.size()) { @@ -953,12 +973,12 @@ private void refreshVisiblePostStats() { } } } - + // If no visible posts with IDs, return if (visiblePostIds.isEmpty()) { return; } - + // Fetch stats for visible posts final List finalVisiblePostIds = visiblePostIds; sdk.getFeedPostsStats(visiblePostIds, new FCCallback() { @@ -967,7 +987,7 @@ public boolean onFailure(APIError error) { // Silent failure - we'll try again next time return CONSUME; } - + @Override public boolean onSuccess(FeedPostsStatsResponse response) { // The SDK has already updated the post objects with new stats @@ -989,49 +1009,45 @@ public boolean onSuccess(FeedPostsStatsResponse response) { } }); } - + /** * Delete a feed post - * + * * @param post The post to delete */ private void deletePost(FeedPost post) { if (sdk == null || post == null || post.getId() == null) { return; } - + sdk.deleteFeedPost(post.getId(), new FCCallback() { @Override public boolean onFailure(APIError error) { handler.post(() -> { String errorMessage; - if (error.getTranslatedError() != null && !error.getTranslatedError().isEmpty()) { + if (error.getTranslatedError() != null + && !error.getTranslatedError().isEmpty()) { errorMessage = error.getTranslatedError(); } else if (error.getReason() != null && !error.getReason().isEmpty()) { errorMessage = error.getReason(); } else { errorMessage = getContext().getString(R.string.error_deleting_post); } - - android.widget.Toast.makeText( - getContext(), - errorMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); + + android.widget.Toast.makeText(getContext(), errorMessage, android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } - + @Override public boolean onSuccess(APIEmptyResponse response) { handler.post(() -> { // Success message android.widget.Toast.makeText( - getContext(), - R.string.post_deleted_successfully, - android.widget.Toast.LENGTH_SHORT - ).show(); - + getContext(), R.string.post_deleted_successfully, android.widget.Toast.LENGTH_SHORT) + .show(); + // Update adapter with the current list from SDK // This ensures the adapter's internal list matches the SDK's list if (sdk != null) { @@ -1045,7 +1061,7 @@ public boolean onSuccess(APIEmptyResponse response) { } }); } - + /** * Clean up resources when the view is detached */ @@ -1054,43 +1070,43 @@ protected void onDetachedFromWindow() { super.onDetachedFromWindow(); cleanup(); } - + /** * Clean up all resources to prevent memory leaks. * Call this method when the view will no longer be used. */ public void cleanup() { stopPolling(); - + // Clear adapter data if (adapter != null) { adapter.updatePosts(new ArrayList<>()); } - + // Clear SDK listener reference if (sdk != null) { sdk.setOnPostDeletedListener(null); sdk.cleanup(); sdk = null; } - + // Clear local references listener = null; - + // Clear collections if (feedPosts != null) { feedPosts.clear(); } - + // Clear handler callbacks if (handler != null && pollStatsRunnable != null) { handler.removeCallbacks(pollStatsRunnable); } - + // Clear polling runnable pollStatsRunnable = null; } - + /** * Returns the RecyclerView adapter used by this view. * @@ -1099,7 +1115,7 @@ public void cleanup() { public FeedPostsAdapter getAdapter() { return adapter; } - + /** * Clears the feed posts from the adapter. * Use this method when switching fragments to avoid memory leaks. @@ -1109,11 +1125,11 @@ public void clearAdapter() { adapter.updatePosts(new ArrayList<>()); } } - + /** * Sets up the demo banner if tenant ID is "demo" */ private void setupDemoBanner() { DemoBannerHelper.setupDemoBanner(this, sdk); } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsSDK.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsSDK.java index 9e7cf97..cb53bfa 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsSDK.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsSDK.java @@ -4,9 +4,7 @@ import android.os.Handler; import android.os.Looper; import android.util.Log; - import androidx.annotation.NonNull; - import com.fastcomments.api.PublicApi; import com.fastcomments.core.CommentWidgetConfig; import com.fastcomments.invoker.ApiCallback; @@ -14,10 +12,8 @@ import com.fastcomments.model.*; import com.fastcomments.pubsub.LiveEventSubscriber; import com.fastcomments.pubsub.SubscribeToChangesResult; - import java.util.ArrayList; import java.util.Date; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -81,19 +77,20 @@ public FastCommentsSDK(@NonNull CommentWidgetConfig config, boolean testMode) { this.config = config; this.api.getApiClient().setLenientOnJson(true); // Force HTTP/1.1 on the API client to prevent H2 interference with WebSocket - this.api.getApiClient().setHttpClient( - this.api.getApiClient().getHttpClient().newBuilder() + this.api + .getApiClient() + .setHttpClient(this.api + .getApiClient() + .getHttpClient() + .newBuilder() .protocols(java.util.Collections.singletonList(okhttp3.Protocol.HTTP_1_1)) - .build() - ); + .build()); this.api.getApiClient().setBasePath(getAPIBasePath(config)); this.commentsTree = new CommentsTree(); this.currentSkip = 0; this.currentPage = 0; this.hasMore = false; - this.liveEventSubscriber = testMode - ? LiveEventSubscriber.createTesting() - : new LiveEventSubscriber(); + this.liveEventSubscriber = testMode ? LiveEventSubscriber.createTesting() : new LiveEventSubscriber(); Log.d("FastCommentsSDK", "Constructor: testMode=" + testMode); // Set up the presence status listener on the comments tree @@ -159,7 +156,8 @@ public void load(FCCallback callba @Override public boolean onFailure(APIError error) { // Set blockingErrorMessage from translatedError or reason - if (error.getTranslatedError() != null && !error.getTranslatedError().isEmpty()) { + if (error.getTranslatedError() != null + && !error.getTranslatedError().isEmpty()) { blockingErrorMessage = error.getTranslatedError(); } else if (error.getReason() != null && !error.getReason().isEmpty()) { blockingErrorMessage = "Comments could not load! Details: " + error.getReason(); @@ -212,8 +210,8 @@ public boolean onSuccess(GetCommentsResponseWithPresencePublicComment response) // Subscribe to live events if we have all required parameters // or if we need to reconnect due to userIdWS change - if ((tenantIdWS != null && urlIdWS != null && userIdWS != null) && - (liveEventSubscription == null || needsWebsocketReconnect)) { + if ((tenantIdWS != null && urlIdWS != null && userIdWS != null) + && (liveEventSubscription == null || needsWebsocketReconnect)) { subscribeToLiveEvents(); } @@ -271,22 +269,20 @@ public void onFailure(ApiException e, int i, Map> map) { } @Override - public void onSuccess(GetCommentsResponseWithPresencePublicComment response, int i, Map> map) { - final GetCommentsResponseWithPresencePublicComment commentsResponse = response; + public void onSuccess( + GetCommentsResponseWithPresencePublicComment response, + int i, + Map> map) { + final GetCommentsResponseWithPresencePublicComment commentsResponse = response; - callback.onSuccess(commentsResponse); - + callback.onSuccess(commentsResponse); } @Override - public void onUploadProgress(long l, long l1, boolean b) { - - } + public void onUploadProgress(long l, long l1, boolean b) {} @Override - public void onDownloadProgress(long l, long l1, boolean b) { - - } + public void onDownloadProgress(long l, long l1, boolean b) {} }); } catch (ApiException e) { CallbackWrapper.handleAPIException(mainHandler, callback, e); @@ -326,22 +322,20 @@ public void onFailure(ApiException e, int i, Map> map) { } @Override - public void onSuccess(GetCommentsResponseWithPresencePublicComment response, int i, Map> map) { - final GetCommentsResponseWithPresencePublicComment commentsResponse = response; - commentsTree.addForParent(parentId, commentsResponse.getComments()); - callback.onSuccess(commentsResponse); - + public void onSuccess( + GetCommentsResponseWithPresencePublicComment response, + int i, + Map> map) { + final GetCommentsResponseWithPresencePublicComment commentsResponse = response; + commentsTree.addForParent(parentId, commentsResponse.getComments()); + callback.onSuccess(commentsResponse); } @Override - public void onUploadProgress(long l, long l1, boolean b) { - - } + public void onUploadProgress(long l, long l1, boolean b) {} @Override - public void onDownloadProgress(long l, long l1, boolean b) { - - } + public void onDownloadProgress(long l, long l1, boolean b) {} }); } catch (ApiException e) { CallbackWrapper.handleAPIException(mainHandler, callback, e); @@ -354,7 +348,7 @@ public void onDownloadProgress(long l, long l1, boolean b) { private CommentData createCommentData(String commentText, String parentId) { return createCommentData(commentText, parentId, null); } - + /** * Create comment data object from the provided parameters, with mentions support * @@ -409,20 +403,20 @@ private CommentData createCommentData(String commentText, String parentId, List< if (config.commentMeta != null && !config.commentMeta.isEmpty()) { commentData.setMeta(config.commentMeta); } - + // Add mentions if available if (mentions != null && !mentions.isEmpty()) { List mentionsData = new ArrayList<>(); - + for (UserMention mention : mentions) { CommentUserMentionInfo mentionInfo = new CommentUserMentionInfo(); mentionInfo.setId(mention.getId()); mentionInfo.setTag("@" + mention.getUsername()); mentionInfo.setSent(false); - + mentionsData.add(mentionInfo); } - + // Set mentions using the typed method commentData.mentions(mentionsData); } @@ -440,7 +434,7 @@ private CommentData createCommentData(String commentText, String parentId, List< public void postComment(String commentText, String parentId, final FCCallback callback) { postComment(commentText, parentId, null, callback); } - + /** * Posts a new comment or reply to the FastComments API with mentions * @@ -449,7 +443,8 @@ public void postComment(String commentText, String parentId, final FCCallback mentions, final FCCallback callback) { + public void postComment( + String commentText, String parentId, List mentions, final FCCallback callback) { if (commentText == null || commentText.trim().isEmpty()) { callback.onFailure(new APIError() .status(APIStatus.FAILED) @@ -473,28 +468,31 @@ public void postComment(String commentText, String parentId, List m .sso(config.getSSOToken()) .executeAsync(new ApiCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { callback.onFailure(CallbackWrapper.createErrorFromException(e)); } @Override - public void onSuccess(SaveCommentsResponseWithPresence result, int statusCode, Map> responseHeaders) { - SaveCommentsResponseWithPresence response = result; - if (response.getUser() != null) { - currentUser = response.getUser(); - } - if (response.getUserIdWS() != null && !Objects.equals(response.getUserIdWS(), userIdWS)) { - userIdWS = response.getUserIdWS(); + public void onSuccess( + SaveCommentsResponseWithPresence result, + int statusCode, + Map> responseHeaders) { + SaveCommentsResponseWithPresence response = result; + if (response.getUser() != null) { + currentUser = response.getUser(); + } + if (response.getUserIdWS() != null && !Objects.equals(response.getUserIdWS(), userIdWS)) { + userIdWS = response.getUserIdWS(); - // Reconnect websocket with new user ID - if (tenantIdWS != null && urlIdWS != null && userIdWS != null) { - subscribeToLiveEvents(); - } + // Reconnect websocket with new user ID + if (tenantIdWS != null && urlIdWS != null && userIdWS != null) { + subscribeToLiveEvents(); } - // The API should return the complete comment - PublicComment createdComment = response.getComment(); - callback.onSuccess(createdComment); - + } + // The API should return the complete comment + PublicComment createdComment = response.getComment(); + callback.onSuccess(createdComment); } @Override @@ -522,32 +520,34 @@ public void loadMore(FCCallback ca currentSkip += pageSize; currentPage++; - getCommentsAndRelatedData(currentSkip, pageSize, 0, false, false, new FCCallback() { - @Override - public boolean onFailure(APIError error) { - // If there's a failure, reset the pagination values to the previous state - currentSkip -= pageSize; - currentPage--; - callback.onFailure(error); - return CONSUME; - } - - @Override - public boolean onSuccess(GetCommentsResponseWithPresencePublicComment response) { - // Update the total server count - commentCountOnServer = response.getCommentCount() != null ? response.getCommentCount() : commentCountOnServer; - - // Determine if we have more comments to load - hasMore = response.getHasMore() != null ? response.getHasMore() : false; + getCommentsAndRelatedData( + currentSkip, pageSize, 0, false, false, new FCCallback() { + @Override + public boolean onFailure(APIError error) { + // If there's a failure, reset the pagination values to the previous state + currentSkip -= pageSize; + currentPage--; + callback.onFailure(error); + return CONSUME; + } - // Append the new comments to the existing ones - mainHandler.post(() -> { - commentsTree.appendComments(response.getComments()); - callback.onSuccess(response); + @Override + public boolean onSuccess(GetCommentsResponseWithPresencePublicComment response) { + // Update the total server count + commentCountOnServer = + response.getCommentCount() != null ? response.getCommentCount() : commentCountOnServer; + + // Determine if we have more comments to load + hasMore = response.getHasMore() != null ? response.getHasMore() : false; + + // Append the new comments to the existing ones + mainHandler.post(() -> { + commentsTree.appendComments(response.getComments()); + callback.onSuccess(response); + }); + return CONSUME; + } }); - return CONSUME; - } - }); } /** @@ -630,14 +630,25 @@ public void deleteCommentVote(String commentId, String voteId, final FCCallback< * @param commenterEmail Email for anonymous user (optional, can be null if user is authenticated) * @param callback Callback to receive the response */ - public void deleteCommentVote(String commentId, String voteId, String commenterName, String commenterEmail, final FCCallback callback) { + public void deleteCommentVote( + String commentId, + String voteId, + String commenterName, + String commenterEmail, + final FCCallback callback) { if (commentId == null || commentId.isEmpty()) { - callback.onFailure(new APIError().status(APIStatus.FAILED).reason("Comment ID is required").code("invalid_comment_id")); + callback.onFailure(new APIError() + .status(APIStatus.FAILED) + .reason("Comment ID is required") + .code("invalid_comment_id")); return; } if (voteId == null || voteId.isEmpty()) { - callback.onFailure(new APIError().status(APIStatus.FAILED).reason("Vote ID is required").code("invalid_vote_id")); + callback.onFailure(new APIError() + .status(APIStatus.FAILED) + .reason("Vote ID is required") + .code("invalid_vote_id")); return; } @@ -654,15 +665,16 @@ public void deleteCommentVote(String commentId, String voteId, String commenterN .sso(config.getSSOToken()) .executeAsync(new ApiCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { callback.onFailure(CallbackWrapper.createErrorFromException(e)); } @Override - public void onSuccess(VoteDeleteResponse result, int statusCode, Map> responseHeaders) { - VoteDeleteResponse response = result; - callback.onSuccess(response); - + public void onSuccess( + VoteDeleteResponse result, int statusCode, Map> responseHeaders) { + VoteDeleteResponse response = result; + callback.onSuccess(response); } @Override @@ -690,7 +702,7 @@ public void onDownloadProgress(long bytesRead, long contentLength, boolean done) public void voteComment(String commentId, boolean isUpvote, final FCCallback callback) { voteComment(commentId, isUpvote, null, null, callback); } - + /** * Delete a comment * @@ -704,44 +716,50 @@ public void deleteComment(String commentId, final FCCallback c callback.onFailure(error); return; } - + // Create a unique broadcast ID to identify this deletion in live events String broadcastId = UUID.randomUUID().toString(); - + // Track broadcast ID before sending broadcastIdsSent.add(broadcastId); - + try { api.deleteCommentPublic(config.tenantId, commentId, broadcastId) - .sso(config.getSSOToken()) - .executeAsync(new ApiCallback() { - @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { - APIError error = CallbackWrapper.createErrorFromException(e); - callback.onFailure(error); - } - - @Override - public void onSuccess(PublicAPIDeleteCommentResponse result, int statusCode, Map> responseHeaders) { - mainHandler.post(() -> { + .sso(config.getSSOToken()) + .executeAsync(new ApiCallback() { + @Override + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { + APIError error = CallbackWrapper.createErrorFromException(e); + callback.onFailure(error); + } + + @Override + public void onSuccess( + PublicAPIDeleteCommentResponse result, + int statusCode, + Map> responseHeaders) { + mainHandler.post(() -> { // Remove the comment from the local tree boolean removed = commentsTree.removeComment(commentId); - Log.d("FastCommentsSDK", "deleteComment onSuccess: removed=" + removed + " commentId=" + commentId + " visibleSize=" + commentsTree.visibleSize()); + Log.d( + "FastCommentsSDK", + "deleteComment onSuccess: removed=" + removed + " commentId=" + commentId + + " visibleSize=" + commentsTree.visibleSize()); callback.onSuccess(new APIEmptyResponse()); }); - - } - - @Override - public void onUploadProgress(long bytesWritten, long contentLength, boolean done) { - // Not used - } - - @Override - public void onDownloadProgress(long bytesRead, long contentLength, boolean done) { - // Not used - } - }); + } + + @Override + public void onUploadProgress(long bytesWritten, long contentLength, boolean done) { + // Not used + } + + @Override + public void onDownloadProgress(long bytesRead, long contentLength, boolean done) { + // Not used + } + }); } catch (ApiException e) { CallbackWrapper.handleAPIException(mainHandler, callback, e); } @@ -777,15 +795,16 @@ private void subscribeToLiveEvents() { urlIdWS, userIdWS, this::checkCommentVisibility, - this::handleLiveEvent - ); + this::handleLiveEvent); } /** * Handle WebSocket connection status changes */ private void handleConnectionStatusChange(boolean isConnected, Long lastEventTime) { - Log.d("FastCommentsSDK", "connectionStatusChange: connected=" + isConnected + " lastEventTime=" + lastEventTime); + Log.d( + "FastCommentsSDK", + "connectionStatusChange: connected=" + isConnected + " lastEventTime=" + lastEventTime); if (connectionStatusListener != null) { mainHandler.post(() -> connectionStatusListener.onConnectionStatusChanged(isConnected)); } @@ -882,13 +901,17 @@ private void fetchPresenceForUsers(String userIdsCSV) { api.getUserPresenceStatuses(config.tenantId, urlIdWS, userIdsCSV) .executeAsync(new ApiCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { // Log error but continue - this is not critical functionality Log.e("FastCommentsSDK", "Failed to get user presence statuses: " + e.getMessage()); } @Override - public void onSuccess(GetUserPresenceStatusesResponse result, int statusCode, Map> responseHeaders) { + public void onSuccess( + GetUserPresenceStatusesResponse result, + int statusCode, + Map> responseHeaders) { // Process presence statuses final Map statuses = result.getUserIdsOnline(); @@ -930,7 +953,9 @@ private void checkCommentVisibility(List commentIds, Consumer userComments = commentsTree.commentsByUserId.get(userId); @@ -1314,7 +1347,8 @@ private void handleBadgeUpdate(LiveEvent eventData) { // Use the first comment to check which badges are new // (All user's comments should have the same badges) RenderableComment firstComment = userComments.get(0); - List existingBadges = firstComment.getComment().getBadges(); + List existingBadges = + firstComment.getComment().getBadges(); // Determine which badges are new for (CommentUserBadgeInfo updatedBadge : eventData.getBadges()) { @@ -1433,22 +1467,25 @@ public void editComment(String commentId, String newText, final FCCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { callback.onFailure(CallbackWrapper.createErrorFromException(e)); } @Override - public void onSuccess(PublicAPISetCommentTextResponse result, int statusCode, Map> responseHeaders) { - PublicAPISetCommentTextResponse response = result; - if (response.getComment() != null) { - callback.onSuccess(response.getComment()); - } else { - callback.onFailure(new APIError() - .status(APIStatus.FAILED) - .reason("No comment returned") - .code("edit_comment_error")); - } - + public void onSuccess( + PublicAPISetCommentTextResponse result, + int statusCode, + Map> responseHeaders) { + PublicAPISetCommentTextResponse response = result; + if (response.getComment() != null) { + callback.onSuccess(response.getComment()); + } else { + callback.onFailure(new APIError() + .status(APIStatus.FAILED) + .reason("No comment returned") + .code("edit_comment_error")); + } } @Override @@ -1487,14 +1524,15 @@ public void flagComment(String commentId, final FCCallback cal .sso(config.getSSOToken()) .executeAsync(new ApiCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { callback.onFailure(CallbackWrapper.createErrorFromException(e)); } @Override - public void onSuccess(APIEmptyResponse result, int statusCode, Map> responseHeaders) { - callback.onSuccess(result); - + public void onSuccess( + APIEmptyResponse result, int statusCode, Map> responseHeaders) { + callback.onSuccess(result); } @Override @@ -1518,7 +1556,8 @@ public void onDownloadProgress(long bytesRead, long contentLength, boolean done) * @param commentId The ID of the comment to block the user from * @param callback Callback to receive the response */ - public void blockUserFromComment(String commentId, List commentIdsToCheck, final FCCallback callback) { + public void blockUserFromComment( + String commentId, List commentIdsToCheck, final FCCallback callback) { if (commentId == null || commentId.isEmpty()) { callback.onFailure(new APIError() .status(APIStatus.FAILED) @@ -1534,15 +1573,16 @@ public void blockUserFromComment(String commentId, List commentIdsToChec .sso(config.getSSOToken()) .executeAsync(new ApiCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { callback.onFailure(CallbackWrapper.createErrorFromException(e)); } @Override - public void onSuccess(BlockSuccess result, int statusCode, Map> responseHeaders) { - BlockSuccess response = result; - callback.onSuccess(response); - + public void onSuccess( + BlockSuccess result, int statusCode, Map> responseHeaders) { + BlockSuccess response = result; + callback.onSuccess(response); } @Override @@ -1580,14 +1620,15 @@ public void unflagComment(String commentId, final FCCallback c .sso(config.getSSOToken()) .executeAsync(new ApiCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { callback.onFailure(CallbackWrapper.createErrorFromException(e)); } @Override - public void onSuccess(APIEmptyResponse result, int statusCode, Map> responseHeaders) { - callback.onSuccess(result); - + public void onSuccess( + APIEmptyResponse result, int statusCode, Map> responseHeaders) { + callback.onSuccess(result); } @Override @@ -1624,14 +1665,17 @@ public void pinComment(String commentId, final FCCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { callback.onFailure(CallbackWrapper.createErrorFromException(e)); } @Override - public void onSuccess(ChangeCommentPinStatusResponse result, int statusCode, Map> responseHeaders) { - callback.onSuccess(result); - + public void onSuccess( + ChangeCommentPinStatusResponse result, + int statusCode, + Map> responseHeaders) { + callback.onSuccess(result); } @Override @@ -1668,14 +1712,17 @@ public void unpinComment(String commentId, final FCCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { callback.onFailure(CallbackWrapper.createErrorFromException(e)); } @Override - public void onSuccess(ChangeCommentPinStatusResponse result, int statusCode, Map> responseHeaders) { - callback.onSuccess(result); - + public void onSuccess( + ChangeCommentPinStatusResponse result, + int statusCode, + Map> responseHeaders) { + callback.onSuccess(result); } @Override @@ -1712,14 +1759,15 @@ public void lockComment(String commentId, final FCCallback cal .sso(config.getSSOToken()) .executeAsync(new ApiCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { callback.onFailure(CallbackWrapper.createErrorFromException(e)); } @Override - public void onSuccess(APIEmptyResponse result, int statusCode, Map> responseHeaders) { - callback.onSuccess(result); - + public void onSuccess( + APIEmptyResponse result, int statusCode, Map> responseHeaders) { + callback.onSuccess(result); } @Override @@ -1756,14 +1804,15 @@ public void unlockComment(String commentId, final FCCallback c .sso(config.getSSOToken()) .executeAsync(new ApiCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { callback.onFailure(CallbackWrapper.createErrorFromException(e)); } @Override - public void onSuccess(APIEmptyResponse result, int statusCode, Map> responseHeaders) { - callback.onSuccess(result); - + public void onSuccess( + APIEmptyResponse result, int statusCode, Map> responseHeaders) { + callback.onSuccess(result); } @Override @@ -1783,7 +1832,8 @@ public void onDownloadProgress(long bytesRead, long contentLength, boolean done) * @param commentId The ID of the comment to unblock the user from * @param callback Callback to receive the response */ - public void unblockUserFromComment(String commentId, List commentIdsToCheck, final FCCallback callback) { + public void unblockUserFromComment( + String commentId, List commentIdsToCheck, final FCCallback callback) { if (commentId == null || commentId.isEmpty()) { callback.onFailure(new APIError() .status(APIStatus.FAILED) @@ -1799,14 +1849,15 @@ public void unblockUserFromComment(String commentId, List commentIdsToCh .sso(config.getSSOToken()) .executeAsync(new ApiCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { callback.onFailure(CallbackWrapper.createErrorFromException(e)); } @Override - public void onSuccess(UnblockSuccess result, int statusCode, Map> responseHeaders) { - callback.onSuccess(result); - + public void onSuccess( + UnblockSuccess result, int statusCode, Map> responseHeaders) { + callback.onSuccess(result); } @Override @@ -1829,9 +1880,17 @@ public void onDownloadProgress(long bytesRead, long contentLength, boolean done) * @param commenterEmail Email for anonymous user (optional, can be null if user is authenticated) * @param callback Callback to receive the response */ - public void voteComment(String commentId, boolean isUpvote, String commenterName, String commenterEmail, final FCCallback callback) { + public void voteComment( + String commentId, + boolean isUpvote, + String commenterName, + String commenterEmail, + final FCCallback callback) { if (commentId == null || commentId.isEmpty()) { - callback.onFailure(new APIError().status(APIStatus.FAILED).reason("Comment ID is required").code("invalid_comment_id")); + callback.onFailure(new APIError() + .status(APIStatus.FAILED) + .reason("Comment ID is required") + .code("invalid_comment_id")); return; } @@ -1868,15 +1927,16 @@ public void voteComment(String commentId, boolean isUpvote, String commenterName .sso(config.getSSOToken()) .executeAsync(new ApiCallback() { @Override - public void onFailure(ApiException e, int statusCode, Map> responseHeaders) { + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) { callback.onFailure(CallbackWrapper.createErrorFromException(e)); } @Override - public void onSuccess(VoteResponse result, int statusCode, Map> responseHeaders) { - VoteResponse response = result; - callback.onSuccess(response); - + public void onSuccess( + VoteResponse result, int statusCode, Map> responseHeaders) { + VoteResponse response = result; + callback.onSuccess(response); } @Override @@ -2010,4 +2070,4 @@ public void applyGlobalToolbarConfiguration(BottomCommentInputView inputView) { } } } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsTheme.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsTheme.java index 99d55b5..5e77876 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsTheme.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsTheme.java @@ -8,117 +8,150 @@ * Allows programmatic color customization without requiring XML resource overrides */ public class FastCommentsTheme { - + // Primary colors - @Nullable private Integer primaryColor; - @Nullable private Integer primaryLightColor; - @Nullable private Integer primaryDarkColor; - + @Nullable + private Integer primaryColor; + + @Nullable + private Integer primaryLightColor; + + @Nullable + private Integer primaryDarkColor; + // Action colors - @Nullable private Integer actionButtonColor; - @Nullable private Integer replyButtonColor; - @Nullable private Integer toggleRepliesButtonColor; - @Nullable private Integer loadMoreButtonTextColor; - + @Nullable + private Integer actionButtonColor; + + @Nullable + private Integer replyButtonColor; + + @Nullable + private Integer toggleRepliesButtonColor; + + @Nullable + private Integer loadMoreButtonTextColor; + // Link colors - @Nullable private Integer linkColor; - @Nullable private Integer linkColorPressed; - + @Nullable + private Integer linkColor; + + @Nullable + private Integer linkColorPressed; + // Vote colors - @Nullable private Integer voteCountColor; - @Nullable private Integer voteCountZeroColor; - @Nullable private Integer voteDividerColor; - + @Nullable + private Integer voteCountColor; + + @Nullable + private Integer voteCountZeroColor; + + @Nullable + private Integer voteDividerColor; + // Dialog colors - @Nullable private Integer dialogHeaderBackgroundColor; - @Nullable private Integer dialogHeaderTextColor; - + @Nullable + private Integer dialogHeaderBackgroundColor; + + @Nullable + private Integer dialogHeaderTextColor; + // Other UI colors - @Nullable private Integer onlineIndicatorColor; + @Nullable + private Integer onlineIndicatorColor; // Live chat header colors - @Nullable private Integer liveChatHeaderBackgroundColor; - @Nullable private Integer liveChatHeaderTextColor; - @Nullable private Integer liveChatConnectedDotColor; - @Nullable private Integer liveChatDisconnectedDotColor; - @Nullable private Integer liveChatUserCountTextColor; - + @Nullable + private Integer liveChatHeaderBackgroundColor; + + @Nullable + private Integer liveChatHeaderTextColor; + + @Nullable + private Integer liveChatConnectedDotColor; + + @Nullable + private Integer liveChatDisconnectedDotColor; + + @Nullable + private Integer liveChatUserCountTextColor; + /** * Builder pattern for easy theme construction */ public static class Builder { private final FastCommentsTheme theme = new FastCommentsTheme(); - + public Builder setPrimaryColor(@ColorInt int color) { theme.primaryColor = color; return this; } - + public Builder setPrimaryLightColor(@ColorInt int color) { theme.primaryLightColor = color; return this; } - + public Builder setPrimaryDarkColor(@ColorInt int color) { theme.primaryDarkColor = color; return this; } - + public Builder setActionButtonColor(@ColorInt int color) { theme.actionButtonColor = color; return this; } - + public Builder setReplyButtonColor(@ColorInt int color) { theme.replyButtonColor = color; return this; } - + public Builder setToggleRepliesButtonColor(@ColorInt int color) { theme.toggleRepliesButtonColor = color; return this; } - + public Builder setLoadMoreButtonTextColor(@ColorInt int color) { theme.loadMoreButtonTextColor = color; return this; } - + public Builder setLinkColor(@ColorInt int color) { theme.linkColor = color; return this; } - + public Builder setLinkColorPressed(@ColorInt int color) { theme.linkColorPressed = color; return this; } - + public Builder setVoteCountColor(@ColorInt int color) { theme.voteCountColor = color; return this; } - + public Builder setVoteCountZeroColor(@ColorInt int color) { theme.voteCountZeroColor = color; return this; } - + public Builder setVoteDividerColor(@ColorInt int color) { theme.voteDividerColor = color; return this; } - + public Builder setDialogHeaderBackgroundColor(@ColorInt int color) { theme.dialogHeaderBackgroundColor = color; return this; } - + public Builder setDialogHeaderTextColor(@ColorInt int color) { theme.dialogHeaderTextColor = color; return this; } - + public Builder setOnlineIndicatorColor(@ColorInt int color) { theme.onlineIndicatorColor = color; return this; @@ -148,7 +181,7 @@ public Builder setLiveChatUserCountTextColor(@ColorInt int color) { theme.liveChatUserCountTextColor = color; return this; } - + /** * Convenience method to set all primary-derived colors at once */ @@ -160,31 +193,110 @@ public Builder setAllPrimaryColors(@ColorInt int primary) { theme.loadMoreButtonTextColor = primary; return this; } - + public FastCommentsTheme build() { return theme; } } - + // Getters - @Nullable public Integer getPrimaryColor() { return primaryColor; } - @Nullable public Integer getPrimaryLightColor() { return primaryLightColor; } - @Nullable public Integer getPrimaryDarkColor() { return primaryDarkColor; } - @Nullable public Integer getActionButtonColor() { return actionButtonColor; } - @Nullable public Integer getReplyButtonColor() { return replyButtonColor; } - @Nullable public Integer getToggleRepliesButtonColor() { return toggleRepliesButtonColor; } - @Nullable public Integer getLoadMoreButtonTextColor() { return loadMoreButtonTextColor; } - @Nullable public Integer getLinkColor() { return linkColor; } - @Nullable public Integer getLinkColorPressed() { return linkColorPressed; } - @Nullable public Integer getVoteCountColor() { return voteCountColor; } - @Nullable public Integer getVoteCountZeroColor() { return voteCountZeroColor; } - @Nullable public Integer getVoteDividerColor() { return voteDividerColor; } - @Nullable public Integer getDialogHeaderBackgroundColor() { return dialogHeaderBackgroundColor; } - @Nullable public Integer getDialogHeaderTextColor() { return dialogHeaderTextColor; } - @Nullable public Integer getOnlineIndicatorColor() { return onlineIndicatorColor; } - @Nullable public Integer getLiveChatHeaderBackgroundColor() { return liveChatHeaderBackgroundColor; } - @Nullable public Integer getLiveChatHeaderTextColor() { return liveChatHeaderTextColor; } - @Nullable public Integer getLiveChatConnectedDotColor() { return liveChatConnectedDotColor; } - @Nullable public Integer getLiveChatDisconnectedDotColor() { return liveChatDisconnectedDotColor; } - @Nullable public Integer getLiveChatUserCountTextColor() { return liveChatUserCountTextColor; } -} \ No newline at end of file + @Nullable + public Integer getPrimaryColor() { + return primaryColor; + } + + @Nullable + public Integer getPrimaryLightColor() { + return primaryLightColor; + } + + @Nullable + public Integer getPrimaryDarkColor() { + return primaryDarkColor; + } + + @Nullable + public Integer getActionButtonColor() { + return actionButtonColor; + } + + @Nullable + public Integer getReplyButtonColor() { + return replyButtonColor; + } + + @Nullable + public Integer getToggleRepliesButtonColor() { + return toggleRepliesButtonColor; + } + + @Nullable + public Integer getLoadMoreButtonTextColor() { + return loadMoreButtonTextColor; + } + + @Nullable + public Integer getLinkColor() { + return linkColor; + } + + @Nullable + public Integer getLinkColorPressed() { + return linkColorPressed; + } + + @Nullable + public Integer getVoteCountColor() { + return voteCountColor; + } + + @Nullable + public Integer getVoteCountZeroColor() { + return voteCountZeroColor; + } + + @Nullable + public Integer getVoteDividerColor() { + return voteDividerColor; + } + + @Nullable + public Integer getDialogHeaderBackgroundColor() { + return dialogHeaderBackgroundColor; + } + + @Nullable + public Integer getDialogHeaderTextColor() { + return dialogHeaderTextColor; + } + + @Nullable + public Integer getOnlineIndicatorColor() { + return onlineIndicatorColor; + } + + @Nullable + public Integer getLiveChatHeaderBackgroundColor() { + return liveChatHeaderBackgroundColor; + } + + @Nullable + public Integer getLiveChatHeaderTextColor() { + return liveChatHeaderTextColor; + } + + @Nullable + public Integer getLiveChatConnectedDotColor() { + return liveChatConnectedDotColor; + } + + @Nullable + public Integer getLiveChatDisconnectedDotColor() { + return liveChatDisconnectedDotColor; + } + + @Nullable + public Integer getLiveChatUserCountTextColor() { + return liveChatUserCountTextColor; + } +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsView.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsView.java index 0b0d9ab..688648a 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsView.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/FastCommentsView.java @@ -2,32 +2,16 @@ import android.app.Activity; import android.content.Context; -import android.content.Intent; -import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.View; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; import android.widget.Button; - -import com.fastcomments.core.VoteStyle; -import com.fastcomments.model.APIEmptyResponse; -import com.fastcomments.model.BlockSuccess; -import com.fastcomments.model.UnblockSuccess; -import com.fastcomments.model.FComment; -import com.fastcomments.model.SetCommentTextResult; -import com.fastcomments.model.APIStatus; -import com.fastcomments.model.PublicComment; -import com.google.android.material.floatingactionbutton.FloatingActionButton; - import android.widget.FrameLayout; import android.widget.ProgressBar; import android.widget.TextView; - import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; @@ -35,11 +19,16 @@ import androidx.core.view.WindowInsetsCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - +import com.fastcomments.core.VoteStyle; +import com.fastcomments.model.APIEmptyResponse; import com.fastcomments.model.APIError; +import com.fastcomments.model.BlockSuccess; import com.fastcomments.model.GetCommentsResponseWithPresencePublicComment; -import com.fastcomments.model.VoteResponse; +import com.fastcomments.model.PublicComment; +import com.fastcomments.model.SetCommentTextResult; +import com.fastcomments.model.UnblockSuccess; import com.fastcomments.model.VoteDeleteResponse; +import com.fastcomments.model.VoteResponse; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -95,7 +84,7 @@ public void setCommentPostListener(CommentPostListener listener) { public void setOnReplyClickListener(OnReplyClickListener listener) { this.replyClickListener = listener; } - + /** * Set a listener to be notified when a user (name or avatar) is clicked * @@ -168,7 +157,10 @@ public void run() { ViewCompat.setOnApplyWindowInsetsListener(bottomCommentInput, (v, insets) -> { int navBars = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom; int ime = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom; - v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(), + v.setPadding( + v.getPaddingLeft(), + v.getPaddingTop(), + v.getPaddingRight(), baseInputPaddingBottom + Math.max(navBars, ime)); return insets; }); @@ -183,7 +175,6 @@ public void run() { this.btnLoadAll = paginationControls.findViewById(R.id.btnLoadAll); this.paginationProgressBar = paginationControls.findViewById(R.id.paginationProgressBar); - LinearLayoutManager layoutManager = new LinearLayoutManager(context); recyclerView.setLayoutManager(layoutManager); @@ -202,7 +193,9 @@ public void handleOnBackPressed() { activity.getOnBackPressedDispatcher().addCallback(backPressedCallback); } else if (context instanceof Activity) { // Log a warning that back press won't be handled - Log.w("FastCommentsView", "Context is an Activity but not AppCompatActivity. Back button handling not supported."); + Log.w( + "FastCommentsView", + "Context is an Activity but not AppCompatActivity. Back button handling not supported."); } // Store the SDK reference @@ -259,8 +252,8 @@ public void onItemRangeRemoved(int positionStart, int itemCount) { }); // Set up infinite scrolling if enabled in config - boolean isInfiniteScrollingEnabled = sdk.getConfig().enableInfiniteScrolling != null && - sdk.getConfig().enableInfiniteScrolling; + boolean isInfiniteScrollingEnabled = + sdk.getConfig().enableInfiniteScrolling != null && sdk.getConfig().enableInfiniteScrolling; if (isInfiniteScrollingEnabled) { recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @@ -276,8 +269,7 @@ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition(); // Load more when user is near the end (last 5 items) - if ((visibleItemCount + firstVisibleItemPosition + 5) >= totalItemCount - && sdk.hasMore) { + if ((visibleItemCount + firstVisibleItemPosition + 5) >= totalItemCount && sdk.hasMore) { loadMoreComments(); } } @@ -320,7 +312,7 @@ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { replyClickListener.onReplyClick(commentToReplyTo); } }); - + // Handle user click events adapter.setUserClickListener(userClickListener); @@ -364,8 +356,8 @@ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { // User hasn't upvoted, so add the vote commentToVote.getComment().setIsVotedUp(true); // Also remove downvote if exists - if (commentToVote.getComment().getIsVotedDown() != null && - commentToVote.getComment().getIsVotedDown()) { + if (commentToVote.getComment().getIsVotedDown() != null + && commentToVote.getComment().getIsVotedDown()) { commentToVote.getComment().setIsVotedDown(false); // Update downvote count Integer votesDown = commentToVote.getComment().getVotesDown(); @@ -419,17 +411,17 @@ public boolean onSuccess(APIError error) { // Show error message getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - R.string.error_voting, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), R.string.error_voting, android.widget.Toast.LENGTH_SHORT) + .show(); // Update the UI - use targeted update for performance int position = adapter.getPositionForComment(commentToVote); if (position != -1) { adapter.notifyItemChanged(position); } else { - Log.w("FastCommentsView", "Could not find comment position for vote rollback, UI may not refresh"); + Log.w( + "FastCommentsView", + "Could not find comment position for vote rollback, UI may not refresh"); } }); @@ -437,50 +429,71 @@ public boolean onSuccess(APIError error) { } }; - // The general vote flow: + // The general vote flow: // 1. If there's an existing vote, delete it first // 2. Then make a new vote if needed (not needed when removing an upvote) // Check if user is logged in or if anonymous votes are allowed - boolean userIsLoggedIn = sdk.getCurrentUser() != null && - sdk.getCurrentUser().getAuthorized() != null && - sdk.getCurrentUser().getAuthorized(); - boolean allowAnonVotes = sdk.getConfig() != null && - Boolean.TRUE.equals(sdk.getConfig().allowAnonVotes); + boolean userIsLoggedIn = sdk.getCurrentUser() != null + && sdk.getCurrentUser().getAuthorized() != null + && sdk.getCurrentUser().getAuthorized(); + boolean allowAnonVotes = sdk.getConfig() != null && Boolean.TRUE.equals(sdk.getConfig().allowAnonVotes); // Special handling if the user is not logged in if (!userIsLoggedIn) { if (!allowAnonVotes) { final boolean needToDeleteFinal = needToDelete; // Show dialog to collect user info for votes that need verification - UserLoginDialog.show(getContext(), sdk.getConfig(), "vote", new UserLoginDialog.OnUserCredentialsListener() { - @Override - public void onUserCredentialsEntered(String username, String email) { - // Proceed with vote using provided credentials - processUpvote(commentToVote, needToDeleteFinal, originalIsVotedUp, originalVoteId, - upvoteErrorHandler, username, email, finalToastMessage); - } + UserLoginDialog.show( + getContext(), sdk.getConfig(), "vote", new UserLoginDialog.OnUserCredentialsListener() { + @Override + public void onUserCredentialsEntered(String username, String email) { + // Proceed with vote using provided credentials + processUpvote( + commentToVote, + needToDeleteFinal, + originalIsVotedUp, + originalVoteId, + upvoteErrorHandler, + username, + email, + finalToastMessage); + } - @Override - public void onCancel() { - // User canceled, revert UI state - commentToVote.getComment().setIsVotedUp(originalIsVotedUp); - commentToVote.getComment().setIsVotedDown(originalIsVotedDown); - commentToVote.getComment().setVotesUp(originalVotesUp); - commentToVote.getComment().setVotesDown(originalVotesDown); - adapter.notifyDataSetChanged(); - } - }); + @Override + public void onCancel() { + // User canceled, revert UI state + commentToVote.getComment().setIsVotedUp(originalIsVotedUp); + commentToVote.getComment().setIsVotedDown(originalIsVotedDown); + commentToVote.getComment().setVotesUp(originalVotesUp); + commentToVote.getComment().setVotesDown(originalVotesDown); + adapter.notifyDataSetChanged(); + } + }); } else { // Anonymous votes allowed, proceed directly without dialog final boolean needToDeleteFinal = needToDelete; - processUpvote(commentToVote, needToDeleteFinal, originalIsVotedUp, originalVoteId, - upvoteErrorHandler, null, null, finalToastMessage); + processUpvote( + commentToVote, + needToDeleteFinal, + originalIsVotedUp, + originalVoteId, + upvoteErrorHandler, + null, + null, + finalToastMessage); } } else { // User is logged in, proceed with vote - processUpvote(commentToVote, needToDelete, originalIsVotedUp, originalVoteId, - upvoteErrorHandler, null, null, finalToastMessage); + processUpvote( + commentToVote, + needToDelete, + originalIsVotedUp, + originalVoteId, + upvoteErrorHandler, + null, + null, + finalToastMessage); } }); @@ -517,8 +530,8 @@ public void onCancel() { // User hasn't downvoted, so add the vote commentToVote.getComment().setIsVotedDown(true); // Also remove upvote if exists - if (commentToVote.getComment().getIsVotedUp() != null && - commentToVote.getComment().getIsVotedUp()) { + if (commentToVote.getComment().getIsVotedUp() != null + && commentToVote.getComment().getIsVotedUp()) { commentToVote.getComment().setIsVotedUp(false); // Update upvote count Integer votesUp = commentToVote.getComment().getVotesUp(); @@ -568,10 +581,8 @@ public boolean onSuccess(APIError error) { // Show error message getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - R.string.error_voting, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), R.string.error_voting, android.widget.Toast.LENGTH_SHORT) + .show(); // Update the UI adapter.notifyDataSetChanged(); @@ -581,50 +592,71 @@ public boolean onSuccess(APIError error) { } }; - // The general vote flow: + // The general vote flow: // 1. If there's an existing vote, delete it first // 2. Then make a new vote if needed (not needed when removing a downvote) // Check if user is logged in or if anonymous votes are allowed - boolean userIsLoggedIn = sdk.getCurrentUser() != null && - sdk.getCurrentUser().getAuthorized() != null && - sdk.getCurrentUser().getAuthorized(); - boolean allowAnonVotes = sdk.getConfig() != null && - Boolean.TRUE.equals(sdk.getConfig().allowAnonVotes); + boolean userIsLoggedIn = sdk.getCurrentUser() != null + && sdk.getCurrentUser().getAuthorized() != null + && sdk.getCurrentUser().getAuthorized(); + boolean allowAnonVotes = sdk.getConfig() != null && Boolean.TRUE.equals(sdk.getConfig().allowAnonVotes); // Special handling if the user is not logged in if (!userIsLoggedIn) { if (!allowAnonVotes) { final boolean needToDeleteFinal = needToDelete; // Show dialog to collect user info for votes that need verification - UserLoginDialog.show(getContext(), sdk.getConfig(), "vote", new UserLoginDialog.OnUserCredentialsListener() { - @Override - public void onUserCredentialsEntered(String username, String email) { - // Proceed with vote using provided credentials - processDownvote(commentToVote, needToDeleteFinal, originalIsVotedDown, originalVoteId, - downvoteErrorHandler, username, email, finalToastMessage); - } + UserLoginDialog.show( + getContext(), sdk.getConfig(), "vote", new UserLoginDialog.OnUserCredentialsListener() { + @Override + public void onUserCredentialsEntered(String username, String email) { + // Proceed with vote using provided credentials + processDownvote( + commentToVote, + needToDeleteFinal, + originalIsVotedDown, + originalVoteId, + downvoteErrorHandler, + username, + email, + finalToastMessage); + } - @Override - public void onCancel() { - // User canceled, revert UI state - commentToVote.getComment().setIsVotedUp(originalIsVotedUp); - commentToVote.getComment().setIsVotedDown(originalIsVotedDown); - commentToVote.getComment().setVotesUp(originalVotesUp); - commentToVote.getComment().setVotesDown(originalVotesDown); - adapter.notifyDataSetChanged(); - } - }); + @Override + public void onCancel() { + // User canceled, revert UI state + commentToVote.getComment().setIsVotedUp(originalIsVotedUp); + commentToVote.getComment().setIsVotedDown(originalIsVotedDown); + commentToVote.getComment().setVotesUp(originalVotesUp); + commentToVote.getComment().setVotesDown(originalVotesDown); + adapter.notifyDataSetChanged(); + } + }); } else { // Anonymous votes allowed, proceed directly without dialog final boolean needToDeleteFinal = needToDelete; - processDownvote(commentToVote, needToDeleteFinal, originalIsVotedDown, originalVoteId, - downvoteErrorHandler, null, null, finalToastMessage); + processDownvote( + commentToVote, + needToDeleteFinal, + originalIsVotedDown, + originalVoteId, + downvoteErrorHandler, + null, + null, + finalToastMessage); } } else { // User is logged in, proceed with vote - processDownvote(commentToVote, needToDelete, originalIsVotedDown, originalVoteId, - downvoteErrorHandler, null, null, finalToastMessage); + processDownvote( + commentToVote, + needToDelete, + originalIsVotedDown, + originalVoteId, + downvoteErrorHandler, + null, + null, + finalToastMessage); } }); @@ -663,57 +695,59 @@ public void onCancel() { getHandler().post(() -> toggleButton.setText(R.string.loading_replies)); } - sdk.getCommentsForParent(skip, limit, 0, parentId, new FCCallback() { - @Override - public boolean onFailure(APIError error) { - getHandler().post(() -> { - // If we have a parent comment, update its loading state - if (parentComment != null) { - parentComment.isLoadingChildren = false; - - // Revert pagination state on failure - if (isLoadMore) { - parentComment.childPage--; - parentComment.childSkip -= parentComment.childPageSize; - } - } + sdk.getCommentsForParent( + skip, limit, 0, parentId, new FCCallback() { + @Override + public boolean onFailure(APIError error) { + getHandler().post(() -> { + // If we have a parent comment, update its loading state + if (parentComment != null) { + parentComment.isLoadingChildren = false; + + // Revert pagination state on failure + if (isLoadMore) { + parentComment.childPage--; + parentComment.childSkip -= parentComment.childPageSize; + } + } - // Show toast with error message - android.widget.Toast.makeText( - getContext(), - R.string.error_loading_replies, - android.widget.Toast.LENGTH_SHORT - ).show(); - - // Reset button text if the toggle button is available - if (toggleButton != null && !isLoadMore) { - // Get the comment to retrieve the child count - RenderableComment comment = sdk.commentsTree.commentsById.get(parentId); - if (comment != null && comment.getComment().getChildCount() != null) { - toggleButton.setText(getContext().getString( - R.string.show_replies, - comment.getComment().getChildCount()) - ); - } + // Show toast with error message + android.widget.Toast.makeText( + getContext(), + R.string.error_loading_replies, + android.widget.Toast.LENGTH_SHORT) + .show(); + + // Reset button text if the toggle button is available + if (toggleButton != null && !isLoadMore) { + // Get the comment to retrieve the child count + RenderableComment comment = sdk.commentsTree.commentsById.get(parentId); + if (comment != null && comment.getComment().getChildCount() != null) { + toggleButton.setText(getContext() + .getString( + R.string.show_replies, + comment.getComment().getChildCount())); + } + } + }); + return CONSUME; } - }); - return CONSUME; - } - @Override - public boolean onSuccess(GetCommentsResponseWithPresencePublicComment response) { - getHandler().post(() -> { - // If we have a parent comment, update its state - if (parentComment != null) { - parentComment.isLoadingChildren = false; - parentComment.hasMoreChildren = response.getHasMore() != null ? response.getHasMore() : false; - } + @Override + public boolean onSuccess(GetCommentsResponseWithPresencePublicComment response) { + getHandler().post(() -> { + // If we have a parent comment, update its state + if (parentComment != null) { + parentComment.isLoadingChildren = false; + parentComment.hasMoreChildren = + response.getHasMore() != null ? response.getHasMore() : false; + } - sendResults.call(response.getComments()); + sendResults.call(response.getComments()); + }); + return CONSUME; + } }); - return CONSUME; - } - }); }); // Set up listener for new child comments button clicks @@ -729,51 +763,55 @@ public void onEdit(String commentId, String commentText) { // Show edit dialog CommentEditDialog dialog = new CommentEditDialog(getContext(), sdk); dialog.setOnSaveCallback(newText -> { - // Call API to edit the comment - sdk.editComment(commentId, newText, new FCCallback() { - @Override - public boolean onFailure(APIError error) { - // Show error message - getHandler().post(() -> { - String errorMessage; - if (error.getTranslatedError() != null && !error.getTranslatedError().isEmpty()) { - errorMessage = error.getTranslatedError(); - } else if (error.getReason() != null && !error.getReason().isEmpty()) { - errorMessage = error.getReason(); - } else { - errorMessage = getContext().getString(R.string.error_editing_comment); - } + // Call API to edit the comment + sdk.editComment(commentId, newText, new FCCallback() { + @Override + public boolean onFailure(APIError error) { + // Show error message + getHandler().post(() -> { + String errorMessage; + if (error.getTranslatedError() != null + && !error.getTranslatedError().isEmpty()) { + errorMessage = error.getTranslatedError(); + } else if (error.getReason() != null + && !error.getReason().isEmpty()) { + errorMessage = error.getReason(); + } else { + errorMessage = getContext().getString(R.string.error_editing_comment); + } - android.widget.Toast.makeText( - getContext(), - errorMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); - }); - return CONSUME; - } + android.widget.Toast.makeText( + getContext(), errorMessage, android.widget.Toast.LENGTH_SHORT) + .show(); + }); + return CONSUME; + } - @Override - public boolean onSuccess(SetCommentTextResult updatedComment) { - // Show success message - getHandler().post(() -> { - android.widget.Toast.makeText( - getContext(), - R.string.comment_edited_successfully, - android.widget.Toast.LENGTH_SHORT - ).show(); - - // Update the comment HTML in the existing comment object - RenderableComment renderableComment = sdk.commentsTree.commentsById.get(commentId); - if (renderableComment != null) { - renderableComment.getComment().setCommentHTML(updatedComment.getCommentHTML()); - adapter.notifyDataSetChanged(); + @Override + public boolean onSuccess(SetCommentTextResult updatedComment) { + // Show success message + getHandler().post(() -> { + android.widget.Toast.makeText( + getContext(), + R.string.comment_edited_successfully, + android.widget.Toast.LENGTH_SHORT) + .show(); + + // Update the comment HTML in the existing comment object + RenderableComment renderableComment = + sdk.commentsTree.commentsById.get(commentId); + if (renderableComment != null) { + renderableComment + .getComment() + .setCommentHTML(updatedComment.getCommentHTML()); + adapter.notifyDataSetChanged(); + } + }); + return CONSUME; } }); - return CONSUME; - } - }); - }).show(commentText); + }) + .show(commentText); } @Override @@ -790,19 +828,19 @@ public boolean onFailure(APIError error) { // Show error message getHandler().post(() -> { String errorMessage; - if (error.getTranslatedError() != null && !error.getTranslatedError().isEmpty()) { + if (error.getTranslatedError() != null + && !error.getTranslatedError().isEmpty()) { errorMessage = error.getTranslatedError(); - } else if (error.getReason() != null && !error.getReason().isEmpty()) { + } else if (error.getReason() != null + && !error.getReason().isEmpty()) { errorMessage = error.getReason(); } else { errorMessage = getContext().getString(R.string.error_deleting_comment); } android.widget.Toast.makeText( - getContext(), - errorMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), errorMessage, android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } @@ -812,10 +850,10 @@ public boolean onSuccess(APIEmptyResponse success) { // Show success message getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - R.string.comment_deleted_successfully, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), + R.string.comment_deleted_successfully, + android.widget.Toast.LENGTH_SHORT) + .show(); // UI is updated from the SDK on success adapter.notifyDataSetChanged(); @@ -846,19 +884,18 @@ public boolean onFailure(APIError error) { // Show error message getHandler().post(() -> { String errorMessage; - if (error.getTranslatedError() != null && !error.getTranslatedError().isEmpty()) { + if (error.getTranslatedError() != null + && !error.getTranslatedError().isEmpty()) { errorMessage = error.getTranslatedError(); - } else if (error.getReason() != null && !error.getReason().isEmpty()) { + } else if (error.getReason() != null + && !error.getReason().isEmpty()) { errorMessage = error.getReason(); } else { errorMessage = getContext().getString(R.string.error_flagging_comment); } - android.widget.Toast.makeText( - getContext(), - errorMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); + android.widget.Toast.makeText(getContext(), errorMessage, android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } @@ -868,10 +905,10 @@ public boolean onSuccess(APIEmptyResponse success) { // Show success message getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - R.string.comment_flagged_successfully, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), + R.string.comment_flagged_successfully, + android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } @@ -893,19 +930,19 @@ public boolean onFailure(APIError error) { // Show error message getHandler().post(() -> { String errorMessage; - if (error.getTranslatedError() != null && !error.getTranslatedError().isEmpty()) { + if (error.getTranslatedError() != null + && !error.getTranslatedError().isEmpty()) { errorMessage = error.getTranslatedError(); - } else if (error.getReason() != null && !error.getReason().isEmpty()) { + } else if (error.getReason() != null + && !error.getReason().isEmpty()) { errorMessage = error.getReason(); } else { errorMessage = getContext().getString(R.string.error_blocking_user); } android.widget.Toast.makeText( - getContext(), - errorMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), errorMessage, android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } @@ -914,10 +951,10 @@ public boolean onFailure(APIError error) { public boolean onSuccess(BlockSuccess success) { getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - R.string.user_blocked_successfully, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), + R.string.user_blocked_successfully, + android.widget.Toast.LENGTH_SHORT) + .show(); // Update blocked statuses in-place from the API response sdk.commentsTree.updateBlockedStatuses(success.getCommentStatuses()); @@ -937,7 +974,12 @@ public void onUnblock(String commentId, String userName) { // Confirm before unblocking android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(getContext()); builder.setTitle(R.string.unblock_user_title) - .setMessage(getContext().getString(R.string.unblock_user_confirm, userName != null ? userName : getContext().getString(R.string.blocked_user_placeholder))) + .setMessage(getContext() + .getString( + R.string.unblock_user_confirm, + userName != null + ? userName + : getContext().getString(R.string.blocked_user_placeholder))) .setPositiveButton(R.string.unblock, (dialog, which) -> { // Snapshot comment IDs on the main thread before the async call List commentIds = new ArrayList<>(sdk.commentsTree.commentsById.keySet()); @@ -946,19 +988,19 @@ public void onUnblock(String commentId, String userName) { public boolean onFailure(APIError error) { getHandler().post(() -> { String errorMessage; - if (error.getTranslatedError() != null && !error.getTranslatedError().isEmpty()) { + if (error.getTranslatedError() != null + && !error.getTranslatedError().isEmpty()) { errorMessage = error.getTranslatedError(); - } else if (error.getReason() != null && !error.getReason().isEmpty()) { + } else if (error.getReason() != null + && !error.getReason().isEmpty()) { errorMessage = error.getReason(); } else { errorMessage = getContext().getString(R.string.error_unblocking_user); } android.widget.Toast.makeText( - getContext(), - errorMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), errorMessage, android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } @@ -967,10 +1009,10 @@ public boolean onFailure(APIError error) { public boolean onSuccess(UnblockSuccess success) { getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - R.string.user_unblocked_successfully, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), + R.string.user_unblocked_successfully, + android.widget.Toast.LENGTH_SHORT) + .show(); // Update blocked statuses in-place from the API response sdk.commentsTree.updateBlockedStatuses(success.getCommentStatuses()); @@ -1068,7 +1110,8 @@ public boolean onFailure(APIError error) { bottomCommentInput.setSubmitting(false); // Check for translated error message String errorMessage; - if (error.getTranslatedError() != null && !error.getTranslatedError().isEmpty()) { + if (error.getTranslatedError() != null + && !error.getTranslatedError().isEmpty()) { errorMessage = error.getTranslatedError(); } else if (error.getReason() != null && !error.getReason().isEmpty()) { errorMessage = error.getReason(); @@ -1090,10 +1133,10 @@ public boolean onSuccess(PublicComment comment) { // Show a toast message to confirm successful posting android.widget.Toast.makeText( - getContext(), - R.string.comment_posted_successfully, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), + R.string.comment_posted_successfully, + android.widget.Toast.LENGTH_SHORT) + .show(); // Add the comment to the tree, display immediately sdk.addComment(comment, true); @@ -1184,8 +1227,8 @@ public void refresh() { */ private void updatePaginationControls() { // Check if infinite scrolling is enabled - boolean isInfiniteScrollingEnabled = sdk.getConfig().enableInfiniteScrolling != null && - sdk.getConfig().enableInfiniteScrolling; + boolean isInfiniteScrollingEnabled = + sdk.getConfig().enableInfiniteScrolling != null && sdk.getConfig().enableInfiniteScrolling; if (isInfiniteScrollingEnabled) { // Hide pagination controls when infinite scrolling is enabled @@ -1228,8 +1271,8 @@ private void loadMoreComments() { } // Check if infinite scrolling is enabled - boolean isInfiniteScrollingEnabled = sdk.getConfig().enableInfiniteScrolling != null && - sdk.getConfig().enableInfiniteScrolling; + boolean isInfiniteScrollingEnabled = + sdk.getConfig().enableInfiniteScrolling != null && sdk.getConfig().enableInfiniteScrolling; // Show loading indicator if (isInfiniteScrollingEnabled) { @@ -1255,8 +1298,8 @@ public boolean onFailure(APIError error) { paginationProgressBar.setVisibility(View.GONE); // Check if infinite scrolling is enabled - boolean isInfiniteScrollingEnabled = sdk.getConfig().enableInfiniteScrolling != null && - sdk.getConfig().enableInfiniteScrolling; + boolean isInfiniteScrollingEnabled = + sdk.getConfig().enableInfiniteScrolling != null && sdk.getConfig().enableInfiniteScrolling; if (isInfiniteScrollingEnabled) { // For infinite scrolling, hide the pagination controls on error @@ -1271,10 +1314,8 @@ public boolean onFailure(APIError error) { // Show error toast android.widget.Toast.makeText( - getContext(), - R.string.error_loading_comments, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), R.string.error_loading_comments, android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } @@ -1286,8 +1327,8 @@ public boolean onSuccess(GetCommentsResponseWithPresencePublicComment response) paginationProgressBar.setVisibility(View.GONE); // Check if infinite scrolling is enabled - boolean isInfiniteScrollingEnabled = sdk.getConfig().enableInfiniteScrolling != null && - sdk.getConfig().enableInfiniteScrolling; + boolean isInfiniteScrollingEnabled = + sdk.getConfig().enableInfiniteScrolling != null && sdk.getConfig().enableInfiniteScrolling; if (isInfiniteScrollingEnabled) { // For infinite scrolling, hide the pagination controls again @@ -1325,10 +1366,8 @@ public boolean onFailure(APIError error) { // Show error toast android.widget.Toast.makeText( - getContext(), - R.string.error_loading_comments, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), R.string.error_loading_comments, android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } @@ -1357,12 +1396,22 @@ public boolean onSuccess(GetCommentsResponseWithPresencePublicComment response) * @param email Email for anonymous voting (null if authenticated) * @param toastMessage Message to show on success */ - private void processUpvote(RenderableComment commentToVote, boolean needToDelete, - Boolean originalIsVotedUp, String originalVoteId, - FCCallback errorHandler, String username, String email, String toastMessage) { + private void processUpvote( + RenderableComment commentToVote, + boolean needToDelete, + Boolean originalIsVotedUp, + String originalVoteId, + FCCallback errorHandler, + String username, + String email, + String toastMessage) { if (needToDelete && originalVoteId != null) { // Delete the existing vote first - sdk.deleteCommentVote(commentToVote.getComment().getId(), originalVoteId, username, email, + sdk.deleteCommentVote( + commentToVote.getComment().getId(), + originalVoteId, + username, + email, new FCCallback() { @Override public boolean onFailure(APIError error) { @@ -1379,16 +1428,18 @@ public boolean onSuccess(VoteDeleteResponse response) { // Show success toast message for removing vote getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - toastMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), toastMessage, android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } // Otherwise, we need to add a new upvote - sdk.voteComment(commentToVote.getComment().getId(), true, username, email, + sdk.voteComment( + commentToVote.getComment().getId(), + true, + username, + email, new FCCallback() { @Override public boolean onFailure(APIError error) { @@ -1403,10 +1454,10 @@ public boolean onSuccess(VoteResponse response) { // Show success toast message getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - toastMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), + toastMessage, + android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; @@ -1419,30 +1470,26 @@ public boolean onSuccess(VoteResponse response) { } else if (originalIsVotedUp == null || !originalIsVotedUp) { // No existing vote to delete or it's a downvote being converted to upvote // Just add a new upvote - sdk.voteComment(commentToVote.getComment().getId(), true, username, email, - new FCCallback() { - @Override - public boolean onFailure(APIError error) { - return errorHandler.onSuccess(error); - } - - @Override - public boolean onSuccess(VoteResponse response) { - // Store new vote ID - commentToVote.getComment().setMyVoteId(response.getVoteId()); + sdk.voteComment(commentToVote.getComment().getId(), true, username, email, new FCCallback() { + @Override + public boolean onFailure(APIError error) { + return errorHandler.onSuccess(error); + } - // Show success toast message - getHandler().post(() -> { - android.widget.Toast.makeText( - getContext(), - toastMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); - }); + @Override + public boolean onSuccess(VoteResponse response) { + // Store new vote ID + commentToVote.getComment().setMyVoteId(response.getVoteId()); - return CONSUME; - } + // Show success toast message + getHandler().post(() -> { + android.widget.Toast.makeText(getContext(), toastMessage, android.widget.Toast.LENGTH_SHORT) + .show(); }); + + return CONSUME; + } + }); } } @@ -1458,12 +1505,22 @@ public boolean onSuccess(VoteResponse response) { * @param email Email for anonymous voting (null if authenticated) * @param toastMessage Message to show on success */ - private void processDownvote(RenderableComment commentToVote, boolean needToDelete, - Boolean originalIsVotedDown, String originalVoteId, - FCCallback errorHandler, String username, String email, String toastMessage) { + private void processDownvote( + RenderableComment commentToVote, + boolean needToDelete, + Boolean originalIsVotedDown, + String originalVoteId, + FCCallback errorHandler, + String username, + String email, + String toastMessage) { if (needToDelete && originalVoteId != null) { // Delete the existing vote first - sdk.deleteCommentVote(commentToVote.getComment().getId(), originalVoteId, username, email, + sdk.deleteCommentVote( + commentToVote.getComment().getId(), + originalVoteId, + username, + email, new FCCallback() { @Override public boolean onFailure(APIError error) { @@ -1480,16 +1537,18 @@ public boolean onSuccess(VoteDeleteResponse response) { // Show success toast message for removing vote getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - toastMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), toastMessage, android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } // Otherwise, we need to add a new downvote - sdk.voteComment(commentToVote.getComment().getId(), false, username, email, + sdk.voteComment( + commentToVote.getComment().getId(), + false, + username, + email, new FCCallback() { @Override public boolean onFailure(APIError error) { @@ -1504,10 +1563,10 @@ public boolean onSuccess(VoteResponse response) { // Show success toast message getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - toastMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), + toastMessage, + android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; @@ -1520,30 +1579,26 @@ public boolean onSuccess(VoteResponse response) { } else if (originalIsVotedDown == null || !originalIsVotedDown) { // No existing vote to delete or it's an upvote being converted to downvote // Just add a new downvote - sdk.voteComment(commentToVote.getComment().getId(), false, username, email, - new FCCallback() { - @Override - public boolean onFailure(APIError error) { - return errorHandler.onSuccess(error); - } - - @Override - public boolean onSuccess(VoteResponse response) { - // Store new vote ID - commentToVote.getComment().setMyVoteId(response.getVoteId()); + sdk.voteComment(commentToVote.getComment().getId(), false, username, email, new FCCallback() { + @Override + public boolean onFailure(APIError error) { + return errorHandler.onSuccess(error); + } - // Show success toast message - getHandler().post(() -> { - android.widget.Toast.makeText( - getContext(), - toastMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); - }); + @Override + public boolean onSuccess(VoteResponse response) { + // Store new vote ID + commentToVote.getComment().setMyVoteId(response.getVoteId()); - return CONSUME; - } + // Show success toast message + getHandler().post(() -> { + android.widget.Toast.makeText(getContext(), toastMessage, android.widget.Toast.LENGTH_SHORT) + .show(); }); + + return CONSUME; + } + }); } } @@ -1649,7 +1704,7 @@ private void updateReplyButtonText(String commentId, boolean isReplying) { } } } - + /** * Handle back button press to warn user before losing comment text */ @@ -1659,7 +1714,7 @@ private void handleBackPress() { // Show confirmation dialog - different message for reply vs new comment String title, message; RenderableComment parentComment = bottomCommentInput.getParentComment(); - + if (parentComment != null) { title = getContext().getString(R.string.cancel_reply_title); message = getContext().getString(R.string.cancel_reply_confirm); @@ -1667,25 +1722,25 @@ private void handleBackPress() { title = getContext().getString(R.string.cancel_comment_title); message = getContext().getString(R.string.cancel_comment_confirm); } - + new android.app.AlertDialog.Builder(getContext()) - .setTitle(title) - .setMessage(message) - .setPositiveButton(android.R.string.yes, (dialog, which) -> { - // Proceed with back navigation - bottomCommentInput.clearText(); - bottomCommentInput.clearReplyState(); - // Allow the back press to proceed - if (backPressedCallback != null) { - backPressedCallback.setEnabled(false); - if (getContext() instanceof Activity) { - ((Activity) getContext()).onBackPressed(); + .setTitle(title) + .setMessage(message) + .setPositiveButton(android.R.string.yes, (dialog, which) -> { + // Proceed with back navigation + bottomCommentInput.clearText(); + bottomCommentInput.clearReplyState(); + // Allow the back press to proceed + if (backPressedCallback != null) { + backPressedCallback.setEnabled(false); + if (getContext() instanceof Activity) { + ((Activity) getContext()).onBackPressed(); + } + backPressedCallback.setEnabled(true); } - backPressedCallback.setEnabled(true); - } - }) - .setNegativeButton(android.R.string.no, null) - .show(); + }) + .setNegativeButton(android.R.string.no, null) + .show(); } else { // No text, allow normal back navigation if (backPressedCallback != null) { @@ -1712,7 +1767,10 @@ protected void onDetachedFromWindow() { } } boolean finishing = activity != null && activity.isFinishing(); - android.util.Log.w("FastCommentsView", "onDetachedFromWindow: activity=" + activity + " isFinishing=" + finishing, new Throwable()); + android.util.Log.w( + "FastCommentsView", + "onDetachedFromWindow: activity=" + activity + " isFinishing=" + finishing, + new Throwable()); if (finishing) { cleanup(); } @@ -1749,7 +1807,6 @@ public void cleanup() { } } - // Clear handler callbacks if (dateUpdateHandler != null) { dateUpdateHandler.removeCallbacksAndMessages(null); @@ -1794,14 +1851,13 @@ public BottomCommentInputView getBottomCommentInput() { * @return true if replying to a comment, false otherwise */ public boolean isReplyingToComment() { - return bottomCommentInput != null && - bottomCommentInput.getParentComment() != null; + return bottomCommentInput != null && bottomCommentInput.getParentComment() != null; } - + /** * Sets up the demo banner if tenant ID is "demo" */ private void setupDemoBanner() { DemoBannerHelper.setupDemoBanner(this, sdk); } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/FeedCustomToolbarButton.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/FeedCustomToolbarButton.java index 86b3322..3439415 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/FeedCustomToolbarButton.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/FeedCustomToolbarButton.java @@ -90,4 +90,4 @@ default void onAttached(FeedPostCreateView view, View buttonView) { default void onDetached(FeedPostCreateView view, View buttonView) { // Default implementation does nothing } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/FeedPostCreateView.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/FeedPostCreateView.java index 02e5703..71f6af7 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/FeedPostCreateView.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/FeedPostCreateView.java @@ -1,7 +1,6 @@ package com.fastcomments.sdk; import android.content.Context; -import android.content.Intent; import android.content.res.ColorStateList; import android.net.Uri; import android.os.Handler; @@ -24,12 +23,10 @@ import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - import com.fastcomments.model.APIError; import com.fastcomments.model.CreateFeedPostParams; import com.fastcomments.model.FeedPost; @@ -37,11 +34,9 @@ import com.fastcomments.model.FeedPostMediaItem; import com.fastcomments.model.FeedPostMediaItemAsset; import com.fastcomments.model.UserSessionInfo; - import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.concurrent.CountDownLatch; /** * A custom view for creating feed posts with text, images and links @@ -244,7 +239,7 @@ private void setCurrentUser(UserSessionInfo userInfo) { // Set user name if (userInfo.getDisplayName() != null && !userInfo.getDisplayName().isEmpty()) { formUserNameTextView.setText(userInfo.getDisplayName()); - } else if(userInfo.getUsername() != null && !userInfo.getUsername().isEmpty()) { + } else if (userInfo.getUsername() != null && !userInfo.getUsername().isEmpty()) { formUserNameTextView.setText(userInfo.getUsername()); } else { formUserNameTextView.setText(R.string.anonymous); @@ -322,9 +317,9 @@ public boolean addImageUri(Uri imageUri) { // Handle as remote media - add directly to remoteMediaItems FeedPostMediaItem mediaItem = new FeedPostMediaItem(); FeedPostMediaItemAsset asset = new FeedPostMediaItemAsset() - .src(imageUri.toString()) - .w(400) // Default dimensions for GIFs - .h(300); + .src(imageUri.toString()) + .w(400) // Default dimensions for GIFs + .h(300); mediaItem.setSizes(java.util.Arrays.asList(asset)); remoteMediaItems.add(mediaItem); @@ -383,9 +378,11 @@ private void removeMedia(int position) { private void showAddLinkDialog() { // Don't show dialog if a link is already attached if (attachedLink != null) { - Toast.makeText(getContext(), - "A link is already attached. Remove it first to add a new one.", - Toast.LENGTH_SHORT).show(); + Toast.makeText( + getContext(), + "A link is already attached. Remove it first to add a new one.", + Toast.LENGTH_SHORT) + .show(); return; } @@ -450,10 +447,10 @@ private void cancelPost() { * Enable/disable submit button based on content */ private void updateSubmitButtonState() { - boolean hasContent = !TextUtils.isEmpty(postContentEditText.getText()) || - !selectedMediaUris.isEmpty() || - !remoteMediaItems.isEmpty() || - attachedLink != null; + boolean hasContent = !TextUtils.isEmpty(postContentEditText.getText()) + || !selectedMediaUris.isEmpty() + || !remoteMediaItems.isEmpty() + || attachedLink != null; submitPostButton.setEnabled(hasContent); @@ -492,12 +489,17 @@ private void validateAndSubmitPost() { hideError(); // Check that we have content — serialize spans to HTML - String plainContent = postContentEditText.getText() != null ? - postContentEditText.getText().toString().trim() : ""; - String content = plainContent.isEmpty() ? "" : - RichTextHelper.toHtml(postContentEditText.getText()).trim(); - - boolean hasContent = !plainContent.isEmpty() || !selectedMediaUris.isEmpty() || !remoteMediaItems.isEmpty() || attachedLink != null; + String plainContent = postContentEditText.getText() != null + ? postContentEditText.getText().toString().trim() + : ""; + String content = plainContent.isEmpty() + ? "" + : RichTextHelper.toHtml(postContentEditText.getText()).trim(); + + boolean hasContent = !plainContent.isEmpty() + || !selectedMediaUris.isEmpty() + || !remoteMediaItems.isEmpty() + || attachedLink != null; if (!hasContent) { showError(getContext().getString(R.string.content_required)); @@ -510,16 +512,19 @@ private void validateAndSubmitPost() { createPostFromInput(content, new FCCallback() { @Override public boolean onFailure(APIError error) { - Log.e("FeedPostCreateView", "Create post from input failed: " + - (error != null ? error.getReason() : "Unknown error") + - ", translated: " + (error != null ? error.getTranslatedError() : "None")); + Log.e( + "FeedPostCreateView", + "Create post from input failed: " + (error != null ? error.getReason() : "Unknown error") + + ", translated: " + + (error != null ? error.getTranslatedError() : "None")); mainHandler.post(() -> { // Hide loading state setSubmitting(false); // Show error message - String errorMessage = error != null && error.getTranslatedError() != null ? - error.getTranslatedError() : getContext().getString(R.string.post_error); + String errorMessage = error != null && error.getTranslatedError() != null + ? error.getTranslatedError() + : getContext().getString(R.string.post_error); showError(errorMessage); @@ -539,16 +544,19 @@ public boolean onSuccess(FeedPost feedPost) { sdk.createPost(params, new FCCallback() { @Override public boolean onFailure(APIError error) { - Log.e("FeedPostCreateView", "Post creation failed: " + - (error != null ? error.getReason() : "Unknown error") + - ", translated: " + (error != null ? error.getTranslatedError() : "None")); + Log.e( + "FeedPostCreateView", + "Post creation failed: " + (error != null ? error.getReason() : "Unknown error") + + ", translated: " + + (error != null ? error.getTranslatedError() : "None")); mainHandler.post(() -> { // Hide loading state setSubmitting(false); // Show error message - String errorMessage = error != null && error.getTranslatedError() != null ? - error.getTranslatedError() : getContext().getString(R.string.post_error); + String errorMessage = error != null && error.getTranslatedError() != null + ? error.getTranslatedError() + : getContext().getString(R.string.post_error); showError(errorMessage); @@ -567,7 +575,8 @@ public boolean onSuccess(FeedPost createdPost) { setSubmitting(false); // Show success message and reset form - Toast.makeText(getContext(), R.string.post_success, Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), R.string.post_success, Toast.LENGTH_SHORT) + .show(); resetForm(); // Notify listener @@ -619,8 +628,9 @@ private void createPostFromInput(String content, FCCallback feedPostCa sdk.uploadImages(getContext(), selectedMediaUris, new FCCallback>() { @Override public boolean onFailure(APIError error) { - Log.e("FeedPostCreateView", "Image upload failed: " + - (error != null ? error.getReason() : "Unknown error")); + Log.e( + "FeedPostCreateView", + "Image upload failed: " + (error != null ? error.getReason() : "Unknown error")); mainHandler.post(() -> feedPostCallback.onFailure(error)); return CONSUME; } @@ -660,7 +670,7 @@ private CreateFeedPostParams convertToParams(FeedPost post) { params.setLinks(post.getLinks()); params.setFromUserId(post.getFromUserId()); params.setFromUserDisplayName(post.getFromUserDisplayName()); - + // Use tags from TagSupplier if available, otherwise use post's tags if (sdk != null && sdk.getTagSupplier() != null) { List supplierTags = sdk.getTagSupplier().getTags(sdk.getCurrentUser()); @@ -672,7 +682,7 @@ private CreateFeedPostParams convertToParams(FeedPost post) { } else { params.setTags(post.getTags()); } - + params.setMeta(post.getMeta()); return params; @@ -906,4 +916,4 @@ public void requestInputFocus() { postContentEditText.requestFocus(); } } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/FeedPostType.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/FeedPostType.java index 1be504b..4a37d90 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/FeedPostType.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/FeedPostType.java @@ -8,19 +8,19 @@ public enum FeedPostType { * Simple text-only post */ TEXT_ONLY, - + /** * Post with a single image and optional text */ SINGLE_IMAGE, - + /** * Post with multiple images/media and optional text */ MULTI_IMAGE, - + /** * Task or activity post with custom action buttons */ TASK -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/FeedPostsAdapter.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/FeedPostsAdapter.java index 3f2680e..fd6e21f 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/FeedPostsAdapter.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/FeedPostsAdapter.java @@ -18,22 +18,18 @@ import android.widget.LinearLayout; import android.widget.PopupMenu; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.core.view.AccessibilityDelegateCompat; import androidx.core.view.ViewCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.recyclerview.widget.RecyclerView; - import com.bumptech.glide.Glide; -import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; import com.fastcomments.model.FeedPost; import com.fastcomments.model.FeedPostLink; import com.fastcomments.model.FeedPostMediaItem; import com.fastcomments.model.FeedPostMediaItemAsset; - import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; @@ -122,7 +118,7 @@ public interface OnFeedPostInteractionListener { void onMediaClick(FeedPostMediaItem mediaItem); void onDeletePost(FeedPost post); - + void onUserClick(FeedPost post, UserInfo userInfo, UserClickSource source); } @@ -130,7 +126,11 @@ public interface OnScrollToTopRequestedListener { void onScrollToTopRequested(); } - public FeedPostsAdapter(Context context, List feedPosts, FastCommentsFeedSDK sdk, OnFeedPostInteractionListener listener) { + public FeedPostsAdapter( + Context context, + List feedPosts, + FastCommentsFeedSDK sdk, + OnFeedPostInteractionListener listener) { this.context = context; this.feedPosts = feedPosts; this.listener = listener; @@ -320,7 +320,6 @@ public void updatePosts(List newPosts, boolean scrollToTop) { Log.d("FeedPostsAdapter", "Updated posts list with " + updatedPosts.size() + " posts"); } - public void addPosts(List morePosts) { int startPosition = this.feedPosts.size(); this.feedPosts.addAll(morePosts); @@ -428,8 +427,7 @@ private FeedPostMediaItemAsset selectBestImageSize(List * @return The calculated height based on aspect ratio, or default height if dimensions are missing */ private int calculateImageHeight(FeedPostMediaItemAsset asset, int displayWidth) { - if (asset == null || asset.getW() == null || asset.getH() == null || - asset.getW() <= 0 || asset.getH() <= 0) { + if (asset == null || asset.getW() == null || asset.getH() == null || asset.getW() <= 0 || asset.getH() <= 0) { return getDefaultImageHeight(); // Fall back to default height } @@ -479,12 +477,10 @@ private void ensureFollowButtonResources() { return; } followActionColor = actionColor; - followFollowingTextColor = ContextCompat.getColor(context, - R.color.fastcomments_feed_follow_following_text); + followFollowingTextColor = ContextCompat.getColor(context, R.color.fastcomments_feed_follow_following_text); followResourcesResolved = true; } - class FeedPostViewHolder extends RecyclerView.ViewHolder { private final FeedPostType postType; @@ -579,8 +575,8 @@ class FeedPostViewHolder extends RecyclerView.ViewHolder { if (followButton != null) { ViewCompat.setAccessibilityDelegate(followButton, new AccessibilityDelegateCompat() { @Override - public void onInitializeAccessibilityNodeInfo(@NonNull View host, - @NonNull AccessibilityNodeInfoCompat info) { + public void onInitializeAccessibilityNodeInfo( + @NonNull View host, @NonNull AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(host, info); info.setClassName(Button.class.getName()); } @@ -590,16 +586,16 @@ public void onInitializeAccessibilityNodeInfo(@NonNull View host, // Apply theme colors applyTheme(); } - + /** * Apply theme colors to buttons and UI elements */ private void applyTheme() { FastCommentsTheme theme = sdk != null ? sdk.getTheme() : null; - + // Apply action button colors to buttons int actionButtonColor = ThemeColorResolver.getActionButtonColor(context, theme); - + if (commentButton != null) { commentButton.setTextColor(actionButtonColor); } @@ -613,7 +609,7 @@ private void applyTheme() { postMenuButton.setImageTintList(ColorStateList.valueOf(actionButtonColor)); } } - + /** * Apply theme colors to a dynamically created button */ @@ -777,7 +773,8 @@ void bind(FeedPost post, int position) { // If there's an avatar URL, load it - otherwise show a default avatar if (avatarImageView != null) { - if (post.getFromUserAvatar() != null && !post.getFromUserAvatar().isEmpty()) { + if (post.getFromUserAvatar() != null + && !post.getFromUserAvatar().isEmpty()) { avatarImageView.setVisibility(View.VISIBLE); AvatarFetcher.fetchTransformInto(context, post.getFromUserAvatar(), avatarImageView); } else { @@ -786,7 +783,7 @@ void bind(FeedPost post, int position) { AvatarFetcher.fetchTransformInto(context, R.drawable.default_avatar, avatarImageView); } } - + // Set up user click listeners userNameTextView.setOnClickListener(v -> { if (listener != null) { @@ -794,7 +791,7 @@ void bind(FeedPost post, int position) { listener.onUserClick(post, userInfo, UserClickSource.NAME); } }); - + if (avatarImageView != null) { avatarImageView.setOnClickListener(v -> { if (listener != null) { @@ -821,7 +818,6 @@ void bind(FeedPost post, int position) { contentTextView.setVisibility(View.GONE); } - // Handle like count and comment count display updateLikeAndCommentCounts(post); @@ -858,7 +854,8 @@ void bind(FeedPost post, int position) { if (postMenuButton != null) { // Determine if this is the current user's post boolean isCurrentUserPost = false; - String currentUserId = sdk.getCurrentUser() != null ? sdk.getCurrentUser().getId() : null; + String currentUserId = + sdk.getCurrentUser() != null ? sdk.getCurrentUser().getId() : null; String postUserId = post.getFromUserId(); if (currentUserId != null && postUserId != null && currentUserId.equals(postUserId)) { @@ -922,10 +919,10 @@ private void bindSingleImagePost(FeedPost post) { .into(mediaImageView); // Determine if it's a video - boolean isVideo = mediaItem.getLinkUrl() != null && - (mediaItem.getLinkUrl().contains("youtube") || - mediaItem.getLinkUrl().contains("vimeo") || - mediaItem.getLinkUrl().contains(".mp4")); + boolean isVideo = mediaItem.getLinkUrl() != null + && (mediaItem.getLinkUrl().contains("youtube") + || mediaItem.getLinkUrl().contains("vimeo") + || mediaItem.getLinkUrl().contains(".mp4")); playButton.setVisibility(isVideo ? View.VISIBLE : View.GONE); @@ -951,12 +948,10 @@ private void bindSingleImagePost(FeedPost post) { } } - private void bindMultiImagePost(FeedPost post) { if (post.getMedia() != null && !post.getMedia().isEmpty()) { mediaItems = post.getMedia(); - // For posts with 1 image, use grid layout if (mediaItems.size() <= 1) { imageGridLayout.setVisibility(View.VISIBLE); @@ -981,18 +976,19 @@ private void bindMultiImagePost(FeedPost post) { // Pre-size the ViewPager to prevent layout shifts int screenWidth = context.getResources().getDisplayMetrics().widthPixels; int viewPagerHeight = getDefaultImageHeight(); // Use default height for consistency - + // Try to calculate a better height from the first image if (!mediaItems.isEmpty()) { FeedPostMediaItem firstItem = mediaItems.get(0); - if (firstItem.getSizes() != null && !firstItem.getSizes().isEmpty()) { + if (firstItem.getSizes() != null + && !firstItem.getSizes().isEmpty()) { FeedPostMediaItemAsset bestAsset = selectBestImageSize(firstItem.getSizes()); if (bestAsset != null) { viewPagerHeight = calculateImageHeight(bestAsset, screenWidth); } } } - + // Set the ViewPager height imageViewPager.getLayoutParams().height = viewPagerHeight; imageViewPager.requestLayout(); @@ -1040,7 +1036,7 @@ private void setupImageGrid(FeedPost post, List mediaItems) { if (count == 1) { // Single image takes full size FeedPostMediaItem mediaItem = mediaItems.get(0); - + // Pre-calculate and set grid height to prevent layout shifts if (mediaItem.getSizes() != null && !mediaItem.getSizes().isEmpty()) { FeedPostMediaItemAsset bestAsset = selectBestImageSize(mediaItem.getSizes()); @@ -1050,11 +1046,10 @@ private void setupImageGrid(FeedPost post, List mediaItems) { imageGridLayout.requestLayout(); } } - + ImageView imageView = createImageView(post, mediaItem, 0); - GridLayout.LayoutParams params = new GridLayout.LayoutParams( - GridLayout.spec(0, 2, 1f), - GridLayout.spec(0, 2, 1f)); + GridLayout.LayoutParams params = + new GridLayout.LayoutParams(GridLayout.spec(0, 2, 1f), GridLayout.spec(0, 2, 1f)); params.width = GridLayout.LayoutParams.MATCH_PARENT; params.height = GridLayout.LayoutParams.MATCH_PARENT; imageGridLayout.addView(imageView, params); @@ -1100,8 +1095,7 @@ private ImageView createImageView(FeedPost post, FeedPostMediaItem mediaItem, in // Set up layout parameters for wrap_content height ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT); + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); imageView.setLayoutParams(layoutParams); int calculatedHeight = calculateImageHeight(bestSizeAsset, parentWidth); @@ -1120,16 +1114,14 @@ private ImageView createImageView(FeedPost post, FeedPostMediaItem mediaItem, in } else { // Set fallback if no valid asset ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT); + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); imageView.setLayoutParams(layoutParams); imageView.setImageResource(R.drawable.image_placeholder); } } else { // Set fallback if no sizes ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT); + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); imageView.setLayoutParams(layoutParams); imageView.setImageResource(R.drawable.image_placeholder); } @@ -1218,9 +1210,10 @@ private void bindTaskPost(FeedPost post) { FeedPostLink primaryLink = post.getLinks().get(0); // Use the first link as primary // Check if we have a title or description to display - boolean hasTitleOrDescription = - (primaryLink.getTitle() != null && !primaryLink.getTitle().isEmpty()) || - (primaryLink.getDescription() != null && !primaryLink.getDescription().isEmpty()); + boolean hasTitleOrDescription = (primaryLink.getTitle() != null + && !primaryLink.getTitle().isEmpty()) + || (primaryLink.getDescription() != null + && !primaryLink.getDescription().isEmpty()); if (hasTitleOrDescription) { // layout with title/description + button on right @@ -1228,7 +1221,8 @@ private void bindTaskPost(FeedPost post) { taskButtonsContainer.setVisibility(View.GONE); // Hide regular buttons container // Display link title if available - if (primaryLink.getTitle() != null && !primaryLink.getTitle().isEmpty()) { + if (primaryLink.getTitle() != null + && !primaryLink.getTitle().isEmpty()) { linkTitleTextView.setText(primaryLink.getTitle()); linkTitleTextView.setVisibility(View.VISIBLE); } else { @@ -1236,7 +1230,8 @@ private void bindTaskPost(FeedPost post) { } // Display link description if available - if (primaryLink.getDescription() != null && !primaryLink.getDescription().isEmpty()) { + if (primaryLink.getDescription() != null + && !primaryLink.getDescription().isEmpty()) { linkDescriptionTextView.setText(primaryLink.getDescription()); linkDescriptionTextView.setVisibility(View.VISIBLE); } else { @@ -1251,8 +1246,7 @@ private void bindTaskPost(FeedPost post) { // Vertical button on right side LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT); + LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); buttonParams.setMargins(0, i > 0 ? 8 : 0, 0, 0); // Add top margin for subsequent buttons actionButton.setLayoutParams(buttonParams); @@ -1260,7 +1254,7 @@ private void bindTaskPost(FeedPost post) { // Apply modern styling with our custom background actionButton.setBackgroundResource(R.drawable.task_button_background); actionButton.setPadding(16, 12, 16, 12); // Increased vertical padding for taller buttons - + // Apply theme colors applyThemeToButton(actionButton); @@ -1269,9 +1263,9 @@ private void bindTaskPost(FeedPost post) { if (buttonText == null || buttonText.isEmpty()) { buttonText = link.getTitle(); // Try title next if (buttonText == null || buttonText.isEmpty()) { - buttonText = link.getUrl() != null ? - context.getString(R.string.view_details) : - context.getString(R.string.learn_more); + buttonText = link.getUrl() != null + ? context.getString(R.string.view_details) + : context.getString(R.string.learn_more); } } @@ -1311,8 +1305,7 @@ private void bindTaskPost(FeedPost post) { // Create a horizontal layout for the buttons LinearLayout buttonRow = new LinearLayout(context); buttonRow.setLayoutParams(new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT)); + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); buttonRow.setOrientation(LinearLayout.HORIZONTAL); // Set equal distribution of buttons @@ -1349,9 +1342,9 @@ private void bindTaskPost(FeedPost post) { // Set button text String buttonText = link.getText(); // Prefer the display text if available if (buttonText == null || buttonText.isEmpty()) { - buttonText = link.getUrl() != null ? - context.getString(R.string.view_details) : - context.getString(R.string.learn_more); + buttonText = link.getUrl() != null + ? context.getString(R.string.view_details) + : context.getString(R.string.learn_more); } actionButton.setText(buttonText); @@ -1383,8 +1376,7 @@ private void bindTaskPost(FeedPost post) { // Full width button LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT); + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); // Add margin between buttons if (i > 0) { @@ -1403,9 +1395,9 @@ private void bindTaskPost(FeedPost post) { // Set button text String buttonText = link.getText(); // Prefer the display text if available if (buttonText == null || buttonText.isEmpty()) { - buttonText = link.getUrl() != null ? - context.getString(R.string.view_details) : - context.getString(R.string.learn_more); + buttonText = link.getUrl() != null + ? context.getString(R.string.view_details) + : context.getString(R.string.learn_more); } actionButton.setText(buttonText); @@ -1434,7 +1426,6 @@ private void bindTaskPost(FeedPost post) { } } - /** * Get the best quality image URL for full-screen viewing * For full-screen viewing, we want the highest quality image available @@ -1443,7 +1434,9 @@ private void bindTaskPost(FeedPost post) { * @return The URL of the highest quality image, or null if not available */ private String getBestQualityImageUrl(FeedPostMediaItem mediaItem) { - if (mediaItem == null || mediaItem.getSizes() == null || mediaItem.getSizes().isEmpty()) { + if (mediaItem == null + || mediaItem.getSizes() == null + || mediaItem.getSizes().isEmpty()) { return null; } @@ -1499,41 +1492,40 @@ private void showPostMenu(FeedPost post) { private void updateLikeAndCommentCounts(FeedPost post) { final int likeCount = sdk.getPostLikeCount(post.getId()); final Integer commentCount = post.getCommentCount(); - + if (likeCount > 0 || (commentCount != null && commentCount > 0)) { likeCountTextView.setVisibility(View.VISIBLE); - + StringBuilder displayText = new StringBuilder(); - + // Add likes text if there are likes if (likeCount > 0) { // Show heart icon only when there are likes likeCountTextView.setCompoundDrawablesWithIntrinsicBounds( - context.getResources().getDrawable(R.drawable.heart_filled_small, null), - null, null, null); - - String likesText = likeCount == 1 ? - context.getString(R.string.like_count_singular, likeCount) : - context.getString(R.string.like_count_plural, likeCount); + context.getResources().getDrawable(R.drawable.heart_filled_small, null), null, null, null); + + String likesText = likeCount == 1 + ? context.getString(R.string.like_count_singular, likeCount) + : context.getString(R.string.like_count_plural, likeCount); displayText.append(likesText); } else { // No likes, so don't show heart icon likeCountTextView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); } - + // Add comment count if there are comments if (commentCount != null && commentCount > 0) { // Add separator if needed if (likeCount > 0) { displayText.append(" · "); } - - String commentsText = commentCount == 1 ? - context.getString(R.string.comment_count_singular, commentCount) : - context.getString(R.string.comment_count_plural, commentCount); + + String commentsText = commentCount == 1 + ? context.getString(R.string.comment_count_singular, commentCount) + : context.getString(R.string.comment_count_plural, commentCount); displayText.append(commentsText); } - + // Set the text likeCountTextView.setText(displayText.toString()); } else { @@ -1599,21 +1591,19 @@ private String formatTimestamp(OffsetDateTime date) { if (useAbsoluteDates) { // Use system's locale-aware date formatting - Locale currentLocale = context.getResources().getConfiguration().getLocales().get(0); - DateTimeFormatter formatter = DateTimeFormatter - .ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT) + Locale currentLocale = + context.getResources().getConfiguration().getLocales().get(0); + DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime( + FormatStyle.MEDIUM, FormatStyle.SHORT) .withLocale(currentLocale); return date.format(formatter); } else { // Format as relative date: 2 minutes ago, 1 hour ago, etc. CharSequence relativeTime = DateUtils.getRelativeTimeSpanString( - date.toInstant().toEpochMilli(), - System.currentTimeMillis(), - DateUtils.MINUTE_IN_MILLIS - ); + date.toInstant().toEpochMilli(), System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS); return relativeTime.toString(); } } } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/FollowStateProvider.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/FollowStateProvider.java index 4929ff0..960d7d5 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/FollowStateProvider.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/FollowStateProvider.java @@ -63,9 +63,7 @@ public interface FollowStateProvider { * @param resultCallback invoked with the actual state after the change */ void onFollowStateChangeRequested( - @NonNull UserInfo user, - boolean desiredFollowing, - @NonNull FollowStateCallback resultCallback); + @NonNull UserInfo user, boolean desiredFollowing, @NonNull FollowStateCallback resultCallback); /** * Result callback used to confirm a follow-state change (or revert it). diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/FullImageDialog.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/FullImageDialog.java index 5cc5baa..d33458b 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/FullImageDialog.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/FullImageDialog.java @@ -9,14 +9,11 @@ import android.view.WindowManager; import android.widget.ImageButton; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.viewpager2.widget.ViewPager2; - import com.bumptech.glide.Glide; import com.fastcomments.model.FeedPostMediaItem; import com.github.chrisbanes.photoview.PhotoView; - import java.util.List; /** @@ -54,9 +51,9 @@ public FullImageDialog(@NonNull Context context, List mediaIt @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - + requestWindowFeature(Window.FEATURE_NO_TITLE); - + if (isGalleryMode) { setContentView(R.layout.dialog_full_image_gallery); setupGalleryMode(); @@ -64,25 +61,24 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.dialog_full_image); setupSingleImageMode(); } - + // Make dialog fill the screen Window window = getWindow(); if (window != null) { - window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.MATCH_PARENT); + window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT); window.setBackgroundDrawable(new ColorDrawable(Color.BLACK)); - + // Set window flags for immersive mode window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - window.getDecorView().setSystemUiVisibility( - android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | android.view.View.SYSTEM_UI_FLAG_FULLSCREEN - | android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + window.getDecorView() + .setSystemUiVisibility(android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | android.view.View.SYSTEM_UI_FLAG_FULLSCREEN + | android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); } - + // Set up close button ImageButton closeButton = findViewById(R.id.closeButton); closeButton.setOnClickListener(v -> dismiss()); @@ -91,7 +87,7 @@ protected void onCreate(Bundle savedInstanceState) { private void setupSingleImageMode() { // Set up photo view with the image PhotoView photoView = findViewById(R.id.photoView); - + // Load image with Glide if (imageUrl != null && !imageUrl.isEmpty()) { Glide.with(getContext()) @@ -142,4 +138,4 @@ private void updateImageCounter(int position, TextView imageCounter) { imageCounter.setText(counterText); } } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/GalleryImageAdapter.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/GalleryImageAdapter.java index c7bffef..7c18aa9 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/GalleryImageAdapter.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/GalleryImageAdapter.java @@ -4,15 +4,12 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; - import com.bumptech.glide.Glide; import com.fastcomments.model.FeedPostMediaItem; import com.fastcomments.model.FeedPostMediaItemAsset; import com.github.chrisbanes.photoview.PhotoView; - import java.util.List; /** @@ -54,7 +51,9 @@ public int getItemCount() { * @return The URL of the highest quality image, or null if not available */ private String getBestQualityImageUrl(FeedPostMediaItem mediaItem) { - if (mediaItem == null || mediaItem.getSizes() == null || mediaItem.getSizes().isEmpty()) { + if (mediaItem == null + || mediaItem.getSizes() == null + || mediaItem.getSizes().isEmpty()) { return null; } @@ -85,7 +84,7 @@ class GalleryImageViewHolder extends RecyclerView.ViewHolder { void bind(FeedPostMediaItem mediaItem) { String imageUrl = getBestQualityImageUrl(mediaItem); - + if (imageUrl != null && !imageUrl.isEmpty()) { Glide.with(context) .load(imageUrl) @@ -99,4 +98,4 @@ void bind(FeedPostMediaItem mediaItem) { } } } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/GetChildrenRequest.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/GetChildrenRequest.java index 222d791..603e388 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/GetChildrenRequest.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/GetChildrenRequest.java @@ -16,7 +16,7 @@ public class GetChildrenRequest { public GetChildrenRequest(String parentId, Button toggleButton) { this(parentId, toggleButton, null, null, false); } - + public GetChildrenRequest(String parentId, Button toggleButton, Integer skip, Integer limit, boolean isLoadMore) { this.parentId = parentId; this.toggleButton = toggleButton; @@ -32,16 +32,16 @@ public String getParentId() { public Button getToggleButton() { return toggleButton; } - + public Integer getSkip() { return skip; } - + public Integer getLimit() { return limit; } - + public boolean isLoadMore() { return isLoadMore; } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/HtmlLinkHandler.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/HtmlLinkHandler.java index 3fefdd3..dabe520 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/HtmlLinkHandler.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/HtmlLinkHandler.java @@ -11,11 +11,7 @@ import android.text.style.URLSpan; import android.view.View; import android.widget.TextView; - import androidx.annotation.NonNull; - -import org.xml.sax.XMLReader; - import java.util.Locale; /** @@ -31,13 +27,13 @@ public class HtmlLinkHandler { * @return Spanned content with clickable links */ public static Spanned parseHtml(Context context, String html, TextView textView) { - Spanned spanned = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY, - new CustomImageGetter(context, textView), null); - + Spanned spanned = + Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY, new CustomImageGetter(context, textView), null); + // Process the spanned text to make links clickable return makeLinkClickable(context, spanned); } - + /** * Makes links in text clickable and opens them in external browser * @param context Context for opening links @@ -46,7 +42,7 @@ public static Spanned parseHtml(Context context, String html, TextView textView) */ private static Spanned makeLinkClickable(final Context context, Spanned spanned) { Editable editableText = Editable.Factory.getInstance().newEditable(spanned); - + // Find all URLSpans in the text URLSpan[] urlSpans = editableText.getSpans(0, editableText.length(), URLSpan.class); for (final URLSpan span : urlSpans) { @@ -72,8 +68,8 @@ public void onClick(@NonNull View widget) { String url = span.getURL(); // Ensure URL has proper scheme - if (!url.toLowerCase(Locale.US).startsWith("http://") && - !url.toLowerCase(Locale.US).startsWith("https://")) { + if (!url.toLowerCase(Locale.US).startsWith("http://") + && !url.toLowerCase(Locale.US).startsWith("https://")) { url = "https://" + url; } @@ -118,4 +114,4 @@ static String findWrappedImageUrl(Spanned text, URLSpan span) { } return imageSpans[0].getSource(); } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/LiveChatView.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/LiveChatView.java index 04dc8c3..f56dd6b 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/LiveChatView.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/LiveChatView.java @@ -8,37 +8,28 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; -import android.widget.RelativeLayout; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; import android.widget.Button; - -import com.fastcomments.core.CommentWidgetConfig; -import com.fastcomments.core.VoteStyle; -import com.fastcomments.model.APIEmptyResponse; -import com.fastcomments.model.BlockSuccess; -import com.fastcomments.model.UnblockSuccess; -import com.fastcomments.model.FComment; -import com.fastcomments.model.SetCommentTextResult; -import com.fastcomments.model.PublicComment; -import com.fastcomments.model.SortDirections; -import com.google.android.material.floatingactionbutton.FloatingActionButton; - import android.widget.FrameLayout; import android.widget.ProgressBar; +import android.widget.RelativeLayout; import android.widget.TextView; - import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - +import com.fastcomments.core.CommentWidgetConfig; +import com.fastcomments.core.VoteStyle; +import com.fastcomments.model.APIEmptyResponse; import com.fastcomments.model.APIError; +import com.fastcomments.model.BlockSuccess; import com.fastcomments.model.GetCommentsResponseWithPresencePublicComment; -import com.fastcomments.model.VoteResponse; +import com.fastcomments.model.PublicComment; +import com.fastcomments.model.SetCommentTextResult; +import com.fastcomments.model.SortDirections; +import com.fastcomments.model.UnblockSuccess; import com.fastcomments.model.VoteDeleteResponse; - +import com.fastcomments.model.VoteResponse; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -83,17 +74,17 @@ public interface CommentActionListener { * @param comment The posted comment */ void onCommentPosted(com.fastcomments.model.PublicComment comment); - + /** * Called when a comment is deleted * @param commentId The ID of the deleted comment */ void onCommentDeleted(String commentId); } - + private CommentActionListener commentActionListener; private OnUserClickListener userClickListener; - + /** * Set a listener to be notified of comment actions * @param listener The listener to set @@ -101,7 +92,7 @@ public interface CommentActionListener { public void setCommentActionListener(CommentActionListener listener) { this.commentActionListener = listener; } - + /** * Set a listener to be notified when a user (name or avatar) is clicked * @param listener The listener to set @@ -192,7 +183,7 @@ public void handleOnBackPressed() { // Check if we have text in either input method boolean hasText = false; RenderableComment parentComment = null; - + if (bottomCommentInput != null) { hasText = !bottomCommentInput.isTextEmpty(); parentComment = bottomCommentInput.getParentComment(); @@ -200,11 +191,11 @@ public void handleOnBackPressed() { hasText = !commentForm.isTextEmpty(); parentComment = commentForm.getParentComment(); } - + if (hasText) { // Show confirmation dialog - different message for reply vs new comment String title, message; - + if (parentComment != null) { title = getContext().getString(R.string.cancel_reply_title); message = getContext().getString(R.string.cancel_reply_confirm); @@ -212,21 +203,21 @@ public void handleOnBackPressed() { title = getContext().getString(R.string.cancel_comment_title); message = getContext().getString(R.string.cancel_comment_confirm); } - + new android.app.AlertDialog.Builder(getContext()) - .setTitle(title) - .setMessage(message) - .setPositiveButton(android.R.string.yes, (dialog, which) -> { - // Proceed with cancellation - if (bottomCommentInput != null) { - bottomCommentInput.clearText(); - bottomCommentInput.clearReplyState(); - } else if (commentForm != null) { - commentForm.resetReplyState(); - } - }) - .setNegativeButton(android.R.string.no, null) - .show(); + .setTitle(title) + .setMessage(message) + .setPositiveButton(android.R.string.yes, (dialog, which) -> { + // Proceed with cancellation + if (bottomCommentInput != null) { + bottomCommentInput.clearText(); + bottomCommentInput.clearReplyState(); + } else if (commentForm != null) { + commentForm.resetReplyState(); + } + }) + .setNegativeButton(android.R.string.no, null) + .show(); } else { // Text is empty, check if we're in a reply state boolean wasInReplyState = false; @@ -237,7 +228,7 @@ public void handleOnBackPressed() { wasInReplyState = commentForm.getParentComment() != null; commentForm.resetReplyState(); } - + // If we weren't in a reply state, allow normal back navigation if (!wasInReplyState) { backPressedCallback.setEnabled(false); @@ -252,7 +243,9 @@ public void handleOnBackPressed() { activity.getOnBackPressedDispatcher().addCallback(backPressedCallback); } else if (context instanceof Activity) { // Log a warning that back press won't be handled - Log.w("LiveChatView", "Context is an Activity but not AppCompatActivity. Back button handling not supported."); + Log.w( + "LiveChatView", + "Context is an Activity but not AppCompatActivity. Back button handling not supported."); } // Create a LinearLayoutManager for bottom-up layout @@ -276,10 +269,10 @@ public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newStat } } }); - + // Store the SDK reference this.sdk = sdk; - + // If SDK is not provided, postpone adapter initialization // It will be initialized when setSDK is called if (sdk != null) { @@ -293,10 +286,10 @@ public static void setupLiveChatConfig(CommentWidgetConfig config) { config.maxReplyDepth = 0; config.disableVoting = true; } - + /** * Set the SDK instance to use with this view (for use when inflating from XML) - * + * * @param sdk The FastCommentsSDK instance */ public void setSDK(FastCommentsSDK sdk) { @@ -304,7 +297,7 @@ public void setSDK(FastCommentsSDK sdk) { if (this.sdk == sdk) { return; } - + this.sdk = sdk; if (sdk != null) { setupLiveChatConfig(sdk.getConfig()); @@ -312,7 +305,7 @@ public void setSDK(FastCommentsSDK sdk) { setupDemoBanner(); } } - + /** * Initialize the adapter and other SDK-dependent functionality */ @@ -367,8 +360,8 @@ public void onItemRangeInserted(int positionStart, int itemCount) { sdk.setPresenceUpdateListener(subscriberCount -> { if (userCountText != null) { userCountText.setVisibility(View.VISIBLE); - userCountText.setText(getResources().getQuantityString( - R.plurals.live_chat_users_online, subscriberCount, subscriberCount)); + userCountText.setText(getResources() + .getQuantityString(R.plurals.live_chat_users_online, subscriberCount, subscriberCount)); } }); @@ -383,11 +376,9 @@ public void onItemRangeInserted(int positionStart, int itemCount) { dot.setShape(android.graphics.drawable.GradientDrawable.OVAL); dot.setColor(dotColor); connectionDot.setBackground(dot); - connectionStatusText.setText(isConnected - ? R.string.live_chat_live - : R.string.live_chat_connecting); + connectionStatusText.setText(isConnected ? R.string.live_chat_live : R.string.live_chat_connecting); }); - + // Set up infinite scrolling (in reverse) for chat mode recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override @@ -408,11 +399,11 @@ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { // Setup bottom comment input for live chat bottomCommentInput.setCurrentUser(sdk.getCurrentUser()); bottomCommentInput.setSDK(sdk); - + bottomCommentInput.setOnCommentSubmitListener((commentText, parentId) -> { postComment(commentText, parentId); }); - + bottomCommentInput.setOnReplyStateChangeListener((isReplying, parentComment) -> { // Handle reply state changes if needed }); @@ -428,14 +419,14 @@ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (parentComment != null && !commentForm.isTextEmpty()) { // Show confirmation dialog new android.app.AlertDialog.Builder(getContext()) - .setTitle(R.string.cancel_reply_title) - .setMessage(R.string.cancel_reply_confirm) - .setPositiveButton(android.R.string.yes, (dialog, which) -> { - // Proceed with cancellation - commentForm.resetReplyState(); - }) - .setNegativeButton(android.R.string.no, null) - .show(); + .setTitle(R.string.cancel_reply_title) + .setMessage(R.string.cancel_reply_confirm) + .setPositiveButton(android.R.string.yes, (dialog, which) -> { + // Proceed with cancellation + commentForm.resetReplyState(); + }) + .setNegativeButton(android.R.string.no, null) + .show(); } else { // Just reset the form if there's no text commentForm.resetReplyState(); @@ -462,7 +453,7 @@ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { } else if (commentForm != null) { commentForm.setReplyingTo(commentToReplyTo); } - + // Scroll to the comment being replied to int pos = adapter.getPositionForComment(commentToReplyTo); if (pos >= 0) { @@ -472,20 +463,20 @@ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { // Set up all the vote handlers similar to FastCommentsView setupVoteHandlers(); - + // Set up comment menu listener setupCommentMenuListener(); - + // Handle user click events adapter.setUserClickListener(userClickListener); - + // The SDK supports loading child comments, but we don't need to show // them in the chat view as they appear as top-level messages adapter.setGetChildrenProducer((request, sendResults) -> { // Simply return an empty list since we don't show threaded replies sendResults.call(new ArrayList<>()); }); - + // Set SDK on the appropriate input method for mentions functionality if (commentForm != null) { commentForm.setSDK(sdk); @@ -534,8 +525,8 @@ private void setupVoteHandlers() { // User hasn't upvoted, so add the vote commentToVote.getComment().setIsVotedUp(true); // Also remove downvote if exists - if (commentToVote.getComment().getIsVotedDown() != null && - commentToVote.getComment().getIsVotedDown()) { + if (commentToVote.getComment().getIsVotedDown() != null + && commentToVote.getComment().getIsVotedDown()) { commentToVote.getComment().setIsVotedDown(false); // Update downvote count Integer votesDown = commentToVote.getComment().getVotesDown(); @@ -584,10 +575,8 @@ public boolean onSuccess(APIError error) { // Show error message getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - R.string.error_voting, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), R.string.error_voting, android.widget.Toast.LENGTH_SHORT) + .show(); // Update the UI adapter.notifyDataSetChanged(); @@ -598,45 +587,66 @@ public boolean onSuccess(APIError error) { }; // Check if user is logged in or if anonymous votes are allowed - boolean userIsLoggedIn = sdk.getCurrentUser() != null && - sdk.getCurrentUser().getAuthorized() != null && - sdk.getCurrentUser().getAuthorized(); - boolean allowAnonVotes = sdk.getConfig() != null && - Boolean.TRUE.equals(sdk.getConfig().allowAnonVotes); + boolean userIsLoggedIn = sdk.getCurrentUser() != null + && sdk.getCurrentUser().getAuthorized() != null + && sdk.getCurrentUser().getAuthorized(); + boolean allowAnonVotes = sdk.getConfig() != null && Boolean.TRUE.equals(sdk.getConfig().allowAnonVotes); // Special handling if the user is not logged in if (!userIsLoggedIn) { if (!allowAnonVotes) { final boolean needToDeleteFinal = needToDelete; // Show dialog to collect user info for votes that need verification - UserLoginDialog.show(getContext(), sdk.getConfig(), "vote", new UserLoginDialog.OnUserCredentialsListener() { - @Override - public void onUserCredentialsEntered(String username, String email) { - // Proceed with vote using provided credentials - processUpvote(commentToVote, needToDeleteFinal, originalIsVotedUp, originalVoteId, - upvoteErrorHandler, username, email, finalToastMessage); - } + UserLoginDialog.show( + getContext(), sdk.getConfig(), "vote", new UserLoginDialog.OnUserCredentialsListener() { + @Override + public void onUserCredentialsEntered(String username, String email) { + // Proceed with vote using provided credentials + processUpvote( + commentToVote, + needToDeleteFinal, + originalIsVotedUp, + originalVoteId, + upvoteErrorHandler, + username, + email, + finalToastMessage); + } - @Override - public void onCancel() { - // User canceled, revert UI state - commentToVote.getComment().setIsVotedUp(originalIsVotedUp); - commentToVote.getComment().setIsVotedDown(originalIsVotedDown); - commentToVote.getComment().setVotesUp(originalVotesUp); - commentToVote.getComment().setVotesDown(originalVotesDown); - adapter.notifyDataSetChanged(); - } - }); + @Override + public void onCancel() { + // User canceled, revert UI state + commentToVote.getComment().setIsVotedUp(originalIsVotedUp); + commentToVote.getComment().setIsVotedDown(originalIsVotedDown); + commentToVote.getComment().setVotesUp(originalVotesUp); + commentToVote.getComment().setVotesDown(originalVotesDown); + adapter.notifyDataSetChanged(); + } + }); } else { // Anonymous votes allowed, proceed directly without dialog final boolean needToDeleteFinal = needToDelete; - processUpvote(commentToVote, needToDeleteFinal, originalIsVotedUp, originalVoteId, - upvoteErrorHandler, null, null, finalToastMessage); + processUpvote( + commentToVote, + needToDeleteFinal, + originalIsVotedUp, + originalVoteId, + upvoteErrorHandler, + null, + null, + finalToastMessage); } } else { // User is logged in, proceed with vote - processUpvote(commentToVote, needToDelete, originalIsVotedUp, originalVoteId, - upvoteErrorHandler, null, null, finalToastMessage); + processUpvote( + commentToVote, + needToDelete, + originalIsVotedUp, + originalVoteId, + upvoteErrorHandler, + null, + null, + finalToastMessage); } }); @@ -646,11 +656,11 @@ public void onCancel() { // For brevity, just calling the appropriate method handleDownVote(commentToVote); }); - + // Set up heart click handler (using same logic as upvote) -// adapter.setHeartClickListener = adapter.setUpVoteClickListener; + // adapter.setHeartClickListener = adapter.setUpVoteClickListener; } - + private void handleDownVote(RenderableComment commentToVote) { // Get commenter name for the toast message String commenterName = commentToVote.getComment().getCommenterName(); @@ -683,8 +693,8 @@ private void handleDownVote(RenderableComment commentToVote) { // User hasn't downvoted, so add the vote commentToVote.getComment().setIsVotedDown(true); // Also remove upvote if exists - if (commentToVote.getComment().getIsVotedUp() != null && - commentToVote.getComment().getIsVotedUp()) { + if (commentToVote.getComment().getIsVotedUp() != null + && commentToVote.getComment().getIsVotedUp()) { commentToVote.getComment().setIsVotedUp(false); // Update upvote count Integer votesUp = commentToVote.getComment().getVotesUp(); @@ -729,10 +739,8 @@ public boolean onSuccess(APIError error) { // Show error message getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - R.string.error_voting, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), R.string.error_voting, android.widget.Toast.LENGTH_SHORT) + .show(); // Update the UI adapter.notifyDataSetChanged(); @@ -743,45 +751,66 @@ public boolean onSuccess(APIError error) { }; // Check if user is logged in or if anonymous votes are allowed - boolean userIsLoggedIn = sdk.getCurrentUser() != null && - sdk.getCurrentUser().getAuthorized() != null && - sdk.getCurrentUser().getAuthorized(); - boolean allowAnonVotes = sdk.getConfig() != null && - Boolean.TRUE.equals(sdk.getConfig().allowAnonVotes); + boolean userIsLoggedIn = sdk.getCurrentUser() != null + && sdk.getCurrentUser().getAuthorized() != null + && sdk.getCurrentUser().getAuthorized(); + boolean allowAnonVotes = sdk.getConfig() != null && Boolean.TRUE.equals(sdk.getConfig().allowAnonVotes); // Special handling if the user is not logged in if (!userIsLoggedIn) { if (!allowAnonVotes) { final boolean needToDeleteFinal = needToDelete; // Show dialog to collect user info for votes that need verification - UserLoginDialog.show(getContext(), sdk.getConfig(), "vote", new UserLoginDialog.OnUserCredentialsListener() { - @Override - public void onUserCredentialsEntered(String username, String email) { - // Proceed with vote using provided credentials - processDownvote(commentToVote, needToDeleteFinal, originalIsVotedDown, originalVoteId, - downvoteErrorHandler, username, email, finalToastMessage); - } + UserLoginDialog.show( + getContext(), sdk.getConfig(), "vote", new UserLoginDialog.OnUserCredentialsListener() { + @Override + public void onUserCredentialsEntered(String username, String email) { + // Proceed with vote using provided credentials + processDownvote( + commentToVote, + needToDeleteFinal, + originalIsVotedDown, + originalVoteId, + downvoteErrorHandler, + username, + email, + finalToastMessage); + } - @Override - public void onCancel() { - // User canceled, revert UI state - commentToVote.getComment().setIsVotedUp(originalIsVotedUp); - commentToVote.getComment().setIsVotedDown(originalIsVotedDown); - commentToVote.getComment().setVotesUp(originalVotesUp); - commentToVote.getComment().setVotesDown(originalVotesDown); - adapter.notifyDataSetChanged(); - } - }); + @Override + public void onCancel() { + // User canceled, revert UI state + commentToVote.getComment().setIsVotedUp(originalIsVotedUp); + commentToVote.getComment().setIsVotedDown(originalIsVotedDown); + commentToVote.getComment().setVotesUp(originalVotesUp); + commentToVote.getComment().setVotesDown(originalVotesDown); + adapter.notifyDataSetChanged(); + } + }); } else { // Anonymous votes allowed, proceed directly without dialog final boolean needToDeleteFinal = needToDelete; - processDownvote(commentToVote, needToDeleteFinal, originalIsVotedDown, originalVoteId, - downvoteErrorHandler, null, null, finalToastMessage); + processDownvote( + commentToVote, + needToDeleteFinal, + originalIsVotedDown, + originalVoteId, + downvoteErrorHandler, + null, + null, + finalToastMessage); } } else { // User is logged in, proceed with vote - processDownvote(commentToVote, needToDelete, originalIsVotedDown, originalVoteId, - downvoteErrorHandler, null, null, finalToastMessage); + processDownvote( + commentToVote, + needToDelete, + originalIsVotedDown, + originalVoteId, + downvoteErrorHandler, + null, + null, + finalToastMessage); } } @@ -793,53 +822,57 @@ public void onEdit(String commentId, String commentText) { // Show edit dialog CommentEditDialog dialog = new CommentEditDialog(getContext(), sdk); dialog.setOnSaveCallback(newText -> { - // Call API to edit the comment - sdk.editComment(commentId, newText, new FCCallback() { - @Override - public boolean onFailure(APIError error) { - // Show error message - getHandler().post(() -> { - String errorMessage; - if (error.getTranslatedError() != null && !error.getTranslatedError().isEmpty()) { - errorMessage = error.getTranslatedError(); - } else if (error.getReason() != null && !error.getReason().isEmpty()) { - errorMessage = error.getReason(); - } else { - errorMessage = getContext().getString(R.string.error_editing_comment); - } + // Call API to edit the comment + sdk.editComment(commentId, newText, new FCCallback() { + @Override + public boolean onFailure(APIError error) { + // Show error message + getHandler().post(() -> { + String errorMessage; + if (error.getTranslatedError() != null + && !error.getTranslatedError().isEmpty()) { + errorMessage = error.getTranslatedError(); + } else if (error.getReason() != null + && !error.getReason().isEmpty()) { + errorMessage = error.getReason(); + } else { + errorMessage = getContext().getString(R.string.error_editing_comment); + } - android.widget.Toast.makeText( - getContext(), - errorMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); - }); - return CONSUME; - } + android.widget.Toast.makeText( + getContext(), errorMessage, android.widget.Toast.LENGTH_SHORT) + .show(); + }); + return CONSUME; + } - @Override - public boolean onSuccess(SetCommentTextResult updatedComment) { - // Show success message - getHandler().post(() -> { - android.widget.Toast.makeText( - getContext(), - R.string.comment_edited_successfully, - android.widget.Toast.LENGTH_SHORT - ).show(); - - // Update the comment HTML in the existing comment object - RenderableComment renderableComment = sdk.commentsTree.commentsById.get(commentId); - if (renderableComment != null) { - renderableComment.getComment().setCommentHTML(updatedComment.getCommentHTML()); - adapter.notifyDataSetChanged(); + @Override + public boolean onSuccess(SetCommentTextResult updatedComment) { + // Show success message + getHandler().post(() -> { + android.widget.Toast.makeText( + getContext(), + R.string.comment_edited_successfully, + android.widget.Toast.LENGTH_SHORT) + .show(); + + // Update the comment HTML in the existing comment object + RenderableComment renderableComment = + sdk.commentsTree.commentsById.get(commentId); + if (renderableComment != null) { + renderableComment + .getComment() + .setCommentHTML(updatedComment.getCommentHTML()); + adapter.notifyDataSetChanged(); + } + }); + return CONSUME; } }); - return CONSUME; - } - }); - }).show(commentText); + }) + .show(commentText); } - + @Override public void onDelete(String commentId) { // Confirm before deleting @@ -854,19 +887,19 @@ public boolean onFailure(APIError error) { // Show error message getHandler().post(() -> { String errorMessage; - if (error.getTranslatedError() != null && !error.getTranslatedError().isEmpty()) { + if (error.getTranslatedError() != null + && !error.getTranslatedError().isEmpty()) { errorMessage = error.getTranslatedError(); - } else if (error.getReason() != null && !error.getReason().isEmpty()) { + } else if (error.getReason() != null + && !error.getReason().isEmpty()) { errorMessage = error.getReason(); } else { errorMessage = getContext().getString(R.string.error_deleting_comment); } android.widget.Toast.makeText( - getContext(), - errorMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), errorMessage, android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } @@ -876,11 +909,11 @@ public boolean onSuccess(APIEmptyResponse success) { // Show success message getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - R.string.comment_deleted_successfully, - android.widget.Toast.LENGTH_SHORT - ).show(); - + getContext(), + R.string.comment_deleted_successfully, + android.widget.Toast.LENGTH_SHORT) + .show(); + // Notify listener if set if (commentActionListener != null) { commentActionListener.onCommentDeleted(commentId); @@ -905,19 +938,18 @@ public boolean onFailure(APIError error) { // Show error message getHandler().post(() -> { String errorMessage; - if (error.getTranslatedError() != null && !error.getTranslatedError().isEmpty()) { + if (error.getTranslatedError() != null + && !error.getTranslatedError().isEmpty()) { errorMessage = error.getTranslatedError(); - } else if (error.getReason() != null && !error.getReason().isEmpty()) { + } else if (error.getReason() != null + && !error.getReason().isEmpty()) { errorMessage = error.getReason(); } else { errorMessage = getContext().getString(R.string.error_flagging_comment); } - android.widget.Toast.makeText( - getContext(), - errorMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); + android.widget.Toast.makeText(getContext(), errorMessage, android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } @@ -927,10 +959,10 @@ public boolean onSuccess(APIEmptyResponse success) { // Show success message getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - R.string.comment_flagged_successfully, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), + R.string.comment_flagged_successfully, + android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } @@ -952,19 +984,19 @@ public boolean onFailure(APIError error) { // Show error message getHandler().post(() -> { String errorMessage; - if (error.getTranslatedError() != null && !error.getTranslatedError().isEmpty()) { + if (error.getTranslatedError() != null + && !error.getTranslatedError().isEmpty()) { errorMessage = error.getTranslatedError(); - } else if (error.getReason() != null && !error.getReason().isEmpty()) { + } else if (error.getReason() != null + && !error.getReason().isEmpty()) { errorMessage = error.getReason(); } else { errorMessage = getContext().getString(R.string.error_blocking_user); } android.widget.Toast.makeText( - getContext(), - errorMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), errorMessage, android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } @@ -973,10 +1005,10 @@ public boolean onFailure(APIError error) { public boolean onSuccess(BlockSuccess success) { getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - R.string.user_blocked_successfully, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), + R.string.user_blocked_successfully, + android.widget.Toast.LENGTH_SHORT) + .show(); // Update blocked statuses in-place from the API response sdk.commentsTree.updateBlockedStatuses(success.getCommentStatuses()); @@ -996,7 +1028,12 @@ public void onUnblock(String commentId, String userName) { // Confirm before unblocking android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(getContext()); builder.setTitle(R.string.unblock_user_title) - .setMessage(getContext().getString(R.string.unblock_user_confirm, userName != null ? userName : getContext().getString(R.string.blocked_user_placeholder))) + .setMessage(getContext() + .getString( + R.string.unblock_user_confirm, + userName != null + ? userName + : getContext().getString(R.string.blocked_user_placeholder))) .setPositiveButton(R.string.unblock, (dialog, which) -> { // Snapshot comment IDs on the main thread before the async call List commentIds = new ArrayList<>(sdk.commentsTree.commentsById.keySet()); @@ -1005,19 +1042,19 @@ public void onUnblock(String commentId, String userName) { public boolean onFailure(APIError error) { getHandler().post(() -> { String errorMessage; - if (error.getTranslatedError() != null && !error.getTranslatedError().isEmpty()) { + if (error.getTranslatedError() != null + && !error.getTranslatedError().isEmpty()) { errorMessage = error.getTranslatedError(); - } else if (error.getReason() != null && !error.getReason().isEmpty()) { + } else if (error.getReason() != null + && !error.getReason().isEmpty()) { errorMessage = error.getReason(); } else { errorMessage = getContext().getString(R.string.error_unblocking_user); } android.widget.Toast.makeText( - getContext(), - errorMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), errorMessage, android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } @@ -1026,10 +1063,10 @@ public boolean onFailure(APIError error) { public boolean onSuccess(UnblockSuccess success) { getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - R.string.user_unblocked_successfully, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), + R.string.user_unblocked_successfully, + android.widget.Toast.LENGTH_SHORT) + .show(); // Update blocked statuses in-place from the API response sdk.commentsTree.updateBlockedStatuses(success.getCommentStatuses()); @@ -1051,7 +1088,7 @@ public void load() { Log.e("LiveChatView", "Cannot load comments: SDK not set. Call setSDK() first."); return; } - + showLoading(true); sdk.load(new FCCallback() { @@ -1092,13 +1129,13 @@ public boolean onSuccess(GetCommentsResponseWithPresencePublicComment response) // Start the date update timer startDateUpdateTimer(); - + // For chat view, always scroll to bottom after loading if (autoScrollToBottom) { scrollToBottom(); } } - + // Initialize the input with current user info if (bottomCommentInput != null) { bottomCommentInput.setCurrentUser(sdk.getCurrentUser()); @@ -1146,10 +1183,11 @@ public boolean onFailure(APIError error) { } else if (commentForm != null) { commentForm.setSubmitting(false); } - + // Check for translated error message String errorMessage; - if (error.getTranslatedError() != null && !error.getTranslatedError().isEmpty()) { + if (error.getTranslatedError() != null + && !error.getTranslatedError().isEmpty()) { errorMessage = error.getTranslatedError(); } else if (error.getReason() != null && !error.getReason().isEmpty()) { errorMessage = error.getReason(); @@ -1183,20 +1221,20 @@ public boolean onSuccess(PublicComment comment) { // Show a toast message to confirm successful posting android.widget.Toast.makeText( - getContext(), - R.string.comment_posted_successfully, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), + R.string.comment_posted_successfully, + android.widget.Toast.LENGTH_SHORT) + .show(); // Add the comment to the tree, display immediately // Note: For LiveChatView, comments will be added at the bottom because // we've set the sort direction to OLDEST_FIRST in setSDK() sdk.addComment(comment, true); - + // Re-enable auto-scroll and scroll to the bottom to show the new comment autoScrollToBottom = true; scrollToBottom(); - + // Notify listener if set if (commentActionListener != null) { commentActionListener.onCommentPosted(comment); @@ -1263,12 +1301,9 @@ private void updatePaginationControls() { private void applyHeaderTheme() { if (liveChatHeader == null || sdk == null) return; FastCommentsTheme theme = sdk.getTheme(); - liveChatHeader.setBackgroundColor( - ThemeColorResolver.getLiveChatHeaderBackgroundColor(getContext(), theme)); - connectionStatusText.setTextColor( - ThemeColorResolver.getLiveChatHeaderTextColor(getContext(), theme)); - userCountText.setTextColor( - ThemeColorResolver.getLiveChatUserCountTextColor(getContext(), theme)); + liveChatHeader.setBackgroundColor(ThemeColorResolver.getLiveChatHeaderBackgroundColor(getContext(), theme)); + connectionStatusText.setTextColor(ThemeColorResolver.getLiveChatHeaderTextColor(getContext(), theme)); + userCountText.setTextColor(ThemeColorResolver.getLiveChatUserCountTextColor(getContext(), theme)); // Apply disconnected dot color as initial state int dotColor = ThemeColorResolver.getLiveChatDisconnectedDotColor(getContext(), theme); @@ -1298,7 +1333,7 @@ public boolean onFailure(APIError error) { getHandler().post(() -> { // Hide loading indicator paginationProgressBar.setVisibility(View.GONE); - + // Restore buttons btnNextComments.setVisibility(View.VISIBLE); if (sdk.shouldShowLoadAll()) { @@ -1307,10 +1342,8 @@ public boolean onFailure(APIError error) { // Show error toast android.widget.Toast.makeText( - getContext(), - R.string.error_loading_comments, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), R.string.error_loading_comments, android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } @@ -1320,7 +1353,7 @@ public boolean onSuccess(GetCommentsResponseWithPresencePublicComment response) getHandler().post(() -> { // Hide loading indicator paginationProgressBar.setVisibility(View.GONE); - + // Update pagination controls updatePaginationControls(); }); @@ -1352,10 +1385,8 @@ public boolean onFailure(APIError error) { // Show error toast android.widget.Toast.makeText( - getContext(), - R.string.error_loading_comments, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), R.string.error_loading_comments, android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } @@ -1366,7 +1397,7 @@ public boolean onSuccess(GetCommentsResponseWithPresencePublicComment response) // Hide loading indicator paginationProgressBar.setVisibility(View.GONE); paginationControls.setVisibility(View.GONE); - + // Re-enable auto-scroll and scroll to bottom autoScrollToBottom = true; scrollToBottom(); @@ -1379,12 +1410,22 @@ public boolean onSuccess(GetCommentsResponseWithPresencePublicComment response) /** * Helper method for processing upvotes with or without anonymous credentials */ - private void processUpvote(RenderableComment commentToVote, boolean needToDelete, - Boolean originalIsVotedUp, String originalVoteId, - FCCallback errorHandler, String username, String email, String toastMessage) { + private void processUpvote( + RenderableComment commentToVote, + boolean needToDelete, + Boolean originalIsVotedUp, + String originalVoteId, + FCCallback errorHandler, + String username, + String email, + String toastMessage) { if (needToDelete && originalVoteId != null) { // Delete the existing vote first - sdk.deleteCommentVote(commentToVote.getComment().getId(), originalVoteId, username, email, + sdk.deleteCommentVote( + commentToVote.getComment().getId(), + originalVoteId, + username, + email, new FCCallback() { @Override public boolean onFailure(APIError error) { @@ -1401,16 +1442,18 @@ public boolean onSuccess(VoteDeleteResponse response) { // Show success toast message for removing vote getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - toastMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), toastMessage, android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } // Otherwise, we need to add a new upvote - sdk.voteComment(commentToVote.getComment().getId(), true, username, email, + sdk.voteComment( + commentToVote.getComment().getId(), + true, + username, + email, new FCCallback() { @Override public boolean onFailure(APIError error) { @@ -1425,10 +1468,10 @@ public boolean onSuccess(VoteResponse response) { // Show success toast message getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - toastMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), + toastMessage, + android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; @@ -1441,42 +1484,48 @@ public boolean onSuccess(VoteResponse response) { } else if (originalIsVotedUp == null || !originalIsVotedUp) { // No existing vote to delete or it's a downvote being converted to upvote // Just add a new upvote - sdk.voteComment(commentToVote.getComment().getId(), true, username, email, - new FCCallback() { - @Override - public boolean onFailure(APIError error) { - return errorHandler.onSuccess(error); - } + sdk.voteComment(commentToVote.getComment().getId(), true, username, email, new FCCallback() { + @Override + public boolean onFailure(APIError error) { + return errorHandler.onSuccess(error); + } - @Override - public boolean onSuccess(VoteResponse response) { - // Store new vote ID - commentToVote.getComment().setMyVoteId(response.getVoteId()); - - // Show success toast message - getHandler().post(() -> { - android.widget.Toast.makeText( - getContext(), - toastMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); - }); + @Override + public boolean onSuccess(VoteResponse response) { + // Store new vote ID + commentToVote.getComment().setMyVoteId(response.getVoteId()); - return CONSUME; - } + // Show success toast message + getHandler().post(() -> { + android.widget.Toast.makeText(getContext(), toastMessage, android.widget.Toast.LENGTH_SHORT) + .show(); }); + + return CONSUME; + } + }); } } /** * Helper method for processing downvotes with or without anonymous credentials */ - private void processDownvote(RenderableComment commentToVote, boolean needToDelete, - Boolean originalIsVotedDown, String originalVoteId, - FCCallback errorHandler, String username, String email, String toastMessage) { + private void processDownvote( + RenderableComment commentToVote, + boolean needToDelete, + Boolean originalIsVotedDown, + String originalVoteId, + FCCallback errorHandler, + String username, + String email, + String toastMessage) { if (needToDelete && originalVoteId != null) { // Delete the existing vote first - sdk.deleteCommentVote(commentToVote.getComment().getId(), originalVoteId, username, email, + sdk.deleteCommentVote( + commentToVote.getComment().getId(), + originalVoteId, + username, + email, new FCCallback() { @Override public boolean onFailure(APIError error) { @@ -1493,16 +1542,18 @@ public boolean onSuccess(VoteDeleteResponse response) { // Show success toast message for removing vote getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - toastMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), toastMessage, android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; } // Otherwise, we need to add a new downvote - sdk.voteComment(commentToVote.getComment().getId(), false, username, email, + sdk.voteComment( + commentToVote.getComment().getId(), + false, + username, + email, new FCCallback() { @Override public boolean onFailure(APIError error) { @@ -1517,10 +1568,10 @@ public boolean onSuccess(VoteResponse response) { // Show success toast message getHandler().post(() -> { android.widget.Toast.makeText( - getContext(), - toastMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); + getContext(), + toastMessage, + android.widget.Toast.LENGTH_SHORT) + .show(); }); return CONSUME; @@ -1533,30 +1584,26 @@ public boolean onSuccess(VoteResponse response) { } else if (originalIsVotedDown == null || !originalIsVotedDown) { // No existing vote to delete or it's an upvote being converted to downvote // Just add a new downvote - sdk.voteComment(commentToVote.getComment().getId(), false, username, email, - new FCCallback() { - @Override - public boolean onFailure(APIError error) { - return errorHandler.onSuccess(error); - } + sdk.voteComment(commentToVote.getComment().getId(), false, username, email, new FCCallback() { + @Override + public boolean onFailure(APIError error) { + return errorHandler.onSuccess(error); + } - @Override - public boolean onSuccess(VoteResponse response) { - // Store new vote ID - commentToVote.getComment().setMyVoteId(response.getVoteId()); - - // Show success toast message - getHandler().post(() -> { - android.widget.Toast.makeText( - getContext(), - toastMessage, - android.widget.Toast.LENGTH_SHORT - ).show(); - }); + @Override + public boolean onSuccess(VoteResponse response) { + // Store new vote ID + commentToVote.getComment().setMyVoteId(response.getVoteId()); - return CONSUME; - } + // Show success toast message + getHandler().post(() -> { + android.widget.Toast.makeText(getContext(), toastMessage, android.widget.Toast.LENGTH_SHORT) + .show(); }); + + return CONSUME; + } + }); } } @@ -1584,7 +1631,7 @@ private void stopDateUpdateTimer() { /** * Get the UI handler for posting to the main thread - * + * * @return Handler for UI updates */ public Handler getHandler() { @@ -1602,7 +1649,7 @@ private void updateDates() { if (sdk == null || (sdk.getConfig().absoluteDates != null && sdk.getConfig().absoluteDates)) { return; } - + // Get visible position range if (layoutManager != null) { int firstVisiblePosition = layoutManager.findFirstVisibleItemPosition(); @@ -1624,47 +1671,47 @@ protected void onDetachedFromWindow() { // Clean up all resources including backpress callback cleanup(); } - + /** * Enable or disable auto-scrolling to bottom when new messages arrive - * + * * @param enable Whether to enable auto-scrolling */ public void setAutoScrollToBottom(boolean enable) { this.autoScrollToBottom = enable; } - + /** * Check if auto-scrolling to bottom is enabled - * + * * @return true if auto-scrolling is enabled */ public boolean isAutoScrollToBottom() { return autoScrollToBottom; } - + /** * Cleanup method to properly release resources and callbacks. * Should be called when the view is no longer needed, especially when used in fragments. */ public void cleanup() { stopDateUpdateTimer(); - + if (dateUpdateHandler != null) { dateUpdateHandler.removeCallbacksAndMessages(null); dateUpdateHandler = null; } dateUpdateRunnable = null; - + if (backPressedCallback != null) { backPressedCallback.setEnabled(false); backPressedCallback = null; } - + if (recyclerView != null) { recyclerView.clearOnScrollListeners(); } - + if (adapter != null) { adapter.setRequestingReplyListener(null); adapter.setUpVoteListener(null); @@ -1674,7 +1721,7 @@ public void cleanup() { adapter.setGetChildrenProducer(null); adapter = null; } - + if (bottomCommentInput != null) { bottomCommentInput.setOnCommentSubmitListener(null); bottomCommentInput.setOnReplyStateChangeListener(null); @@ -1683,17 +1730,17 @@ public void cleanup() { commentForm.setOnCommentSubmitListener(null); commentForm.setOnCancelReplyListener(null); } - + if (btnNextComments != null) { btnNextComments.setOnClickListener(null); } if (btnLoadAll != null) { btnLoadAll.setOnClickListener(null); } - + commentActionListener = null; userClickListener = null; - + if (sdk != null) { sdk.setPresenceUpdateListener(null); sdk.setConnectionStatusListener(null); @@ -1701,11 +1748,11 @@ public void cleanup() { sdk = null; } } - + /** * Enable or disable back press handling. * Useful for fragment lifecycle management. - * + * * @param enabled Whether back press handling should be active */ public void setBackPressHandlingEnabled(boolean enabled) { @@ -1713,7 +1760,7 @@ public void setBackPressHandlingEnabled(boolean enabled) { backPressedCallback.setEnabled(enabled); } } - + /** * Lifecycle method to call when the view/fragment is paused. * Stops timers and disables back press handling to prevent memory leaks. @@ -1722,7 +1769,7 @@ public void onPause() { stopDateUpdateTimer(); setBackPressHandlingEnabled(false); } - + /** * Lifecycle method to call when the view/fragment is resumed. * Restarts timers and enables back press handling. @@ -1731,11 +1778,11 @@ public void onResume() { setBackPressHandlingEnabled(true); startDateUpdateTimer(); } - + /** * Sets up the demo banner if tenant ID is "demo" */ private void setupDemoBanner() { DemoBannerHelper.setupDemoBanner(this, sdk); } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/MentionSuggestionsAdapter.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/MentionSuggestionsAdapter.java index 3488c5d..fc4328c 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/MentionSuggestionsAdapter.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/MentionSuggestionsAdapter.java @@ -7,10 +7,8 @@ import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import java.util.List; /** @@ -62,4 +60,4 @@ private static class ViewHolder { ImageView avatar; TextView username; } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/NestedScrollableHost.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/NestedScrollableHost.java index 9c95a0b..7dae0b5 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/NestedScrollableHost.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/NestedScrollableHost.java @@ -6,7 +6,6 @@ import android.view.View; import android.view.ViewConfiguration; import android.widget.FrameLayout; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.viewpager2.widget.ViewPager2; @@ -15,13 +14,13 @@ * Layout that prevents horizontal touch event conflicts when nesting a horizontal ViewPager2 * inside another horizontal ViewPager2. This specifically handles the case where image galleries * need to work properly when the feed is embedded in a parent ViewPager2. - * + * * Key behaviors: * - Only handles horizontal scroll conflicts (preserves vertical scrolling) * - Allows child ViewPager2 to consume horizontal swipes when it can scroll * - Delegates to parent when child reaches horizontal bounds * - Never interferes with vertical scrolling of the feed - * + * * Based on Google's NestedScrollableHost but optimized for horizontal-only conflicts. */ public class NestedScrollableHost extends FrameLayout { @@ -65,12 +64,12 @@ private void handleInterceptTouchEvent(MotionEvent ev) { case MotionEvent.ACTION_MOVE: float deltaX = Math.abs(ev.getX() - initialX); float deltaY = Math.abs(ev.getY() - initialY); - + // Only handle horizontal scroll conflicts when parent is horizontal ViewPager2 if (isParentHorizontalViewPager() && deltaX > touchSlop && deltaX > deltaY) { // This is primarily a horizontal swipe boolean swipeLeft = (ev.getX() - initialX) < 0; - + // Check if our child ViewPager2 can scroll in the swipe direction if (canChildScrollHorizontally(swipeLeft)) { // Child can handle this horizontal scroll, block parent @@ -85,7 +84,7 @@ private void handleInterceptTouchEvent(MotionEvent ev) { getParent().requestDisallowInterceptTouchEvent(false); } break; - + case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: // Reset state @@ -144,4 +143,4 @@ private ViewPager2 findParentViewPager2() { } return null; } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/OnUserClickListener.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/OnUserClickListener.java index 1e5b431..c834d1e 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/OnUserClickListener.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/OnUserClickListener.java @@ -5,13 +5,13 @@ * Uses a unified approach with a single method and context object. */ public interface OnUserClickListener { - + /** * Called when a user is clicked in any context (comments or feed posts) - * + * * @param context The context containing either a comment or feed post * @param userInfo Information about the user that was clicked * @param source Whether the click came from the name or avatar */ void onUserClicked(UserClickContext context, UserInfo userInfo, UserClickSource source); -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/PostImagesAdapter.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/PostImagesAdapter.java index 3a653e8..c58f73c 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/PostImagesAdapter.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/PostImagesAdapter.java @@ -5,15 +5,12 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; - import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; - import com.bumptech.glide.Glide; import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; import com.fastcomments.model.FeedPostMediaItem; import com.fastcomments.model.FeedPostMediaItemAsset; - import java.util.List; /** @@ -90,11 +87,11 @@ void bind(FeedPostMediaItem mediaItem) { if (mediaItem.getSizes() != null && !mediaItem.getSizes().isEmpty()) { // Select best size for display FeedPostMediaItemAsset bestSizeAsset = selectBestImageSize(mediaItem.getSizes()); - + if (bestSizeAsset != null && bestSizeAsset.getSrc() != null) { // Pre-set a minimum height for consistency imageView.setMinimumHeight(context.getResources().getDimensionPixelSize(R.dimen.feed_image_height)); - + Glide.with(context) .load(bestSizeAsset.getSrc()) .centerCrop() @@ -105,30 +102,32 @@ void bind(FeedPostMediaItem mediaItem) { } // Determine if it's a video - boolean isVideo = mediaItem.getLinkUrl() != null && - (mediaItem.getLinkUrl().contains("youtube") || - mediaItem.getLinkUrl().contains("vimeo") || - mediaItem.getLinkUrl().contains(".mp4")); - + boolean isVideo = mediaItem.getLinkUrl() != null + && (mediaItem.getLinkUrl().contains("youtube") + || mediaItem.getLinkUrl().contains("vimeo") + || mediaItem.getLinkUrl().contains(".mp4")); + playButton.setVisibility(isVideo ? View.VISIBLE : View.GONE); } - + /** * Get the best quality image URL for full-screen viewing * For full-screen viewing, we want the highest quality image available - * + * * @param mediaItem The media item to get the URL from * @return The URL of the highest quality image, or null if not available */ private String getBestQualityImageUrl(FeedPostMediaItem mediaItem) { - if (mediaItem == null || mediaItem.getSizes() == null || mediaItem.getSizes().isEmpty()) { + if (mediaItem == null + || mediaItem.getSizes() == null + || mediaItem.getSizes().isEmpty()) { return null; } - + // Find the image with the highest resolution (largest width) FeedPostMediaItemAsset highestQualityAsset = null; double maxWidth = 0; - + for (FeedPostMediaItemAsset asset : mediaItem.getSizes()) { if (asset != null && asset.getSrc() != null && asset.getW() != null) { double width = asset.getW(); @@ -138,10 +137,10 @@ private String getBestQualityImageUrl(FeedPostMediaItem mediaItem) { } } } - + return highestQualityAsset != null ? highestQualityAsset.getSrc() : null; } - + /** * Select the best image size based on device display metrics */ @@ -149,56 +148,56 @@ private FeedPostMediaItemAsset selectBestImageSize(List if (sizes == null || sizes.isEmpty()) { return null; } - + // If there's only one size, use it if (sizes.size() == 1) { return sizes.get(0); } - + // Get screen width for comparison int screenWidth = context.getResources().getDisplayMetrics().widthPixels; - + // Target a size that's close to the screen width for optimal display double optimalWidth = screenWidth; double maxAcceptableWidth = screenWidth * 1.5; - + FeedPostMediaItemAsset bestMatch = null; double smallestDiff = Double.MAX_VALUE; - + // First pass: find close matches to optimal width for (FeedPostMediaItemAsset asset : sizes) { if (asset == null || asset.getW() == null || asset.getSrc() == null) { continue; } - + double width = asset.getW(); double diff = Math.abs(width - optimalWidth); - + // If width is within acceptable range and has smaller difference than current best match if (width <= maxAcceptableWidth && diff < smallestDiff) { bestMatch = asset; smallestDiff = diff; } } - + // If no match found in optimal range, just use the largest that's not excessively large if (bestMatch == null) { double largestAcceptableWidth = 0; - + for (FeedPostMediaItemAsset asset : sizes) { if (asset == null || asset.getW() == null || asset.getSrc() == null) { continue; } - + double width = asset.getW(); - + // Find largest image that's not too oversized if (width > largestAcceptableWidth && width <= maxAcceptableWidth * 2) { bestMatch = asset; largestAcceptableWidth = width; } } - + // If still no match, use the first valid asset if (bestMatch == null) { for (FeedPostMediaItemAsset asset : sizes) { @@ -208,9 +207,9 @@ private FeedPostMediaItemAsset selectBestImageSize(List } } } - + // Return best match or first asset if no match found return bestMatch != null ? bestMatch : sizes.get(0); } } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/RenderableButton.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/RenderableButton.java index 6bde5a2..bac1fab 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/RenderableButton.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/RenderableButton.java @@ -8,33 +8,33 @@ public class RenderableButton extends RenderableNode { public static final int TYPE_NEW_ROOT_COMMENTS = 1; public static final int TYPE_NEW_CHILD_COMMENTS = 2; - + private final int buttonType; private final int commentCount; private final String parentId; // Only used for TYPE_NEW_CHILD_COMMENTS - + public RenderableButton(int buttonType, int commentCount) { this(buttonType, commentCount, null); } - + public RenderableButton(int buttonType, int commentCount, String parentId) { this.buttonType = buttonType; this.commentCount = commentCount; this.parentId = parentId; } - + public int getButtonType() { return buttonType; } - + public int getCommentCount() { return commentCount; } - + public String getParentId() { return parentId; } - + @Override public int determineNestingLevel(Map commentMap) { if (buttonType == TYPE_NEW_ROOT_COMMENTS) { @@ -48,4 +48,4 @@ public int determineNestingLevel(Map commentMap) { } return 0; } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/RenderableComment.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/RenderableComment.java index 431b204..42e2716 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/RenderableComment.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/RenderableComment.java @@ -1,7 +1,6 @@ package com.fastcomments.sdk; import com.fastcomments.model.PublicComment; - import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -13,10 +12,10 @@ public class RenderableComment extends RenderableNode { private final PublicComment comment; public boolean isRepliesShown = false; - + // Online status flag public boolean isOnline = false; - + // Pagination state for child comments public int childSkip = 0; public int childPage = 0; @@ -52,7 +51,7 @@ public int determineNestingLevel(Map commentMap) { } return 1 + parent.determineNestingLevel(commentMap); } - + /** * Reset child pagination state, typically called when hiding replies */ @@ -61,7 +60,7 @@ public void resetChildPagination() { this.childPage = 0; this.isLoadingChildren = false; } - + /** * Get the remaining child count that can be loaded in the next page * @@ -72,19 +71,19 @@ public int getRemainingChildCount() { if (childCount == null) { return 0; } - + int loadedCount = 0; if (getComment().getChildren() != null) { loadedCount = getComment().getChildren().size(); } - + int remaining = childCount - loadedCount; return Math.min(Math.max(remaining, 0), childPageSize); } - + /** * Add a new child comment that hasn't been shown yet - * + * * @param childComment The new child comment */ public void addNewChildComment(PublicComment childComment) { @@ -93,26 +92,26 @@ public void addNewChildComment(PublicComment childComment) { } newChildComments.add(childComment); } - + /** * Get the count of new child comments that haven't been shown yet - * + * * @return The count of new child comments */ public int getNewChildCommentsCount() { return newChildComments == null ? 0 : newChildComments.size(); } - + /** * Get the list of new child comments and clear the buffer - * + * * @return The list of new child comments, or null if none */ public List getAndClearNewChildComments() { if (newChildComments == null || newChildComments.isEmpty()) { return null; } - + List result = newChildComments; newChildComments = null; return result; diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/RenderableNode.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/RenderableNode.java index dc343a3..4cab414 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/RenderableNode.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/RenderableNode.java @@ -11,33 +11,32 @@ */ public abstract class RenderableNode { public abstract int determineNestingLevel(Map commentMap); - + /** * Date separator node type for grouping comments by date in live chat */ public static class DateSeparator extends RenderableNode { private final LocalDate date; - + public DateSeparator(LocalDate date) { this.date = date; } - + public LocalDate getDate() { return date; } - + public String getFormattedDate() { // Format date based on user's locale - DateTimeFormatter formatter = DateTimeFormatter - .ofLocalizedDate(FormatStyle.MEDIUM) - .withLocale(Locale.getDefault()); + DateTimeFormatter formatter = + DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.getDefault()); return date.format(formatter); } - + @Override public int determineNestingLevel(Map commentMap) { // Date separators are always at top level return 0; } } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/RichEditText.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/RichEditText.java index 47ae1f0..d791523 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/RichEditText.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/RichEditText.java @@ -2,7 +2,6 @@ import android.content.Context; import android.util.AttributeSet; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatEditText; diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/RichTextHelper.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/RichTextHelper.java index 794baf4..ecf10af 100644 --- a/libraries/sdk/src/main/java/com/fastcomments/sdk/RichTextHelper.java +++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/RichTextHelper.java @@ -15,22 +15,18 @@ import android.text.style.URLSpan; import android.util.DisplayMetrics; import android.widget.EditText; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.bumptech.glide.Glide; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; - -import org.xml.sax.XMLReader; - import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.xml.sax.XMLReader; /** * Utility class for WYSIWYG rich text editing using Android's Spannable system. @@ -64,17 +60,25 @@ public static class RichImageSpan extends ImageSpan { private final String alt; private final String style; - public RichImageSpan(@NonNull Drawable drawable, @NonNull String src, - @Nullable String alt, @Nullable String style) { + public RichImageSpan( + @NonNull Drawable drawable, @NonNull String src, @Nullable String alt, @Nullable String style) { super(drawable); this.src = src; this.alt = alt; this.style = style; } - public String getSrc() { return src; } - public String getAlt() { return alt; } - public String getStyle() { return style; } + public String getSrc() { + return src; + } + + public String getAlt() { + return alt; + } + + public String getStyle() { + return style; + } } // ========== EditableImageGetter ========== @@ -108,7 +112,8 @@ public Drawable getDrawable(String source) { int placeholderH = (int) (48 * editText.getResources().getDisplayMetrics().density); urlDrawable.setBounds(0, 0, maxWidth, placeholderH); - boolean isGif = source != null && (source.endsWith(".gif") || source.contains(".gif?") || source.contains("/giphy.gif")); + boolean isGif = source != null + && (source.endsWith(".gif") || source.contains(".gif?") || source.contains("/giphy.gif")); if (isGif) { loadGif(source, urlDrawable); @@ -122,62 +127,62 @@ public Drawable getDrawable(String source) { private void loadGif(String source, URLDrawable urlDrawable) { CustomTarget target = new CustomTarget() { - @Override - public void onResourceReady(@NonNull com.bumptech.glide.load.resource.gif.GifDrawable resource, - @Nullable Transition transition) { - int w = resource.getIntrinsicWidth(); - int h = resource.getIntrinsicHeight(); - if (w > maxWidth) { - h = (int) ((long) h * maxWidth / w); - w = maxWidth; - } - if (h > maxHeightPx) { - w = (int) ((long) w * maxHeightPx / h); - h = maxHeightPx; - } - - resource.setBounds(0, 0, w, h); - resource.setLoopCount(com.bumptech.glide.load.resource.gif.GifDrawable.LOOP_FOREVER); - - resource.setCallback(new Drawable.Callback() { @Override - public void invalidateDrawable(@NonNull Drawable who) { + public void onResourceReady( + @NonNull com.bumptech.glide.load.resource.gif.GifDrawable resource, + @Nullable + Transition + transition) { + int w = resource.getIntrinsicWidth(); + int h = resource.getIntrinsicHeight(); + if (w > maxWidth) { + h = (int) ((long) h * maxWidth / w); + w = maxWidth; + } + if (h > maxHeightPx) { + w = (int) ((long) w * maxHeightPx / h); + h = maxHeightPx; + } + + resource.setBounds(0, 0, w, h); + resource.setLoopCount(com.bumptech.glide.load.resource.gif.GifDrawable.LOOP_FOREVER); + + resource.setCallback(new Drawable.Callback() { + @Override + public void invalidateDrawable(@NonNull Drawable who) { + editText.invalidate(); + } + + @Override + public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { + editText.postDelayed(what, when - android.os.SystemClock.uptimeMillis()); + } + + @Override + public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { + editText.removeCallbacks(what); + } + }); + + urlDrawable.setDrawable(resource); + urlDrawable.setBounds(0, 0, w, h); + resource.start(); editText.invalidate(); + editText.requestLayout(); } - @Override - public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { - editText.postDelayed(what, when - android.os.SystemClock.uptimeMillis()); - } - @Override - public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { - editText.removeCallbacks(what); - } - }); - - urlDrawable.setDrawable(resource); - urlDrawable.setBounds(0, 0, w, h); - resource.start(); - editText.invalidate(); - editText.requestLayout(); - } - @Override - public void onLoadCleared(@Nullable Drawable placeholder) { - } - }; + @Override + public void onLoadCleared(@Nullable Drawable placeholder) {} + }; activeTargets.add(target); - Glide.with(editText) - .asGif() - .load(source) - .into(target); + Glide.with(editText).asGif().load(source).into(target); } private void loadBitmap(String source, URLDrawable urlDrawable) { CustomTarget target = new CustomTarget() { @Override - public void onResourceReady(@NonNull Bitmap resource, - @Nullable Transition transition) { + public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { BitmapDrawable bd = new BitmapDrawable(editText.getResources(), resource); int w = bd.getIntrinsicWidth(); int h = bd.getIntrinsicHeight(); @@ -200,15 +205,11 @@ public void onResourceReady(@NonNull Bitmap resource, } @Override - public void onLoadCleared(@Nullable Drawable placeholder) { - } + public void onLoadCleared(@Nullable Drawable placeholder) {} }; activeTargets.add(target); - Glide.with(editText) - .asBitmap() - .load(source) - .into(target); + Glide.with(editText).asBitmap().load(source).into(target); } /** @@ -244,18 +245,16 @@ public static boolean toggleCode(Editable editable, int selStart, int selEnd) { if (selStart == selEnd) { return !isCodeActive(editable, selStart); } - if (isRangeCovered(editable, selStart, selEnd, TypefaceSpan.class, - s -> "monospace".equals(s.getFamily()))) { - removeSpansInRange(editable, selStart, selEnd, TypefaceSpan.class, - s -> "monospace".equals(s.getFamily())); + if (isRangeCovered(editable, selStart, selEnd, TypefaceSpan.class, s -> "monospace".equals(s.getFamily()))) { + removeSpansInRange(editable, selStart, selEnd, TypefaceSpan.class, s -> "monospace".equals(s.getFamily())); removeSpansInRange(editable, selStart, selEnd, BackgroundColorSpan.class, null); removeSpansInRange(editable, selStart, selEnd, CodeBlockSpan.class, null); return false; } else { - int[] merged = mergeRange(editable, selStart, selEnd, TypefaceSpan.class, - s -> "monospace".equals(s.getFamily())); - removeSpansInRange(editable, merged[0], merged[1], TypefaceSpan.class, - s -> "monospace".equals(s.getFamily())); + int[] merged = + mergeRange(editable, selStart, selEnd, TypefaceSpan.class, s -> "monospace".equals(s.getFamily())); + removeSpansInRange( + editable, merged[0], merged[1], TypefaceSpan.class, s -> "monospace".equals(s.getFamily())); removeSpansInRange(editable, merged[0], merged[1], BackgroundColorSpan.class, null); removeSpansInRange(editable, merged[0], merged[1], CodeBlockSpan.class, null); editable.setSpan(new TypefaceSpan("monospace"), merged[0], merged[1], SPAN_FLAGS); @@ -270,18 +269,18 @@ public static boolean toggleCodeBlock(Editable editable, int selStart, int selEn } // Check if already a code block CodeBlockSpan[] blocks = editable.getSpans(selStart, selEnd, CodeBlockSpan.class); - if (blocks.length > 0 && isRangeCovered(editable, selStart, selEnd, TypefaceSpan.class, - s -> "monospace".equals(s.getFamily()))) { - removeSpansInRange(editable, selStart, selEnd, TypefaceSpan.class, - s -> "monospace".equals(s.getFamily())); + if (blocks.length > 0 + && isRangeCovered( + editable, selStart, selEnd, TypefaceSpan.class, s -> "monospace".equals(s.getFamily()))) { + removeSpansInRange(editable, selStart, selEnd, TypefaceSpan.class, s -> "monospace".equals(s.getFamily())); removeSpansInRange(editable, selStart, selEnd, BackgroundColorSpan.class, null); removeSpansInRange(editable, selStart, selEnd, CodeBlockSpan.class, null); return false; } else { - int[] merged = mergeRange(editable, selStart, selEnd, TypefaceSpan.class, - s -> "monospace".equals(s.getFamily())); - removeSpansInRange(editable, merged[0], merged[1], TypefaceSpan.class, - s -> "monospace".equals(s.getFamily())); + int[] merged = + mergeRange(editable, selStart, selEnd, TypefaceSpan.class, s -> "monospace".equals(s.getFamily())); + removeSpansInRange( + editable, merged[0], merged[1], TypefaceSpan.class, s -> "monospace".equals(s.getFamily())); removeSpansInRange(editable, merged[0], merged[1], BackgroundColorSpan.class, null); removeSpansInRange(editable, merged[0], merged[1], CodeBlockSpan.class, null); editable.setSpan(new TypefaceSpan("monospace"), merged[0], merged[1], SPAN_FLAGS); @@ -481,8 +480,7 @@ public static String toHtml(Editable editable) { * Parse HTML into a Spanned, using the given EditableImageGetter for images. */ public static Spanned fromHtml(@NonNull String html, @Nullable EditableImageGetter imageGetter) { - Spanned result = Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT, - imageGetter, new CodePreTagHandler()); + Spanned result = Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT, imageGetter, new CodePreTagHandler()); SpannableStringBuilder ssb = new SpannableStringBuilder(result); // Replace plain ImageSpan instances (created by Html.fromHtml) with RichImageSpan @@ -495,8 +493,7 @@ public static Spanned fromHtml(@NonNull String html, @Nullable EditableImageGett String src = imgSpan.getSource(); if (src == null) src = ""; ssb.removeSpan(imgSpan); - ssb.setSpan(new RichImageSpan(imgSpan.getDrawable(), src, null, null), - start, end, flags); + ssb.setSpan(new RichImageSpan(imgSpan.getDrawable(), src, null, null), start, end, flags); } // Strip trailing newlines added by Html.fromHtml @@ -575,8 +572,8 @@ public static String extractHref(@NonNull String startTag) { * @param imageGetter Optional image getter for rendering images (may be null) * @return The new cursor position after insertion */ - public static int insertHtmlAtCursor(@NonNull EditText editText, @NonNull String html, - @Nullable EditableImageGetter imageGetter) { + public static int insertHtmlAtCursor( + @NonNull EditText editText, @NonNull String html, @Nullable EditableImageGetter imageGetter) { Spanned spanned = fromHtml(html, imageGetter); int start = Math.max(editText.getSelectionStart(), 0); int end = Math.max(editText.getSelectionEnd(), 0); @@ -595,7 +592,8 @@ public static int insertHtmlAtCursor(@NonNull EditText editText, @NonNull String * @param endTag The closing tag string * @return The new cursor position after wrapping */ - public static int wrapSelectionLiteral(@NonNull EditText editText, @NonNull String startTag, @NonNull String endTag) { + public static int wrapSelectionLiteral( + @NonNull EditText editText, @NonNull String startTag, @NonNull String endTag) { int start = editText.getSelectionStart(); int end = editText.getSelectionEnd(); Editable editable = editText.getText(); @@ -625,7 +623,8 @@ public static int wrapSelectionLiteral(@NonNull EditText editText, @NonNull Stri * @param endTag The closing tag string * @return true if handled as a recognized format/link, false if caller should use literal fallback */ - public static boolean applyWrapFormat(@NonNull EditText editText, @NonNull String startTag, @NonNull String endTag) { + public static boolean applyWrapFormat( + @NonNull EditText editText, @NonNull String startTag, @NonNull String endTag) { Editable editable = editText.getText(); int selStart = editText.getSelectionStart(); int selEnd = editText.getSelectionEnd(); @@ -636,10 +635,18 @@ public static boolean applyWrapFormat(@NonNull EditText editText, @NonNull Strin int start = Math.min(selStart, selEnd); int end = Math.max(selStart, selEnd); switch (formatName) { - case "bold": toggleBold(editable, start, end); break; - case "italic": toggleItalic(editable, start, end); break; - case "code": toggleCode(editable, start, end); break; - case "codeblock": toggleCodeBlock(editable, start, end); break; + case "bold": + toggleBold(editable, start, end); + break; + case "italic": + toggleItalic(editable, start, end); + break; + case "code": + toggleCode(editable, start, end); + break; + case "codeblock": + toggleCodeBlock(editable, start, end); + break; } } return true; @@ -681,8 +688,7 @@ public static void truncateCodeAtCursor(Editable editable, int cursor) { int spanStart = editable.getSpanStart(s); int flags = editable.getSpanFlags(s); editable.removeSpan(s); - editable.setSpan(new TypefaceSpan("monospace"), spanStart, cursor, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + editable.setSpan(new TypefaceSpan("monospace"), spanStart, cursor, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); if (spanEnd > cursor) { editable.setSpan(new TypefaceSpan("monospace"), cursor, spanEnd, flags); } @@ -696,8 +702,7 @@ public static void truncateCodeAtCursor(Editable editable, int cursor) { int flags = editable.getSpanFlags(s); int color = s.getBackgroundColor(); editable.removeSpan(s); - editable.setSpan(new BackgroundColorSpan(color), spanStart, cursor, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + editable.setSpan(new BackgroundColorSpan(color), spanStart, cursor, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); if (spanEnd > cursor) { editable.setSpan(new BackgroundColorSpan(color), cursor, spanEnd, flags); } @@ -716,8 +721,7 @@ private static void truncateStyleAtCursor(Editable editable, int cursor, int sty int spanStart = editable.getSpanStart(s); int flags = editable.getSpanFlags(s); editable.removeSpan(s); - editable.setSpan(new StyleSpan(style), spanStart, cursor, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + editable.setSpan(new StyleSpan(style), spanStart, cursor, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // If the span extended beyond cursor, re-create the remainder with original flags if (spanEnd > cursor) { editable.setSpan(new StyleSpan(style), cursor, spanEnd, flags); @@ -794,14 +798,14 @@ private static int[] mergeStyleRange(Editable editable, int selStart, int selEnd mergedEnd = Math.max(mergedEnd, editable.getSpanEnd(s)); } } - return new int[]{mergedStart, mergedEnd}; + return new int[] {mergedStart, mergedEnd}; } /** * Generic range coverage check with an optional filter predicate. */ - private static boolean isRangeCovered(Editable editable, int start, int end, - Class clazz, SpanFilter filter) { + private static boolean isRangeCovered( + Editable editable, int start, int end, Class clazz, SpanFilter filter) { for (int i = start; i < end; i++) { boolean covered = false; for (T s : editable.getSpans(i, i + 1, clazz)) { @@ -815,8 +819,8 @@ private static boolean isRangeCovered(Editable editable, int start, int end, return true; } - private static void removeSpansInRange(Editable editable, int start, int end, - Class clazz, SpanFilter filter) { + private static void removeSpansInRange( + Editable editable, int start, int end, Class clazz, SpanFilter filter) { for (T s : editable.getSpans(start, end, clazz)) { if (filter != null && !filter.matches(s)) continue; int spanStart = editable.getSpanStart(s); @@ -830,16 +834,24 @@ private static void removeSpansInRange(Editable editable, int start, int end editable.setSpan(new TypefaceSpan(((TypefaceSpan) s).getFamily()), end, spanEnd, SPAN_FLAGS); } if (s instanceof BackgroundColorSpan && spanStart < start) { - editable.setSpan(new BackgroundColorSpan(((BackgroundColorSpan) s).getBackgroundColor()), spanStart, start, SPAN_FLAGS); + editable.setSpan( + new BackgroundColorSpan(((BackgroundColorSpan) s).getBackgroundColor()), + spanStart, + start, + SPAN_FLAGS); } if (s instanceof BackgroundColorSpan && spanEnd > end) { - editable.setSpan(new BackgroundColorSpan(((BackgroundColorSpan) s).getBackgroundColor()), end, spanEnd, SPAN_FLAGS); + editable.setSpan( + new BackgroundColorSpan(((BackgroundColorSpan) s).getBackgroundColor()), + end, + spanEnd, + SPAN_FLAGS); } } } - private static int[] mergeRange(Editable editable, int selStart, int selEnd, - Class clazz, SpanFilter filter) { + private static int[] mergeRange( + Editable editable, int selStart, int selEnd, Class clazz, SpanFilter filter) { int mergedStart = selStart; int mergedEnd = selEnd; for (T s : editable.getSpans(selStart, selEnd, clazz)) { @@ -847,7 +859,7 @@ private static int[] mergeRange(Editable editable, int selStart, int selEnd, mergedStart = Math.min(mergedStart, editable.getSpanStart(s)); mergedEnd = Math.max(mergedEnd, editable.getSpanEnd(s)); } - return new int[]{mergedStart, mergedEnd}; + return new int[] {mergedStart, mergedEnd}; } private interface SpanFilter { @@ -861,8 +873,8 @@ private static class SpanEvent { final boolean isOpen; final String tag; final String attrs; // for open tags only - final Object span; // the original span, for determining open-order - final int seq; // creation order, used as a tiebreaker for identical ranges + final Object span; // the original span, for determining open-order + final int seq; // creation order, used as a tiebreaker for identical ranges SpanEvent(int position, boolean isOpen, String tag, String attrs, Object span, int seq) { this.position = position; @@ -917,12 +929,22 @@ private static void appendEscapedText(StringBuilder sb, String text, int start, for (int i = start; i < end; i++) { char c = text.charAt(i); switch (c) { - case '\uFFFC': break; // skip object replacement char (used by ImageSpan) - case '<': sb.append("<"); break; - case '>': sb.append(">"); break; - case '&': sb.append("&"); break; - case '\n': sb.append("
"); break; - default: sb.append(c); + case '\uFFFC': + break; // skip object replacement char (used by ImageSpan) + case '<': + sb.append("<"); + break; + case '>': + sb.append(">"); + break; + case '&': + sb.append("&"); + break; + case '\n': + sb.append("
"); + break; + default: + sb.append(c); } } } @@ -940,14 +962,14 @@ private static String escapeAttr(String value) { private static class CodePreTagHandler implements Html.TagHandler { // Marker class for tracking tag open positions private static class CodeMark {} + private static class PreMark {} @Override public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) { if ("code".equalsIgnoreCase(tag)) { if (opening) { - output.setSpan(new CodeMark(), output.length(), output.length(), - Spanned.SPAN_MARK_MARK); + output.setSpan(new CodeMark(), output.length(), output.length(), Spanned.SPAN_MARK_MARK); } else { CodeMark mark = getLast(output, CodeMark.class); if (mark != null) { @@ -956,8 +978,7 @@ public void handleTag(boolean opening, String tag, Editable output, XMLReader xm if (start < output.length()) { // Check if this code is inside a
 block
                             PreMark preMark = getLast(output, PreMark.class);
-                            boolean isBlock = preMark != null &&
-                                    output.getSpanStart(preMark) <= start;
+                            boolean isBlock = preMark != null && output.getSpanStart(preMark) <= start;
 
                             output.setSpan(new TypefaceSpan("monospace"), start, output.length(), SPAN_FLAGS);
                             output.setSpan(new BackgroundColorSpan(CODE_BG_COLOR), start, output.length(), SPAN_FLAGS);
@@ -969,8 +990,7 @@ public void handleTag(boolean opening, String tag, Editable output, XMLReader xm
                 }
             } else if ("pre".equalsIgnoreCase(tag)) {
                 if (opening) {
-                    output.setSpan(new PreMark(), output.length(), output.length(),
-                            Spanned.SPAN_MARK_MARK);
+                    output.setSpan(new PreMark(), output.length(), output.length(), Spanned.SPAN_MARK_MARK);
                 } else {
                     PreMark mark = getLast(output, PreMark.class);
                     if (mark != null) {
diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/SelectedMediaAdapter.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/SelectedMediaAdapter.java
index b720009..c44d34e 100644
--- a/libraries/sdk/src/main/java/com/fastcomments/sdk/SelectedMediaAdapter.java
+++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/SelectedMediaAdapter.java
@@ -7,13 +7,10 @@
 import android.view.ViewGroup;
 import android.widget.ImageButton;
 import android.widget.ImageView;
-
 import androidx.annotation.NonNull;
 import androidx.recyclerview.widget.RecyclerView;
-
 import com.bumptech.glide.Glide;
 import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
-
 import java.util.ArrayList;
 import java.util.List;
 
@@ -58,7 +55,7 @@ public int getItemCount() {
 
     /**
      * Add a new media item to the adapter
-     * 
+     *
      * @param uri Uri of the media item
      */
     public void addMedia(Uri uri) {
@@ -68,7 +65,7 @@ public void addMedia(Uri uri) {
 
     /**
      * Remove a media item from the adapter
-     * 
+     *
      * @param position Position of the media item to remove
      */
     public void removeMedia(int position) {
@@ -81,7 +78,7 @@ public void removeMedia(int position) {
 
     /**
      * Get all media URIs
-     * 
+     *
      * @return List of media URIs
      */
     public List getMediaUris() {
@@ -125,4 +122,4 @@ void bind(Uri mediaUri, int position) {
             });
         }
     }
-}
\ No newline at end of file
+}
diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/TagSupplier.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/TagSupplier.java
index 6ed1061..643e976 100644
--- a/libraries/sdk/src/main/java/com/fastcomments/sdk/TagSupplier.java
+++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/TagSupplier.java
@@ -1,7 +1,6 @@
 package com.fastcomments.sdk;
 
 import com.fastcomments.model.UserSessionInfo;
-
 import java.util.List;
 
 /**
@@ -14,9 +13,9 @@ public interface TagSupplier {
     /**
      * Get the list of tags to use for filtering feed posts.
      * This method is called when loading the feed and when creating new posts.
-     * 
+     *
      * @param currentUser The current authenticated user, or null if not authenticated
      * @return List of tags to filter by, or null/empty list for no filtering
      */
     List getTags(UserSessionInfo currentUser);
-}
\ No newline at end of file
+}
diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/ThemeColorResolver.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/ThemeColorResolver.java
index bdc1070..cdef64b 100644
--- a/libraries/sdk/src/main/java/com/fastcomments/sdk/ThemeColorResolver.java
+++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/ThemeColorResolver.java
@@ -11,10 +11,10 @@
  * Helper class to resolve colors from theme or fallback to resources
  */
 public class ThemeColorResolver {
-    
+
     /**
      * Get color from theme or fallback to resource
-     * 
+     *
      * @param context The context to use for resource lookup
      * @param theme The theme to check first (can be null)
      * @param colorGetter Function to get color from theme
@@ -22,10 +22,11 @@ public class ThemeColorResolver {
      * @return The resolved color
      */
     @ColorInt
-    public static int getColor(@NonNull Context context, 
-                              @Nullable FastCommentsTheme theme,
-                              @NonNull ColorGetter colorGetter,
-                              @ColorRes int fallbackResId) {
+    public static int getColor(
+            @NonNull Context context,
+            @Nullable FastCommentsTheme theme,
+            @NonNull ColorGetter colorGetter,
+            @ColorRes int fallbackResId) {
         if (theme != null) {
             Integer themeColor = colorGetter.getColor(theme);
             if (themeColor != null) {
@@ -34,7 +35,7 @@ public static int getColor(@NonNull Context context,
         }
         return ContextCompat.getColor(context, fallbackResId);
     }
-    
+
     /**
      * Functional interface for getting color from theme
      */
@@ -42,86 +43,130 @@ public interface ColorGetter {
         @Nullable
         Integer getColor(@NonNull FastCommentsTheme theme);
     }
-    
+
     // Convenience methods for specific colors
-    
+
     @ColorInt
     public static int getVoteCountColor(@NonNull Context context, @Nullable FastCommentsTheme theme) {
         return getColor(context, theme, FastCommentsTheme::getVoteCountColor, R.color.fastcomments_vote_count_color);
     }
-    
+
     @ColorInt
     public static int getVoteCountZeroColor(@NonNull Context context, @Nullable FastCommentsTheme theme) {
-        return getColor(context, theme, FastCommentsTheme::getVoteCountZeroColor, R.color.fastcomments_vote_count_zero_color);
+        return getColor(
+                context, theme, FastCommentsTheme::getVoteCountZeroColor, R.color.fastcomments_vote_count_zero_color);
     }
-    
+
     @ColorInt
     public static int getReplyButtonColor(@NonNull Context context, @Nullable FastCommentsTheme theme) {
-        return getColor(context, theme, FastCommentsTheme::getReplyButtonColor, R.color.fastcomments_reply_button_color);
+        return getColor(
+                context, theme, FastCommentsTheme::getReplyButtonColor, R.color.fastcomments_reply_button_color);
     }
-    
+
     @ColorInt
     public static int getToggleRepliesButtonColor(@NonNull Context context, @Nullable FastCommentsTheme theme) {
-        return getColor(context, theme, FastCommentsTheme::getToggleRepliesButtonColor, R.color.fastcomments_toggle_replies_button_color);
+        return getColor(
+                context,
+                theme,
+                FastCommentsTheme::getToggleRepliesButtonColor,
+                R.color.fastcomments_toggle_replies_button_color);
     }
-    
+
     @ColorInt
     public static int getActionButtonColor(@NonNull Context context, @Nullable FastCommentsTheme theme) {
-        return getColor(context, theme, FastCommentsTheme::getActionButtonColor, R.color.fastcomments_action_button_color);
+        return getColor(
+                context, theme, FastCommentsTheme::getActionButtonColor, R.color.fastcomments_action_button_color);
     }
-    
+
     @ColorInt
     public static int getLoadMoreButtonTextColor(@NonNull Context context, @Nullable FastCommentsTheme theme) {
-        return getColor(context, theme, FastCommentsTheme::getLoadMoreButtonTextColor, R.color.fastcomments_load_more_button_text_color);
+        return getColor(
+                context,
+                theme,
+                FastCommentsTheme::getLoadMoreButtonTextColor,
+                R.color.fastcomments_load_more_button_text_color);
     }
-    
+
     @ColorInt
     public static int getLinkColor(@NonNull Context context, @Nullable FastCommentsTheme theme) {
         return getColor(context, theme, FastCommentsTheme::getLinkColor, R.color.fastcomments_link_color);
     }
-    
+
     @ColorInt
     public static int getLinkColorPressed(@NonNull Context context, @Nullable FastCommentsTheme theme) {
-        return getColor(context, theme, FastCommentsTheme::getLinkColorPressed, R.color.fastcomments_link_color_pressed);
+        return getColor(
+                context, theme, FastCommentsTheme::getLinkColorPressed, R.color.fastcomments_link_color_pressed);
     }
-    
+
     @ColorInt
     public static int getOnlineIndicatorColor(@NonNull Context context, @Nullable FastCommentsTheme theme) {
-        return getColor(context, theme, FastCommentsTheme::getOnlineIndicatorColor, R.color.fastcomments_online_indicator_color);
+        return getColor(
+                context,
+                theme,
+                FastCommentsTheme::getOnlineIndicatorColor,
+                R.color.fastcomments_online_indicator_color);
     }
-    
+
     @ColorInt
     public static int getDialogHeaderBackgroundColor(@NonNull Context context, @Nullable FastCommentsTheme theme) {
-        return getColor(context, theme, FastCommentsTheme::getDialogHeaderBackgroundColor, R.color.fastcomments_dialog_header_background);
+        return getColor(
+                context,
+                theme,
+                FastCommentsTheme::getDialogHeaderBackgroundColor,
+                R.color.fastcomments_dialog_header_background);
     }
-    
+
     @ColorInt
     public static int getDialogHeaderTextColor(@NonNull Context context, @Nullable FastCommentsTheme theme) {
-        return getColor(context, theme, FastCommentsTheme::getDialogHeaderTextColor, R.color.fastcomments_dialog_header_text_color);
+        return getColor(
+                context,
+                theme,
+                FastCommentsTheme::getDialogHeaderTextColor,
+                R.color.fastcomments_dialog_header_text_color);
     }
 
     @ColorInt
     public static int getLiveChatHeaderBackgroundColor(@NonNull Context context, @Nullable FastCommentsTheme theme) {
-        return getColor(context, theme, FastCommentsTheme::getLiveChatHeaderBackgroundColor, R.color.fastcomments_live_chat_header_background);
+        return getColor(
+                context,
+                theme,
+                FastCommentsTheme::getLiveChatHeaderBackgroundColor,
+                R.color.fastcomments_live_chat_header_background);
     }
 
     @ColorInt
     public static int getLiveChatHeaderTextColor(@NonNull Context context, @Nullable FastCommentsTheme theme) {
-        return getColor(context, theme, FastCommentsTheme::getLiveChatHeaderTextColor, R.color.fastcomments_live_chat_header_text);
+        return getColor(
+                context,
+                theme,
+                FastCommentsTheme::getLiveChatHeaderTextColor,
+                R.color.fastcomments_live_chat_header_text);
     }
 
     @ColorInt
     public static int getLiveChatConnectedDotColor(@NonNull Context context, @Nullable FastCommentsTheme theme) {
-        return getColor(context, theme, FastCommentsTheme::getLiveChatConnectedDotColor, R.color.fastcomments_live_chat_connected_dot);
+        return getColor(
+                context,
+                theme,
+                FastCommentsTheme::getLiveChatConnectedDotColor,
+                R.color.fastcomments_live_chat_connected_dot);
     }
 
     @ColorInt
     public static int getLiveChatDisconnectedDotColor(@NonNull Context context, @Nullable FastCommentsTheme theme) {
-        return getColor(context, theme, FastCommentsTheme::getLiveChatDisconnectedDotColor, R.color.fastcomments_live_chat_disconnected_dot);
+        return getColor(
+                context,
+                theme,
+                FastCommentsTheme::getLiveChatDisconnectedDotColor,
+                R.color.fastcomments_live_chat_disconnected_dot);
     }
 
     @ColorInt
     public static int getLiveChatUserCountTextColor(@NonNull Context context, @Nullable FastCommentsTheme theme) {
-        return getColor(context, theme, FastCommentsTheme::getLiveChatUserCountTextColor, R.color.fastcomments_live_chat_user_count_text);
+        return getColor(
+                context,
+                theme,
+                FastCommentsTheme::getLiveChatUserCountTextColor,
+                R.color.fastcomments_live_chat_user_count_text);
     }
-}
\ No newline at end of file
+}
diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/URLDrawable.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/URLDrawable.java
index 880aaa0..9577cff 100644
--- a/libraries/sdk/src/main/java/com/fastcomments/sdk/URLDrawable.java
+++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/URLDrawable.java
@@ -20,4 +20,4 @@ public void draw(Canvas canvas) {
     public void setDrawable(Drawable drawable) {
         this.drawable = drawable;
     }
-}
\ No newline at end of file
+}
diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/UserClickContext.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/UserClickContext.java
index 39c308f..7215966 100644
--- a/libraries/sdk/src/main/java/com/fastcomments/sdk/UserClickContext.java
+++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/UserClickContext.java
@@ -1,45 +1,45 @@
 package com.fastcomments.sdk;
 
 import androidx.annotation.Nullable;
-import com.fastcomments.model.PublicComment;
 import com.fastcomments.model.FeedPost;
+import com.fastcomments.model.PublicComment;
 
 public class UserClickContext {
-    
+
     @Nullable
     private final PublicComment comment;
-    
+
     @Nullable
     private final FeedPost feedPost;
-    
+
     private UserClickContext(@Nullable PublicComment comment, @Nullable FeedPost feedPost) {
         this.comment = comment;
         this.feedPost = feedPost;
     }
-    
+
     public static UserClickContext fromComment(PublicComment comment) {
         return new UserClickContext(comment, null);
     }
-    
+
     public static UserClickContext fromFeedPost(FeedPost feedPost) {
         return new UserClickContext(null, feedPost);
     }
-    
+
     @Nullable
     public PublicComment getComment() {
         return comment;
     }
-    
+
     @Nullable
     public FeedPost getFeedPost() {
         return feedPost;
     }
-    
+
     public boolean isComment() {
         return comment != null;
     }
-    
+
     public boolean isFeedPost() {
         return feedPost != null;
     }
-}
\ No newline at end of file
+}
diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/UserClickSource.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/UserClickSource.java
index 06f8ff3..378d918 100644
--- a/libraries/sdk/src/main/java/com/fastcomments/sdk/UserClickSource.java
+++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/UserClickSource.java
@@ -8,9 +8,9 @@ public enum UserClickSource {
      * User clicked on the name/username
      */
     NAME,
-    
+
     /**
      * User clicked on the avatar/profile picture
      */
     AVATAR
-}
\ No newline at end of file
+}
diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/UserInfo.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/UserInfo.java
index 89133d6..cec77e4 100644
--- a/libraries/sdk/src/main/java/com/fastcomments/sdk/UserInfo.java
+++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/UserInfo.java
@@ -1,49 +1,41 @@
 package com.fastcomments.sdk;
 
-import com.fastcomments.model.PublicComment;
 import com.fastcomments.model.FeedPost;
+import com.fastcomments.model.PublicComment;
 
 public class UserInfo {
-    
+
     private final String userId;
     private final String userName;
     private final String userAvatarUrl;
-    
+
     public UserInfo(String userId, String userName, String userAvatarUrl) {
         this.userId = userId;
         this.userName = userName;
         this.userAvatarUrl = userAvatarUrl;
     }
-    
+
     public static UserInfo fromComment(PublicComment comment) {
-        return new UserInfo(
-            comment.getUserId(),
-            comment.getCommenterName(),
-            comment.getAvatarSrc()
-        );
+        return new UserInfo(comment.getUserId(), comment.getCommenterName(), comment.getAvatarSrc());
     }
-    
+
     public static UserInfo fromFeedPost(FeedPost feedPost) {
-        return new UserInfo(
-            feedPost.getFromUserId(),
-            feedPost.getFromUserDisplayName(),
-            feedPost.getFromUserAvatar()
-        );
+        return new UserInfo(feedPost.getFromUserId(), feedPost.getFromUserDisplayName(), feedPost.getFromUserAvatar());
     }
-    
+
     public String getUserId() {
         return userId;
     }
-    
+
     public String getUserName() {
         return userName;
     }
-    
+
     public String getUserAvatarUrl() {
         return userAvatarUrl;
     }
-    
+
     public String getDisplayName() {
         return userName != null ? userName : "Anonymous";
     }
-}
\ No newline at end of file
+}
diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/UserLoginDialog.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/UserLoginDialog.java
index 23721eb..8bc5734 100644
--- a/libraries/sdk/src/main/java/com/fastcomments/sdk/UserLoginDialog.java
+++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/UserLoginDialog.java
@@ -8,9 +8,7 @@
 import android.widget.Button;
 import android.widget.EditText;
 import android.widget.TextView;
-
 import com.fastcomments.core.CommentWidgetConfig;
-import com.fastcomments.model.UserSessionInfo;
 
 /**
  * Dialog for collecting user credentials when voting/commenting as an anonymous user
@@ -22,25 +20,27 @@ public class UserLoginDialog {
      */
     public interface OnUserCredentialsListener {
         void onUserCredentialsEntered(String username, String email);
+
         void onCancel();
     }
 
     /**
      * Show login dialog for anonymous users
-     * 
+     *
      * @param context Context for dialog
      * @param config FastComments widget config
      * @param action The action being performed ("vote" or "comment")
      * @param listener Callback for dialog actions
      */
-    public static void show(Context context, CommentWidgetConfig config, String action, OnUserCredentialsListener listener) {
+    public static void show(
+            Context context, CommentWidgetConfig config, String action, OnUserCredentialsListener listener) {
         AlertDialog.Builder builder = new AlertDialog.Builder(context);
         LayoutInflater inflater = LayoutInflater.from(context);
-        
+
         // Inflate custom dialog layout
         View dialogView = inflater.inflate(R.layout.dialog_user_login, null);
         builder.setView(dialogView);
-        
+
         // Get dialog elements
         TextView titleText = dialogView.findViewById(R.id.dialogTitle);
         TextView messageText = dialogView.findViewById(R.id.dialogMessage);
@@ -49,31 +49,27 @@ public static void show(Context context, CommentWidgetConfig config, String acti
         Button submitButton = dialogView.findViewById(R.id.submitButton);
         Button cancelButton = dialogView.findViewById(R.id.cancelButton);
         TextView anonNoticeText = dialogView.findViewById(R.id.anonNoticeText);
-        
+
         // Set title based on action
-        titleText.setText(action.equals("vote") ? 
-                R.string.vote_login_title : 
-                R.string.comment_login_title);
-                
+        titleText.setText(action.equals("vote") ? R.string.vote_login_title : R.string.comment_login_title);
+
         // Show anonymous notice if anonymous actions are allowed
-        boolean allowAnon = action.equals("vote") ? 
-                Boolean.TRUE.equals(config.allowAnonVotes) : 
-                Boolean.TRUE.equals(config.allowAnon);
-                
+        boolean allowAnon = action.equals("vote")
+                ? Boolean.TRUE.equals(config.allowAnonVotes)
+                : Boolean.TRUE.equals(config.allowAnon);
+
         if (allowAnon) {
             anonNoticeText.setVisibility(View.VISIBLE);
-            anonNoticeText.setText(action.equals("vote") ? 
-                    R.string.vote_anon_notice : 
-                    R.string.comment_anon_notice);
+            anonNoticeText.setText(action.equals("vote") ? R.string.vote_anon_notice : R.string.comment_anon_notice);
         } else {
             anonNoticeText.setVisibility(View.GONE);
         }
-        
+
         // Set default username if available
         if (config.defaultUsername != null && !config.defaultUsername.isEmpty()) {
             usernameInput.setText(config.defaultUsername);
         }
-        
+
         // Hide email input if emails are disabled
         if (Boolean.TRUE.equals(config.disableEmailInputs)) {
             emailInput.setVisibility(View.GONE);
@@ -81,37 +77,37 @@ public static void show(Context context, CommentWidgetConfig config, String acti
         } else {
             messageText.setText(R.string.enter_username_email_message);
         }
-        
+
         // Create and configure dialog
         final AlertDialog dialog = builder.create();
-        
+
         // Submit button click handler
         submitButton.setOnClickListener(v -> {
             String username = usernameInput.getText().toString().trim();
             String email = emailInput.getText().toString().trim();
-            
+
             // Validate inputs
             if (TextUtils.isEmpty(username)) {
                 usernameInput.setError(context.getString(R.string.username_required));
                 return;
             }
-            
+
             // Email validation (only if email inputs not disabled and anon not allowed)
             if (!Boolean.TRUE.equals(config.disableEmailInputs) && !allowAnon && TextUtils.isEmpty(email)) {
                 emailInput.setError(context.getString(R.string.email_required));
                 return;
             }
-            
+
             dialog.dismiss();
             listener.onUserCredentialsEntered(username, email);
         });
-        
+
         // Cancel button click handler
         cancelButton.setOnClickListener(v -> {
             dialog.dismiss();
             listener.onCancel();
         });
-        
+
         dialog.show();
     }
-}
\ No newline at end of file
+}
diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/UserMention.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/UserMention.java
index 5ff2a1a..f49dac4 100644
--- a/libraries/sdk/src/main/java/com/fastcomments/sdk/UserMention.java
+++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/UserMention.java
@@ -57,4 +57,4 @@ public boolean isNotificationSent() {
     public void setNotificationSent(boolean notificationSent) {
         this.notificationSent = notificationSent;
     }
-}
\ No newline at end of file
+}
diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/CodeFormattingToolbarButton.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/CodeFormattingToolbarButton.java
index 044c29a..58f5320 100644
--- a/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/CodeFormattingToolbarButton.java
+++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/CodeFormattingToolbarButton.java
@@ -1,7 +1,6 @@
 package com.fastcomments.sdk.examples;
 
 import android.view.View;
-
 import com.fastcomments.sdk.BottomCommentInputView;
 import com.fastcomments.sdk.CustomToolbarButton;
 import com.fastcomments.sdk.R;
@@ -78,4 +77,4 @@ public void onAttached(BottomCommentInputView view, View buttonView) {
     public void onDetached(BottomCommentInputView view, View buttonView) {
         // Optional: Clean up any resources when button is removed
     }
-}
\ No newline at end of file
+}
diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/EmojiPickerToolbarButton.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/EmojiPickerToolbarButton.java
index 954192d..6fa0cde 100644
--- a/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/EmojiPickerToolbarButton.java
+++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/EmojiPickerToolbarButton.java
@@ -5,9 +5,8 @@
 import android.util.TypedValue;
 import android.view.View;
 import android.widget.GridLayout;
-import android.widget.TextView;
 import android.widget.ScrollView;
-
+import android.widget.TextView;
 import com.fastcomments.sdk.BottomCommentInputView;
 import com.fastcomments.sdk.CustomToolbarButton;
 import com.fastcomments.sdk.R;
@@ -149,4 +148,4 @@ private void showEmojiPicker(BottomCommentInputView inputView, Context context)
 
         dialog.show();
     }
-}
\ No newline at end of file
+}
diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/GifPickerFeedToolbarButton.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/GifPickerFeedToolbarButton.java
index 2b2b2b6..0dfcf1d 100644
--- a/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/GifPickerFeedToolbarButton.java
+++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/GifPickerFeedToolbarButton.java
@@ -2,11 +2,9 @@
 
 import android.net.Uri;
 import android.view.View;
-
 import com.fastcomments.sdk.FeedCustomToolbarButton;
 import com.fastcomments.sdk.FeedPostCreateView;
 import com.fastcomments.sdk.R;
-
 import java.util.Random;
 
 /**
@@ -58,4 +56,4 @@ public void onClick(FeedPostCreateView view, View buttonView) {
     public String getId() {
         return BUTTON_ID;
     }
-}
\ No newline at end of file
+}
diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/GifPickerToolbarButton.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/GifPickerToolbarButton.java
index 012fa57..883edad 100644
--- a/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/GifPickerToolbarButton.java
+++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/GifPickerToolbarButton.java
@@ -2,7 +2,6 @@
 
 import android.view.View;
 import android.widget.Toast;
-
 import com.fastcomments.sdk.BottomCommentInputView;
 import com.fastcomments.sdk.CustomToolbarButton;
 import com.fastcomments.sdk.R;
@@ -16,9 +15,9 @@ public class GifPickerToolbarButton implements CustomToolbarButton {
     private static final String BUTTON_ID = "gif_picker";
 
     private static final String[] DEMO_GIFS = {
-            "https://media.giphy.com/media/l3q2K5jinAlChoCLS/giphy.gif",
-            "https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif",
-            "https://media.giphy.com/media/3oEjI6SIIHBdRxXI40/giphy.gif",
+        "https://media.giphy.com/media/l3q2K5jinAlChoCLS/giphy.gif",
+        "https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif",
+        "https://media.giphy.com/media/3oEjI6SIIHBdRxXI40/giphy.gif",
     };
 
     private int gifIndex = 0;
@@ -44,7 +43,8 @@ public void onClick(BottomCommentInputView view, View buttonView) {
         gifIndex++;
         String gifHtml = "\"GIF\"";
         view.insertHtmlAtCursor(gifHtml);
-        Toast.makeText(buttonView.getContext(), "GIF inserted!", Toast.LENGTH_SHORT).show();
+        Toast.makeText(buttonView.getContext(), "GIF inserted!", Toast.LENGTH_SHORT)
+                .show();
     }
 
     @Override
diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/ImagePickerToolbarButton.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/ImagePickerToolbarButton.java
index fcd4c5c..2033299 100644
--- a/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/ImagePickerToolbarButton.java
+++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/ImagePickerToolbarButton.java
@@ -4,7 +4,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.view.View;
-
 import com.fastcomments.sdk.BottomCommentInputView;
 import com.fastcomments.sdk.CustomToolbarButton;
 import com.fastcomments.sdk.R;
diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/MentionToolbarButton.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/MentionToolbarButton.java
index 9489860..a3509cc 100644
--- a/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/MentionToolbarButton.java
+++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/MentionToolbarButton.java
@@ -1,7 +1,6 @@
 package com.fastcomments.sdk.examples;
 
 import android.view.View;
-
 import com.fastcomments.sdk.BottomCommentInputView;
 import com.fastcomments.sdk.CustomToolbarButton;
 import com.fastcomments.sdk.R;
@@ -54,4 +53,4 @@ public boolean isEnabled(BottomCommentInputView view) {
     public boolean isVisible(BottomCommentInputView view) {
         return true; // Always visible
     }
-}
\ No newline at end of file
+}
diff --git a/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/ToolbarUsageExample.java b/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/ToolbarUsageExample.java
index 0677e69..cdc2f28 100644
--- a/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/ToolbarUsageExample.java
+++ b/libraries/sdk/src/main/java/com/fastcomments/sdk/examples/ToolbarUsageExample.java
@@ -1,7 +1,6 @@
 package com.fastcomments.sdk.examples;
 
 import android.content.Context;
-
 import com.fastcomments.sdk.BottomCommentInputView;
 import com.fastcomments.sdk.FastCommentsSDK;
 
@@ -129,4 +128,4 @@ public static void programmaticContentInsertion(BottomCommentInputView inputView
         // Move cursor to specific position
         inputView.setCursorPosition(10);
     }
-}
\ No newline at end of file
+}
diff --git a/libraries/sdk/src/test/java/com/fastcomments/sdk/CommentCRUDIntegrationTests.java b/libraries/sdk/src/test/java/com/fastcomments/sdk/CommentCRUDIntegrationTests.java
index b960f96..d7df786 100644
--- a/libraries/sdk/src/test/java/com/fastcomments/sdk/CommentCRUDIntegrationTests.java
+++ b/libraries/sdk/src/test/java/com/fastcomments/sdk/CommentCRUDIntegrationTests.java
@@ -1,18 +1,17 @@
 package com.fastcomments.sdk;
 
-import com.fastcomments.model.PublicComment;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
+import com.fastcomments.model.PublicComment;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
 /**
  * Integration tests for comment Create, Read, Update, Delete operations.
  */
@@ -82,7 +81,8 @@ public void testPostReply() throws Exception {
         // But the parent should have the child
         PublicComment reloadedParent = sdk2.commentsTree.getPublicComment(parent.getId());
         assertNotNull(reloadedParent);
-        assertTrue("Parent should have children",
+        assertTrue(
+                "Parent should have children",
                 reloadedParent.getChildCount() != null && reloadedParent.getChildCount() >= 1);
     }
 
@@ -166,8 +166,7 @@ public void testPaginationViaLoadMore() throws Exception {
         // Load more
         loadMoreSync(sdk2);
 
-        assertTrue("Total should increase after loadMore",
-                sdk2.commentsTree.totalSize() > firstPageSize);
+        assertTrue("Total should increase after loadMore", sdk2.commentsTree.totalSize() > firstPageSize);
     }
 
     @Test
diff --git a/libraries/sdk/src/test/java/com/fastcomments/sdk/CommentsTreeTests.java b/libraries/sdk/src/test/java/com/fastcomments/sdk/CommentsTreeTests.java
index 949418d..1b9b689 100644
--- a/libraries/sdk/src/test/java/com/fastcomments/sdk/CommentsTreeTests.java
+++ b/libraries/sdk/src/test/java/com/fastcomments/sdk/CommentsTreeTests.java
@@ -1,21 +1,5 @@
 package com.fastcomments.sdk;
 
-import com.fastcomments.model.PublicComment;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
-import java.time.LocalDate;
-import java.time.OffsetDateTime;
-import java.time.ZoneOffset;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -23,6 +7,18 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
 
+import com.fastcomments.model.PublicComment;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
 /**
  * Unit tests for CommentsTree in-memory data structure.
  */
@@ -40,11 +36,8 @@ public void setUp() {
 
     @Test
     public void testBuild() {
-        List comments = Arrays.asList(
-                MockComment.make("c1"),
-                MockComment.make("c2"),
-                MockComment.make("c3")
-        );
+        List comments =
+                Arrays.asList(MockComment.make("c1"), MockComment.make("c2"), MockComment.make("c3"));
 
         tree.build(comments);
 
@@ -61,8 +54,20 @@ public void testBuildWithChildren() {
         PublicComment child2 = MockComment.make("child2", null, "parent");
         List children = Arrays.asList(child1, child2);
 
-        PublicComment parent = MockComment.make("parent", null, "Test User", "

Parent

", - null, OffsetDateTime.now(), 0, true, 2, children, null, null, null); + PublicComment parent = MockComment.make( + "parent", + null, + "Test User", + "

Parent

", + null, + OffsetDateTime.now(), + 0, + true, + 2, + children, + null, + null, + null); tree.build(Collections.singletonList(parent)); @@ -115,8 +120,8 @@ public void testAddCommentRootDisplayNow() { // Should be in visibleNodes boolean found = false; for (RenderableNode node : tree.visibleNodes) { - if (node instanceof RenderableComment && - "c2".equals(((RenderableComment) node).getComment().getId())) { + if (node instanceof RenderableComment + && "c2".equals(((RenderableComment) node).getComment().getId())) { found = true; break; } @@ -184,9 +189,20 @@ public void testRemoveNonexistent() { @Test public void testUpdateComment() { - tree.build(Collections.singletonList( - MockComment.make("c1", null, "Test User", "

original

", - null, OffsetDateTime.now(), 0, true, null, null, null, null, null))); + tree.build(Collections.singletonList(MockComment.make( + "c1", + null, + "Test User", + "

original

", + null, + OffsetDateTime.now(), + 0, + true, + null, + null, + null, + null, + null))); PublicComment comment = tree.getPublicComment("c1"); assertNotNull(comment); @@ -214,7 +230,8 @@ public void testShowNewRootComments() { tree.showNewRootComments(); int visibleCommentsAfter = countVisibleComments(); - assertTrue("Visible comments should increase after showNewRootComments", + assertTrue( + "Visible comments should increase after showNewRootComments", visibleCommentsAfter > visibleCommentsBefore); } @@ -240,10 +257,7 @@ public void testShowNewChildComments() { @Test public void testPresenceUpdate() { List comments = Arrays.asList( - MockComment.make("c1", "user1"), - MockComment.make("c2", "user1"), - MockComment.make("c3", "user2") - ); + MockComment.make("c1", "user1"), MockComment.make("c2", "user1"), MockComment.make("c3", "user2")); tree.build(comments); tree.updateUserPresence("user1", true); @@ -259,10 +273,7 @@ public void testPresenceUpdate() { @Test public void testPresenceIndexingAndPropagation() { - List comments = Arrays.asList( - MockComment.make("c1", "user1"), - MockComment.make("c2", "user1") - ); + List comments = Arrays.asList(MockComment.make("c1", "user1"), MockComment.make("c2", "user1")); tree.build(comments); // commentsByUserId should have entries for "user1" @@ -285,8 +296,8 @@ public void testLiveChatDateSeparators() { List comments = Arrays.asList( MockComment.make("c1", null, "User", "

Day 1

", null, day1, 0, true, null, null, null, null, null), - MockComment.make("c2", null, "User", "

Day 2

", null, day2, 0, true, null, null, null, null, null) - ); + MockComment.make( + "c2", null, "User", "

Day 2

", null, day2, 0, true, null, null, null, null, null)); tree.build(comments); @@ -302,14 +313,24 @@ public void testLiveChatDateSeparators() { @Test public void testAddForParent() { - PublicComment parent = MockComment.make("parent", null, "User", "

Parent

", - null, OffsetDateTime.now(), 0, true, 5, null, null, null, null); + PublicComment parent = MockComment.make( + "parent", + null, + "User", + "

Parent

", + null, + OffsetDateTime.now(), + 0, + true, + 5, + null, + null, + null, + null); tree.build(Collections.singletonList(parent)); - List children = Arrays.asList( - MockComment.make("child1", null, "parent"), - MockComment.make("child2", null, "parent") - ); + List children = + Arrays.asList(MockComment.make("child1", null, "parent"), MockComment.make("child2", null, "parent")); tree.addForParent("parent", children); assertEquals(3, tree.totalSize()); @@ -326,10 +347,7 @@ public void testBuildNullComments() { @Test public void testResetPresence() { - List comments = Arrays.asList( - MockComment.make("c1", "user1"), - MockComment.make("c2", "user2") - ); + List comments = Arrays.asList(MockComment.make("c1", "user1"), MockComment.make("c2", "user2")); tree.build(comments); // Manually set online diff --git a/libraries/sdk/src/test/java/com/fastcomments/sdk/ExampleUnitTest.java b/libraries/sdk/src/test/java/com/fastcomments/sdk/ExampleUnitTest.java index dc6bc5d..2ba1dc2 100644 --- a/libraries/sdk/src/test/java/com/fastcomments/sdk/ExampleUnitTest.java +++ b/libraries/sdk/src/test/java/com/fastcomments/sdk/ExampleUnitTest.java @@ -1,9 +1,9 @@ package com.fastcomments.sdk; -import org.junit.Test; - import static org.junit.Assert.*; +import org.junit.Test; + /** * Example local unit test, which will execute on the development machine (host). * @@ -14,4 +14,4 @@ public class ExampleUnitTest { public void addition_isCorrect() { assertEquals(4, 2 + 2); } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/test/java/com/fastcomments/sdk/FeedIntegrationTests.java b/libraries/sdk/src/test/java/com/fastcomments/sdk/FeedIntegrationTests.java index e00e84d..f8f0836 100644 --- a/libraries/sdk/src/test/java/com/fastcomments/sdk/FeedIntegrationTests.java +++ b/libraries/sdk/src/test/java/com/fastcomments/sdk/FeedIntegrationTests.java @@ -1,18 +1,17 @@ package com.fastcomments.sdk; -import com.fastcomments.model.FeedPost; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import com.fastcomments.model.FeedPost; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + /** * Integration tests for Feed SDK operations. */ @@ -74,8 +73,7 @@ public void testLikePost() throws Exception { likeFeedPostSync(sdk, post.getId()); - assertTrue("User should have reacted to post", - sdk.hasUserReactedToPost(post.getId(), "l")); + assertTrue("User should have reacted to post", sdk.hasUserReactedToPost(post.getId(), "l")); assertEquals("Like count should be 1", 1, sdk.getPostLikeCount(post.getId())); } @@ -96,8 +94,7 @@ public void testUnlikePost() throws Exception { // Unlike (toggle) likeFeedPostSync(sdk, post.getId()); - assertFalse("User should no longer have reacted after toggle", - sdk.hasUserReactedToPost(post.getId(), "l")); + assertFalse("User should no longer have reacted after toggle", sdk.hasUserReactedToPost(post.getId(), "l")); assertEquals("Like count should be 0 after toggle", 0, sdk.getPostLikeCount(post.getId())); } @@ -127,7 +124,10 @@ public void testSaveRestorePaginationState() throws Exception { sdk2.restorePaginationState(state); assertNotNull(sdk2.getFeedPosts()); - assertEquals("Feed posts count should match", state.getFeedPosts().size(), sdk2.getFeedPosts().size()); + assertEquals( + "Feed posts count should match", + state.getFeedPosts().size(), + sdk2.getFeedPosts().size()); } @Test @@ -140,9 +140,9 @@ public void testCreateCommentsSDKForPost() throws Exception { FastCommentsSDK commentsSDK = sdk.createCommentsSDKForPost(post); assertNotNull(commentsSDK); - assertEquals("Comments SDK tenantId should match", - getTenantId(), commentsSDK.getConfig().tenantId); - assertTrue("Comments SDK urlId should start with 'post:'", + assertEquals("Comments SDK tenantId should match", getTenantId(), commentsSDK.getConfig().tenantId); + assertTrue( + "Comments SDK urlId should start with 'post:'", commentsSDK.getConfig().urlId.startsWith("post:")); } diff --git a/libraries/sdk/src/test/java/com/fastcomments/sdk/FeedPostsAdapterFollowButtonTest.java b/libraries/sdk/src/test/java/com/fastcomments/sdk/FeedPostsAdapterFollowButtonTest.java index 073cfa4..0e9c656 100644 --- a/libraries/sdk/src/test/java/com/fastcomments/sdk/FeedPostsAdapterFollowButtonTest.java +++ b/libraries/sdk/src/test/java/com/fastcomments/sdk/FeedPostsAdapterFollowButtonTest.java @@ -15,14 +15,14 @@ import android.view.View; import android.widget.FrameLayout; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; - import com.fastcomments.core.CommentWidgetConfig; import com.fastcomments.model.FeedPost; import com.fastcomments.model.UserSessionInfo; - +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -32,10 +32,6 @@ import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLooper; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - /** * Unit tests for the follow / unfollow button wiring inside * {@link FeedPostsAdapter}. Covers visibility rules, state queries, click @@ -94,8 +90,8 @@ private FeedPost makePost(String userId, String displayName) { private FeedPostsAdapter.FeedPostViewHolder createAndBind(FeedPost post) { posts.clear(); posts.add(post); - FeedPostsAdapter.FeedPostViewHolder holder = adapter.onCreateViewHolder( - parent, FeedPostType.TEXT_ONLY.ordinal()); + FeedPostsAdapter.FeedPostViewHolder holder = + adapter.onCreateViewHolder(parent, FeedPostType.TEXT_ONLY.ordinal()); adapter.onBindViewHolder(holder, 0); return holder; } @@ -134,8 +130,7 @@ public void bindFollowButton_anonymousViewer_hidesButton() { public void bindFollowButton_selfPost_hidesButton() { when(sdk.getFollowStateProvider()).thenReturn(new StubProvider(false)); - FeedPostsAdapter.FeedPostViewHolder holder = - createAndBind(makePost("viewer-1", "Me")); + FeedPostsAdapter.FeedPostViewHolder holder = createAndBind(makePost("viewer-1", "Me")); assertEquals(View.GONE, followButton(holder).getVisibility()); } @@ -235,8 +230,7 @@ public void staleCallback_afterRecycling_doesNotPaintOldRow() { when(sdk.getFollowStateProvider()).thenReturn(provider); // Bind holder to Alice - FeedPostsAdapter.FeedPostViewHolder holder = - createAndBind(makePost("u-alice", "Alice")); + FeedPostsAdapter.FeedPostViewHolder holder = createAndBind(makePost("u-alice", "Alice")); TextView btn = followButton(holder); assertEquals("Follow", btn.getText().toString()); @@ -279,8 +273,7 @@ public void clickFollow_doesNotBroadcastUntilProviderResolves() { RecordingProvider provider = new RecordingProvider(); when(sdk.getFollowStateProvider()).thenReturn(provider); - FeedPostsAdapter.FeedPostViewHolder holder = - createAndBind(makePost("u-alice", "Alice")); + FeedPostsAdapter.FeedPostViewHolder holder = createAndBind(makePost("u-alice", "Alice")); followButton(holder).performClick(); // Provider hasn't called back yet → nothing broadcast either coarse @@ -303,8 +296,7 @@ public void clickFollow_thenRowRecycled_stillBroadcastsInvalidation() { RecordingProvider provider = new RecordingProvider(); when(sdk.getFollowStateProvider()).thenReturn(provider); - FeedPostsAdapter.FeedPostViewHolder holder = - createAndBind(makePost("u-alice", "Alice")); + FeedPostsAdapter.FeedPostViewHolder holder = createAndBind(makePost("u-alice", "Alice")); followButton(holder).performClick(); assertNotNull(provider.lastCallback); @@ -337,8 +329,7 @@ public void partialBind_withFollowStatePayload_refreshesFromProvider() { RecordingProvider provider = new RecordingProvider(); when(sdk.getFollowStateProvider()).thenReturn(provider); - FeedPostsAdapter.FeedPostViewHolder holder = - createAndBind(makePost("u-alice", "Alice")); + FeedPostsAdapter.FeedPostViewHolder holder = createAndBind(makePost("u-alice", "Alice")); TextView btn = followButton(holder); assertEquals("Follow", btn.getText().toString()); @@ -347,8 +338,7 @@ public void partialBind_withFollowStatePayload_refreshesFromProvider() { // another row). A payload-only rebind should pick that up without a // full rebind of the rest of the row. provider.backingState = true; - adapter.onBindViewHolder(holder, 0, - Collections.singletonList(FeedPostsAdapter.UpdateType.FOLLOW_STATE_UPDATE)); + adapter.onBindViewHolder(holder, 0, Collections.singletonList(FeedPostsAdapter.UpdateType.FOLLOW_STATE_UPDATE)); assertEquals("Following", btn.getText().toString()); assertTrue(btn.isEnabled()); @@ -396,8 +386,7 @@ public void onInvalidation_broadcast_notifiesItemRangeChangedWithFollowStatePayl captor.getValue().onFollowStateInvalidated(null); - verify(observer).onItemRangeChanged(eq(0), eq(3), - eq(FeedPostsAdapter.UpdateType.FOLLOW_STATE_UPDATE)); + verify(observer).onItemRangeChanged(eq(0), eq(3), eq(FeedPostsAdapter.UpdateType.FOLLOW_STATE_UPDATE)); } /** @@ -414,11 +403,11 @@ public void onInvalidation_scopedToUserId_onlyNotifiesMatchingRows() { verify(sdk).addFollowStateInvalidationListener(captor.capture()); posts.clear(); - posts.add(makePost("u-alice", "Alice")); // 0 - posts.add(makePost("u-bob", "Bob")); // 1 - posts.add(makePost("u-alice", "Alice")); // 2 - posts.add(makePost("u-bob", "Bob")); // 3 - posts.add(makePost("u-alice", "Alice")); // 4 + posts.add(makePost("u-alice", "Alice")); // 0 + posts.add(makePost("u-bob", "Bob")); // 1 + posts.add(makePost("u-alice", "Alice")); // 2 + posts.add(makePost("u-bob", "Bob")); // 3 + posts.add(makePost("u-alice", "Alice")); // 4 RecyclerView.AdapterDataObserver observer = mock(RecyclerView.AdapterDataObserver.class); adapter.registerAdapterDataObserver(observer); @@ -426,17 +415,12 @@ public void onInvalidation_scopedToUserId_onlyNotifiesMatchingRows() { captor.getValue().onFollowStateInvalidated("u-alice"); // Only the three Alice rows should be invalidated. - verify(observer).onItemRangeChanged(eq(0), eq(1), - eq(FeedPostsAdapter.UpdateType.FOLLOW_STATE_UPDATE)); - verify(observer).onItemRangeChanged(eq(2), eq(1), - eq(FeedPostsAdapter.UpdateType.FOLLOW_STATE_UPDATE)); - verify(observer).onItemRangeChanged(eq(4), eq(1), - eq(FeedPostsAdapter.UpdateType.FOLLOW_STATE_UPDATE)); + verify(observer).onItemRangeChanged(eq(0), eq(1), eq(FeedPostsAdapter.UpdateType.FOLLOW_STATE_UPDATE)); + verify(observer).onItemRangeChanged(eq(2), eq(1), eq(FeedPostsAdapter.UpdateType.FOLLOW_STATE_UPDATE)); + verify(observer).onItemRangeChanged(eq(4), eq(1), eq(FeedPostsAdapter.UpdateType.FOLLOW_STATE_UPDATE)); // And none of the Bob rows. - verify(observer, never()).onItemRangeChanged(eq(1), eq(1), - eq(FeedPostsAdapter.UpdateType.FOLLOW_STATE_UPDATE)); - verify(observer, never()).onItemRangeChanged(eq(3), eq(1), - eq(FeedPostsAdapter.UpdateType.FOLLOW_STATE_UPDATE)); + verify(observer, never()).onItemRangeChanged(eq(1), eq(1), eq(FeedPostsAdapter.UpdateType.FOLLOW_STATE_UPDATE)); + verify(observer, never()).onItemRangeChanged(eq(3), eq(1), eq(FeedPostsAdapter.UpdateType.FOLLOW_STATE_UPDATE)); } /** @@ -448,8 +432,7 @@ public void clickFollow_broadcastsInvalidationScopedToAuthorUserId() { RecordingProvider provider = new RecordingProvider(); when(sdk.getFollowStateProvider()).thenReturn(provider); - FeedPostsAdapter.FeedPostViewHolder holder = - createAndBind(makePost("u-alice", "Alice")); + FeedPostsAdapter.FeedPostViewHolder holder = createAndBind(makePost("u-alice", "Alice")); followButton(holder).performClick(); provider.backingState = true; @@ -475,8 +458,8 @@ public boolean isFollowing(@NonNull UserInfo user) { } @Override - public void onFollowStateChangeRequested(@NonNull UserInfo user, boolean desiredFollowing, - @NonNull FollowStateCallback resultCallback) { + public void onFollowStateChangeRequested( + @NonNull UserInfo user, boolean desiredFollowing, @NonNull FollowStateCallback resultCallback) { // No-op. Tests using this stub never tap the button. } } @@ -494,8 +477,8 @@ public boolean isFollowing(@NonNull UserInfo user) { } @Override - public void onFollowStateChangeRequested(@NonNull UserInfo user, boolean desiredFollowing, - @NonNull FollowStateCallback resultCallback) { + public void onFollowStateChangeRequested( + @NonNull UserInfo user, boolean desiredFollowing, @NonNull FollowStateCallback resultCallback) { lastUser = user; lastDesired = desiredFollowing; lastCallback = resultCallback; diff --git a/libraries/sdk/src/test/java/com/fastcomments/sdk/FeedPostsAdapterTest.java b/libraries/sdk/src/test/java/com/fastcomments/sdk/FeedPostsAdapterTest.java index 6649012..87cb878 100644 --- a/libraries/sdk/src/test/java/com/fastcomments/sdk/FeedPostsAdapterTest.java +++ b/libraries/sdk/src/test/java/com/fastcomments/sdk/FeedPostsAdapterTest.java @@ -1,18 +1,17 @@ package com.fastcomments.sdk; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; -import android.widget.ImageButton; - import com.fastcomments.core.CommentWidgetConfig; import com.fastcomments.model.FeedPost; import com.fastcomments.model.FeedPostLink; import com.fastcomments.model.FeedPostMediaItem; import com.fastcomments.model.FeedPostMediaItemAsset; - +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -20,10 +19,6 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - /** * Unit tests for the FeedPostsAdapter, focusing on the multi-image navigation */ @@ -42,7 +37,7 @@ public class FeedPostsAdapterTest { @Before public void setup() { MockitoAnnotations.openMocks(this); - + // Set up SDK config CommentWidgetConfig config = new CommentWidgetConfig(); when(sdk.getConfig()).thenReturn(config); @@ -52,32 +47,32 @@ public void setup() { public void testDeterminePostType() { List posts = new ArrayList<>(); FeedPostsAdapter adapter = new FeedPostsAdapter(context, posts, sdk, listener); - + // Create test posts FeedPost textPost = createTextOnlyPost(); FeedPost singleImagePost = createSingleImagePost(); FeedPost multiImagePost = createMultiImagePost(); FeedPost taskPost = createTaskPost(); - + // Test through reflection since the method is private FeedPostType textType = FeedPostType.TEXT_ONLY; FeedPostType singleImageType = FeedPostType.SINGLE_IMAGE; FeedPostType multiImageType = FeedPostType.MULTI_IMAGE; FeedPostType taskType = FeedPostType.TASK; - + // We can't test private methods directly, so we'll test the getItemViewType posts.clear(); posts.add(textPost); assertEquals(textType.ordinal(), adapter.getItemViewType(0)); - + posts.clear(); posts.add(singleImagePost); assertEquals(singleImageType.ordinal(), adapter.getItemViewType(0)); - + posts.clear(); posts.add(multiImagePost); assertEquals(multiImageType.ordinal(), adapter.getItemViewType(0)); - + posts.clear(); posts.add(taskPost); assertEquals(taskType.ordinal(), adapter.getItemViewType(0)); @@ -93,14 +88,14 @@ private FeedPost createTextOnlyPost() { private FeedPost createSingleImagePost() { FeedPost post = new FeedPost(); post.setContentHTML("

This is a post with a single image

"); - + List media = new ArrayList<>(); FeedPostMediaItem item = new FeedPostMediaItem(); FeedPostMediaItemAsset sizes = new FeedPostMediaItemAsset(); sizes.setSrc("https://example.com/image.jpg"); item.setSizes(Arrays.asList(sizes)); media.add(item); - + post.setMedia(media); return post; } @@ -108,23 +103,23 @@ private FeedPost createSingleImagePost() { private FeedPost createMultiImagePost() { FeedPost post = new FeedPost(); post.setContentHTML("

This is a post with multiple images

"); - + List media = new ArrayList<>(); - + // Add first image FeedPostMediaItem item1 = new FeedPostMediaItem(); FeedPostMediaItemAsset sizes1 = new FeedPostMediaItemAsset(); sizes1.setSrc("https://example.com/image1.jpg"); item1.setSizes(Arrays.asList(sizes1)); media.add(item1); - + // Add second image FeedPostMediaItem item2 = new FeedPostMediaItem(); FeedPostMediaItemAsset sizes2 = new FeedPostMediaItemAsset(); sizes2.setSrc("https://example.com/image2.jpg"); item2.setSizes(Arrays.asList(sizes2)); media.add(item2); - + post.setMedia(media); return post; } @@ -141,4 +136,4 @@ private FeedPost createTaskPost() { return post; } -} \ No newline at end of file +} diff --git a/libraries/sdk/src/test/java/com/fastcomments/sdk/FeedStateTests.java b/libraries/sdk/src/test/java/com/fastcomments/sdk/FeedStateTests.java index 08b3a8d..a4e168b 100644 --- a/libraries/sdk/src/test/java/com/fastcomments/sdk/FeedStateTests.java +++ b/libraries/sdk/src/test/java/com/fastcomments/sdk/FeedStateTests.java @@ -1,6 +1,9 @@ package com.fastcomments.sdk; -import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -8,11 +11,7 @@ import java.io.ObjectOutputStream; import java.util.HashMap; import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; +import org.junit.Test; /** * Unit tests for FastCommentsFeedSDK.FeedState serialization and defaults. diff --git a/libraries/sdk/src/test/java/com/fastcomments/sdk/HtmlLinkHandlerTest.java b/libraries/sdk/src/test/java/com/fastcomments/sdk/HtmlLinkHandlerTest.java index 8852756..7871b70 100644 --- a/libraries/sdk/src/test/java/com/fastcomments/sdk/HtmlLinkHandlerTest.java +++ b/libraries/sdk/src/test/java/com/fastcomments/sdk/HtmlLinkHandlerTest.java @@ -10,7 +10,6 @@ import android.text.Spanned; import android.text.style.ImageSpan; import android.text.style.URLSpan; - import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -39,8 +38,7 @@ public void linkWrappingImagePrefersAnchorHref() { text.setSpan(image, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); text.setSpan(link, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - assertEquals("https://example.com/full.jpg", - HtmlLinkHandler.findWrappedImageUrl(text, link)); + assertEquals("https://example.com/full.jpg", HtmlLinkHandler.findWrappedImageUrl(text, link)); } @Test @@ -52,8 +50,7 @@ public void linkWrappingImageFallsBackToImageSource() { text.setSpan(image, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); text.setSpan(link, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - assertEquals("https://example.com/thumb.jpg", - HtmlLinkHandler.findWrappedImageUrl(text, link)); + assertEquals("https://example.com/thumb.jpg", HtmlLinkHandler.findWrappedImageUrl(text, link)); } @Test diff --git a/libraries/sdk/src/test/java/com/fastcomments/sdk/IntegrationTestBase.java b/libraries/sdk/src/test/java/com/fastcomments/sdk/IntegrationTestBase.java index 88cec92..fb37042 100644 --- a/libraries/sdk/src/test/java/com/fastcomments/sdk/IntegrationTestBase.java +++ b/libraries/sdk/src/test/java/com/fastcomments/sdk/IntegrationTestBase.java @@ -1,27 +1,24 @@ package com.fastcomments.sdk; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + import com.fastcomments.core.CommentWidgetConfig; import com.fastcomments.core.sso.FastCommentsSSO; import com.fastcomments.core.sso.SecureSSOUserData; import com.fastcomments.model.APIEmptyResponse; import com.fastcomments.model.APIError; -import com.fastcomments.model.PublicComment; -import com.fastcomments.model.SetCommentTextResult; -import com.fastcomments.model.VoteResponse; -import com.fastcomments.model.VoteDeleteResponse; import com.fastcomments.model.BlockSuccess; -import com.fastcomments.model.UnblockSuccess; import com.fastcomments.model.ChangeCommentPinStatusResponse; import com.fastcomments.model.CreateFeedPostParams; import com.fastcomments.model.FeedPost; import com.fastcomments.model.GetCommentsResponseWithPresencePublicComment; +import com.fastcomments.model.PublicComment; import com.fastcomments.model.PublicFeedPostsResponse; - -import org.json.JSONObject; -import org.junit.After; -import org.junit.Before; -import org.robolectric.shadows.ShadowLooper; - +import com.fastcomments.model.SetCommentTextResult; +import com.fastcomments.model.UnblockSuccess; +import com.fastcomments.model.VoteDeleteResponse; +import com.fastcomments.model.VoteResponse; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -32,7 +29,6 @@ import java.util.function.BooleanSupplier; import java.util.regex.Matcher; import java.util.regex.Pattern; - import okhttp3.Cookie; import okhttp3.CookieJar; import okhttp3.FormBody; @@ -41,9 +37,10 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; - -import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.robolectric.shadows.ShadowLooper; /** * Base class for integration tests that hit the real FastComments API. @@ -119,8 +116,8 @@ public List loadForRequest(HttpUrl url) { // 2. Get tenant ID via e2e test API Request tenantRequest = new Request.Builder() - .url(TestConfig.HOST + "/test-e2e/api/tenant/by-email/" + testTenantEmail - + "?API_KEY=" + TestConfig.E2E_API_KEY) + .url(TestConfig.HOST + "/test-e2e/api/tenant/by-email/" + testTenantEmail + "?API_KEY=" + + TestConfig.E2E_API_KEY) .get() .build(); @@ -159,10 +156,16 @@ public List loadForRequest(HttpUrl url) { public void tearDown() throws Exception { // Close all SDK WebSocket connections for (FastCommentsSDK sdk : sdksToCleanup) { - try { sdk.cleanup(); } catch (Exception ignored) {} + try { + sdk.cleanup(); + } catch (Exception ignored) { + } } for (FastCommentsFeedSDK sdk : feedSdksToCleanup) { - try { sdk.cleanup(); } catch (Exception ignored) {} + try { + sdk.cleanup(); + } catch (Exception ignored) { + } } // Clean up comments for each urlId @@ -174,14 +177,15 @@ public void tearDown() throws Exception { if (testTenantEmail != null) { try { Request deleteRequest = new Request.Builder() - .url(TestConfig.HOST + "/test-e2e/api/tenant/by-email/" + testTenantEmail - + "?API_KEY=" + TestConfig.E2E_API_KEY) + .url(TestConfig.HOST + "/test-e2e/api/tenant/by-email/" + testTenantEmail + "?API_KEY=" + + TestConfig.E2E_API_KEY) .delete() .build(); try (Response ignored = httpClient.newCall(deleteRequest).execute()) { // Best effort } - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } urlIdsToCleanup.clear(); @@ -216,8 +220,7 @@ protected String makeSSOToken(String userId) { userId, "tester-" + userId.substring(0, Math.min(8, userId.length())) + "@fctest.com", "Tester " + userId.substring(0, Math.min(6, userId.length())), - "" - ); + ""); FastCommentsSSO sso = FastCommentsSSO.createSecure(testTenantApiKey, userData); return sso.prepareToSend(); } catch (Exception e) { @@ -235,8 +238,7 @@ protected String makeAdminSSOToken(String userId) { userId, "admin-" + userId.substring(0, Math.min(8, userId.length())) + "@fctest.com", "Admin " + userId.substring(0, Math.min(6, userId.length())), - "" - ); + ""); userData.isAdmin = true; FastCommentsSSO sso = FastCommentsSSO.createSecure(testTenantApiKey, userData); return sso.prepareToSend(); @@ -456,7 +458,8 @@ public boolean onSuccess(APIEmptyResponse response) { } } - protected SetCommentTextResult editCommentSync(FastCommentsSDK sdk, String commentId, String newText) throws Exception { + protected SetCommentTextResult editCommentSync(FastCommentsSDK sdk, String commentId, String newText) + throws Exception { AtomicReference resultRef = new AtomicReference<>(); AtomicReference errorRef = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); @@ -541,20 +544,23 @@ protected void blockUserSync(FastCommentsSDK sdk, String commentId) throws Excep AtomicReference errorRef = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); - sdk.blockUserFromComment(commentId, new java.util.ArrayList<>(sdk.commentsTree.commentsById.keySet()), new FCCallback() { - @Override - public boolean onFailure(APIError error) { - errorRef.set(error); - latch.countDown(); - return FCCallback.CONSUME; - } + sdk.blockUserFromComment( + commentId, + new java.util.ArrayList<>(sdk.commentsTree.commentsById.keySet()), + new FCCallback() { + @Override + public boolean onFailure(APIError error) { + errorRef.set(error); + latch.countDown(); + return FCCallback.CONSUME; + } - @Override - public boolean onSuccess(BlockSuccess response) { - latch.countDown(); - return FCCallback.CONSUME; - } - }); + @Override + public boolean onSuccess(BlockSuccess response) { + latch.countDown(); + return FCCallback.CONSUME; + } + }); awaitLatch(latch); @@ -747,20 +753,23 @@ protected void unblockUserSync(FastCommentsSDK sdk, String commentId) throws Exc AtomicReference errorRef = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); - sdk.unblockUserFromComment(commentId, new java.util.ArrayList<>(sdk.commentsTree.commentsById.keySet()), new FCCallback() { - @Override - public boolean onFailure(APIError error) { - errorRef.set(error); - latch.countDown(); - return FCCallback.CONSUME; - } + sdk.unblockUserFromComment( + commentId, + new java.util.ArrayList<>(sdk.commentsTree.commentsById.keySet()), + new FCCallback() { + @Override + public boolean onFailure(APIError error) { + errorRef.set(error); + latch.countDown(); + return FCCallback.CONSUME; + } - @Override - public boolean onSuccess(UnblockSuccess response) { - latch.countDown(); - return FCCallback.CONSUME; - } - }); + @Override + public boolean onSuccess(UnblockSuccess response) { + latch.countDown(); + return FCCallback.CONSUME; + } + }); awaitLatch(latch); diff --git a/libraries/sdk/src/test/java/com/fastcomments/sdk/MockComment.java b/libraries/sdk/src/test/java/com/fastcomments/sdk/MockComment.java index 882ffa3..1482ad4 100644 --- a/libraries/sdk/src/test/java/com/fastcomments/sdk/MockComment.java +++ b/libraries/sdk/src/test/java/com/fastcomments/sdk/MockComment.java @@ -1,7 +1,6 @@ package com.fastcomments.sdk; import com.fastcomments.model.PublicComment; - import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; @@ -16,33 +15,70 @@ static PublicComment make() { } static PublicComment make(String id) { - return make(id, null, "Test User", "

Test comment

", null, - OffsetDateTime.now(), 0, true, null, null, null, null, null); + return make( + id, + null, + "Test User", + "

Test comment

", + null, + OffsetDateTime.now(), + 0, + true, + null, + null, + null, + null, + null); } static PublicComment make(String id, String userId) { - return make(id, userId, "Test User", "

Test comment

", null, - OffsetDateTime.now(), 0, true, null, null, null, null, null); + return make( + id, + userId, + "Test User", + "

Test comment

", + null, + OffsetDateTime.now(), + 0, + true, + null, + null, + null, + null, + null); } static PublicComment make(String id, String userId, String parentId) { - return make(id, userId, "Test User", "

Test comment

", parentId, - OffsetDateTime.now(), 0, true, null, null, null, null, null); + return make( + id, + userId, + "Test User", + "

Test comment

", + parentId, + OffsetDateTime.now(), + 0, + true, + null, + null, + null, + null, + null); } - static PublicComment make(String id, - String userId, - String commenterName, - String commentHTML, - String parentId, - OffsetDateTime date, - Integer votes, - Boolean verified, - Integer childCount, - List children, - Boolean isPinned, - Boolean isDeleted, - Boolean isLocked) { + static PublicComment make( + String id, + String userId, + String commenterName, + String commentHTML, + String parentId, + OffsetDateTime date, + Integer votes, + Boolean verified, + Integer childCount, + List children, + Boolean isPinned, + Boolean isDeleted, + Boolean isLocked) { PublicComment comment = new PublicComment(); comment.setId(id); comment.setUserId(userId); diff --git a/libraries/sdk/src/test/java/com/fastcomments/sdk/ModerationIntegrationTests.java b/libraries/sdk/src/test/java/com/fastcomments/sdk/ModerationIntegrationTests.java index 3b06e9b..86fac8f 100644 --- a/libraries/sdk/src/test/java/com/fastcomments/sdk/ModerationIntegrationTests.java +++ b/libraries/sdk/src/test/java/com/fastcomments/sdk/ModerationIntegrationTests.java @@ -1,17 +1,15 @@ package com.fastcomments.sdk; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + import com.fastcomments.model.APIError; import com.fastcomments.model.PublicComment; - -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - /** * Integration tests for admin moderation operations (pin, lock, flag, block). */ @@ -72,8 +70,7 @@ public void testPinAndUnpin() throws Exception { loadSync(sdk3); PublicComment reloaded2 = sdk3.commentsTree.getPublicComment(comment.getId()); assertNotNull(reloaded2); - assertTrue("Comment should not be pinned", - reloaded2.getIsPinned() == null || !reloaded2.getIsPinned()); + assertTrue("Comment should not be pinned", reloaded2.getIsPinned() == null || !reloaded2.getIsPinned()); } @Test @@ -103,8 +100,7 @@ public void testLockAndUnlock() throws Exception { loadSync(sdk3); PublicComment reloaded2 = sdk3.commentsTree.getPublicComment(comment.getId()); assertNotNull(reloaded2); - assertTrue("Comment should not be locked", - reloaded2.getIsLocked() == null || !reloaded2.getIsLocked()); + assertTrue("Comment should not be locked", reloaded2.getIsLocked() == null || !reloaded2.getIsLocked()); } @Test diff --git a/libraries/sdk/src/test/java/com/fastcomments/sdk/PresenceIntegrationTests.java b/libraries/sdk/src/test/java/com/fastcomments/sdk/PresenceIntegrationTests.java index d2c7490..6dc1feb 100644 --- a/libraries/sdk/src/test/java/com/fastcomments/sdk/PresenceIntegrationTests.java +++ b/libraries/sdk/src/test/java/com/fastcomments/sdk/PresenceIntegrationTests.java @@ -1,15 +1,13 @@ package com.fastcomments.sdk; -import com.fastcomments.model.PublicComment; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; - /** * Integration tests for user presence tracking. */ diff --git a/libraries/sdk/src/test/java/com/fastcomments/sdk/RenderableNodeTests.java b/libraries/sdk/src/test/java/com/fastcomments/sdk/RenderableNodeTests.java index 4bfaaf9..c32cf0b 100644 --- a/libraries/sdk/src/test/java/com/fastcomments/sdk/RenderableNodeTests.java +++ b/libraries/sdk/src/test/java/com/fastcomments/sdk/RenderableNodeTests.java @@ -1,11 +1,10 @@ package com.fastcomments.sdk; -import org.junit.Test; +import static org.junit.Assert.assertEquals; import java.util.HashMap; import java.util.Map; - -import static org.junit.Assert.assertEquals; +import org.junit.Test; /** * Unit tests for RenderableNode/RenderableComment nesting level calculations. diff --git a/libraries/sdk/src/test/java/com/fastcomments/sdk/RichTextHelperTest.java b/libraries/sdk/src/test/java/com/fastcomments/sdk/RichTextHelperTest.java index f5bd350..f785535 100644 --- a/libraries/sdk/src/test/java/com/fastcomments/sdk/RichTextHelperTest.java +++ b/libraries/sdk/src/test/java/com/fastcomments/sdk/RichTextHelperTest.java @@ -1,21 +1,19 @@ package com.fastcomments.sdk; +import static org.junit.Assert.*; + import android.graphics.Typeface; -import android.text.Editable; +import android.graphics.drawable.Drawable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.BackgroundColorSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; -import android.graphics.drawable.Drawable; import android.text.style.URLSpan; - import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import static org.junit.Assert.*; - @RunWith(RobolectricTestRunner.class) public class RichTextHelperTest { @@ -188,8 +186,7 @@ public void fromHtml_code_appliesMonospaceAndBackground() { @Test public void fromHtml_preCode_appliesCodeBlockSpan() { Spanned result = RichTextHelper.fromHtml("
int x = 1;
"); - RichTextHelper.CodeBlockSpan[] blocks = result.getSpans(0, result.length(), - RichTextHelper.CodeBlockSpan.class); + RichTextHelper.CodeBlockSpan[] blocks = result.getSpans(0, result.length(), RichTextHelper.CodeBlockSpan.class); assertTrue("Should have CodeBlockSpan for
", blocks.length > 0);
     }
 
@@ -303,8 +300,7 @@ public void parseHtmlTags_unrecognizedTag_returnsNull() {
 
     @Test
     public void extractHref_validAnchor() {
-        assertEquals("https://example.com",
-                RichTextHelper.extractHref(""));
+        assertEquals("https://example.com", RichTextHelper.extractHref(""));
     }
 
     @Test
@@ -320,9 +316,11 @@ public void toHtml_richImageSpan_emitsImgTag() {
         SpannableStringBuilder ssb = new SpannableStringBuilder("before\uFFFCafter");
         Drawable dummyDrawable = new android.graphics.drawable.ColorDrawable(0xFF000000);
         dummyDrawable.setBounds(0, 0, 100, 100);
-        ssb.setSpan(new RichTextHelper.RichImageSpan(dummyDrawable,
-                        "https://example.com/img.png", "alt text", null),
-                6, 7, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        ssb.setSpan(
+                new RichTextHelper.RichImageSpan(dummyDrawable, "https://example.com/img.png", "alt text", null),
+                6,
+                7,
+                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
         String html = RichTextHelper.toHtml(ssb);
         assertTrue("Should contain img tag", html.contains("= 3);
         RenderableComment first = getFirstVisibleComment(sdk2);
         assertNotNull(first);
-        assertTrue("Oldest first: first comment should contain 'Comment A'",
+        assertTrue(
+                "Oldest first: first comment should contain 'Comment A'",
                 first.getComment().getCommentHTML().contains("Comment A"));
     }
 
@@ -96,7 +96,8 @@ public void testMostRelevant() throws Exception {
         assertTrue("Should have comments", sdk2.commentsTree.visibleNodes.size() >= 2);
         RenderableComment first = getFirstVisibleComment(sdk2);
         assertNotNull(first);
-        assertTrue("Most relevant: first comment should be the upvoted one",
+        assertTrue(
+                "Most relevant: first comment should be the upvoted one",
                 first.getComment().getCommentHTML().contains("Comment A"));
     }
 
@@ -125,7 +126,8 @@ public void testSortDirectionApplied() throws Exception {
         assertNotNull(firstNF);
         assertNotNull(firstOF);
         // The first comment should differ between NF and OF
-        assertTrue("Different sort directions should produce different first comment",
+        assertTrue(
+                "Different sort directions should produce different first comment",
                 !firstNF.getComment().getId().equals(firstOF.getComment().getId()));
     }
 
@@ -148,14 +150,16 @@ public void testPinnedCommentStaysFirst() throws Exception {
         pinCommentSync(adminSdk, a.getId());
 
         // Reload with each sort direction and verify pinned comment is first
-        for (SortDirections dir : new SortDirections[]{SortDirections.NF, SortDirections.OF, SortDirections.MR}) {
+        for (SortDirections dir : new SortDirections[] {SortDirections.NF, SortDirections.OF, SortDirections.MR}) {
             FastCommentsSDK sorted = makeSDKWithSort(urlId, dir);
             loadSync(sorted);
 
             RenderableComment first = getFirstVisibleComment(sorted);
             assertNotNull("First comment should exist for sort " + dir, first);
-            assertEquals("Pinned comment should be first for sort " + dir,
-                    a.getId(), first.getComment().getId());
+            assertEquals(
+                    "Pinned comment should be first for sort " + dir,
+                    a.getId(),
+                    first.getComment().getId());
         }
     }
 
diff --git a/libraries/sdk/src/test/java/com/fastcomments/sdk/ThemeTests.java b/libraries/sdk/src/test/java/com/fastcomments/sdk/ThemeTests.java
index fbb78c1..4c4925a 100644
--- a/libraries/sdk/src/test/java/com/fastcomments/sdk/ThemeTests.java
+++ b/libraries/sdk/src/test/java/com/fastcomments/sdk/ThemeTests.java
@@ -1,10 +1,10 @@
 package com.fastcomments.sdk;
 
-import org.junit.Test;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 
+import org.junit.Test;
+
 /**
  * Unit tests for FastCommentsTheme builder and getters.
  * Note: ThemeColorResolver.getXxxColor() methods require Android Context with SDK resources,
@@ -27,21 +27,18 @@ public void testPrimaryColorFallback() {
         // Android's ThemeColorResolver does NOT auto-fallback to primary.
         // Setting primary alone doesn't affect actionButtonColor getter.
         int red = 0xFFFF0000;
-        FastCommentsTheme theme = new FastCommentsTheme.Builder()
-                .setPrimaryColor(red)
-                .build();
+        FastCommentsTheme theme =
+                new FastCommentsTheme.Builder().setPrimaryColor(red).build();
 
         assertEquals(Integer.valueOf(red), theme.getPrimaryColor());
-        assertNull("actionButtonColor should be null when only primary is set",
-                theme.getActionButtonColor());
+        assertNull("actionButtonColor should be null when only primary is set", theme.getActionButtonColor());
     }
 
     @Test
     public void testSpecificColorOverride() {
         int green = 0xFF00FF00;
-        FastCommentsTheme theme = new FastCommentsTheme.Builder()
-                .setActionButtonColor(green)
-                .build();
+        FastCommentsTheme theme =
+                new FastCommentsTheme.Builder().setActionButtonColor(green).build();
 
         assertEquals(Integer.valueOf(green), theme.getActionButtonColor());
     }
@@ -49,9 +46,8 @@ public void testSpecificColorOverride() {
     @Test
     public void testAllPrimary() {
         int purple = 0xFF800080;
-        FastCommentsTheme theme = new FastCommentsTheme.Builder()
-                .setAllPrimaryColors(purple)
-                .build();
+        FastCommentsTheme theme =
+                new FastCommentsTheme.Builder().setAllPrimaryColors(purple).build();
 
         assertEquals(Integer.valueOf(purple), theme.getPrimaryColor());
         assertEquals(Integer.valueOf(purple), theme.getActionButtonColor());
@@ -100,9 +96,8 @@ public void testNullThemeFallsBackCorrectly() {
         // When theme is null, ThemeColorResolver.getColor() returns the resource color.
         // We can't test with a real context here, but we verify that the ColorGetter
         // functional interface works correctly with a non-null theme.
-        FastCommentsTheme theme = new FastCommentsTheme.Builder()
-                .setActionButtonColor(0xFFABCDEF)
-                .build();
+        FastCommentsTheme theme =
+                new FastCommentsTheme.Builder().setActionButtonColor(0xFFABCDEF).build();
 
         ThemeColorResolver.ColorGetter getter = FastCommentsTheme::getActionButtonColor;
         assertEquals(Integer.valueOf(0xFFABCDEF), getter.getColor(theme));
diff --git a/libraries/sdk/src/test/java/com/fastcomments/sdk/ThreadingIntegrationTests.java b/libraries/sdk/src/test/java/com/fastcomments/sdk/ThreadingIntegrationTests.java
index 19d767b..fa8634d 100644
--- a/libraries/sdk/src/test/java/com/fastcomments/sdk/ThreadingIntegrationTests.java
+++ b/libraries/sdk/src/test/java/com/fastcomments/sdk/ThreadingIntegrationTests.java
@@ -1,17 +1,16 @@
 package com.fastcomments.sdk;
 
-import com.fastcomments.model.PublicComment;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
+import com.fastcomments.model.PublicComment;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.annotation.Config;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
 /**
  * Integration tests for comment threading/nesting operations.
  */
@@ -36,7 +35,8 @@ public void testNestedReplies() throws Exception {
         // Verify the child chain via the reloaded tree
         PublicComment reloadedRoot = sdk2.commentsTree.getPublicComment(root.getId());
         assertNotNull(reloadedRoot);
-        assertTrue("Root should have childCount >= 1",
+        assertTrue(
+                "Root should have childCount >= 1",
                 reloadedRoot.getChildCount() != null && reloadedRoot.getChildCount() >= 1);
 
         assertEquals(root.getId(), child.getParentId());
@@ -80,7 +80,8 @@ public void testChildCountUpdated() throws Exception {
 
         PublicComment reloaded = sdk2.commentsTree.getPublicComment(parent.getId());
         assertNotNull(reloaded);
-        assertTrue("Parent should have childCount >= 2",
+        assertTrue(
+                "Parent should have childCount >= 2",
                 reloaded.getChildCount() != null && reloaded.getChildCount() >= 2);
     }
 
@@ -158,7 +159,6 @@ public void testLoadChildrenPagination() throws Exception {
 
         PublicComment reloaded = sdk2.commentsTree.getPublicComment(parent.getId());
         assertNotNull(reloaded);
-        assertTrue("Should have childCount > 0",
-                reloaded.getChildCount() != null && reloaded.getChildCount() > 0);
+        assertTrue("Should have childCount > 0", reloaded.getChildCount() != null && reloaded.getChildCount() > 0);
     }
 }
diff --git a/libraries/sdk/src/test/java/com/fastcomments/sdk/VoteIntegrationTests.java b/libraries/sdk/src/test/java/com/fastcomments/sdk/VoteIntegrationTests.java
index 68627e1..945afc3 100644
--- a/libraries/sdk/src/test/java/com/fastcomments/sdk/VoteIntegrationTests.java
+++ b/libraries/sdk/src/test/java/com/fastcomments/sdk/VoteIntegrationTests.java
@@ -1,17 +1,15 @@
 package com.fastcomments.sdk;
 
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
 import com.fastcomments.model.PublicComment;
 import com.fastcomments.model.VoteResponse;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.annotation.Config;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
 /**
  * Integration tests for comment voting operations.
  */
@@ -107,8 +105,7 @@ public void testVoteUpdatesNetVotes() throws Exception {
         PublicComment reloaded = sdk2.commentsTree.getPublicComment(comment.getId());
         assertNotNull(reloaded);
         // Net votes should be positive after upvote
-        assertTrue("Votes should be >= 1 after upvote",
-                reloaded.getVotes() != null && reloaded.getVotes() >= 1);
+        assertTrue("Votes should be >= 1 after upvote", reloaded.getVotes() != null && reloaded.getVotes() >= 1);
     }
 
     @Test