From a6159fe85e614865f16adef0f92106166864fd3a Mon Sep 17 00:00:00 2001 From: Corey Bryant Date: Wed, 1 Oct 2025 12:15:43 -0400 Subject: [PATCH 001/136] chore(ci): Add localizable strings statement to uplift template --- .github/workflows/pulls-opened.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pulls-opened.yml b/.github/workflows/pulls-opened.yml index 225c80971a..c9b2c6904b 100644 --- a/.github/workflows/pulls-opened.yml +++ b/.github/workflows/pulls-opened.yml @@ -36,7 +36,8 @@ jobs: Original Issue/Pull request: Regression caused by (issue #): User impact if declined: - Testing completed (on daily, etc.): + Testing completed (on daily, beta, etc.): + Introduces or modifies localizable strings (yes/no): Risk to taking this patch (and alternatives if risky): run: | if gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json comments \ -- GitLab From e6d0a16aaedb9b90f6985941c101e3f772bd34c1 Mon Sep 17 00:00:00 2001 From: Corey Bryant Date: Wed, 1 Oct 2025 11:46:51 -0400 Subject: [PATCH 002/136] docs: move release docs to release directory --- docs/SUMMARY.md | 8 ++++---- docs/ci/README.md | 3 --- docs/{ci => release}/AUTOMATION.md | 0 docs/{ci => release}/HISTORICAL_RELEASE.md | 0 docs/{ci => release}/RELEASE.md | 0 docs/release/developer-checklist.md | 20 ++++++++++---------- 6 files changed, 14 insertions(+), 17 deletions(-) delete mode 100644 docs/ci/README.md rename docs/{ci => release}/AUTOMATION.md (100%) rename docs/{ci => release}/HISTORICAL_RELEASE.md (100%) rename docs/{ci => release}/RELEASE.md (100%) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 4649bc0be2..461b97f550 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -45,11 +45,11 @@ generator, in this case, **mdbook**. It defines the structure and navigation of - [Troubleshooting]() - [Collecting Debug Logs](user-guide/troubleshooting/collecting-debug-logs.md) - [Find your app version](user-guide/troubleshooting/find-your-app-version.md) -- [Release](ci/README.md) - - [Release Process](ci/RELEASE.md) - - [Release Automation](ci/AUTOMATION.md) +- [Release]() + - [Release Process](release/RELEASE.md) + - [Release Automation](release/AUTOMATION.md) - [Developer Release Checklist](release/developer-checklist.md) - - [Manual Release (historical)](ci/HISTORICAL_RELEASE.md) + - [Manual Release (historical)](release/HISTORICAL_RELEASE.md) - [Security]() - [Threat Modeling Guide](security/threat-modeling-guide.md) diff --git a/docs/ci/README.md b/docs/ci/README.md deleted file mode 100644 index 5f671ab8ac..0000000000 --- a/docs/ci/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Thunderbird for Android Release Documentation - -Please see the sub-pages for release documentation diff --git a/docs/ci/AUTOMATION.md b/docs/release/AUTOMATION.md similarity index 100% rename from docs/ci/AUTOMATION.md rename to docs/release/AUTOMATION.md diff --git a/docs/ci/HISTORICAL_RELEASE.md b/docs/release/HISTORICAL_RELEASE.md similarity index 100% rename from docs/ci/HISTORICAL_RELEASE.md rename to docs/release/HISTORICAL_RELEASE.md diff --git a/docs/ci/RELEASE.md b/docs/release/RELEASE.md similarity index 100% rename from docs/ci/RELEASE.md rename to docs/release/RELEASE.md diff --git a/docs/release/developer-checklist.md b/docs/release/developer-checklist.md index 0e6f406eb5..e4b218021f 100644 --- a/docs/release/developer-checklist.md +++ b/docs/release/developer-checklist.md @@ -4,14 +4,14 @@ This checklist is for developers. It summarizes what you (as a contributor/featu - main → beta - beta → release -For the full release-driver process (branch locks, announcements, publishing), see [Release → Release Process](../ci/RELEASE.md). +For the full release-driver process (branch locks, announcements, publishing), see [Release → Release Process](../release/RELEASE.md). ## Ongoing (between merges) Do these as part of regular development: - Identify potential uplifts early - - Add risk and user impact notes to the issue/PR; ensure the fix lands on `main` and bakes on Daily first — see [Uplifts](../ci/RELEASE.md#uplifts) and [Uplift Criteria](../ci/RELEASE.md#uplift-criteria) + - Add risk and user impact notes to the issue/PR; ensure the fix lands on `main` and bakes on Daily first — see [Uplifts](../release/RELEASE.md#uplifts) and [Uplift Criteria](../release/RELEASE.md#uplift-criteria) - Strings and translations - Avoid late string changes; if unavoidable, keep them small, so translators can catch up - Prefer not changing localizable strings for uplifts @@ -29,7 +29,7 @@ Do these as part of regular development: ## Before main → beta (developer responsibilities) > [!NOTE] -> A one-week [Soft Freeze](../ci/RELEASE.md#soft-freeze) occurs before merging `main` into `beta`. +> A one-week [Soft Freeze](../release/RELEASE.md#soft-freeze) occurs before merging `main` into `beta`. During soft freeze: - Avoid landing risky code changes @@ -38,10 +38,10 @@ During soft freeze: Goal: Changes on `main` are safe to expose to a broader audience. - Feature flags - - Ensure flags match the [rules for beta](../ci/RELEASE.md#feature-flags) + - Ensure flags match the [rules for beta](../release/RELEASE.md#feature-flags) - New features are disabled by default unless explicitly approved for beta - Not-ready features must be disabled - - Prepare and merge a PR on `main` with necessary flag changes (before merge day) + - Prepare and merge a PR on `main` with necessary flag changes (before soft freeze starts) - Translations - Ensure translation updates needed for your features are merged to `main` - If no Weblate PR is pending, trigger one and help review it (fix conflicts if needed) @@ -51,10 +51,10 @@ Goal: Changes on `main` are safe to expose to a broader audience. Goal: Changes on `beta` are safe for general availability. - Feature flags - - Verify flags align with [rules for release](../ci/RELEASE.md#feature-flags) + - Verify flags align with [rules for release](../release/RELEASE.md#feature-flags) - Features are disabled unless explicitly approved for release - Not-ready features remain disabled - - If changes are required, open a PR on `main` and request uplift to `beta` following the criteria in [Uplift Criteria](../ci/RELEASE.md#uplift-criteria) + - If changes are required, open a PR on `main` and request uplift to `beta` following the criteria in [Uplift Criteria](../release/RELEASE.md#uplift-criteria) - Translations - No new string changes at this stage; confirm your changes don’t introduce them - Stability checks you can influence @@ -67,11 +67,11 @@ Goal: Changes on `beta` are safe for general availability. Paste the following snippet into your PR description to help reviewers and release drivers verify readiness for merge: ```markdown -- [ ] Feature flags set according to target branch rules ([beta](../ci/RELEASE.md#feature-flags) / [release](../ci/RELEASE.md#feature-flags)) +- [ ] Feature flags set according to target branch rules ([beta](../release/RELEASE.md#feature-flags) / [release](../release/RELEASE.md#feature-flags)) - [ ] Tests added/updated; CI green on affected modules - [ ] No new localizable strings (or justified and coordinated) - [ ] Translations accounted for (Weblate PR merged or not required) -- [ ] Uplift label and risk/impact notes added if proposing uplift ([criteria](../ci/RELEASE.md#uplift-criteria)) +- [ ] Uplift template with risk/impact notes filled out if proposing uplift ([criteria](../release/RELEASE.md#uplift-criteria)) ``` ## After merges (what developers should verify) @@ -81,5 +81,5 @@ Paste the following snippet into your PR description to help reviewers and relea - Be prepared to propose/prepare a hotfix via the uplift process if necessary > [!NOTE] -> Merge-day coordination (branch locks, Matrix announcements, running scripts) is handled by release drivers. See [Merge Process](../ci/RELEASE.md#merge-process) for details. +> Merge-day coordination (branch locks, Matrix announcements, running scripts) is handled by release drivers. See [Merge Process](../release/RELEASE.md#merge-process) for details. -- GitLab From bda332925d42368cdb35bcce84f79b48e47f5492 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 14:57:39 +0000 Subject: [PATCH 003/136] chore(deps): bump mozillaAndroidComponents from 143.0.2 to 143.0.4 Bumps `mozillaAndroidComponents` from 143.0.2 to 143.0.4. Updates `org.mozilla.components:service-glean` from 143.0.2 to 143.0.4 Updates `org.mozilla.components:lib-fetch-okhttp` from 143.0.2 to 143.0.4 --- updated-dependencies: - dependency-name: org.mozilla.components:service-glean dependency-version: 143.0.4 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.mozilla.components:lib-fetch-okhttp dependency-version: 143.0.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 62607f50b7..c348f0998a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -98,7 +98,7 @@ mockito = "5.20.0" mockitoKotlin = "6.0.0" mokkery = "2.10.0" moshi = "1.15.2" -mozillaAndroidComponents = "143.0.2" +mozillaAndroidComponents = "143.0.4" okhttp = "5.1.0" okio = "3.16.0" preferencesFix = "1.1.0" -- GitLab From 7daaafa89fb5457e7f7506e4163885e409464920 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 08:14:53 +0000 Subject: [PATCH 004/136] chore(deps): bump com.github.skydoves:landscapist-coil3 Bumps [com.github.skydoves:landscapist-coil3](https://github.com/skydoves/landscapist) from 2.5.1 to 2.6.1. - [Release notes](https://github.com/skydoves/landscapist/releases) - [Commits](https://github.com/skydoves/landscapist/compare/2.5.1...2.6.1) --- updated-dependencies: - dependency-name: com.github.skydoves:landscapist-coil3 dependency-version: 2.6.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index beda6fbc1b..7dece6d7d4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -89,7 +89,7 @@ kotlinxSerialization = "1.9.0" ktlint = "1.5.0" ktor = "3.3.0" kxml2 = "1.0" -landscapist = "2.5.1" +landscapist = "2.6.1" leakcanary = "2.14" logbackClassic = "1.5.19" mime4j = "0.8.13" -- GitLab From 275f9e4e1fc3341ee8b1c889ce4314c284584114 Mon Sep 17 00:00:00 2001 From: thunderbird-botmobile <202943968+thunderbird-botmobile[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 08:16:31 +0000 Subject: [PATCH 005/136] chore(deps): Update dependency guard files on behalf of @dependabot --- .../fossReleaseRuntimeClasspath.txt | 32 +++++++++---------- .../fullReleaseRuntimeClasspath.txt | 32 +++++++++---------- .../dependencies/fossBetaRuntimeClasspath.txt | 32 +++++++++---------- .../fossDailyRuntimeClasspath.txt | 32 +++++++++---------- .../fossReleaseRuntimeClasspath.txt | 32 +++++++++---------- .../dependencies/fullBetaRuntimeClasspath.txt | 32 +++++++++---------- .../fullDailyRuntimeClasspath.txt | 32 +++++++++---------- .../fullReleaseRuntimeClasspath.txt | 32 +++++++++---------- 8 files changed, 128 insertions(+), 128 deletions(-) diff --git a/app-k9mail/dependencies/fossReleaseRuntimeClasspath.txt b/app-k9mail/dependencies/fossReleaseRuntimeClasspath.txt index fd3ba8c643..1918df491b 100644 --- a/app-k9mail/dependencies/fossReleaseRuntimeClasspath.txt +++ b/app-k9mail/dependencies/fossReleaseRuntimeClasspath.txt @@ -63,11 +63,11 @@ androidx.concurrent:concurrent-futures:1.1.0 androidx.constraintlayout:constraintlayout-core:1.1.1 androidx.constraintlayout:constraintlayout:2.2.1 androidx.coordinatorlayout:coordinatorlayout:1.3.0 -androidx.core:core-ktx:1.16.0 +androidx.core:core-ktx:1.17.0 androidx.core:core-remoteviews:1.1.0 androidx.core:core-splashscreen:1.0.1 androidx.core:core-viewtree:1.0.0 -androidx.core:core:1.16.0 +androidx.core:core:1.17.0 androidx.cursoradapter:cursoradapter:1.0.0 androidx.customview:customview-poolingcontainer:1.0.0 androidx.customview:customview:1.1.0 @@ -177,10 +177,10 @@ com.github.bumptech.glide:annotations:4.16.0 com.github.bumptech.glide:disklrucache:4.16.0 com.github.bumptech.glide:gifdecoder:4.16.0 com.github.bumptech.glide:glide:4.16.0 -com.github.skydoves:landscapist-android:2.5.1 -com.github.skydoves:landscapist-coil3-android:2.5.1 -com.github.skydoves:landscapist-coil3:2.5.1 -com.github.skydoves:landscapist:2.5.1 +com.github.skydoves:landscapist-android:2.6.1 +com.github.skydoves:landscapist-coil3-android:2.6.1 +com.github.skydoves:landscapist-coil3:2.6.1 +com.github.skydoves:landscapist:2.6.1 com.google.android.flexbox:flexbox:3.0.0 com.google.android.material:material:1.12.0 com.google.errorprone:error_prone_annotations:2.15.0 @@ -206,16 +206,16 @@ commons-io:commons-io:2.20.0 de.cketti.library.changelog:ckchangelog-core:2.0.0-beta02 de.cketti.safecontentresolver:safe-content-resolver-v21:1.0.0 de.hdodenhof:circleimageview:3.1.0 -io.coil-kt.coil3:coil-android:3.2.0 -io.coil-kt.coil3:coil-core-android:3.2.0 -io.coil-kt.coil3:coil-core:3.2.0 -io.coil-kt.coil3:coil-gif:3.2.0 -io.coil-kt.coil3:coil-network-core-android:3.2.0 -io.coil-kt.coil3:coil-network-core:3.2.0 -io.coil-kt.coil3:coil-network-okhttp-jvm:3.2.0 -io.coil-kt.coil3:coil-network-okhttp:3.2.0 -io.coil-kt.coil3:coil-video:3.2.0 -io.coil-kt.coil3:coil:3.2.0 +io.coil-kt.coil3:coil-android:3.3.0 +io.coil-kt.coil3:coil-core-android:3.3.0 +io.coil-kt.coil3:coil-core:3.3.0 +io.coil-kt.coil3:coil-gif:3.3.0 +io.coil-kt.coil3:coil-network-core-android:3.3.0 +io.coil-kt.coil3:coil-network-core:3.3.0 +io.coil-kt.coil3:coil-network-okhttp-jvm:3.3.0 +io.coil-kt.coil3:coil-network-okhttp:3.3.0 +io.coil-kt.coil3:coil-video:3.3.0 +io.coil-kt.coil3:coil:3.3.0 io.insert-koin:koin-android:4.1.1 io.insert-koin:koin-androidx-compose:4.1.1 io.insert-koin:koin-bom:4.1.1 diff --git a/app-k9mail/dependencies/fullReleaseRuntimeClasspath.txt b/app-k9mail/dependencies/fullReleaseRuntimeClasspath.txt index de8d7e2c9c..14025e401c 100644 --- a/app-k9mail/dependencies/fullReleaseRuntimeClasspath.txt +++ b/app-k9mail/dependencies/fullReleaseRuntimeClasspath.txt @@ -63,11 +63,11 @@ androidx.concurrent:concurrent-futures:1.1.0 androidx.constraintlayout:constraintlayout-core:1.1.1 androidx.constraintlayout:constraintlayout:2.2.1 androidx.coordinatorlayout:coordinatorlayout:1.3.0 -androidx.core:core-ktx:1.16.0 +androidx.core:core-ktx:1.17.0 androidx.core:core-remoteviews:1.1.0 androidx.core:core-splashscreen:1.0.1 androidx.core:core-viewtree:1.0.0 -androidx.core:core:1.16.0 +androidx.core:core:1.17.0 androidx.cursoradapter:cursoradapter:1.0.0 androidx.customview:customview-poolingcontainer:1.0.0 androidx.customview:customview:1.1.0 @@ -179,10 +179,10 @@ com.github.bumptech.glide:annotations:4.16.0 com.github.bumptech.glide:disklrucache:4.16.0 com.github.bumptech.glide:gifdecoder:4.16.0 com.github.bumptech.glide:glide:4.16.0 -com.github.skydoves:landscapist-android:2.5.1 -com.github.skydoves:landscapist-coil3-android:2.5.1 -com.github.skydoves:landscapist-coil3:2.5.1 -com.github.skydoves:landscapist:2.5.1 +com.github.skydoves:landscapist-android:2.6.1 +com.github.skydoves:landscapist-coil3-android:2.6.1 +com.github.skydoves:landscapist-coil3:2.6.1 +com.github.skydoves:landscapist:2.6.1 com.google.android.datatransport:transport-api:3.0.0 com.google.android.datatransport:transport-backend-cct:3.1.8 com.google.android.datatransport:transport-runtime:3.1.8 @@ -219,16 +219,16 @@ commons-io:commons-io:2.20.0 de.cketti.library.changelog:ckchangelog-core:2.0.0-beta02 de.cketti.safecontentresolver:safe-content-resolver-v21:1.0.0 de.hdodenhof:circleimageview:3.1.0 -io.coil-kt.coil3:coil-android:3.2.0 -io.coil-kt.coil3:coil-core-android:3.2.0 -io.coil-kt.coil3:coil-core:3.2.0 -io.coil-kt.coil3:coil-gif:3.2.0 -io.coil-kt.coil3:coil-network-core-android:3.2.0 -io.coil-kt.coil3:coil-network-core:3.2.0 -io.coil-kt.coil3:coil-network-okhttp-jvm:3.2.0 -io.coil-kt.coil3:coil-network-okhttp:3.2.0 -io.coil-kt.coil3:coil-video:3.2.0 -io.coil-kt.coil3:coil:3.2.0 +io.coil-kt.coil3:coil-android:3.3.0 +io.coil-kt.coil3:coil-core-android:3.3.0 +io.coil-kt.coil3:coil-core:3.3.0 +io.coil-kt.coil3:coil-gif:3.3.0 +io.coil-kt.coil3:coil-network-core-android:3.3.0 +io.coil-kt.coil3:coil-network-core:3.3.0 +io.coil-kt.coil3:coil-network-okhttp-jvm:3.3.0 +io.coil-kt.coil3:coil-network-okhttp:3.3.0 +io.coil-kt.coil3:coil-video:3.3.0 +io.coil-kt.coil3:coil:3.3.0 io.insert-koin:koin-android:4.1.1 io.insert-koin:koin-androidx-compose:4.1.1 io.insert-koin:koin-bom:4.1.1 diff --git a/app-thunderbird/dependencies/fossBetaRuntimeClasspath.txt b/app-thunderbird/dependencies/fossBetaRuntimeClasspath.txt index 6f18d9b53d..192070a0e7 100644 --- a/app-thunderbird/dependencies/fossBetaRuntimeClasspath.txt +++ b/app-thunderbird/dependencies/fossBetaRuntimeClasspath.txt @@ -68,11 +68,11 @@ androidx.concurrent:concurrent-futures:1.1.0 androidx.constraintlayout:constraintlayout-core:1.1.1 androidx.constraintlayout:constraintlayout:2.2.1 androidx.coordinatorlayout:coordinatorlayout:1.3.0 -androidx.core:core-ktx:1.16.0 +androidx.core:core-ktx:1.17.0 androidx.core:core-remoteviews:1.1.0 androidx.core:core-splashscreen:1.0.1 androidx.core:core-viewtree:1.0.0 -androidx.core:core:1.16.0 +androidx.core:core:1.17.0 androidx.cursoradapter:cursoradapter:1.0.0 androidx.customview:customview-poolingcontainer:1.0.0 androidx.customview:customview:1.1.0 @@ -182,10 +182,10 @@ com.github.bumptech.glide:annotations:4.16.0 com.github.bumptech.glide:disklrucache:4.16.0 com.github.bumptech.glide:gifdecoder:4.16.0 com.github.bumptech.glide:glide:4.16.0 -com.github.skydoves:landscapist-android:2.5.1 -com.github.skydoves:landscapist-coil3-android:2.5.1 -com.github.skydoves:landscapist-coil3:2.5.1 -com.github.skydoves:landscapist:2.5.1 +com.github.skydoves:landscapist-android:2.6.1 +com.github.skydoves:landscapist-coil3-android:2.6.1 +com.github.skydoves:landscapist-coil3:2.6.1 +com.github.skydoves:landscapist:2.6.1 com.google.android.flexbox:flexbox:3.0.0 com.google.android.material:material:1.12.0 com.google.auto.value:auto-value-annotations:1.6.3 @@ -213,16 +213,16 @@ commons-io:commons-io:2.20.0 de.cketti.library.changelog:ckchangelog-core:2.0.0-beta02 de.cketti.safecontentresolver:safe-content-resolver-v21:1.0.0 de.hdodenhof:circleimageview:3.1.0 -io.coil-kt.coil3:coil-android:3.2.0 -io.coil-kt.coil3:coil-core-android:3.2.0 -io.coil-kt.coil3:coil-core:3.2.0 -io.coil-kt.coil3:coil-gif:3.2.0 -io.coil-kt.coil3:coil-network-core-android:3.2.0 -io.coil-kt.coil3:coil-network-core:3.2.0 -io.coil-kt.coil3:coil-network-okhttp-jvm:3.2.0 -io.coil-kt.coil3:coil-network-okhttp:3.2.0 -io.coil-kt.coil3:coil-video:3.2.0 -io.coil-kt.coil3:coil:3.2.0 +io.coil-kt.coil3:coil-android:3.3.0 +io.coil-kt.coil3:coil-core-android:3.3.0 +io.coil-kt.coil3:coil-core:3.3.0 +io.coil-kt.coil3:coil-gif:3.3.0 +io.coil-kt.coil3:coil-network-core-android:3.3.0 +io.coil-kt.coil3:coil-network-core:3.3.0 +io.coil-kt.coil3:coil-network-okhttp-jvm:3.3.0 +io.coil-kt.coil3:coil-network-okhttp:3.3.0 +io.coil-kt.coil3:coil-video:3.3.0 +io.coil-kt.coil3:coil:3.3.0 io.insert-koin:koin-android:4.1.1 io.insert-koin:koin-androidx-compose:4.1.1 io.insert-koin:koin-bom:4.1.1 diff --git a/app-thunderbird/dependencies/fossDailyRuntimeClasspath.txt b/app-thunderbird/dependencies/fossDailyRuntimeClasspath.txt index 6f18d9b53d..192070a0e7 100644 --- a/app-thunderbird/dependencies/fossDailyRuntimeClasspath.txt +++ b/app-thunderbird/dependencies/fossDailyRuntimeClasspath.txt @@ -68,11 +68,11 @@ androidx.concurrent:concurrent-futures:1.1.0 androidx.constraintlayout:constraintlayout-core:1.1.1 androidx.constraintlayout:constraintlayout:2.2.1 androidx.coordinatorlayout:coordinatorlayout:1.3.0 -androidx.core:core-ktx:1.16.0 +androidx.core:core-ktx:1.17.0 androidx.core:core-remoteviews:1.1.0 androidx.core:core-splashscreen:1.0.1 androidx.core:core-viewtree:1.0.0 -androidx.core:core:1.16.0 +androidx.core:core:1.17.0 androidx.cursoradapter:cursoradapter:1.0.0 androidx.customview:customview-poolingcontainer:1.0.0 androidx.customview:customview:1.1.0 @@ -182,10 +182,10 @@ com.github.bumptech.glide:annotations:4.16.0 com.github.bumptech.glide:disklrucache:4.16.0 com.github.bumptech.glide:gifdecoder:4.16.0 com.github.bumptech.glide:glide:4.16.0 -com.github.skydoves:landscapist-android:2.5.1 -com.github.skydoves:landscapist-coil3-android:2.5.1 -com.github.skydoves:landscapist-coil3:2.5.1 -com.github.skydoves:landscapist:2.5.1 +com.github.skydoves:landscapist-android:2.6.1 +com.github.skydoves:landscapist-coil3-android:2.6.1 +com.github.skydoves:landscapist-coil3:2.6.1 +com.github.skydoves:landscapist:2.6.1 com.google.android.flexbox:flexbox:3.0.0 com.google.android.material:material:1.12.0 com.google.auto.value:auto-value-annotations:1.6.3 @@ -213,16 +213,16 @@ commons-io:commons-io:2.20.0 de.cketti.library.changelog:ckchangelog-core:2.0.0-beta02 de.cketti.safecontentresolver:safe-content-resolver-v21:1.0.0 de.hdodenhof:circleimageview:3.1.0 -io.coil-kt.coil3:coil-android:3.2.0 -io.coil-kt.coil3:coil-core-android:3.2.0 -io.coil-kt.coil3:coil-core:3.2.0 -io.coil-kt.coil3:coil-gif:3.2.0 -io.coil-kt.coil3:coil-network-core-android:3.2.0 -io.coil-kt.coil3:coil-network-core:3.2.0 -io.coil-kt.coil3:coil-network-okhttp-jvm:3.2.0 -io.coil-kt.coil3:coil-network-okhttp:3.2.0 -io.coil-kt.coil3:coil-video:3.2.0 -io.coil-kt.coil3:coil:3.2.0 +io.coil-kt.coil3:coil-android:3.3.0 +io.coil-kt.coil3:coil-core-android:3.3.0 +io.coil-kt.coil3:coil-core:3.3.0 +io.coil-kt.coil3:coil-gif:3.3.0 +io.coil-kt.coil3:coil-network-core-android:3.3.0 +io.coil-kt.coil3:coil-network-core:3.3.0 +io.coil-kt.coil3:coil-network-okhttp-jvm:3.3.0 +io.coil-kt.coil3:coil-network-okhttp:3.3.0 +io.coil-kt.coil3:coil-video:3.3.0 +io.coil-kt.coil3:coil:3.3.0 io.insert-koin:koin-android:4.1.1 io.insert-koin:koin-androidx-compose:4.1.1 io.insert-koin:koin-bom:4.1.1 diff --git a/app-thunderbird/dependencies/fossReleaseRuntimeClasspath.txt b/app-thunderbird/dependencies/fossReleaseRuntimeClasspath.txt index 6f18d9b53d..192070a0e7 100644 --- a/app-thunderbird/dependencies/fossReleaseRuntimeClasspath.txt +++ b/app-thunderbird/dependencies/fossReleaseRuntimeClasspath.txt @@ -68,11 +68,11 @@ androidx.concurrent:concurrent-futures:1.1.0 androidx.constraintlayout:constraintlayout-core:1.1.1 androidx.constraintlayout:constraintlayout:2.2.1 androidx.coordinatorlayout:coordinatorlayout:1.3.0 -androidx.core:core-ktx:1.16.0 +androidx.core:core-ktx:1.17.0 androidx.core:core-remoteviews:1.1.0 androidx.core:core-splashscreen:1.0.1 androidx.core:core-viewtree:1.0.0 -androidx.core:core:1.16.0 +androidx.core:core:1.17.0 androidx.cursoradapter:cursoradapter:1.0.0 androidx.customview:customview-poolingcontainer:1.0.0 androidx.customview:customview:1.1.0 @@ -182,10 +182,10 @@ com.github.bumptech.glide:annotations:4.16.0 com.github.bumptech.glide:disklrucache:4.16.0 com.github.bumptech.glide:gifdecoder:4.16.0 com.github.bumptech.glide:glide:4.16.0 -com.github.skydoves:landscapist-android:2.5.1 -com.github.skydoves:landscapist-coil3-android:2.5.1 -com.github.skydoves:landscapist-coil3:2.5.1 -com.github.skydoves:landscapist:2.5.1 +com.github.skydoves:landscapist-android:2.6.1 +com.github.skydoves:landscapist-coil3-android:2.6.1 +com.github.skydoves:landscapist-coil3:2.6.1 +com.github.skydoves:landscapist:2.6.1 com.google.android.flexbox:flexbox:3.0.0 com.google.android.material:material:1.12.0 com.google.auto.value:auto-value-annotations:1.6.3 @@ -213,16 +213,16 @@ commons-io:commons-io:2.20.0 de.cketti.library.changelog:ckchangelog-core:2.0.0-beta02 de.cketti.safecontentresolver:safe-content-resolver-v21:1.0.0 de.hdodenhof:circleimageview:3.1.0 -io.coil-kt.coil3:coil-android:3.2.0 -io.coil-kt.coil3:coil-core-android:3.2.0 -io.coil-kt.coil3:coil-core:3.2.0 -io.coil-kt.coil3:coil-gif:3.2.0 -io.coil-kt.coil3:coil-network-core-android:3.2.0 -io.coil-kt.coil3:coil-network-core:3.2.0 -io.coil-kt.coil3:coil-network-okhttp-jvm:3.2.0 -io.coil-kt.coil3:coil-network-okhttp:3.2.0 -io.coil-kt.coil3:coil-video:3.2.0 -io.coil-kt.coil3:coil:3.2.0 +io.coil-kt.coil3:coil-android:3.3.0 +io.coil-kt.coil3:coil-core-android:3.3.0 +io.coil-kt.coil3:coil-core:3.3.0 +io.coil-kt.coil3:coil-gif:3.3.0 +io.coil-kt.coil3:coil-network-core-android:3.3.0 +io.coil-kt.coil3:coil-network-core:3.3.0 +io.coil-kt.coil3:coil-network-okhttp-jvm:3.3.0 +io.coil-kt.coil3:coil-network-okhttp:3.3.0 +io.coil-kt.coil3:coil-video:3.3.0 +io.coil-kt.coil3:coil:3.3.0 io.insert-koin:koin-android:4.1.1 io.insert-koin:koin-androidx-compose:4.1.1 io.insert-koin:koin-bom:4.1.1 diff --git a/app-thunderbird/dependencies/fullBetaRuntimeClasspath.txt b/app-thunderbird/dependencies/fullBetaRuntimeClasspath.txt index d4540eedfc..d3d3af3f2f 100644 --- a/app-thunderbird/dependencies/fullBetaRuntimeClasspath.txt +++ b/app-thunderbird/dependencies/fullBetaRuntimeClasspath.txt @@ -68,11 +68,11 @@ androidx.concurrent:concurrent-futures:1.1.0 androidx.constraintlayout:constraintlayout-core:1.1.1 androidx.constraintlayout:constraintlayout:2.2.1 androidx.coordinatorlayout:coordinatorlayout:1.3.0 -androidx.core:core-ktx:1.16.0 +androidx.core:core-ktx:1.17.0 androidx.core:core-remoteviews:1.1.0 androidx.core:core-splashscreen:1.0.1 androidx.core:core-viewtree:1.0.0 -androidx.core:core:1.16.0 +androidx.core:core:1.17.0 androidx.cursoradapter:cursoradapter:1.0.0 androidx.customview:customview-poolingcontainer:1.0.0 androidx.customview:customview:1.1.0 @@ -184,10 +184,10 @@ com.github.bumptech.glide:annotations:4.16.0 com.github.bumptech.glide:disklrucache:4.16.0 com.github.bumptech.glide:gifdecoder:4.16.0 com.github.bumptech.glide:glide:4.16.0 -com.github.skydoves:landscapist-android:2.5.1 -com.github.skydoves:landscapist-coil3-android:2.5.1 -com.github.skydoves:landscapist-coil3:2.5.1 -com.github.skydoves:landscapist:2.5.1 +com.github.skydoves:landscapist-android:2.6.1 +com.github.skydoves:landscapist-coil3-android:2.6.1 +com.github.skydoves:landscapist-coil3:2.6.1 +com.github.skydoves:landscapist:2.6.1 com.google.android.datatransport:transport-api:3.0.0 com.google.android.datatransport:transport-backend-cct:3.1.8 com.google.android.datatransport:transport-runtime:3.1.8 @@ -226,16 +226,16 @@ commons-io:commons-io:2.20.0 de.cketti.library.changelog:ckchangelog-core:2.0.0-beta02 de.cketti.safecontentresolver:safe-content-resolver-v21:1.0.0 de.hdodenhof:circleimageview:3.1.0 -io.coil-kt.coil3:coil-android:3.2.0 -io.coil-kt.coil3:coil-core-android:3.2.0 -io.coil-kt.coil3:coil-core:3.2.0 -io.coil-kt.coil3:coil-gif:3.2.0 -io.coil-kt.coil3:coil-network-core-android:3.2.0 -io.coil-kt.coil3:coil-network-core:3.2.0 -io.coil-kt.coil3:coil-network-okhttp-jvm:3.2.0 -io.coil-kt.coil3:coil-network-okhttp:3.2.0 -io.coil-kt.coil3:coil-video:3.2.0 -io.coil-kt.coil3:coil:3.2.0 +io.coil-kt.coil3:coil-android:3.3.0 +io.coil-kt.coil3:coil-core-android:3.3.0 +io.coil-kt.coil3:coil-core:3.3.0 +io.coil-kt.coil3:coil-gif:3.3.0 +io.coil-kt.coil3:coil-network-core-android:3.3.0 +io.coil-kt.coil3:coil-network-core:3.3.0 +io.coil-kt.coil3:coil-network-okhttp-jvm:3.3.0 +io.coil-kt.coil3:coil-network-okhttp:3.3.0 +io.coil-kt.coil3:coil-video:3.3.0 +io.coil-kt.coil3:coil:3.3.0 io.insert-koin:koin-android:4.1.1 io.insert-koin:koin-androidx-compose:4.1.1 io.insert-koin:koin-bom:4.1.1 diff --git a/app-thunderbird/dependencies/fullDailyRuntimeClasspath.txt b/app-thunderbird/dependencies/fullDailyRuntimeClasspath.txt index d4540eedfc..d3d3af3f2f 100644 --- a/app-thunderbird/dependencies/fullDailyRuntimeClasspath.txt +++ b/app-thunderbird/dependencies/fullDailyRuntimeClasspath.txt @@ -68,11 +68,11 @@ androidx.concurrent:concurrent-futures:1.1.0 androidx.constraintlayout:constraintlayout-core:1.1.1 androidx.constraintlayout:constraintlayout:2.2.1 androidx.coordinatorlayout:coordinatorlayout:1.3.0 -androidx.core:core-ktx:1.16.0 +androidx.core:core-ktx:1.17.0 androidx.core:core-remoteviews:1.1.0 androidx.core:core-splashscreen:1.0.1 androidx.core:core-viewtree:1.0.0 -androidx.core:core:1.16.0 +androidx.core:core:1.17.0 androidx.cursoradapter:cursoradapter:1.0.0 androidx.customview:customview-poolingcontainer:1.0.0 androidx.customview:customview:1.1.0 @@ -184,10 +184,10 @@ com.github.bumptech.glide:annotations:4.16.0 com.github.bumptech.glide:disklrucache:4.16.0 com.github.bumptech.glide:gifdecoder:4.16.0 com.github.bumptech.glide:glide:4.16.0 -com.github.skydoves:landscapist-android:2.5.1 -com.github.skydoves:landscapist-coil3-android:2.5.1 -com.github.skydoves:landscapist-coil3:2.5.1 -com.github.skydoves:landscapist:2.5.1 +com.github.skydoves:landscapist-android:2.6.1 +com.github.skydoves:landscapist-coil3-android:2.6.1 +com.github.skydoves:landscapist-coil3:2.6.1 +com.github.skydoves:landscapist:2.6.1 com.google.android.datatransport:transport-api:3.0.0 com.google.android.datatransport:transport-backend-cct:3.1.8 com.google.android.datatransport:transport-runtime:3.1.8 @@ -226,16 +226,16 @@ commons-io:commons-io:2.20.0 de.cketti.library.changelog:ckchangelog-core:2.0.0-beta02 de.cketti.safecontentresolver:safe-content-resolver-v21:1.0.0 de.hdodenhof:circleimageview:3.1.0 -io.coil-kt.coil3:coil-android:3.2.0 -io.coil-kt.coil3:coil-core-android:3.2.0 -io.coil-kt.coil3:coil-core:3.2.0 -io.coil-kt.coil3:coil-gif:3.2.0 -io.coil-kt.coil3:coil-network-core-android:3.2.0 -io.coil-kt.coil3:coil-network-core:3.2.0 -io.coil-kt.coil3:coil-network-okhttp-jvm:3.2.0 -io.coil-kt.coil3:coil-network-okhttp:3.2.0 -io.coil-kt.coil3:coil-video:3.2.0 -io.coil-kt.coil3:coil:3.2.0 +io.coil-kt.coil3:coil-android:3.3.0 +io.coil-kt.coil3:coil-core-android:3.3.0 +io.coil-kt.coil3:coil-core:3.3.0 +io.coil-kt.coil3:coil-gif:3.3.0 +io.coil-kt.coil3:coil-network-core-android:3.3.0 +io.coil-kt.coil3:coil-network-core:3.3.0 +io.coil-kt.coil3:coil-network-okhttp-jvm:3.3.0 +io.coil-kt.coil3:coil-network-okhttp:3.3.0 +io.coil-kt.coil3:coil-video:3.3.0 +io.coil-kt.coil3:coil:3.3.0 io.insert-koin:koin-android:4.1.1 io.insert-koin:koin-androidx-compose:4.1.1 io.insert-koin:koin-bom:4.1.1 diff --git a/app-thunderbird/dependencies/fullReleaseRuntimeClasspath.txt b/app-thunderbird/dependencies/fullReleaseRuntimeClasspath.txt index d4540eedfc..d3d3af3f2f 100644 --- a/app-thunderbird/dependencies/fullReleaseRuntimeClasspath.txt +++ b/app-thunderbird/dependencies/fullReleaseRuntimeClasspath.txt @@ -68,11 +68,11 @@ androidx.concurrent:concurrent-futures:1.1.0 androidx.constraintlayout:constraintlayout-core:1.1.1 androidx.constraintlayout:constraintlayout:2.2.1 androidx.coordinatorlayout:coordinatorlayout:1.3.0 -androidx.core:core-ktx:1.16.0 +androidx.core:core-ktx:1.17.0 androidx.core:core-remoteviews:1.1.0 androidx.core:core-splashscreen:1.0.1 androidx.core:core-viewtree:1.0.0 -androidx.core:core:1.16.0 +androidx.core:core:1.17.0 androidx.cursoradapter:cursoradapter:1.0.0 androidx.customview:customview-poolingcontainer:1.0.0 androidx.customview:customview:1.1.0 @@ -184,10 +184,10 @@ com.github.bumptech.glide:annotations:4.16.0 com.github.bumptech.glide:disklrucache:4.16.0 com.github.bumptech.glide:gifdecoder:4.16.0 com.github.bumptech.glide:glide:4.16.0 -com.github.skydoves:landscapist-android:2.5.1 -com.github.skydoves:landscapist-coil3-android:2.5.1 -com.github.skydoves:landscapist-coil3:2.5.1 -com.github.skydoves:landscapist:2.5.1 +com.github.skydoves:landscapist-android:2.6.1 +com.github.skydoves:landscapist-coil3-android:2.6.1 +com.github.skydoves:landscapist-coil3:2.6.1 +com.github.skydoves:landscapist:2.6.1 com.google.android.datatransport:transport-api:3.0.0 com.google.android.datatransport:transport-backend-cct:3.1.8 com.google.android.datatransport:transport-runtime:3.1.8 @@ -226,16 +226,16 @@ commons-io:commons-io:2.20.0 de.cketti.library.changelog:ckchangelog-core:2.0.0-beta02 de.cketti.safecontentresolver:safe-content-resolver-v21:1.0.0 de.hdodenhof:circleimageview:3.1.0 -io.coil-kt.coil3:coil-android:3.2.0 -io.coil-kt.coil3:coil-core-android:3.2.0 -io.coil-kt.coil3:coil-core:3.2.0 -io.coil-kt.coil3:coil-gif:3.2.0 -io.coil-kt.coil3:coil-network-core-android:3.2.0 -io.coil-kt.coil3:coil-network-core:3.2.0 -io.coil-kt.coil3:coil-network-okhttp-jvm:3.2.0 -io.coil-kt.coil3:coil-network-okhttp:3.2.0 -io.coil-kt.coil3:coil-video:3.2.0 -io.coil-kt.coil3:coil:3.2.0 +io.coil-kt.coil3:coil-android:3.3.0 +io.coil-kt.coil3:coil-core-android:3.3.0 +io.coil-kt.coil3:coil-core:3.3.0 +io.coil-kt.coil3:coil-gif:3.3.0 +io.coil-kt.coil3:coil-network-core-android:3.3.0 +io.coil-kt.coil3:coil-network-core:3.3.0 +io.coil-kt.coil3:coil-network-okhttp-jvm:3.3.0 +io.coil-kt.coil3:coil-network-okhttp:3.3.0 +io.coil-kt.coil3:coil-video:3.3.0 +io.coil-kt.coil3:coil:3.3.0 io.insert-koin:koin-android:4.1.1 io.insert-koin:koin-androidx-compose:4.1.1 io.insert-koin:koin-bom:4.1.1 -- GitLab From 031173a4e747b5dc8b7f493f7817e4ac9f6d903e Mon Sep 17 00:00:00 2001 From: Rafael Tonholo Date: Wed, 1 Oct 2025 10:45:18 -0300 Subject: [PATCH 006/136] feat(notifications): add in-app notification to message compose --- .../com/fsck/k9/activity/MessageCompose.java | 36 ++++++ ...MessageComposeInAppNotificationFragment.kt | 109 ++++++++++++++++++ .../src/main/res/layout/message_compose.xml | 27 +++-- 3 files changed, 165 insertions(+), 7 deletions(-) create mode 100644 legacy/ui/legacy/src/main/java/com/fsck/k9/activity/compose/MessageComposeInAppNotificationFragment.kt diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java index 0d679db8cc..8c5d356c42 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java @@ -49,9 +49,12 @@ import androidx.core.graphics.Insets; import androidx.core.os.BundleCompat; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; import app.k9mail.core.ui.legacy.designsystem.atom.icon.Icons; import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.fsck.k9.activity.compose.MessageComposeInAppNotificationFragment; import net.thunderbird.core.android.account.LegacyAccountDto; import app.k9mail.legacy.di.DI; import net.thunderbird.core.android.account.Identity; @@ -121,6 +124,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textview.MaterialTextView; import net.thunderbird.core.android.account.MessageFormat; import net.thunderbird.core.android.contact.ContactIntentHelper; +import net.thunderbird.core.featureflag.FeatureFlagProvider; +import net.thunderbird.core.featureflag.compat.FeatureFlagProviderCompat; import net.thunderbird.core.preference.GeneralSettingsManager; import net.thunderbird.core.ui.theme.manager.ThemeManager; import net.thunderbird.feature.search.legacy.LocalMessageSearch; @@ -202,6 +207,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, private final Contacts contacts = DI.get(Contacts.class); private final CameraCaptureHandler cameraCaptureHandler = DI.get(CameraCaptureHandler.class); + private final FeatureFlagProvider featureFlagProvider = DI.get(FeatureFlagProvider.class); private QuotedMessagePresenter quotedMessagePresenter; private MessageLoaderHelper messageLoaderHelper; @@ -313,6 +319,8 @@ public class MessageCompose extends K9Activity implements OnClickListener, return; } + initializeInAppNotificationFragment(); + chooseIdentityView = findViewById(R.id.identity); chooseIdentityView.setOnClickListener(this); @@ -1733,6 +1741,34 @@ public class MessageCompose extends K9Activity implements OnClickListener, actionBar.setDisplayHomeAsUpEnabled(true); } + private void initializeInAppNotificationFragment() { + if (FeatureFlagProviderCompat + .provide(featureFlagProvider, "display_in_app_notifications") + .isDisabledOrUnavailable()) { + return; + } + + if (account == null) { + Log.w("Can't initialize in-app notifications. Account is currently null"); + return; + } + final FragmentManager fragmentManager = getSupportFragmentManager(); + final Fragment currentFragment = fragmentManager + .findFragmentByTag(MessageComposeInAppNotificationFragment.FRAGMENT_TAG); + + if (currentFragment != null) { + return; + } + + final MessageComposeInAppNotificationFragment inAppNotificationFragment = + MessageComposeInAppNotificationFragment.newInstance(account.getUuid()); + fragmentManager + .beginTransaction() + .add(R.id.message_compose_in_app_notifications_container, inAppNotificationFragment, + MessageComposeInAppNotificationFragment.FRAGMENT_TAG) + .commit(); + } + // TODO We miss callbacks for this listener if they happens while we are paused! public MessagingListener messagingListener = new SimpleMessagingListener() { diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/compose/MessageComposeInAppNotificationFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/compose/MessageComposeInAppNotificationFragment.kt new file mode 100644 index 0000000000..a4acca448f --- /dev/null +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/compose/MessageComposeInAppNotificationFragment.kt @@ -0,0 +1,109 @@ +package com.fsck.k9.activity.compose + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.animation.animateContentSize +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import com.google.android.material.snackbar.Snackbar +import kotlinx.collections.immutable.persistentSetOf +import net.thunderbird.core.logging.Logger +import net.thunderbird.core.ui.theme.api.FeatureThemeProvider +import net.thunderbird.feature.account.AccountId +import net.thunderbird.feature.account.AccountIdFactory +import net.thunderbird.feature.notification.api.ui.InAppNotificationHost +import net.thunderbird.feature.notification.api.ui.action.NotificationAction +import net.thunderbird.feature.notification.api.ui.host.DisplayInAppNotificationFlag +import net.thunderbird.feature.notification.api.ui.host.visual.SnackbarVisual +import net.thunderbird.feature.notification.api.ui.style.SnackbarDuration +import org.koin.android.ext.android.inject + +private const val TAG = "MessageComposeInAppNotificationFragment" + +class MessageComposeInAppNotificationFragment : Fragment() { + private val themeProvider: FeatureThemeProvider by inject() + private val logger: Logger by inject() + private var parentView: View? = null + private var accountId: AccountId? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { arg -> + accountId = requireNotNull(arg.getString(ARG_ACCOUNT_ID)?.let { AccountIdFactory.of(it) }) { + "Argument $ARG_ACCOUNT_ID is required" + } + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + parentView = container + setContent { + themeProvider.WithTheme { + InAppNotificationHost( + onActionClick = ::onNotificationActionClick, + enabled = persistentSetOf( + DisplayInAppNotificationFlag.BannerGlobalNotifications, + DisplayInAppNotificationFlag.SnackbarNotifications, + ), + onSnackbarNotificationEvent = ::onSnackbarInAppNotificationEvent, + eventFilter = { event -> + val accountUuid = event.notification.accountUuid + accountUuid != null && accountUuid == accountId?.asRaw() + }, + modifier = Modifier.animateContentSize(), + ) + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + parentView = null + } + + private suspend fun onSnackbarInAppNotificationEvent(visual: SnackbarVisual) { + parentView?.let { view -> + val (message, action, duration) = visual + Snackbar.make( + view, + message, + when (duration) { + SnackbarDuration.Short -> Snackbar.LENGTH_SHORT + SnackbarDuration.Long -> Snackbar.LENGTH_LONG + SnackbarDuration.Indefinite -> Snackbar.LENGTH_INDEFINITE + }, + ).apply { + if (action != null) { + setAction(action.resolveTitle()) { + // TODO. + } + } + }.show() + } + } + + private fun onNotificationActionClick(action: NotificationAction) { + logger.verbose(TAG) { "onNotificationActionClick() called with: action = $action" } + } + + companion object { + private const val ARG_ACCOUNT_ID = "MessageComposeInAppNotificationFragment_account_id" + const val FRAGMENT_TAG = "MessageComposeInAppNotificationFragment" + + fun newInstance(accountId: AccountId): MessageComposeInAppNotificationFragment = + MessageComposeInAppNotificationFragment().apply { + arguments = bundleOf(ARG_ACCOUNT_ID to accountId.asRaw()) + } + + @JvmStatic + fun newInstance(accountUuid: String): MessageComposeInAppNotificationFragment = + newInstance(AccountIdFactory.of(accountUuid)) + } +} diff --git a/legacy/ui/legacy/src/main/res/layout/message_compose.xml b/legacy/ui/legacy/src/main/res/layout/message_compose.xml index 30ca8c8c26..218d76f0cc 100644 --- a/legacy/ui/legacy/src/main/res/layout/message_compose.xml +++ b/legacy/ui/legacy/src/main/res/layout/message_compose.xml @@ -10,13 +10,26 @@ - + android:layout_height="match_parent" + android:orientation="vertical" + > + + + + -- GitLab From 4d3f42152f04b7780dd472a3b538d535518ebc85 Mon Sep 17 00:00:00 2001 From: Rafael Tonholo Date: Thu, 2 Oct 2025 08:36:27 -0300 Subject: [PATCH 007/136] chore(notifications): improve banner animations --- .../BannerSlideInSlideOutAnimationSpec.kt | 24 ++++++++++++------- ...MessageComposeInAppNotificationFragment.kt | 3 --- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/animation/BannerSlideInSlideOutAnimationSpec.kt b/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/animation/BannerSlideInSlideOutAnimationSpec.kt index 466669cfb2..1cac739cbd 100644 --- a/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/animation/BannerSlideInSlideOutAnimationSpec.kt +++ b/feature/notification/api/src/androidMain/kotlin/net/thunderbird/feature/notification/api/ui/animation/BannerSlideInSlideOutAnimationSpec.kt @@ -4,10 +4,10 @@ import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.ContentTransform import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.keyframes +import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.shrinkVertically import androidx.compose.animation.togetherWith import androidx.compose.ui.unit.IntSize @@ -30,12 +30,20 @@ private const val A_QUARTER = 4 * as well as the size transformation. */ fun AnimatedContentTransitionScope.bannerSlideInSlideOutAnimationSpec(): ContentTransform { - val enter = fadeIn() + slideInVertically() - val exit = fadeOut() + slideOutVertically() - return enter togetherWith exit using SizeTransform { initialSize, targetSize -> - keyframes { - IntSize(width = targetSize.width, height = initialSize.height) at durationMillis / A_QUARTER - IntSize(width = targetSize.width, height = targetSize.height) + val enter = fadeIn() + expandVertically() + val exit = fadeOut() + shrinkVertically() + return (enter togetherWith exit) using SizeTransform { initialSize, targetSize -> + this.contentAlignment + if (targetState != null) { + keyframes { + IntSize(width = targetSize.width, height = initialSize.height) at durationMillis / A_QUARTER + IntSize(width = targetSize.width, height = targetSize.height) + } + } else { + keyframes { + IntSize(width = initialSize.width, height = initialSize.height) at durationMillis / A_QUARTER + IntSize(width = initialSize.width, height = 0) + } } } } diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/compose/MessageComposeInAppNotificationFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/compose/MessageComposeInAppNotificationFragment.kt index a4acca448f..9250c94389 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/compose/MessageComposeInAppNotificationFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/compose/MessageComposeInAppNotificationFragment.kt @@ -4,8 +4,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.animation.animateContentSize -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.os.bundleOf @@ -57,7 +55,6 @@ class MessageComposeInAppNotificationFragment : Fragment() { val accountUuid = event.notification.accountUuid accountUuid != null && accountUuid == accountId?.asRaw() }, - modifier = Modifier.animateContentSize(), ) } } -- GitLab From 058d244c2e667724721332ea50ac5c0312291152 Mon Sep 17 00:00:00 2001 From: shamim-emon Date: Tue, 30 Sep 2025 20:10:12 +0600 Subject: [PATCH 008/136] feat(message-list): add avatar to message list content --- .../common/contact/ContactRepository.kt | 10 ++ legacy/ui/legacy/build.gradle.kts | 2 + .../item/MessageItemContentPreview.kt | 26 +++++ .../fsck/k9/contacts/ContactPhotoLoader.kt | 10 +- .../k9/ui/messagelist/MessageListAdapter.kt | 6 + .../k9/ui/messagelist/MessageListFragment.kt | 7 ++ .../item/ComposableMessageViewHolder.kt | 13 ++- .../ui/messagelist/item/MessageItemContent.kt | 105 +++++++++++++++++- .../ui/messagelist/MessageListAdapterTest.kt | 2 + 9 files changed, 167 insertions(+), 14 deletions(-) diff --git a/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/ContactRepository.kt b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/ContactRepository.kt index ef97d63a05..a03105d31c 100644 --- a/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/ContactRepository.kt +++ b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/ContactRepository.kt @@ -1,7 +1,9 @@ package app.k9mail.core.android.common.contact +import android.net.Uri import net.thunderbird.core.common.cache.Cache import net.thunderbird.core.common.mail.EmailAddress +import net.thunderbird.core.common.mail.toEmailAddressOrNull interface ContactRepository { @@ -10,6 +12,8 @@ interface ContactRepository { fun hasContactFor(emailAddress: EmailAddress): Boolean fun hasAnyContactFor(emailAddresses: List): Boolean + + fun getPhotoUri(emailAddress: String): Uri? } interface CachingRepository { @@ -42,6 +46,12 @@ internal class CachingContactRepository( override fun hasAnyContactFor(emailAddresses: List): Boolean = emailAddresses.any { emailAddress -> hasContactFor(emailAddress) } + override fun getPhotoUri(emailAddress: String): Uri? { + return emailAddress.toEmailAddressOrNull()?.let { emailAddress -> + getContactFor(emailAddress)?.photoUri + } + } + override fun clearCache() { cache.clear() } diff --git a/legacy/ui/legacy/build.gradle.kts b/legacy/ui/legacy/build.gradle.kts index 6042f39e6e..909775faad 100644 --- a/legacy/ui/legacy/build.gradle.kts +++ b/legacy/ui/legacy/build.gradle.kts @@ -27,6 +27,8 @@ dependencies { implementation(projects.feature.notification.api) // TODO: Remove AccountOauth dependency implementation(projects.feature.account.oauth) + implementation(projects.feature.account.avatar.api) + implementation(projects.feature.account.avatar.impl) implementation(projects.feature.funding.api) implementation(projects.feature.search.implLegacy) implementation(projects.feature.settings.import) diff --git a/legacy/ui/legacy/src/debug/kotlin/com/fsck/k9/ui/messagelist/item/MessageItemContentPreview.kt b/legacy/ui/legacy/src/debug/kotlin/com/fsck/k9/ui/messagelist/item/MessageItemContentPreview.kt index aae82769ac..cdee97efbb 100644 --- a/legacy/ui/legacy/src/debug/kotlin/com/fsck/k9/ui/messagelist/item/MessageItemContentPreview.kt +++ b/legacy/ui/legacy/src/debug/kotlin/com/fsck/k9/ui/messagelist/item/MessageItemContentPreview.kt @@ -2,6 +2,8 @@ package com.fsck.k9.ui.messagelist.item import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.PreviewLightDark +import app.k9mail.core.android.common.contact.Contact +import app.k9mail.core.android.common.contact.ContactRepository import app.k9mail.core.ui.compose.designsystem.PreviewWithThemesLightDark import com.fsck.k9.FontSizes import com.fsck.k9.UiDensity @@ -12,7 +14,9 @@ import com.fsck.k9.ui.messagelist.MessageListAppearance import com.fsck.k9.ui.messagelist.MessageListItem import net.thunderbird.core.android.account.Identity import net.thunderbird.core.android.account.LegacyAccount +import net.thunderbird.core.common.mail.EmailAddress import net.thunderbird.feature.account.AccountIdFactory +import net.thunderbird.feature.account.avatar.AvatarMonogramCreator import net.thunderbird.feature.account.storage.profile.AvatarDto import net.thunderbird.feature.account.storage.profile.AvatarTypeDto import net.thunderbird.feature.account.storage.profile.ProfileDto @@ -25,6 +29,8 @@ internal fun MessageItemContentPreview() { item = fakeMessageListItem, isActive = true, isSelected = false, + contactRepository = fakeContactRepository, + avatarMonogramCreator = fakeAvatarMonogramCreator, onClick = {}, onLongClick = {}, onAvatarClick = {}, @@ -97,3 +103,23 @@ private val fakeMessageListAppearance = MessageListAppearance( showAccountIndicator = true, density = UiDensity.Default, ) + +private val fakeContactRepository = object : ContactRepository { + override fun getContactFor(emailAddress: EmailAddress): Contact? { + error("Not implemented") + } + + override fun hasContactFor(emailAddress: EmailAddress): Boolean { + error("Not implemented") + } + + override fun hasAnyContactFor(emailAddresses: List): Boolean { + error("Not implemented") + } + + override fun getPhotoUri(emailAddress: String) = null +} + +private val fakeAvatarMonogramCreator = object : AvatarMonogramCreator { + override fun create(name: String?, email: String?) = "SE" +} diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/contacts/ContactPhotoLoader.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/contacts/ContactPhotoLoader.kt index 589f5319dd..0c8a2be7b0 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/contacts/ContactPhotoLoader.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/contacts/ContactPhotoLoader.kt @@ -3,9 +3,7 @@ package com.fsck.k9.contacts import android.content.ContentResolver import android.graphics.Bitmap import android.graphics.BitmapFactory -import android.net.Uri import app.k9mail.core.android.common.contact.ContactRepository -import net.thunderbird.core.common.mail.toEmailAddressOrNull import net.thunderbird.core.logging.legacy.Log internal class ContactPhotoLoader( @@ -13,7 +11,7 @@ internal class ContactPhotoLoader( private val contactRepository: ContactRepository, ) { fun loadContactPhoto(emailAddress: String): Bitmap? { - val photoUri = getPhotoUri(emailAddress) ?: return null + val photoUri = contactRepository.getPhotoUri(emailAddress = emailAddress) ?: return null return try { contentResolver.openInputStream(photoUri).use { inputStream -> BitmapFactory.decodeStream(inputStream) @@ -23,10 +21,4 @@ internal class ContactPhotoLoader( null } } - - private fun getPhotoUri(email: String): Uri? { - return email.toEmailAddressOrNull()?.let { emailAddress -> - contactRepository.getContactFor(emailAddress)?.photoUri - } - } } diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt index 4490af34d0..054ad19ed8 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt @@ -11,6 +11,7 @@ import android.view.ViewGroup import androidx.compose.ui.platform.ComposeView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import app.k9mail.core.android.common.contact.ContactRepository import app.k9mail.feature.launcher.FeatureLauncherActivity import app.k9mail.feature.launcher.FeatureLauncherTarget import app.k9mail.legacy.message.controller.MessageReference @@ -27,6 +28,7 @@ import net.thunderbird.core.featureflag.FeatureFlagKey import net.thunderbird.core.featureflag.FeatureFlagProvider import net.thunderbird.core.featureflag.FeatureFlagResult import net.thunderbird.core.ui.theme.api.FeatureThemeProvider +import net.thunderbird.feature.account.avatar.AvatarMonogramCreator import net.thunderbird.feature.notification.api.ui.action.NotificationAction private const val FOOTER_ID = 1L @@ -47,6 +49,8 @@ class MessageListAdapter internal constructor( private val relativeDateTimeFormatter: RelativeDateTimeFormatter, private val themeProvider: FeatureThemeProvider, private val featureFlagProvider: FeatureFlagProvider, + private val contactRepository: ContactRepository, + private val avatarMonogramCreator: AvatarMonogramCreator, ) : RecyclerView.Adapter() { val colors: MessageViewHolderColors = MessageViewHolderColors.resolveColors(theme) @@ -266,6 +270,8 @@ class MessageListAdapter internal constructor( ComposableMessageViewHolder.create( context = parent.context, themeProvider = themeProvider, + contactRepository = contactRepository, + avatarMonogramCreator = avatarMonogramCreator, onClick = { listItemListener.onMessageClicked(it) }, onLongClick = { listItemListener.onToggleMessageSelection(it) }, onFavouriteClick = { listItemListener.onToggleMessageFlag(it) }, diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt index 73f27f7364..be916cf229 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt @@ -34,6 +34,7 @@ import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import app.k9mail.core.android.common.contact.ContactRepository import app.k9mail.legacy.message.controller.MessageReference import app.k9mail.legacy.message.controller.MessagingControllerRegistry import app.k9mail.legacy.message.controller.SimpleMessagingListener @@ -92,6 +93,7 @@ import net.thunderbird.core.logging.Logger import net.thunderbird.core.logging.legacy.Log import net.thunderbird.core.preference.GeneralSettingsManager import net.thunderbird.core.ui.theme.api.FeatureThemeProvider +import net.thunderbird.feature.account.avatar.AvatarMonogramCreator import net.thunderbird.feature.mail.folder.api.OutboxFolderManager import net.thunderbird.feature.mail.message.list.domain.DomainContract import net.thunderbird.feature.mail.message.list.ui.dialog.SetupArchiveFolderDialogFragmentFactory @@ -145,6 +147,9 @@ class MessageListFragment : private val activityListener = MessageListActivityListener() private val actionModeCallback = ActionModeCallback() + private val contactRepository: ContactRepository by inject() + private val avatarMonogramCreator: AvatarMonogramCreator by inject() + private val chooseFolderForMoveLauncher: ActivityResultLauncher = registerForActivityResult(ChooseFolderResultContract(ChooseFolderActivity.Action.MOVE)) { result -> handleChooseFolderResult(result) { folderId, messages -> @@ -348,6 +353,8 @@ class MessageListFragment : relativeDateTimeFormatter = RelativeDateTimeFormatter(requireContext(), clock), themeProvider = featureThemeProvider, featureFlagProvider = featureFlagProvider, + contactRepository = contactRepository, + avatarMonogramCreator = avatarMonogramCreator, ).apply { activeMessage = this@MessageListFragment.activeMessage } diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/ComposableMessageViewHolder.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/ComposableMessageViewHolder.kt index 13d92b69df..0bd2ceb95f 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/ComposableMessageViewHolder.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/ComposableMessageViewHolder.kt @@ -2,13 +2,16 @@ package com.fsck.k9.ui.messagelist.item import android.content.Context import androidx.compose.ui.platform.ComposeView +import app.k9mail.core.android.common.contact.ContactRepository import com.fsck.k9.ui.messagelist.MessageListAppearance import com.fsck.k9.ui.messagelist.MessageListItem import net.thunderbird.core.ui.theme.api.FeatureThemeProvider +import net.thunderbird.feature.account.avatar.AvatarMonogramCreator /** * A composable view holder for message list items. */ +@Suppress("LongParameterList") class ComposableMessageViewHolder( private val composeView: ComposeView, private val themeProvider: FeatureThemeProvider, @@ -17,6 +20,8 @@ class ComposableMessageViewHolder( private val onAvatarClick: (MessageListItem) -> Unit, private val onFavouriteClick: (MessageListItem) -> Unit, private val appearance: MessageListAppearance, + private val contactRepository: ContactRepository, + private val avatarMonogramCreator: AvatarMonogramCreator, ) : MessageListViewHolder(composeView) { var uniqueId: Long = -1L @@ -30,6 +35,8 @@ class ComposableMessageViewHolder( item = item, isActive = isActive, isSelected = isSelected, + contactRepository = contactRepository, + avatarMonogramCreator = avatarMonogramCreator, onClick = { onClick(item) }, onLongClick = { onLongClick(item) }, onAvatarClick = { onAvatarClick(item) }, @@ -41,10 +48,12 @@ class ComposableMessageViewHolder( } companion object { - + @Suppress("LongParameterList") fun create( context: Context, themeProvider: FeatureThemeProvider, + contactRepository: ContactRepository, + avatarMonogramCreator: AvatarMonogramCreator, onClick: (MessageListItem) -> Unit, onLongClick: (MessageListItem) -> Unit, onFavouriteClick: (MessageListItem) -> Unit, @@ -56,6 +65,8 @@ class ComposableMessageViewHolder( val holder = ComposableMessageViewHolder( composeView = composeView, themeProvider = themeProvider, + contactRepository = contactRepository, + avatarMonogramCreator = avatarMonogramCreator, onClick = onClick, onLongClick = onLongClick, onAvatarClick = onAvatarClick, diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/MessageItemContent.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/MessageItemContent.kt index 3ff073a960..e9059d461b 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/MessageItemContent.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/MessageItemContent.kt @@ -1,7 +1,28 @@ package com.fsck.k9.ui.messagelist.item +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import app.k9mail.core.android.common.contact.ContactRepository +import app.k9mail.core.ui.compose.designsystem.atom.CircularProgressIndicator +import app.k9mail.core.ui.compose.designsystem.atom.image.RemoteImage +import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleSmall +import app.k9mail.core.ui.compose.theme2.MainTheme import com.fsck.k9.ui.messagelist.MessageListAppearance import com.fsck.k9.ui.messagelist.MessageListItem import kotlin.time.ExperimentalTime @@ -11,14 +32,17 @@ import kotlinx.datetime.toLocalDateTime import net.thunderbird.core.ui.compose.designsystem.organism.message.ActiveMessageItem import net.thunderbird.core.ui.compose.designsystem.organism.message.ReadMessageItem import net.thunderbird.core.ui.compose.designsystem.organism.message.UnreadMessageItem +import net.thunderbird.feature.account.avatar.AvatarMonogramCreator -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LongMethod") @OptIn(ExperimentalTime::class) @Composable internal fun MessageItemContent( item: MessageListItem, isActive: Boolean, isSelected: Boolean, + contactRepository: ContactRepository, + avatarMonogramCreator: AvatarMonogramCreator, onClick: () -> Unit, onLongClick: () -> Unit, onAvatarClick: () -> Unit, @@ -30,13 +54,32 @@ internal fun MessageItemContent( .toLocalDateTime(TimeZone.currentSystemDefault()) } + val uri by remember(item.displayAddress?.address) { + mutableStateOf( + contactRepository.getPhotoUri( + item.displayAddress?.address ?: "", + ), + ) + } + val monogram by remember(item.displayName.toString(), item.displayAddress?.address) { + mutableStateOf(avatarMonogramCreator.create(item.displayName.toString(), item.displayAddress?.address)) + } + when { isActive -> ActiveMessageItem( sender = "${item.displayName}", subject = item.subject ?: "n/a", preview = item.previewText, receivedAt = receivedAt, - avatar = {}, + avatar = { + if (appearance.showContactPicture) { + ContactImageAvatar( + contactImageUri = uri, + contactImageMonogram = monogram, + onAvatarClick = onAvatarClick, + ) + } + }, onClick = onClick, onLongClick = onLongClick, onLeadingClick = onAvatarClick, @@ -48,12 +91,21 @@ internal fun MessageItemContent( hasAttachments = item.hasAttachments, swapSenderWithSubject = !appearance.senderAboveSubject, ) + item.isRead -> ReadMessageItem( sender = "${item.displayName}", subject = item.subject ?: "n/a", preview = item.previewText, receivedAt = receivedAt, - avatar = {}, + avatar = { + if (appearance.showContactPicture) { + ContactImageAvatar( + contactImageUri = uri, + contactImageMonogram = monogram, + onAvatarClick = onAvatarClick, + ) + } + }, onClick = onClick, onLongClick = onLongClick, onLeadingClick = onAvatarClick, @@ -65,12 +117,21 @@ internal fun MessageItemContent( hasAttachments = item.hasAttachments, swapSenderWithSubject = !appearance.senderAboveSubject, ) + else -> UnreadMessageItem( sender = "${item.displayName}", subject = item.subject ?: "n/a", preview = item.previewText, receivedAt = receivedAt, - avatar = {}, + avatar = { + if (appearance.showContactPicture) { + ContactImageAvatar( + contactImageUri = uri, + contactImageMonogram = monogram, + onAvatarClick = onAvatarClick, + ) + } + }, onClick = onClick, onLongClick = onLongClick, onLeadingClick = onAvatarClick, @@ -84,3 +145,39 @@ internal fun MessageItemContent( ) } } + +@Composable +fun ContactImageAvatar( + contactImageUri: Uri?, + contactImageMonogram: String, + modifier: Modifier = Modifier, + onAvatarClick: () -> Unit, +) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(MainTheme.sizes.iconAvatar) + .padding(MainTheme.spacings.half) + .background(color = MainTheme.colors.primaryContainer.copy(alpha = 0.15f), shape = CircleShape) + .border(width = 1.dp, color = MainTheme.colors.primary, shape = CircleShape) + .clickable(onClick = onAvatarClick), + ) { + contactImageUri?.let { + RemoteImage( + url = it.toString(), + contentScale = ContentScale.Crop, + alignment = Alignment.Center, + modifier = modifier + .fillMaxSize() + .clip(CircleShape), + placeholder = { + Box(contentAlignment = Alignment.Center, modifier = Modifier.size(MainTheme.sizes.iconAvatar)) { + CircularProgressIndicator(modifier = Modifier.size(MainTheme.sizes.icon)) + } + }, + ) + } ?: run { + TextTitleSmall(text = contactImageMonogram) + } + } +} diff --git a/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt b/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt index c1a4e6f0ea..a42052cf25 100644 --- a/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt +++ b/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt @@ -438,6 +438,8 @@ class MessageListAdapterTest : RobolectricTest() { relativeDateTimeFormatter = RelativeDateTimeFormatter(context, TestClock()), themeProvider = FakeThemeProvider(), featureFlagProvider = FakeFeatureFlagProvider(), + avatarMonogramCreator = mock(), + contactRepository = mock(), ) } -- GitLab From eb32b4730585386fc0ccef2fa05cb58865f26cf3 Mon Sep 17 00:00:00 2001 From: shamim-emon Date: Fri, 10 Oct 2025 20:34:16 +0600 Subject: [PATCH 009/136] test(CachingContactRepositoryTest): add test coverage for getPhotoUri() --- .../contact/CachingContactRepositoryTest.kt | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/CachingContactRepositoryTest.kt b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/CachingContactRepositoryTest.kt index a4eed9a1e6..3854450ef7 100644 --- a/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/CachingContactRepositoryTest.kt +++ b/core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/CachingContactRepositoryTest.kt @@ -1,5 +1,6 @@ package app.k9mail.core.android.common.contact +import android.net.Uri import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isFalse @@ -140,4 +141,53 @@ internal class CachingContactRepositoryTest { assertThat(cache[CONTACT_EMAIL_ADDRESS]).isNull() } + + @Test + fun `getPhotoUri() returns null when email is invalid`() { + val result = testSubject.getPhotoUri("invalid-email") + + assertThat(result).isNull() + } + + @Test + fun `getPhotoUri() returns null when no contact found for valid email`() { + dataSource.stub { on { getContactFor(CONTACT_EMAIL_ADDRESS) } doReturn null } + + val result = testSubject.getPhotoUri(CONTACT_EMAIL_ADDRESS.address) + + assertThat(result).isNull() + } + + @Test + fun `getPhotoUri() returns contact photo uri when contact exists`() { + dataSource.stub { on { getContactFor(CONTACT_EMAIL_ADDRESS) } doReturn CONTACT } + + val result = testSubject.getPhotoUri(CONTACT_EMAIL_ADDRESS.address) + + assertThat(result).isEqualTo(CONTACT.photoUri) + } + + @Test + fun `getPhotoUri() returns cached photo uri when contact already cached`() { + cache[CONTACT_EMAIL_ADDRESS] = CONTACT + + val result = testSubject.getPhotoUri(CONTACT_EMAIL_ADDRESS.address) + + assertThat(result).isEqualTo(CONTACT.photoUri) + } + + @Test + fun `getPhotoUri() caches result after first fetch`() { + dataSource.stub { + on { getContactFor(CONTACT_EMAIL_ADDRESS) } doReturnConsecutively listOf( + CONTACT, + CONTACT.copy(photoUri = Uri.parse("content://other/photo")), + ) + } + + val result1 = testSubject.getPhotoUri(CONTACT_EMAIL_ADDRESS.address) + val result2 = testSubject.getPhotoUri(CONTACT_EMAIL_ADDRESS.address) + + assertThat(result1).isEqualTo(result2) + } } -- GitLab From 72b99a7bb7629c45e466317dde6e6f05b1e004c8 Mon Sep 17 00:00:00 2001 From: shamim-emon Date: Sat, 11 Oct 2025 21:05:00 +0600 Subject: [PATCH 010/136] refactor: migrate K9.isNotificationDuringQuietTimeEnabled to PreferenceDataStore --- .../notification/NotificationPreference.kt | 3 +++ .../NotificationPreferenceManager.kt | 1 + .../DefaultNotificationPreferenceManager.kt | 8 ++++++++ .../k9/notification/K9NotificationStrategy.kt | 5 +++-- legacy/core/src/main/java/com/fsck/k9/K9.kt | 5 ----- .../settings/general/GeneralSettingsDataStore.kt | 16 ++++++++++++++-- 6 files changed, 29 insertions(+), 9 deletions(-) diff --git a/core/preference/api/src/commonMain/kotlin/net/thunderbird/core/preference/notification/NotificationPreference.kt b/core/preference/api/src/commonMain/kotlin/net/thunderbird/core/preference/notification/NotificationPreference.kt index 43cc2a6a87..07b660b994 100644 --- a/core/preference/api/src/commonMain/kotlin/net/thunderbird/core/preference/notification/NotificationPreference.kt +++ b/core/preference/api/src/commonMain/kotlin/net/thunderbird/core/preference/notification/NotificationPreference.kt @@ -3,9 +3,12 @@ package net.thunderbird.core.preference.notification const val NOTIFICATION_PREFERENCE_DEFAULT_IS_QUIET_TIME_ENABLED = false const val NOTIFICATION_PREFERENCE_DEFAULT_QUIET_TIME_STARTS = "21:00" const val NOTIFICATION_PREFERENCE_DEFAULT_QUIET_TIME_END = "7:00" +const val NOTIFICATION_PREFERENCE_DEFAULT_IS_NOTIFICATION_DURING_QUIET_TIME_ENABLED = true data class NotificationPreference( val isQuietTimeEnabled: Boolean = NOTIFICATION_PREFERENCE_DEFAULT_IS_QUIET_TIME_ENABLED, val quietTimeStarts: String = NOTIFICATION_PREFERENCE_DEFAULT_QUIET_TIME_STARTS, val quietTimeEnds: String = NOTIFICATION_PREFERENCE_DEFAULT_QUIET_TIME_END, + val isNotificationDuringQuietTimeEnabled: Boolean = + NOTIFICATION_PREFERENCE_DEFAULT_IS_NOTIFICATION_DURING_QUIET_TIME_ENABLED, ) diff --git a/core/preference/api/src/commonMain/kotlin/net/thunderbird/core/preference/notification/NotificationPreferenceManager.kt b/core/preference/api/src/commonMain/kotlin/net/thunderbird/core/preference/notification/NotificationPreferenceManager.kt index e9a402a658..b28e1ab536 100644 --- a/core/preference/api/src/commonMain/kotlin/net/thunderbird/core/preference/notification/NotificationPreferenceManager.kt +++ b/core/preference/api/src/commonMain/kotlin/net/thunderbird/core/preference/notification/NotificationPreferenceManager.kt @@ -5,5 +5,6 @@ import net.thunderbird.core.preference.PreferenceManager const val KEY_QUIET_TIME_ENDS = "quietTimeEnds" const val KEY_QUIET_TIME_STARTS = "quietTimeStarts" const val KEY_QUIET_TIME_ENABLED = "quietTimeEnabled" +const val KEY_NOTIFICATION_DURING_QUIET_TIME_ENABLED = "notificationDuringQuietTimeEnabled" interface NotificationPreferenceManager : PreferenceManager diff --git a/core/preference/impl/src/commonMain/kotlin/net/thunderbird/core/preference/notification/DefaultNotificationPreferenceManager.kt b/core/preference/impl/src/commonMain/kotlin/net/thunderbird/core/preference/notification/DefaultNotificationPreferenceManager.kt index 9549c3af62..96121c53ab 100644 --- a/core/preference/impl/src/commonMain/kotlin/net/thunderbird/core/preference/notification/DefaultNotificationPreferenceManager.kt +++ b/core/preference/impl/src/commonMain/kotlin/net/thunderbird/core/preference/notification/DefaultNotificationPreferenceManager.kt @@ -38,6 +38,10 @@ class DefaultNotificationPreferenceManager( key = KEY_QUIET_TIME_ENDS, defValue = NOTIFICATION_PREFERENCE_DEFAULT_QUIET_TIME_END, ), + isNotificationDuringQuietTimeEnabled = storage.getBoolean( + key = KEY_NOTIFICATION_DURING_QUIET_TIME_ENABLED, + defValue = NOTIFICATION_PREFERENCE_DEFAULT_IS_NOTIFICATION_DURING_QUIET_TIME_ENABLED, + ), ), ) @@ -54,6 +58,10 @@ class DefaultNotificationPreferenceManager( KEY_QUIET_TIME_ENABLED, config.isQuietTimeEnabled, ) + storageEditor.putBoolean( + KEY_NOTIFICATION_DURING_QUIET_TIME_ENABLED, + config.isNotificationDuringQuietTimeEnabled, + ) storageEditor.commit().also { commited -> logger.verbose(TAG) { "writeConfig: storageEditor.commit() resulted in: $commited" } } diff --git a/legacy/common/src/main/java/com/fsck/k9/notification/K9NotificationStrategy.kt b/legacy/common/src/main/java/com/fsck/k9/notification/K9NotificationStrategy.kt index 447602db6f..b0367a66fa 100644 --- a/legacy/common/src/main/java/com/fsck/k9/notification/K9NotificationStrategy.kt +++ b/legacy/common/src/main/java/com/fsck/k9/notification/K9NotificationStrategy.kt @@ -2,7 +2,6 @@ package com.fsck.k9.notification import app.k9mail.core.android.common.contact.ContactRepository import app.k9mail.legacy.di.DI -import com.fsck.k9.K9 import com.fsck.k9.QuietTimeChecker import com.fsck.k9.mail.Flag import com.fsck.k9.mail.K9MailLib @@ -29,7 +28,9 @@ class K9NotificationStrategy( message: LocalMessage, isOldMessage: Boolean, ): Boolean { - if (!K9.isNotificationDuringQuietTimeEnabled && generalSettingsManager.getConfig().notification.isQuietTime) { + if (!generalSettingsManager.getConfig().notification.isNotificationDuringQuietTimeEnabled && + generalSettingsManager.getConfig().notification.isQuietTime + ) { Log.v("No notification: Quiet time is active") return false } diff --git a/legacy/core/src/main/java/com/fsck/k9/K9.kt b/legacy/core/src/main/java/com/fsck/k9/K9.kt index 657e5557ad..a265fe3025 100644 --- a/legacy/core/src/main/java/com/fsck/k9/K9.kt +++ b/legacy/core/src/main/java/com/fsck/k9/K9.kt @@ -155,8 +155,6 @@ object K9 : KoinComponent { @JvmStatic var isShowAccountSelector = true - var isNotificationDuringQuietTimeEnabled = true - @get:Synchronized @set:Synchronized @JvmStatic @@ -237,8 +235,6 @@ object K9 : KoinComponent { isShowAccountSelector = storage.getBoolean("showAccountSelector", true) messageListPreviewLines = storage.getInt("messageListPreviewLines", 2) - isNotificationDuringQuietTimeEnabled = storage.getBoolean("notificationDuringQuietTimeEnabled", true) - messageListDensity = storage.getEnum("messageListDensity", UiDensity.Default) contactNameColor = storage.getInt("registeredNameColor", 0xFF1093F5.toInt()) messageViewPostMarkAsUnreadNavigation = @@ -296,7 +292,6 @@ object K9 : KoinComponent { @Suppress("LongMethod") internal fun save(editor: StorageEditor) { - editor.putBoolean("notificationDuringQuietTimeEnabled", isNotificationDuringQuietTimeEnabled) editor.putEnum("messageListDensity", messageListDensity) editor.putBoolean("showAccountSelector", isShowAccountSelector) editor.putInt("messageListPreviewLines", messageListPreviewLines) diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsDataStore.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsDataStore.kt index 57e709e504..13be46d406 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsDataStore.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsDataStore.kt @@ -68,7 +68,8 @@ class GeneralSettingsDataStore( "quiet_time_enabled" -> generalSettingsManager.getConfig() .notification.isQuietTimeEnabled - "disable_notifications_during_quiet_time" -> !K9.isNotificationDuringQuietTimeEnabled + "disable_notifications_during_quiet_time" -> !generalSettingsManager.getConfig() + .notification.isNotificationDuringQuietTimeEnabled "privacy_hide_useragent" -> generalSettingsManager.getConfig().privacy.isHideUserAgent "privacy_hide_timezone" -> generalSettingsManager.getConfig().privacy.isHideTimeZone "debug_logging" -> generalSettingsManager.getConfig().debugging.isDebugLoggingEnabled @@ -108,7 +109,7 @@ class GeneralSettingsDataStore( "messageview_fixedwidth_font" -> setIsUseMessageViewFixedWidthFont(isUseMessageViewFixedWidthFont = value) "messageview_autofit_width" -> setIsAutoFitWidth(isAutoFitWidth = value) "quiet_time_enabled" -> setIsQuietTimeEnabled(isQuietTimeEnabled = value) - "disable_notifications_during_quiet_time" -> K9.isNotificationDuringQuietTimeEnabled = !value + "disable_notifications_during_quiet_time" -> setIsNotificationDuringQuietTimeEnabled(!value) "privacy_hide_useragent" -> setIsHideUserAgent(isHideUserAgent = value) "privacy_hide_timezone" -> setIsHideTimeZone(isHideTimeZone = value) "debug_logging" -> setIsDebugLoggingEnabled(isDebugLoggingEnabled = value) @@ -585,6 +586,17 @@ class GeneralSettingsDataStore( } } + private fun setIsNotificationDuringQuietTimeEnabled(isNotificationDuringQuietTimeEnabled: Boolean) { + skipSaveSettings = true + generalSettingsManager.update { settings -> + settings.copy( + notification = settings.notification.copy( + isNotificationDuringQuietTimeEnabled = isNotificationDuringQuietTimeEnabled, + ), + ) + } + } + private fun setIsHideTimeZone(isHideTimeZone: Boolean) { skipSaveSettings = true generalSettingsManager.update { settings -> -- GitLab From f30536c8d3042f1d3ad10c654650fd4c92077065 Mon Sep 17 00:00:00 2001 From: Corey Bryant Date: Thu, 16 Oct 2025 17:57:40 -0400 Subject: [PATCH 011/136] Bump versionName to 15.0 --- app-k9mail/build.gradle.kts | 2 +- app-thunderbird/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app-k9mail/build.gradle.kts b/app-k9mail/build.gradle.kts index a12a0ba93b..328cc5e569 100644 --- a/app-k9mail/build.gradle.kts +++ b/app-k9mail/build.gradle.kts @@ -18,7 +18,7 @@ android { testApplicationId = "com.fsck.k9.tests" versionCode = 39004 - versionName = "14.0" + versionName = "15.0" versionNameSuffix = "a1" buildConfigField("String", "CLIENT_INFO_APP_NAME", "\"K-9 Mail\"") diff --git a/app-thunderbird/build.gradle.kts b/app-thunderbird/build.gradle.kts index 8e8b9c4965..42e90c34b6 100644 --- a/app-thunderbird/build.gradle.kts +++ b/app-thunderbird/build.gradle.kts @@ -18,7 +18,7 @@ android { testApplicationId = "net.thunderbird.android.tests" versionCode = 4 - versionName = "14.0" + versionName = "15.0" buildConfigField("String", "CLIENT_INFO_APP_NAME", "\"Thunderbird for Android\"") } -- GitLab From ba06cd115b27e15178153d10025b046836ac848a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolf-Martell=20Montw=C3=A9?= Date: Fri, 17 Oct 2025 11:22:56 +0200 Subject: [PATCH 012/136] fix: sync debug logger not injected --- .../thunderbird/app/common/core/logging/LoggerModule.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/core/logging/LoggerModule.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/core/logging/LoggerModule.kt index e31b9f2c0b..2dfe2abe1c 100644 --- a/app-common/src/main/kotlin/net/thunderbird/app/common/core/logging/LoggerModule.kt +++ b/app-common/src/main/kotlin/net/thunderbird/app/common/core/logging/LoggerModule.kt @@ -46,10 +46,16 @@ val appCommonCoreLogger = module { ) } + // Setup for sync debug logger + // Define this list lazily to avoid eager initialization at app startup + single>(qualifier = named(SYNC_DEBUG_LOG), createdAtStart = false) { + listOf(get(named(SYNC_DEBUG_LOG))) + } + single(named(SYNC_DEBUG_LOG)) { CompositeLogSink( logLevelProvider = get(), - sinks = getList(), + sinks = get>(named(SYNC_DEBUG_LOG)), ) } -- GitLab From 353c1a65effddde77a497385b149ea4784c4a048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolf-Martell=20Montw=C3=A9?= Date: Wed, 8 Oct 2025 14:02:09 +0200 Subject: [PATCH 013/136] feat(core-setting): add setting ui api module --- core/ui/setting/api/build.gradle.kts | 7 + .../thunderbird/core/ui/setting/Setting.kt | 8 + .../core/ui/setting/SettingDecoration.kt | 37 +++++ .../core/ui/setting/SettingValue.kt | 138 ++++++++++++++++++ .../thunderbird/core/ui/setting/Settings.kt | 8 + settings.gradle.kts | 4 + 6 files changed, 202 insertions(+) create mode 100644 core/ui/setting/api/build.gradle.kts create mode 100644 core/ui/setting/api/src/commonMain/kotlin/net/thunderbird/core/ui/setting/Setting.kt create mode 100644 core/ui/setting/api/src/commonMain/kotlin/net/thunderbird/core/ui/setting/SettingDecoration.kt create mode 100644 core/ui/setting/api/src/commonMain/kotlin/net/thunderbird/core/ui/setting/SettingValue.kt create mode 100644 core/ui/setting/api/src/commonMain/kotlin/net/thunderbird/core/ui/setting/Settings.kt diff --git a/core/ui/setting/api/build.gradle.kts b/core/ui/setting/api/build.gradle.kts new file mode 100644 index 0000000000..13b3b9a150 --- /dev/null +++ b/core/ui/setting/api/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id(ThunderbirdPlugins.Library.kmpCompose) +} + +android { + namespace = "net.thunderbird.core.ui.setting" +} diff --git a/core/ui/setting/api/src/commonMain/kotlin/net/thunderbird/core/ui/setting/Setting.kt b/core/ui/setting/api/src/commonMain/kotlin/net/thunderbird/core/ui/setting/Setting.kt new file mode 100644 index 0000000000..5cb7ef6d61 --- /dev/null +++ b/core/ui/setting/api/src/commonMain/kotlin/net/thunderbird/core/ui/setting/Setting.kt @@ -0,0 +1,8 @@ +package net.thunderbird.core.ui.setting + +/** + * A setting that can be displayed in a setting screen. + */ +sealed interface Setting { + val id: String +} diff --git a/core/ui/setting/api/src/commonMain/kotlin/net/thunderbird/core/ui/setting/SettingDecoration.kt b/core/ui/setting/api/src/commonMain/kotlin/net/thunderbird/core/ui/setting/SettingDecoration.kt new file mode 100644 index 0000000000..200a133fb7 --- /dev/null +++ b/core/ui/setting/api/src/commonMain/kotlin/net/thunderbird/core/ui/setting/SettingDecoration.kt @@ -0,0 +1,37 @@ +package net.thunderbird.core.ui.setting + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +/** + * A setting decoration could be used for enhancing the UI of a settings screen. It does not hold any value. + * + * Examples include section headers, dividers, or custom UI components. + */ +sealed interface SettingDecoration : Setting { + + /** + * A setting that displays custom UI. + */ + data class Custom( + override val id: String, + val customUi: @Composable (Modifier) -> Unit, + ) : SettingDecoration + + /** + * A setting that displays a section header. + */ + data class SectionHeader( + override val id: String, + val title: () -> String, + val color: () -> Color = { Color.Unspecified }, + ) : SettingDecoration + + /** + * A setting that displays a divider. + */ + data class SectionDivider( + override val id: String, + ) : SettingDecoration +} diff --git a/core/ui/setting/api/src/commonMain/kotlin/net/thunderbird/core/ui/setting/SettingValue.kt b/core/ui/setting/api/src/commonMain/kotlin/net/thunderbird/core/ui/setting/SettingValue.kt new file mode 100644 index 0000000000..673a06f2c6 --- /dev/null +++ b/core/ui/setting/api/src/commonMain/kotlin/net/thunderbird/core/ui/setting/SettingValue.kt @@ -0,0 +1,138 @@ +package net.thunderbird.core.ui.setting + +import androidx.compose.ui.graphics.vector.ImageVector +import kotlinx.collections.immutable.ImmutableList +import net.thunderbird.core.ui.setting.SettingValue.CompactSelectSingleOption.CompactOption +import net.thunderbird.core.ui.setting.SettingValue.SelectSingleOption.Option + +/** + * A setting that holds a value of type [T]. + */ +sealed interface SettingValue : Setting { + val value: T + val requiresEditView: Boolean + + /** + * A setting that holds a string value. + * + * This requires an edit view to modify the value. + * + * @param id The unique identifier for the setting. + * @param title A lambda that returns the title of the setting. + * @param description A lambda that returns the description of the setting. Default is null. + * @param icon A lambda that returns the icon of the setting as an [ImageVector]. Default is null. + * @param value The current value of the setting. + */ + data class Text( + override val id: String, + val title: () -> String, + val description: () -> String? = { null }, + val icon: () -> ImageVector? = { null }, + override val value: String, + ) : SettingValue { + override val requiresEditView: Boolean = true + } + + /** + * A setting that holds a color value. + * + * This requires an edit view to select a color from the provided list of colors. + * + * @param id The unique identifier for the setting. + * @param title A lambda that returns the title of the setting. + * @param description A lambda that returns the description of the setting. Default is null. + * @param icon A lambda that returns the icon of the setting as an [ImageVector]. Default is null. + * @param value The current color value of the setting, represented as an integer. + */ + data class Color( + override val id: String, + val title: () -> String, + val description: () -> String? = { null }, + val icon: () -> ImageVector? = { null }, + override val value: Int, + val colors: ImmutableList, + ) : SettingValue { + override val requiresEditView: Boolean = true + } + + /** + * A setting that allows the user to select a single option from a list of options. + * + * The options are displayed in a compact manner, suitable for scenarios where space is limited. + * The number of options must be between 2 and 4. + * + * This requires no edit view to modify the value. The selection can be made directly from the setting item. + * + * @param id The unique identifier for the setting. + * @param title A lambda that returns the title of the setting. + * @param description A lambda that returns the description of the setting. Default is null. + * @param value The currently selected option. + * @param options The list of available options to choose from. + */ + data class CompactSelectSingleOption( + override val id: String, + val title: () -> String, + val description: () -> String? = { null }, + override val value: CompactOption, + val options: ImmutableList, + ) : SettingValue { + override val requiresEditView: Boolean = false + + init { + require(options.size >= 2) { "There must be at least two options." } + require(options.size <= 4) { "There can be at most four options." } + } + + data class CompactOption( + val id: String, + val title: () -> String, + ) + } + + /** + * A setting that allows the user to select a single option from a list of options. + * + * Requires an edit view to select the option from the provided list of options. + * + * @param id The unique identifier for the setting. + * @param title A lambda that returns the title of the setting. + * @param description A lambda that returns the description of the setting. Default is null. + * @param icon A lambda that returns the icon of the setting as an [ImageVector]. Default is null. + * @param value The currently selected option. + * @param options The list of available options to choose from. + */ + data class SelectSingleOption( + override val id: String, + val title: () -> String, + val description: () -> String? = { null }, + val icon: () -> ImageVector? = { null }, + override val value: Option, + val options: ImmutableList