Compare commits

..
952 Commits
Author SHA1 Message Date
tramline-github[bot]andGitHub ad4afe4a3a [1.602.*] Pre-release merge (#1252) 2026-08-01 14:30:06 +00:00
renovate[bot]andGitHub d41485f8c1 fix(deps): update metro to v1.4.0
CI / check (push) Canceled after 0s
CI / dependency-review (push) Canceled after 0s
CI / release-app (push) Canceled after 0s
2026-08-01 01:13:46 +00:00
renovate[bot]andGitHub 65da42f5ba chore(deps): update actions/setup-java action to v5.7.0 2026-07-31 21:06:16 +00:00
Harsh Shandilya 0b0494f35c Merge branch 'main' into nightly
* main:
  fix(deps): updte all dependencies
  fix(ci): use same-repository reference syntax for actions
  fix: disable SQL logging in release builds
  feat: convert more inefficient queries
  feat: optimize persisted post state
  fix: hide logout for anonymous users
  fix: make settings screen scrollable
  fix(deps): update sentry
  fix(deps): update agp to v9.3.1
2026-08-01 02:35:01 +05:30
Harsh Shandilya 66fb497fa4 fix(deps): updte all dependencies 2026-07-31 00:22:34 +05:30
Harsh Shandilya 8571dda68a fix(ci): use same-repository reference syntax for actions
Launched minutes ago at https://github.blog/changelog/2026-07-30-reference-same-repository-actions-with-self-repository-syntax/
2026-07-30 23:49:26 +05:30
Harsh Shandilya 75b3050ddb fix: disable SQL logging in release builds 2026-07-30 22:41:39 +05:30
Harsh Shandilya 88113c94cf feat: convert more inefficient queries
- Continues the work of replacing unnecessarily reactive queries
- Pushed down some more sorting work into SQL
- Spell out columns in queries that were doing full row scans when
  not required.
2026-07-30 22:41:39 +05:30
Harsh Shandilya 5d1d6b1c34 feat: optimize persisted post state
- Read posts are an append only list, so instead of re-fetching it from the database
  every time it is now cached on repository init and then subsequent `markRead` calls
  just manually update the set and avoid the unnecessary trip through SQLite.
- Since ReadPosts will grow forever in the current set up, I've optimised the table for
  on disk space savings by creating it without ROWID. In a local test with 100K rows the
  difference between no ROWID and the default was about 1.27 MB versus 3.08 MB. I am not
  at 100K yet AFAICT, but this is still just a nice thing to have under my belt.
- Splits out more focused queries for SavedPosts table since re-fetching every column
  to extract a field or every row to do a count is pretty wasteful.
- There is also now an index for SavedPosts with createdAt and shortId fields since they
  are fetched the most often.
- A lot of queries were unnecessarily going through Flow-based APIs when they were used
  for oneshot operations, those are now direct calls.
- Foreign key relations can cause `INSERT OR REPLACE` to accidentally delete a different
  table's row which I don't use yet, but I found the `ON CONFLICT DO UPDATE SET` that
  deals with it so I've adopted it pre-emptively so future me can be confused and dig up
  this commit message.
2026-07-30 22:41:39 +05:30
Harsh Shandilya c58b0a3478 fix: hide logout for anonymous users 2026-07-30 22:41:38 +05:30
Harsh Shandilya 120120ab48 fix: make settings screen scrollable 2026-07-30 22:41:38 +05:30
renovate[bot]andHarsh Shandilya 33bfbbb23a fix(deps): update sentry 2026-07-30 22:41:38 +05:30
renovate[bot]andHarsh Shandilya 820f2caef5 fix(deps): update agp to v9.3.1 2026-07-30 22:41:38 +05:30
tramline-github[bot]andGitHub a6900710b8 [1.600.*] Pre-release merge (#1250) 2026-07-30 14:30:10 +00:00
Harsh Shandilya aed76089a7 fix: hide logout for anonymous users 2026-07-30 10:06:38 +05:30
Harsh Shandilya 14b5a4877f fix: make settings screen scrollable 2026-07-30 10:06:38 +05:30
tramline-github[bot]andGitHub f43d557fac [1.599.*] Pre-release merge (#1248) 2026-07-24 14:30:07 +00:00
Harsh Shandilya 19a845fe64 Merge remote-tracking branches 'origin/renovate/actions-checkout-7.x', 'origin/renovate/agp' and 'origin/renovate/sentry'
* origin/renovate/actions-checkout-7.x:
  chore(deps): update actions/checkout action to v7.0.1

* origin/renovate/agp:
  fix(deps): update agp to v9.3.1

* origin/renovate/sentry:
  fix(deps): update sentry
2026-07-24 14:53:19 +05:30
renovate[bot]andGitHub dd63a80019 fix(deps): update sentry 2026-07-24 07:39:28 +00:00
renovate[bot]andGitHub 0bb9c4fcd3 fix(deps): update agp to v9.3.1 2026-07-24 07:39:20 +00:00
renovate[bot]andGitHub 9fac927c40 chore(deps): update actions/checkout action to v7.0.1 2026-07-24 07:39:16 +00:00
tramline-github[bot]andGitHub dbba31a06c [1.598.*] Pre-release merge (#1247) 2026-07-18 14:30:06 +00:00
GitHub Actions 42142f9890 feat(release): start next development iteration 2026-07-18 08:20:09 +00:00
GitHub Actions a3c2e5b6ec feat(release): bump version 2026-07-18 08:15:11 +00:00
Harsh Shandilya f75f5c897e chore: update changelog 2026-07-18 13:41:53 +05:30
Harsh Shandilya a7685236c2 fix: properly parse the new /filters page 2026-07-18 09:12:24 +05:30
Harsh Shandilya 17d2dd98e4 fix: update tags test fixture 2026-07-18 08:44:25 +05:30
Harsh Shandilya 2264d9cd7f chore: remove unused test file 2026-07-18 08:44:01 +05:30
Harsh Shandilya f24054a0a1 chore: un-ignore test resources 2026-07-18 08:43:47 +05:30
tramline-github[bot]andGitHub 147b4713c8 [1.597.*] Pre-release merge (#1246) 2026-07-17 10:55:54 +00:00
Harsh Shandilya 9e7472a94e Merge remote-tracking branch 'origin/renovate/aboutlibraries'
* origin/renovate/aboutlibraries:
  fix(deps): update aboutlibraries to v15.0.4
2026-07-17 16:25:29 +05:30
Harsh Shandilya 9600558b00 refactor: remove unused method 2026-07-17 16:14:22 +05:30
Harsh Shandilya 123e43bb99 fix: move clickable one level above to ensure ripple covers whole comment 2026-07-17 16:14:22 +05:30
Harsh Shandilya 335dd8eebe fix: use a single bigger palette for comment tree colors 2026-07-17 16:14:22 +05:30
renovate[bot]andGitHub f692430181 fix(deps): update aboutlibraries to v15.0.4 2026-07-16 21:08:17 +00:00
Harsh Shandilya 4531ac9e43 fix: migrate properly to the new SearchBar 2026-07-17 02:35:56 +05:30
Harsh Shandilya d0edbb3967 Reapply "fix(deps): update androidx (#1240)"
This reverts commit 3a945fd448.
2026-07-17 02:27:29 +05:30
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
274d6c6cd0 chore(deps): update actions/setup-java action to v5.6.0 (#1245)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/setup-java](https://redirect.github.com/actions/setup-java) |
action | minor | `v5.5.0` → `v5.6.0` |

---

### Release Notes

<details>
<summary>actions/setup-java (actions/setup-java)</summary>

###
[`v5.6.0`](https://redirect.github.com/actions/setup-java/compare/v5.5.0...v5.6.0)

[Compare
Source](https://redirect.github.com/actions/setup-java/compare/v5.5.0...v5.6.0)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/msfjarvis/compose-lobsters).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNjUuMSIsInVwZGF0ZWRJblZlciI6IjQzLjI2NS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-16 19:04:55 +00:00
Harsh Shandilya e273cecaef fix: replace Requery SQLite with an updated fork 2026-07-16 21:02:58 +05:30
renovate[bot]andGitHub beb1890e5d fix(deps): update sentry 2026-07-16 14:31:08 +00:00
tramline-github[bot]andGitHub 88d13fb21d [1.596.*] Pre-release merge (#1243) 2026-07-16 14:30:06 +00:00
Harsh Shandilya 3a945fd448 Revert "fix(deps): update androidx (#1240)"
Isn't ready fo release.

This reverts commit 9ceab5082f.
2026-07-16 09:22:57 +05:30
Harsh Shandilya 74579e52ff Merge remote-tracking branch 'origin/renovate/com.autonomousapps.dependency-analysis-3.x'
* origin/renovate/com.autonomousapps.dependency-analysis-3.x:
  chore(deps): update plugin dependencyanalysis to v3.17.0
2026-07-16 09:22:46 +05:30
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Harsh Shandilya
9ceab5082f fix(deps): update androidx (#1240)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
|
[androidx.navigation3:navigation3-ui](https://developer.android.com/jetpack/androidx/releases/navigation3#1.1.4)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.2.0-alpha05` → `1.2.0-alpha06` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.navigation3:navigation3-ui/1.2.0-alpha06?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.navigation3:navigation3-ui/1.2.0-alpha05/1.2.0-alpha06?slim=true)
|
|
[androidx.navigation3:navigation3-runtime](https://developer.android.com/jetpack/androidx/releases/navigation3#1.1.4)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.2.0-alpha05` → `1.2.0-alpha06` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.navigation3:navigation3-runtime/1.2.0-alpha06?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.navigation3:navigation3-runtime/1.2.0-alpha05/1.2.0-alpha06?slim=true)
|
|
[androidx.compose.material3:material3-window-size-class](https://developer.android.com/jetpack/androidx/releases/compose-material3#1.4.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.5.0-alpha23` → `1.5.0-alpha24` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.material3:material3-window-size-class/1.5.0-alpha24?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.material3:material3-window-size-class/1.5.0-alpha23/1.5.0-alpha24?slim=true)
|
|
[androidx.compose.material3:material3](https://developer.android.com/jetpack/androidx/releases/compose-material3#1.4.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.5.0-alpha23` → `1.5.0-alpha24` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.material3:material3/1.5.0-alpha24?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.material3:material3/1.5.0-alpha23/1.5.0-alpha24?slim=true)
|

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/msfjarvis/compose-lobsters).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNTkuMiIsInVwZGF0ZWRJblZlciI6IjQzLjI1OS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
2026-07-16 03:46:06 +00:00
renovate[bot]andGitHub f3f71971d5 chore(deps): update plugin dependencyanalysis to v3.17.0 2026-07-16 02:02:22 +00:00
Harsh Shandilya 244d687ab4 fixup! fix: kludge together a way to do ABI evolution for Zipline 2026-07-15 22:18:07 +05:30
tramline-github[bot]andGitHub 158eebbd22 [1.595.*] Pre-release merge (#1239) 2026-07-15 16:47:00 +00:00
Harsh Shandilya 94b26b3bf3 fix: kludge together a way to do ABI evolution for Zipline 2026-07-15 22:13:14 +05:30
tramline-github[bot]andGitHub 1cb9214020 [1.594.*] Pre-release merge (#1238) 2026-07-15 14:30:06 +00:00
renovate[bot]andGitHub e2efa73f5b fix(deps): update agp to v9.3.0 2026-07-14 14:30:56 +00:00
tramline-github[bot]andGitHub 6c96960b6f [1.593.*] Pre-release merge (#1236) 2026-07-14 14:30:07 +00:00
renovate[bot]andGitHub 71fb2fd208 fix(deps): update metro to v1.3.2 2026-07-14 07:22:15 +00:00
Harsh ShandilyaandGitHub 1001bbc386 fix: remove smoke-tests
This is rather useless in the current form since it doesn't actually update the fixtures so the tests don't do anything useful yet. Will reintroduce it at a later point.
2026-07-14 12:51:23 +05:30
Harsh Shandilya 194ccb540a refactor: remove LLM hallucination
The string-based serializer was ultimately meaningless and the real bug fix was in the stack size change.
2026-07-14 09:49:23 +05:30
Harsh Shandilya 2c9fc7e5dc fix: fall back to embedded Zipline assets if manifest load fails 2026-07-13 20:23:03 +05:30
tramline-github[bot]andGitHub 3246f8461d [1.592.*] Pre-release merge (#1235) 2026-07-13 14:30:10 +00:00
Harsh Shandilya 07c3013400 chore: update changelog 2026-07-13 19:48:18 +05:30
Harsh Shandilya e887edda70 fix: change comments action to web
Technically this is no longer restricted to replies, for logged in users
2026-07-13 19:26:08 +05:30
Harsh Shandilya 7370e48cd2 fix: set preview apiLevel to fix rendering 2026-07-13 19:25:42 +05:30
Harsh Shandilya de1c23c39c feat: add a share button to comments 2026-07-13 18:57:57 +05:30
Harsh Shandilya 0bb5be60a3 fixup! fix: properly detect OP for new submitter/author split 2026-07-12 23:15:54 +05:30
renovate[bot]andGitHub 4035921eb3 fix(deps): update dependency org.junit:junit-bom to v6.1.2 2026-07-12 14:31:16 +00:00
tramline-github[bot]andGitHub 64e1e64263 [1.591.*] Pre-release merge (#1233) 2026-07-12 14:30:06 +00:00
Harsh Shandilya 79491f536d fix: properly detect OP for new submitter/author split 2026-07-12 16:59:02 +05:30
Harsh Shandilya 03270a45ba chore: add changelog entry 2026-07-12 16:33:11 +05:30
tramline-github[bot]andGitHub 266d83b265 [1.590.*] Pre-release merge (#1232) 2026-07-11 14:30:09 +00:00
renovate[bot]andGitHub 7de552f836 fix(deps): update metro to v1.3.1 2026-07-11 12:36:14 +00:00
renovate[bot]andGitHub 956e225d63 fix(deps): update dependency com.slack.lint.compose:compose-lint-checks to v1.5.3 2026-07-11 07:42:19 +00:00
tramline-github[bot]andGitHub e64e7e982f [1.589.*] Pre-release merge (#1231) 2026-07-10 14:30:11 +00:00
Harsh Shandilya 1a201a5430 fix: make comment vote parsing more robust 2026-07-10 12:28:45 +05:30
Harsh Shandilya 859cd9af8e fix: enable parallel Gradle sync 2026-07-10 12:19:11 +05:30
Harsh Shandilya 4d02c0e29f fix: extend tag parsing fix to search page 2026-07-10 12:10:52 +05:30
GitHub Actions 67730d613a feat(release): start next development iteration 2026-07-10 05:51:20 +00:00
GitHub Actions 3189e71c91 feat(release): bump version 2026-07-10 05:44:28 +00:00
tramline-github[bot]andGitHub 2e3d7697e5 [1.588.*] Pre-release merge (#1230) 2026-07-10 05:37:24 +00:00
Harsh Shandilya a194088185 fix: evict stale manifest cache entry on bootup 2026-07-10 11:02:27 +05:30
tramline-github[bot]andGitHub 543f8cea30 [1.587.*] Pre-release merge (#1229) 2026-07-10 04:55:51 +00:00
Harsh Shandilya ba4d8146bb chore: update changelog 2026-07-10 10:10:13 +05:30
Harsh Shandilya 69565098df fix(build): warn about unused Zipline signing key 2026-07-10 09:42:21 +05:30
Harsh Shandilya 5c37772226 fix: match assertions for new fixture and parse tags for new markup 2026-07-10 01:51:41 +05:30
Harsh Shandilya 94ac00875a fix: update hottest_page fixture 2026-07-10 01:31:44 +05:30
Harsh Shandilya 66b70ec34f fix(smoke-tests): assert that there is at least one parsed tag
Without this we failed to detect that tag parsing had regressed
2026-07-10 01:31:14 +05:30
tramline-github[bot]andGitHub 45b82538dd [1.586.*] Pre-release merge (#1228) 2026-07-09 14:30:06 +00:00
renovate[bot]andGitHub 67735fdbe5 fix(deps): update agp to v9.3.0-rc02 2026-07-08 23:15:28 +00:00
Harsh Shandilya 9ba05fbca8 fix: silence Lint warnings 2026-07-08 22:28:47 +05:30
tramline-github[bot]andGitHub 128e3aeb6b [1.585.*] Pre-release merge (#1227) 2026-07-08 16:13:47 +00:00
Harsh Shandilya fc07a45f84 fix: make widget previews lightweight and move them off the main thread
Fixes COMPOSE-LOBSTERS-AS
2026-07-08 21:43:17 +05:30
Harsh Shandilya 5604709b37 fix: downgrade AGP
The 9.4.x R8 series seems to have a verifier bug causing the comments page to trigger a crash.
This is fixed in https://issuetracker.google.com/issues/530633707 but there is no version out
with this included.

Fixes COMPOSE-LOBSTERS-BR
2026-07-08 20:43:53 +05:30
tramline-github[bot]andGitHub feb1a1f782 [1.584.*] Pre-release merge (#1225) 2026-07-08 14:30:06 +00:00
renovate[bot]andGitHub 4a64a9b557 fix(deps): update sentry to v8.48.0 2026-07-08 13:05:44 +00:00
renovate[bot]andGitHub 9743b38c88 chore(deps): update actions/setup-java action to v5.5.0 2026-07-08 00:38:49 +00:00
renovate[bot]andGitHub c66afef6df chore(deps): update plugin dependencyanalysis to v3.16.1 2026-07-07 21:53:39 +00:00
tramline-github[bot]andGitHub f30a9f4ea4 [1.583.*] Pre-release merge (#1224) 2026-07-07 14:30:06 +00:00
Harsh Shandilya 1efddc61b8 chore: refresh Lint baselines 2026-07-07 00:30:17 +05:30
Harsh Shandilya b19b173339 refactor(build): add a Kotlin multiplatform plugin 2026-07-07 00:28:56 +05:30
Harsh Shandilya 7e53354a14 refactor(build): eliminate the zipline-parser-api indirection
This shouldn't be necessary now with the 8 MB stack size
2026-07-07 00:28:56 +05:30
Harsh Shandilya 9a52d80d50 refactor(build): improve Spotless configuration performance
https://www.liutikas.net/2026/01/27/Spray-and-Pray.html
2026-07-07 00:28:56 +05:30
renovate[bot]andGitHub 65c39121b7 fix(deps): update agp to v9.4.0-alpha03 2026-07-04 14:31:18 +00:00
tramline-github[bot]andGitHub 9f10c060e7 [1.582.*] Pre-release merge (#1223) 2026-07-04 14:30:08 +00:00
Harsh Shandilya df6bb45cb9 fix(smoke-tests): exit the process explicitly
Without this the `run` task seems to just get stuck
2026-07-04 10:46:46 +05:30
Harsh Shandilya 299d853689 fix(build): replace application plugin with java 2026-07-04 10:46:46 +05:30
Harsh Shandilya 5bed76c7bb fix(deps): upgrade to current unstable tracks 2026-07-04 10:46:46 +05:30
tramline-github[bot]andGitHub eee6f5e6fd [1.581.*] Pre-release merge (#1222) 2026-07-03 14:30:06 +00:00
renovate[bot]andGitHub c6de3fdd7d fix(deps): update dependency io.sentry.android.gradle:io.sentry.android.gradle.gradle.plugin to v6.14.0 2026-07-02 22:28:36 +00:00
renovate[bot]andGitHub a967552c2e fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.5.1 2026-07-02 18:39:45 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
0e699c14d1 fix(deps): update sentry to v8.47.0 (#1221)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
|
[io.sentry:sentry-bom](https://redirect.github.com/getsentry/sentry-java)
| `8.46.0` → `8.47.0` |
![age](https://developer.mend.io/api/mc/badges/age/maven/io.sentry:sentry-bom/8.47.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/io.sentry:sentry-bom/8.46.0/8.47.0?slim=true)
|
|
[io.sentry:sentry-android-sqlite](https://redirect.github.com/getsentry/sentry-java)
| `8.46.0` → `8.47.0` |
![age](https://developer.mend.io/api/mc/badges/age/maven/io.sentry:sentry-android-sqlite/8.47.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/io.sentry:sentry-android-sqlite/8.46.0/8.47.0?slim=true)
|
|
[io.sentry:sentry-android-core](https://redirect.github.com/getsentry/sentry-java)
| `8.46.0` → `8.47.0` |
![age](https://developer.mend.io/api/mc/badges/age/maven/io.sentry:sentry-android-core/8.47.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/io.sentry:sentry-android-core/8.46.0/8.47.0?slim=true)
|
| [io.sentry:sentry](https://redirect.github.com/getsentry/sentry-java)
| `8.46.0` → `8.47.0` |
![age](https://developer.mend.io/api/mc/badges/age/maven/io.sentry:sentry/8.47.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/io.sentry:sentry/8.46.0/8.47.0?slim=true)
|

---

### Release Notes

<details>
<summary>getsentry/sentry-java (io.sentry:sentry-bom)</summary>

###
[`v8.47.0`](https://redirect.github.com/getsentry/sentry-java/blob/HEAD/CHANGELOG.md#8470)

[Compare
Source](https://redirect.github.com/getsentry/sentry-java/compare/8.46.0...8.47.0)

##### Behavioral Changes

- `SentryOkHttpInterceptor::intercept` now throws `IOException`. This is
a source-only and Java-only breaking change
([#&#8203;5654](https://redirect.github.com/getsentry/sentry-java/pull/5654))

##### Fixes

- Don't start a redundant UI interaction transaction when a transaction
is already bound to the Scope
([#&#8203;5491](https://redirect.github.com/getsentry/sentry-java/issues/5491))
- Previously, `SentryGestureListener` always started a UI transaction
and only afterwards skipped binding it to the Scope when a
manually-bound transaction already existed, leaving the new transaction
to be dropped as an idle transaction without children.
- Fix potential NPE within `Scope.endSession()`
([#&#8203;5657](https://redirect.github.com/getsentry/sentry-java/pull/5657))
- Fix memory leak in `ReplayIntegration` due to persisting executor not
being shut down
([#&#8203;5627](https://redirect.github.com/getsentry/sentry-java/pull/5627))
- Fix AbstractMethodError when compose-ui 1.11+ is used in combination
with `Modifier.sentryTag()` or the Sentry Kotlin compiler plugin
([#&#8203;5672](https://redirect.github.com/getsentry/sentry-java/pull/5672))

##### Performance

- Speed up touch gesture target detection on deeply nested view
hierarchies by hit-testing in local coordinates instead of calling
`getLocationOnScreen` per view
([#&#8203;5595](https://redirect.github.com/getsentry/sentry-java/pull/5595))
- Probe class availability without initializing the class during SDK
init
([#&#8203;5635](https://redirect.github.com/getsentry/sentry-java/pull/5635))
- Avoid constructing an exception per view when resolving view ids
during view-hierarchy and gesture capture
([#&#8203;5631](https://redirect.github.com/getsentry/sentry-java/pull/5631))
- Start the frame metrics thread lazily on first collection instead of
during SDK init
([#&#8203;5641](https://redirect.github.com/getsentry/sentry-java/pull/5641))
- Reduce `SentryId` and `SpanId` allocation overhead by replacing their
per-instance `LazyEvaluator` (and its lock) with a lightweight
lazily-generated `String`.
([#&#8203;5645](https://redirect.github.com/getsentry/sentry-java/pull/5645))
- Lazily allocate the `ReentrantLock` backing
`AutoClosableReentrantLock` to avoid eager lock allocations for SDK
objects that never contend during `SentryAndroid.init`
([#&#8203;5643](https://redirect.github.com/getsentry/sentry-java/pull/5643))

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/msfjarvis/compose-lobsters).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNDIuMiIsInVwZGF0ZWRJblZlciI6IjQzLjI0Mi4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-02 14:42:40 +00:00
tramline-github[bot]andGitHub 3982a7f6da [1.580.*] Pre-release merge (#1220) 2026-07-02 14:30:10 +00:00
Harsh Shandilya b4f946c1fd fix: switch away from deprecated ListItem override 2026-07-02 18:16:41 +05:30
renovate[bot]andGitHub 93f5ceb85c fix(deps): update androidx 2026-07-02 02:26:17 +00:00
Harsh Shandilya 0eb45a4f99 Merge remote-tracking branches 'origin/renovate/metro', 'origin/renovate/com.diffplug.spotless-spotless-plugin-gradle-8.x', 'origin/renovate/com.gradle.develocity-4.x', 'origin/renovate/junit' and 'origin/renovate/aboutlibraries'
* origin/renovate/metro:
  fix(deps): update metro to v1.3.0

* origin/renovate/com.diffplug.spotless-spotless-plugin-gradle-8.x:
  fix(deps): update dependency com.diffplug.spotless:spotless-plugin-gradle to v8.8.0

* origin/renovate/com.gradle.develocity-4.x:
  chore(deps): update plugin com.gradle.develocity to v4.5.0

* origin/renovate/junit:
  fix(deps): update dependency org.junit:junit-bom to v6.1.1

* origin/renovate/aboutlibraries:
  fix(deps): update aboutlibraries to v15.0.3
2026-07-02 07:54:56 +05:30
renovate[bot]andGitHub 4c233cdd8b fix(deps): update metro to v1.3.0 2026-07-01 19:21:17 +00:00
renovate[bot]andGitHub 45c0f9755c fix(deps): update dependency com.diffplug.spotless:spotless-plugin-gradle to v8.8.0 2026-07-01 19:21:13 +00:00
renovate[bot]andGitHub cb77f17665 chore(deps): update plugin com.gradle.develocity to v4.5.0 2026-07-01 19:21:09 +00:00
renovate[bot]andGitHub 9be2a9f11c fix(deps): update dependency org.junit:junit-bom to v6.1.1 2026-07-01 19:20:58 +00:00
renovate[bot]andGitHub 842d95bc43 fix(deps): update aboutlibraries to v15.0.3 2026-07-01 19:20:50 +00:00
renovate[bot]andGitHub 31c937cd56 chore(deps): update gradle to v9.6.1 2026-07-01 00:02:23 +00:00
tramline-github[bot]andGitHub 71180f8607 [1.579.*] Pre-release merge (#1218) 2026-06-26 14:30:05 +00:00
renovate[bot]andGitHub 0b477b11c0 fix(deps): update sentry 2026-06-26 06:48:44 +00:00
renovate[bot]andGitHub cf662762c4 chore(deps): update plugin dependencyanalysis to v3.16.0 2026-06-26 01:54:25 +00:00
renovate[bot]andGitHub db2b51e5e6 chore(deps): update actions/setup-java action to v5.4.0 2026-06-25 22:56:00 +00:00
renovate[bot]andGitHub 504f31f9c6 fix(deps): update aboutlibraries to v15.0.2 2026-06-25 17:54:41 +00:00
tramline-github[bot]andGitHub 3227460be8 [1.578.*] Pre-release merge (#1217) 2026-06-25 14:30:09 +00:00
renovate[bot]andGitHub e50058c057 fix(deps): update aboutlibraries to v15.0.1 2026-06-25 11:07:44 +00:00
renovate[bot]andGitHub 1b082eaa08 fix(deps): update sentry to v8.45.0 2026-06-24 18:08:47 +00:00
tramline-github[bot]andGitHub 77f1c61ae3 [1.577.*] Pre-release merge (#1216) 2026-06-24 14:30:07 +00:00
Harsh Shandilya 43c82eb449 fixup! chore(deps): update dependency com.facebook:ktfmt to v0.64 2026-06-24 15:35:53 +05:30
renovate[bot]andGitHub 15ab6008d1 chore(deps): update dependency com.facebook:ktfmt to v0.64 2026-06-24 05:51:18 +00:00
tramline-github[bot]andGitHub c6b3dc4d6c [1.576.*] Pre-release merge (#1214) 2026-06-21 14:30:06 +00:00
Harsh Shandilya b863a655fc fix: pin CI versions correctly 2026-06-20 20:54:33 +05:30
renovate[bot]andGitHub 3551189ad2 fix(deps): update aboutlibraries to v15 2026-06-20 14:31:06 +00:00
tramline-github[bot]andGitHub 7b8186bd62 [1.575.*] Pre-release merge (#1213) 2026-06-20 14:30:08 +00:00
renovate[bot]andGitHub 73d8e09e08 fix(deps): update sentry to v8.44.1 2026-06-19 14:34:36 +00:00
tramline-github[bot]andGitHub fc740124c2 [1.574.*] Pre-release merge (#1212) 2026-06-19 14:30:05 +00:00
renovate[bot]andGitHub 0ba4902a6f chore(deps): update gradle to v9.6.0 2026-06-19 00:41:33 +00:00
renovate[bot]andGitHub 235be8c5de chore(deps): update actions/checkout action to v7 2026-06-18 16:44:30 +00:00
renovate[bot]andGitHub a236611357 fix(deps): update sentry to v6.12.0 2026-06-18 14:34:57 +00:00
tramline-github[bot]andGitHub b94fc467f9 [1.573.*] Pre-release merge (#1211) 2026-06-18 14:30:07 +00:00
Harsh Shandilya cef2f4990d ci: directly invoke Gradle 2026-06-18 13:56:39 +05:30
Harsh Shandilya 165d87ecf7 chore: cleanup duplicate dependency 2026-06-18 07:57:07 +05:30
renovate[bot]andGitHub 5c16562920 fix(deps): update sentry to v8.44.0 2026-06-18 01:04:10 +00:00
renovate[bot]andGitHub 4cbc7059b8 fix(deps): update androidx 2026-06-17 19:25:21 +00:00
Harsh Shandilya 3c73dc3784 chore: refresh Lint baselines 2026-06-18 00:50:19 +05:30
Harsh Shandilya e45cd9009d ci: add scheduled smoke probe workflow 2026-06-18 00:49:01 +05:30
Harsh Shandilya d9b1563198 feat: add live smoke probe cli 2026-06-18 00:49:01 +05:30
Harsh Shandilya 655916f5a3 refactor: make core jvm-only 2026-06-18 00:49:01 +05:30
Harsh Shandilya c1bb75d67d fix: bump AGP to 9.3.0-rc01 2026-06-18 00:25:51 +05:30
Harsh Shandilya c0b99786df chore: rework changelog entries a bit 2026-06-18 00:08:33 +05:30
tramline-github[bot]andGitHub 56a21326ac [1.572.*] Pre-release merge (#1210) 2026-06-17 14:30:05 +00:00
renovate[bot]andGitHub c1bcc33bd2 chore(deps): update actions/setup-java action to v5.3.0 2026-06-17 01:30:43 +00:00
renovate[bot]andGitHub 851d4eee0e chore(deps): update plugin com.gradle.develocity to v4.4.3 2026-06-16 22:56:05 +00:00
renovate[bot]andGitHub 5dd32eb218 chore(deps): update gradle to v9.6.0-rc-3 2026-06-16 19:14:07 +00:00
renovate[bot]andGitHub bdedc3b688 fix(deps): update dependency com.diffplug.spotless:spotless-plugin-gradle to v8.7.0 2026-06-16 11:41:25 +00:00
tramline-github[bot]andGitHub 8f7f8c08dd [1.571.*] Pre-release merge (#1209) 2026-06-15 17:35:05 +00:00
Harsh Shandilya 75bfc1a754 fix: ensure reply screen can be scrolled 2026-06-15 21:03:37 +05:30
tramline-github[bot]andGitHub 4021e3d975 [1.570.*] Pre-release merge (#1208) 2026-06-13 14:30:04 +00:00
Harsh Shandilya 31fff1fb79 chore: update README 2026-06-13 16:12:51 +05:30
Harsh Shandilya b14b7794c9 fix(renovate): move Kotlin disablement to the right place 2026-06-13 15:58:03 +05:30
renovate[bot]andGitHub 0e9584b547 chore(deps): update gradle/actions digest to 3f131e8 2026-06-12 19:50:00 +00:00
tramline-github[bot]andGitHub 379dc5c120 [1.569.*] Pre-release merge (#1207) 2026-06-12 14:30:06 +00:00
renovate[bot]andGitHub 5f999edd68 fix(deps): update metro to v1.2.1 2026-06-12 03:00:37 +00:00
renovate[bot]andGitHub 1d641ef439 fix(deps): update sentry to v6.11.0 2026-06-12 00:01:52 +00:00
tramline-github[bot]andGitHub 0184858fcf [1.568.*] Pre-release merge (#1206) 2026-06-11 14:30:08 +00:00
renovate[bot]andGitHub 9600a83c19 chore(deps): update gradle to v9.6.0-rc-2 2026-06-11 13:44:44 +00:00
Harsh Shandilya c221a3baf8 fix: disable parallel configuration cache 2026-06-11 18:58:56 +05:30
Harsh Shandilya 7740177b86 refactor: add some tests for the Zipline ParserSerializers file 2026-06-11 18:58:56 +05:30
renovate[bot]andGitHub dddbc1d394 chore(deps): update plugin dependencyanalysis to v3.15.0 2026-06-11 01:49:11 +00:00
Harsh Shandilya be0154cde0 fixup! fix: disable Kotlin updates 2026-06-11 03:35:10 +05:30
Harsh Shandilya 7f48b9a3f0 Merge remote-tracking branch 'origin/renovate/sentry'
* origin/renovate/sentry:
  fix(deps): update sentry to v8.43.2
2026-06-11 03:27:45 +05:30
Harsh Shandilya 2cd58e5241 fix: disable Kotlin updates
Blocked on https://github.com/cashapp/zipline/pull/1829
2026-06-11 03:27:01 +05:30
renovate[bot]andGitHub 719c9c2012 fix(deps): update sentry to v8.43.2 2026-06-10 21:53:20 +00:00
Harsh Shandilya 6caddbfa3b Merge remote-tracking branches 'origin/renovate/metro' and 'origin/renovate/coil3'
* origin/renovate/metro:
  fix(deps): update metro to v1.2.0

* origin/renovate/coil3:
  fix(deps): update coil3 to v3.5.0
2026-06-11 03:16:11 +05:30
renovate[bot]andGitHub 47f13e4b8b fix(deps): update metro to v1.2.0 2026-06-10 17:09:19 +00:00
renovate[bot]andGitHub c5cee7c6ce fix(deps): update coil3 to v3.5.0 2026-06-10 17:09:14 +00:00
tramline-github[bot]andGitHub 8e7341571c [1.567.*] Pre-release merge (#1205) 2026-06-10 04:29:41 +00:00
renovate[bot]andGitHub 73c8c98707 chore(deps): update gradle/actions digest to 5e2ebd0 2026-06-10 03:48:15 +00:00
Harsh Shandilya e2618c8619 chore: update changelog 2026-06-10 09:17:31 +05:30
Harsh Shandilya 81904c6a57 fix: migrate all uses of System.currentTimeMillis to Clock.System 2026-06-10 09:05:49 +05:30
Harsh Shandilya 85abb382d8 fix: use explicit backing fields in CommentsHandler 2026-06-10 09:05:49 +05:30
Harsh Shandilya 2aac86bbcc feat: add staged save UI for tag filters 2026-06-10 09:05:49 +05:30
Harsh Shandilya a1e7812fa3 feat: stage tag filter edits in ViewModel 2026-06-10 09:05:49 +05:30
Harsh Shandilya 7388bef218 feat: add bulk tag block replacement 2026-06-10 09:04:34 +05:30
Harsh Shandilya 8bac817e7a feat: add Zipline parser support for /filters 2026-06-09 23:36:18 +05:30
renovate[bot]andGitHub bbc752009a fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.5.0 2026-06-09 17:23:02 +00:00
tramline-github[bot]andGitHub 55a169129c [1.566.*] Pre-release merge (#1201) 2026-06-09 17:14:38 +00:00
Harsh Shandilya 606b4f708e fix: require signing key to be present when making releases in CI 2026-06-09 22:44:13 +05:30
Harsh Shandilya 69c8502876 fix: add missing Zipline signing key 2026-06-09 22:44:13 +05:30
tramline-github[bot]andGitHub 8e3533c2be [1.565.*] Pre-release merge (#1200) 2026-06-09 14:30:11 +00:00
renovate[bot]andGitHub 705d980cc3 fix(deps): update haze to v2.0.0-alpha03 2026-06-09 10:30:46 +00:00
renovate[bot]andGitHub 72ea972197 fix(deps): update dependency com.squareup.okhttp3:okhttp-bom to v5.4.0 2026-06-08 22:04:57 +00:00
Harsh Shandilya 6c1ce23f75 feat: add an instrumentation test to ensure Zipline loading parsing succeeds 2026-06-09 01:02:51 +05:30
Harsh Shandilya 2aa0bda388 fix: cleanup injection setup for AndroidZiplineParserClient 2026-06-09 00:34:03 +05:30
Harsh Shandilya 97cb577224 chore: update changelog 2026-06-08 23:09:17 +05:30
tramline-github[bot]andGitHub fcefa4b85c [1.564.*] Pre-release merge (#1199) 2026-06-08 14:30:08 +00:00
Harsh ShandilyaandGitHub 6522ca36c9 feat: migrate parsing logic to Zipline (#1183)
Replaces the Kspoon-based parsing via Retrofit into a lower level
version based on Ksoup directly, implemented in a Zipline module that
can be updated independently of the app itself.

The primary motivation is to be able to update the HTML scraper
independent of the app since the site will likely to continue having
changes in the markup that break my parsers, and cutting emergency
releases is tiresome.

~~This currently doesn't work due to a `stack overflow` error from
QuickJS, but it did work *briefly* before I managed to break it and
never got it working again.~~

Managed to fix the problems through multiple changes

- Improve code size by removing JSON as an intermediary between the
Zipline-defined API and the Android app, instead using a simpler text
based format that's just a straight concatenation. This let us drop
`kotlinx-serialization-json` from the module dependencies.
- Once the code size was under control we started seeing crashes because
we were violating JS threading invariants where JS loading and JS
execution needed to happen on the same thread. This was resolved using
the `DispatcherConfinedLobstersParserService` wrapper that forces all
parser method calls to run through the same Dispatcher as
`ZiplineLoader`.
2026-06-08 17:46:21 +05:30
Harsh Shandilya 495124ea50 fix: always load Zipline assets for release builds 2026-06-08 17:12:56 +05:30
Harsh Shandilya 822d3953a1 chore: add a script to do the end-to-end release 2026-06-08 17:12:56 +05:30
Harsh Shandilya 13a9d72c35 refactor: confine all Zipline parser service ops to Zipline thread 2026-06-08 17:12:56 +05:30
Harsh Shandilya 4539e0a6b9 feat: raise the Zipline thread stack size to 8 MiB 2026-06-08 17:12:56 +05:30
Harsh Shandilya ed9ba64162 refactor: remove JSON object fallback serializer 2026-06-08 17:12:56 +05:30
Harsh Shandilya 829a6fe471 fix: stabilize Zipline parser boundary and embedded loading
Why this change exists:
- The real problem was not just missing serializers; it was that the Zipline parser boundary was unstable in actual Android runtime loading.
- The goal of this change is to make the parser service boundary reliable without collapsing it into app model types or otherwise defeating the parser/API split.
- The important outcome is that Android can load and use the embedded parser consistently in the verified runtime path.

What this change guarantees:
- parser-specific boundary types remain supported explicitly
- embedded parser loading follows the stable path used in debug verification
- the previously observed runtime failures are removed from the validated path

Verification:
- build-brief ./gradlew :zipline-parser-api:jvmTest :zipline-parser:jvmTest :zipline-parser:compileProductionExecutableKotlinJsZipline :android:installDebug
- Emulator launch on emulator-5554: no missing LobstersPost serializer, no SIGSEGV/Scudo crash, no QuickJs stack overflow in final run, posts loaded
2026-06-08 17:12:56 +05:30
Harsh Shandilya 43441289ee fix: remove useless data classes 2026-06-08 17:12:56 +05:30
Harsh Shandilya c78a003cb9 build: switch zipline js targets to node 2026-06-08 17:12:56 +05:30
Harsh Shandilya de6530ddc3 build: align kotlin with zipline support 2026-06-08 17:12:56 +05:30
Harsh Shandilya a8e9a5bf0f refactor: share parser base url constant 2026-06-08 17:12:56 +05:30
Harsh Shandilya c2fd16939b refactor: lazily initialize comment count regex 2026-06-08 17:12:56 +05:30
Harsh Shandilya 8d122636fd refactor: iterate comment subtree parsing 2026-06-08 17:12:56 +05:30
Harsh Shandilya fbc0201c10 refactor: move parser comment times to epoch seconds 2026-06-08 17:12:56 +05:30
Harsh Shandilya fbe6f65e74 build: drop parser serialization dependency 2026-06-08 17:12:56 +05:30
Harsh Shandilya b2adb28275 refactor: extract zipline parser api module 2026-06-08 17:12:56 +05:30
Harsh Shandilya 7b80e1726a fix: always prefer embedded assets on development builds 2026-06-08 17:12:56 +05:30
Harsh Shandilya 344da06591 refactor: improve memory footprint of zipline-parser code
I miss the ART GC already...
2026-06-08 17:12:56 +05:30
Harsh Shandilya 3658aafd61 feat: embed current version of the parser service in the APK 2026-06-08 17:12:56 +05:30
Harsh Shandilya a015794e1f feat: route API parsing through Zipline
Replace the old Retrofit HTML converters with a Zipline-backed parser
client and a Retrofit converter that delegates HTML decoding to the
parser module. Update the app and tests to consume the new parsed API
results.
2026-06-08 17:12:56 +05:30
Harsh Shandilya c1b35eb201 refactor: add Zipline HTML parser module
Introduce a dedicated zipline-parser module for Lobsters HTML parsing,
upgrade Zipline to a compatible release, and add the generated API
dump plus a helper script for deploying parser artifacts.
2026-06-08 17:12:56 +05:30
Harsh Shandilya 22f4e542e4 refactor: make model module multiplatform
Move the shared parser-facing contracts into commonMain and keep the
JVM-only database mappers in jvmMain. This separates shared model
types from platform-specific storage code and prepares the branch for
a dedicated parser module.
2026-06-08 17:12:56 +05:30
Harsh Shandilya dc0df630f8 chore: check in K/JS lockfile 2026-06-07 13:00:09 +05:30
tramline-github[bot]andGitHub cf8e56d345 [1.563.*] Pre-release merge (#1198) 2026-06-07 03:35:15 +00:00
Harsh Shandilya dca0e64427 chore: bump CHANGELOG 2026-06-07 08:59:59 +05:30
Harsh Shandilya 9290f8ae06 fix: show empty state for search 2026-06-07 08:47:49 +05:30
Harsh Shandilya 7e5e9c8edb fix: reduce inline search result padding 2026-06-07 08:47:48 +05:30
Harsh Shandilya d229c76b80 fix: clear inline search before dismissing 2026-06-07 08:46:35 +05:30
Harsh Shandilya 03d0310ef5 fix: hide stale search results during debounce 2026-06-07 08:46:34 +05:30
Harsh Shandilya 19c4f189de feat: host debounced search in LobstersPostsScreen 2026-06-07 08:41:38 +05:30
Harsh Shandilya dddbcbecb6 refactor: make ClawAppBar switchable for search 2026-06-07 08:29:07 +05:30
Harsh Shandilya c0fd2814e2 fix: prevent action bar from being visible if comment is collapsed 2026-06-06 22:00:54 +05:30
tramline-github[bot]andGitHub fced66e263 [1.562.*] Pre-release merge (#1195) 2026-06-06 14:30:07 +00:00
Harsh Shandilya 38ee340d32 fix(build): add Sonatype snapshots repo 2026-06-06 19:39:57 +05:30
Harsh Shandilya 9f149018a1 chore: update README 2026-06-06 18:15:51 +05:30
Harsh Shandilya e069e16ad2 fix: set display name for preview parameter provider 2026-06-06 10:32:21 +05:30
Harsh Shandilya a97616d722 feat: rework comments page animations 2026-06-06 10:32:21 +05:30
Harsh Shandilya f8085a2962 fix: only show descendant count when it's >0 2026-06-06 08:20:38 +05:30
Harsh Shandilya e24a3217d5 fix: match the UI of highlighting post authors with the website 2026-06-06 08:16:21 +05:30
tramline-github[bot]andGitHub aa41e05173 [1.561.*] Pre-release merge (#1194) 2026-06-05 14:30:07 +00:00
renovate[bot]andGitHub e1d296a825 fix(deps): update sentry to v6.10.0 2026-06-05 09:30:50 +00:00
tramline-github[bot]andGitHub e05ba3aee4 [1.560.*] Pre-release merge (#1193) 2026-06-04 14:30:07 +00:00
renovate[bot]andGitHub af3bec76e9 fix(deps): update sentry to v8.43.1 2026-06-04 01:47:13 +00:00
renovate[bot]andGitHub 672884bac9 fix(deps): update dependency com.slack.lint.compose:compose-lint-checks to v1.5.2 2026-06-03 20:23:58 +00:00
renovate[bot]andGitHub 3af1752855 fix(deps): update androidx 2026-06-03 17:50:19 +00:00
Harsh Shandilya 707f50c93f fix: merge all Kotlin updates together 2026-06-03 21:20:02 +05:30
tramline-github[bot]andGitHub 13f730b6be [1.559.*] Pre-release merge (#1190) 2026-06-03 14:30:08 +00:00
renovate[bot]andGitHub bf2aee4df7 chore(deps): update actions/checkout action to v6.0.3 2026-06-02 17:51:54 +00:00
tramline-github[bot]andGitHub abd1d2d83c [1.558.*] Pre-release merge (#1189) 2026-06-02 14:30:11 +00:00
GitHub Actions 451a411702 feat(release): start next development iteration 2026-06-02 10:25:55 +00:00
GitHub Actions 55678d3914 feat(release): bump version 2026-06-02 10:20:42 +00:00
Harsh ShandilyaandGitHub 2661b82950 Update CHANGELOG.md 2026-06-02 15:45:56 +05:30
ClaudeGitHubanthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com>msfjarvis
4bbf092c2d [WIP] Fix certificate pinning error on app launch (#1188)
Thanks for asking me to work on this. I will get started on it and keep
this PR's description up to date as I form a plan and make progress.

> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>1.65.0 Certificate Pinning error </issue_title>
> <issue_description>Opening the App just gives the error
> 
> > Certificate pinning failure
> 
> I imagine that this is due to the new certificate on lobster:
> 
> > Not Before Tue, 02 Jun 2026 03:17:13 GMT 
> 
> 
> backtrace:
> ```
> javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
>   Peer certificate chain:
>     sha256/nmaozNNuQ1MLbF/avDVhKPEu1JiZibqa/rcIhF6rJaU=: CN=lobste.rs
> sha256/brzvtCELCIZUo4sD/qPX0ccRtPsd3DY6RfmxpOU9oB4=: CN=YE1,O=Let's
Encrypt,C=US
> sha256/sCkq5UWXjg+7mKu9lMhhYF5bGLsy7VI/UNW3tccdR7w=: CN=Root
YE,O=ISRG,C=US
> sha256/diGVwiVYbubAI3RW4hB9xU8e/CH2GnkuvVFZE8zmgzI=: CN=ISRG Root
X2,O=Internet Security Research Group,C=US
>   Pinned certificates for lobste.rs:
>     sha256/Bla1TIdpGeHXQS0/CIrA5hhFhOTZd94IIJRS3G3AcIo=
>     sha256/jQJTbIh0grw0/1TkHSumWb+Fs0Ggogr621gT3PvPKG0=
>     sha256/C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M=
> at
okhttp3.CertificatePinner.check$okhttp(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:427)
> at
okhttp3.internal.connection.ConnectPlan.connectTls(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:223)
> at
okhttp3.internal.connection.ConnectPlan.connectTlsEtc(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:153)
> at
okhttp3.internal.connection.FastFallbackExchangeFinder.find(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:139)
> at
okhttp3.internal.connection.ConnectInterceptor.intercept(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:30)
> at
okhttp3.internal.http.RealInterceptorChain.proceed(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:104)
> at
okhttp3.internal.cache.CacheInterceptor.intercept(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:1872)
> at
okhttp3.internal.http.RealInterceptorChain.proceed(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:104)
> at
okhttp3.internal.cache.CacheInterceptor.intercept(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:666)
> at
okhttp3.internal.http.RealInterceptorChain.proceed(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:104)
> at
okhttp3.internal.cache.CacheInterceptor.intercept(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:220)
> at
okhttp3.internal.http.RealInterceptorChain.proceed(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:104)
> at
io.sentry.okhttp.SentryOkHttpInterceptor.intercept(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:461)
> at
okhttp3.internal.http.RealInterceptorChain.proceed(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:104)
> at
okhttp3.logging.HttpLoggingInterceptor.intercept(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:576)
> at
okhttp3.internal.http.RealInterceptorChain.proceed(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:104)
> at
okhttp3.internal.cache.CacheInterceptor.intercept(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:773)
> at
okhttp3.internal.http.RealInterceptorChain.proceed(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:104)
> at
dev.msfjarvis.claw.core.network.UserAgentInterceptor.intercept(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:19)
> at
okhttp3.internal.http.RealInterceptorChain.proceed(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:104)
> at
dev.msfjarvis.claw.core.network.RetryAfterInterceptor.intercept(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:3)
> at
okhttp3.internal.http.RealInterceptorChain.proceed(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:104)
> at
okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:118)
> at
okhttp3.internal.connection.RealCall$AsyncCall.run(r8-map-id-4720644bb8de639be45bbb06d58eed30b6402544fe3e08afbfc1a8ff6d7ebbc6:42)
> at
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1154)
> at
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:652)
> 	at java.lang.Thread.run(Thread.java:1563)
> Suppressed: javax.net.ssl.SSLPeerUnverifiedException: Certificate
pinning failure!
>   Peer certificate chain:
>     sha256/nmaozNNuQ1MLbF/avDVhKPEu1JiZibqa/rcIhF6rJaU=: CN=lobste.rs
> sha256/brzvtCELCIZUo4sD/qPX0ccRtPsd3DY6RfmxpOU9oB4=: CN=YE1,O=Let's
Encrypt,C=US
> sha256/sCkq5UWXjg+7mKu9lMhhYF5bGLsy7VI/UNW3tccdR7w=: CN=Root
YE,O=ISRG,C=US
> sha256/diGVwiVYbubAI3RW4hB9xU8e/CH2GnkuvVFZE8zmgzI=: CN=ISRG Root
X2,O=Internet Security Research Group,C=US
>   Pinned certificates for lobste.rs:
>     sha256/Bla1TIdpGeHXQS0/CIrA5hhFhOTZd94IIJRS3G3AcIo=
>     sha256/jQJTbIh0grw0/1TkHSumWb+Fs0Ggogr621gT3PvPKG0=
>     sha256/C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M=
> 		... 27 more
> ```
> 
> 
> Version: 1.65.0
> </issue_description>
> 
> <agent_instructions>Remove the certificate pinning
logic</agent_instructions>
> 
> ## Comments on the Issue (you are @claude[agent] in this section)
> 
> <comments>
> </comments>

---------

Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com>
Co-authored-by: msfjarvis <13348378+msfjarvis@users.noreply.github.com>
2026-06-02 09:27:35 +00:00
renovate[bot]andGitHub 7cbf87080d fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-datetime to v0.8.0-0.6.x-compat 2026-06-02 04:01:12 +00:00
Harsh Shandilya 784bda4105 refactor: drop all uses of java.time 2026-06-02 09:26:43 +05:30
renovate[bot]andGitHub 8adcef03d6 chore(deps): update plugin dependencyanalysis to v3.14.1 2026-06-01 22:09:41 +00:00
Harsh Shandilya 7efa5600d0 fix: give some padding to the collapsed comment count 2026-06-02 00:22:19 +05:30
Harsh Shandilya cb75ef0673 feat: add an indicator for collapsed comment count
Update the tests to account for the new UI affordance
2026-06-02 00:16:29 +05:30
tramline-github[bot]andGitHub 8fa620145f [1.557.*] Pre-release merge (#1186) 2026-06-01 14:30:08 +00:00
Harsh Shandilya ae8db6d851 chore: update changelog 2026-06-01 00:11:33 +05:30
Harsh Shandilya 265a00e9b1 refactor: match the reply API request shape more closely
Without this we get a failed 404 reply even though the comment goes through just fine
2026-05-31 23:59:35 +05:30
Harsh Shandilya 94aa52b669 feat: add reply screen ui 2026-05-31 23:59:35 +05:30
Harsh Shandilya ca2dbd758c feat: add reply viewmodel 2026-05-31 23:59:35 +05:30
tramline-github[bot]andGitHub ba8edb4cdc [1.556.*] Pre-release merge (#1185) 2026-05-31 14:30:06 +00:00
Harsh Shandilya fa22dbe4cf feat: add reply quote insertion helper 2026-05-31 18:57:53 +05:30
Harsh Shandilya 481efd24ab fix: improve comment click UX semantics 2026-05-31 18:43:37 +05:30
Harsh Shandilya 19346101c6 fix: use the correct thread guide color for each level 2026-05-31 18:43:25 +05:30
Harsh Shandilya b19a48fa09 fix: bump to Gradle 9.6.0-rc-1 2026-05-31 12:11:43 +05:30
renovate[bot]andGitHub 5457c204cf fix(deps): update dependency com.slack.lint.compose:compose-lint-checks to v1.5.1 2026-05-31 00:40:05 +00:00
tramline-github[bot]andGitHub 2a551a3f0a [1.555.*] Pre-release merge (#1182) 2026-05-30 14:30:09 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
05e0664d14 fix(deps): update screenshot to v0.0.1-alpha15 (#1181)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
|
[com.android.compose.screenshot](https://developer.android.com/studio/build)
([source](https://android.googlesource.com/platform/tools/base)) |
`0.0.1-alpha14` → `0.0.1-alpha15` |
![age](https://developer.mend.io/api/mc/badges/age/maven/com.android.compose.screenshot:com.android.compose.screenshot.gradle.plugin/0.0.1-alpha15?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.android.compose.screenshot:com.android.compose.screenshot.gradle.plugin/0.0.1-alpha14/0.0.1-alpha15?slim=true)
|
|
[com.android.tools.screenshot:screenshot-validation-api](http://tools.android.com/)
| `0.0.1-alpha14` → `0.0.1-alpha15` |
![age](https://developer.mend.io/api/mc/badges/age/maven/com.android.tools.screenshot:screenshot-validation-api/0.0.1-alpha15?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.android.tools.screenshot:screenshot-validation-api/0.0.1-alpha14/0.0.1-alpha15?slim=true)
|

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/msfjarvis/compose-lobsters).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yMDIuMSIsInVwZGF0ZWRJblZlciI6IjQzLjIwMi4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-30 03:40:01 +00:00
Harsh Shandilya e91338b31e fix: improve comment tree rendering 2026-05-29 23:51:12 +05:30
Harsh Shandilya f77221aeea chore: update changelog 2026-05-29 22:53:32 +05:30
Harsh Shandilya 8ee7d5dc28 Revert "feat: revert comments UI back to current stable for hotfix release"
This reverts commit 6d932c84d5.
2026-05-29 22:45:47 +05:30
GitHub Actions 76cb3376b7 feat(release): start next development iteration 2026-05-29 16:44:51 +00:00
GitHub Actions 9ba9791de8 feat(release): bump version 2026-05-29 16:39:35 +00:00
Harsh Shandilya 43408c2bec chore: update changelog 2026-05-29 21:59:24 +05:30
tramline-github[bot]andGitHub fdade293bf [1.554.*] Pre-release merge (#1180) 2026-05-29 16:28:35 +00:00
Harsh Shandilya 7ec580d9c6 fix: update selectors and test assertions 2026-05-29 21:52:59 +05:30
Harsh Shandilya 18b33e550a fix: refresh test fixtures 2026-05-29 21:52:59 +05:30
Harsh Shandilya 6d932c84d5 feat: revert comments UI back to current stable for hotfix release
This will be reverted back after the release

Revert "feat: re-do comment UI to look like Boost for Reddit"

This reverts commit 03e0a13e1d.

Revert "build: add compose screenshot testing support"

This reverts commit 5992a5fa15.

Revert "feat: rework comment entry layout and enable upvote action"

This reverts commit 4904c76220.

Revert "feat: re-do comment upvote UI"

This reverts commit c32ed849d2.
2026-05-29 21:52:59 +05:30
tramline-github[bot]andGitHub b31a6c783a [1.553.*] Pre-release merge (#1178) 2026-05-29 14:30:06 +00:00
Harsh Shandilya 03e0a13e1d feat: re-do comment UI to look like Boost for Reddit 2026-05-29 19:51:05 +05:30
Harsh Shandilya 5992a5fa15 build: add compose screenshot testing support 2026-05-29 19:44:16 +05:30
renovate[bot]andGitHub 6b84d5277b chore(deps): update dependency com.facebook:ktfmt to v0.63 2026-05-28 21:04:01 +00:00
renovate[bot]andGitHub f41c36c24c fix(deps): update dependency io.sentry.android.gradle:io.sentry.android.gradle.gradle.plugin to v6.9.0 2026-05-28 14:40:29 +00:00
tramline-github[bot]andGitHub 3623de2690 [1.552.*] Pre-release merge (#1177) 2026-05-28 14:30:10 +00:00
renovate[bot]andGitHub 9b4449c890 chore(deps): update plugin dependencyanalysis to v3.14.0 2026-05-28 06:08:21 +00:00
Harsh Shandilya 4904c76220 feat: rework comment entry layout and enable upvote action 2026-05-28 00:12:13 +05:30
Harsh Shandilya c5a8dea7d2 Merge remote-tracking branches 'origin/renovate/com.diffplug.spotless-spotless-plugin-gradle-8.x' and 'origin/renovate/sentry'
* origin/renovate/com.diffplug.spotless-spotless-plugin-gradle-8.x:
  fix(deps): update dependency com.diffplug.spotless:spotless-plugin-gradle to v8.6.0

* origin/renovate/sentry:
  fix(deps): update sentry to v8.43.0
2026-05-27 23:18:05 +05:30
renovate[bot]andGitHub 8930c0c5dc fix(deps): update sentry to v8.43.0 2026-05-27 17:47:05 +00:00
renovate[bot]andGitHub 5dc4afbf36 fix(deps): update dependency com.diffplug.spotless:spotless-plugin-gradle to v8.6.0 2026-05-27 17:47:01 +00:00
renovate[bot]andHarsh Shandilya c3ec92582f fix(deps): update dependency com.slack.lint.compose:compose-lint-checks to v1.5.0 2026-05-27 23:15:00 +05:30
tramline-github[bot]andGitHub 549ba4c9b1 [1.551.*] Pre-release merge (#1176) 2026-05-27 14:30:12 +00:00
renovate[bot]andGitHub b8ee16be84 chore(deps): update plugin ksp to v2.3.9 2026-05-26 21:55:32 +00:00
Harsh Shandilya c32ed849d2 feat: re-do comment upvote UI 2026-05-27 00:33:46 +05:30
Harsh Shandilya 997b37ccb0 feat: switch expand/collapse UI to match Lobsters 2026-05-27 00:24:06 +05:30
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
976ed3b31a chore(deps): update plugin com.gradle.develocity to v4.4.2 (#1174)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| com.gradle.develocity | `4.4.1` → `4.4.2` |
![age](https://developer.mend.io/api/mc/badges/age/maven/com.gradle.develocity:com.gradle.develocity.gradle.plugin/4.4.2?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.gradle.develocity:com.gradle.develocity.gradle.plugin/4.4.1/4.4.2?slim=true)
|

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/msfjarvis/compose-lobsters).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xOTQuMCIsInVwZGF0ZWRJblZlciI6IjQzLjE5NC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-26 15:16:49 +00:00
tramline-github[bot]andGitHub 3171f0638c [1.550.*] Pre-release merge (#1173) 2026-05-26 14:30:09 +00:00
Harsh Shandilya 75863cdf94 refactor: move CommentEntry composable to CommentEntry.kt 2026-05-26 17:13:04 +05:30
GitHub Actions a3687d0b4c feat(release): start next development iteration 2026-05-26 08:56:03 +00:00
GitHub Actions 2b571be006 feat(release): bump version 2026-05-26 08:49:32 +00:00
renovate[bot]andGitHub 820744409e fix(deps): update aboutlibraries to v14.2.1 2026-05-26 03:45:25 +00:00
tramline-github[bot]andGitHub 9d19ee0a71 [1.549.*] Pre-release merge (#1172) 2026-05-23 14:30:07 +00:00
Harsh Shandilya 1b6ae68e0d Fix login session detection and comment timestamps
Only persist Lobsters login cookies after the WebView proves the
session is authenticated, and treat anonymous lobster_trap cookies as
logged out state. Also clear the WebView cookie jar on logout so the
embedded login flow cannot silently restore a prior session.

Simplify comment timestamps to match the current site markup by
tracking one timestamp plus an edited flag instead of fabricating a
creation time we no longer receive.
2026-05-23 13:04:38 +05:30
tramline-github[bot]andGitHub 62196b48a6 [1.548.*] Pre-release merge (#1171) 2026-05-22 14:30:07 +00:00
renovate[bot]andGitHub 8c67a874a5 chore(deps): update plugin dependencyanalysis to v3.13.0 2026-05-21 22:43:29 +00:00
renovate[bot]andGitHub 8296f58ad9 fix(deps): update dependency io.sentry.android.gradle:io.sentry.android.gradle.gradle.plugin to v6.8.1 2026-05-21 18:11:24 +00:00
renovate[bot]andGitHub 1cd40a1383 fix(deps): update dependency io.sentry.android.gradle:io.sentry.android.gradle.gradle.plugin to v6.8.0 2026-05-21 13:28:46 +00:00
Harsh Shandilya dce9dc33fe chore: refresh baseline profiles 2026-05-21 18:56:33 +05:30
Harsh Shandilya 12c98aa2c4 chore: fix some dependency health violations 2026-05-21 18:56:33 +05:30
renovate[bot]andGitHub bb05c46e57 chore(deps): update plugin dependencyanalysis to v3.12.2 2026-05-21 01:35:42 +00:00
renovate[bot]andGitHub 12f075a23d fix(deps): update sentry to v8.42.0 2026-05-20 20:58:51 +00:00
renovate[bot]andGitHub 5065889e27 chore(deps): update mindsers/changelog-reader-action action to v2.4.0 2026-05-20 18:48:06 +00:00
renovate[bot]andGitHub 39ea7c9c2e chore(deps): update plugin dependencyanalysis to v3.12.1 2026-05-20 14:31:57 +00:00
tramline-github[bot]andGitHub d3e83ba14c [1.547.*] Pre-release merge (#1169) 2026-05-20 14:30:09 +00:00
renovate[bot]andGitHub 6f972a8099 fix(deps): update dependency org.junit:junit-bom to v6.1.0 2026-05-19 22:17:27 +00:00
renovate[bot]andGitHub 4d8357c774 fix(deps): update androidx 2026-05-19 17:28:52 +00:00
renovate[bot]andGitHub c92be255d2 fix(deps): update dependency io.sentry.android.gradle:io.sentry.android.gradle.gradle.plugin to v6.7.1 2026-05-19 14:40:26 +00:00
tramline-github[bot]andGitHub 1e4197f34b [1.546.*] Pre-release merge (#1168) 2026-05-16 14:30:04 +00:00
renovate[bot]andGitHub 007c303272 fix(deps): update dependency com.diffplug.spotless:spotless-plugin-gradle to v8.5.1 2026-05-16 01:37:53 +00:00
renovate[bot]andGitHub b922dfafee fix(deps): update dependency io.sentry.android.gradle:io.sentry.android.gradle.gradle.plugin to v6.7.0 2026-05-15 14:31:15 +00:00
tramline-github[bot]andGitHub 50aed77c8b [1.545.*] Pre-release merge (#1167) 2026-05-15 14:30:07 +00:00
renovate[bot]andGitHub 9fab85419c fix(deps): update dependency be.digitalia.compose.htmlconverter:htmlconverter to v1.1.1 2026-05-15 14:14:26 +00:00
renovate[bot]andGitHub 09bc8f8ff8 fix(deps): update dependency com.diffplug.spotless:spotless-plugin-gradle to v8.5.0 2026-05-15 01:03:30 +00:00
renovate[bot]andGitHub e248796c49 chore(deps): update plugin dependencyanalysis to v3.12.0 2026-05-14 22:09:07 +00:00
tramline-github[bot]andGitHub 88f61f38ee [1.544.*] Pre-release merge (#1166) 2026-05-14 14:30:08 +00:00
renovate[bot]andGitHub 9297799549 fix(deps): update metro to v1.1.1 2026-05-14 04:47:25 +00:00
renovate[bot]andGitHub 26fdbe0bed chore(deps): update plugin ksp to v2.3.8 2026-05-13 17:04:35 +00:00
tramline-github[bot]andGitHub 7f6161bc77 [1.543.*] Pre-release merge (#1165) 2026-05-13 14:30:06 +00:00
renovate[bot]andGitHub 4faf965851 chore(deps): update gradle to v9.5.1 2026-05-12 14:57:06 +00:00
tramline-github[bot]andGitHub f412b1ff30 [1.542.*] Pre-release merge (#1164) 2026-05-12 14:30:08 +00:00
renovate[bot]andGitHub 62016180b9 chore(deps): update plugin dependencyanalysis to v3.11.0 2026-05-12 04:47:11 +00:00
Harsh Shandilya 875b41d0bf fix: default comment count to 1 instead of 0 2026-05-12 08:53:33 +05:30
Harsh ShandilyaandGitHub 29771ecd20 [1.541.*] Pre-release merge (#1163) 2026-05-10 20:47:48 +05:30
Harsh Shandilya 052cce6a05 chore: update changelog 2026-05-10 17:12:31 +05:30
Harsh ShandilyaandGitHub 54697ce4ca feat: migrate from unsupported JSON endpoints to parsing HTML (#1162) 2026-05-10 17:01:26 +05:30
Harsh Shandilya 378f140d1a fix(api): remove now unused test dependencies 2026-05-10 15:02:14 +05:30
Harsh Shandilya 00f0e27a0b feat(api): parse tags from html 2026-05-10 15:02:14 +05:30
Harsh Shandilya e21759e6f1 feat(api): parse user profiles from html 2026-05-10 15:01:47 +05:30
Harsh Shandilya 0d88a7cf78 feat(api): parse post details from html 2026-05-10 15:01:45 +05:30
Harsh Shandilya 87fe06a419 feat(api): parse post lists from html 2026-05-10 15:01:35 +05:30
Harsh Shandilya fcdc9db79c feat(api): add kspoon retrofit integration 2026-05-10 14:53:54 +05:30
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
943398bb67 fix(deps): update dependency com.fleeksoft.ksoup:ksoup to v0.2.6 (#1161)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
|
[com.fleeksoft.ksoup:ksoup](https://redirect.github.com/fleeksoft/ksoup)
| `0.2.4` → `0.2.6` |
![age](https://developer.mend.io/api/mc/badges/age/maven/com.fleeksoft.ksoup:ksoup/0.2.6?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.fleeksoft.ksoup:ksoup/0.2.4/0.2.6?slim=true)
|

---

### Release Notes

<details>
<summary>fleeksoft/ksoup (com.fleeksoft.ksoup:ksoup)</summary>

###
[`v0.2.6`](https://redirect.github.com/fleeksoft/ksoup/releases/tag/0.2.6)

[Compare
Source](https://redirect.github.com/fleeksoft/ksoup/compare/0.2.5...0.2.6)

- Updated build + CI (Gradle **9.3.1**, workflow refresh, cleaner build
scripts)
- Dependency upgrades: Kotlin **2.3.10**, AGP **8.13.2**, Ktor
**3.4.0**, plus kotlinx/Okio/Dokka updates
- Improved KMP/JS exports (moved to platform-agnostic `@KmpJsExport`)
- Aligned code  with **jsoup 1.22.1**
- Better HTML/XML parsing reliability + expanded fuzz/integration test
coverage

#### New Contributors

- [@&#8203;peacefulprogram](https://redirect.github.com/peacefulprogram)
made their first contribution in
[#&#8203;124](https://redirect.github.com/fleeksoft/ksoup/pull/124)

**Full Changelog**:
<https://github.com/fleeksoft/ksoup/compare/0.2.5...0.2.6>

###
[`v0.2.5`](https://redirect.github.com/fleeksoft/ksoup/releases/tag/0.2.5)

[Compare
Source](https://redirect.github.com/fleeksoft/ksoup/compare/0.2.4...0.2.5)

#### 🛠️ Build & Dependencies

- Kotlin `2.2.0`, Ktor `3.2.2`, Okio `3.16.0`, kotlinx-io `0.8.0`
- Gradle Wrapper updated to `8.14.3`
- Removed deprecated: `kotlin.native.ignoreIncorrectDependencies`

#### 🔬 CI & Compatibility

- GitHub Actions matrix: Java `17`, `21`, `24`
- Expanded test coverage: unit, concurrency, edge cases

#### ⚙️ Enhancements

- Improved DOM performance with `childNodes` cache
- Refactored iterators, attribute, and parser logic
- Build cleanup and code simplification

#### 🐛 Bug Fixes

- Base URI resolution, text node whitespace, stream reader edge cases
- Maven publish ordering
-  Resolved **Bug
[#&#8203;122](https://redirect.github.com/fleeksoft/ksoup/issues/122)**

#### 📚 Documentation

- Deprecated `PseudoTextElement` → use `Element#selectNodes`

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/msfjarvis/compose-lobsters).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNTkuMiIsInVwZGF0ZWRJblZlciI6IjQzLjE1OS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 18:41:58 +00:00
Harsh Shandilya fc67bda7cb refactor: remove jsoup dependency 2026-05-09 23:56:40 +05:30
Harsh Shandilya 62a71a309b refactor(api): parse search results with ksoup 2026-05-09 23:56:29 +05:30
Harsh Shandilya 3b0a0e3289 refactor(api): parse reply forms with ksoup 2026-05-09 23:56:24 +05:30
Harsh Shandilya 1274897eac refactor(api): parse csrf token with ksoup 2026-05-09 23:56:19 +05:30
Harsh Shandilya 017f970f61 test(api): characterize html converters 2026-05-09 23:55:33 +05:30
Harsh Shandilya bda3c4c26c feat(common): add an ugly as sin test UI for upvote/unvote 2026-05-09 23:55:03 +05:30
Harsh Shandilya c14f529f5e refactor(api): ensure correct ordering for Retrofit converters 2026-05-09 23:55:01 +05:30
Harsh Shandilya 0ff78e5b00 test(api): cover authenticated Lobsters facade 2026-05-09 23:54:50 +05:30
Harsh Shandilya de966de4ec feat(api): add authenticated Lobsters facade 2026-05-09 23:54:43 +05:30
Harsh Shandilya 8a1a64d173 feat(api): add raw authenticated Lobsters endpoints 2026-05-09 18:42:12 +05:30
Harsh Shandilya d1e5df8bc4 feat(api): parse Lobsters reply form 2026-05-09 18:42:12 +05:30
renovate[bot]andGitHub d80ccfe284 chore(deps): update actions/dependency-review-action action to v5 2026-05-09 01:59:35 +00:00
renovate[bot]andGitHub c2cf007b34 fix(deps): update haze to v2.0.0-alpha02 2026-05-08 21:25:45 +00:00
renovate[bot]andGitHub 9cd9d3830a fix(deps): update kotlinx-coroutines monorepo to v1.11.0 2026-05-08 14:01:43 +00:00
tramline-github[bot]andGitHub 957d7adf7a [1.539.*] Pre-release merge (#1159) 2026-05-07 14:30:09 +00:00
renovate[bot]andGitHub 38052b359e fix(deps): update aboutlibraries to v14.2.0 2026-05-07 08:57:12 +00:00
renovate[bot]andGitHub 96151df57f fix(deps): update dependency com.slack.lint.compose:compose-lint-checks to v1.4.3 2026-05-07 01:35:51 +00:00
renovate[bot]andGitHub a141573442 fix(deps): update sentry to v8.41.0 2026-05-06 22:31:41 +00:00
renovate[bot]andGitHub ac537183fb fix(deps): update androidx 2026-05-06 17:16:29 +00:00
tramline-github[bot]andGitHub 88ef5d29da [1.538.*] Pre-release merge (#1158) 2026-05-06 14:30:07 +00:00
renovate[bot]andGitHub 9faac26d57 fix(deps): update agp to v9.2.1 2026-05-05 18:18:26 +00:00
tramline-github[bot]andGitHub 20a57d8819 [1.537.*] Pre-release merge (#1157) 2026-05-02 14:30:10 +00:00
renovate[bot]andGitHub 03baf5edc1 fix(deps): update aboutlibraries to v14.1.0 2026-05-01 19:03:01 +00:00
tramline-github[bot]andGitHub dd3d07df11 [1.536.*] Pre-release merge (#1156) 2026-05-01 14:30:08 +00:00
renovate[bot]andGitHub 2b8a245481 fix(deps): update dependency io.sentry.android.gradle:io.sentry.android.gradle.gradle.plugin to v6.6.0 2026-04-30 20:01:43 +00:00
tramline-github[bot]andGitHub f5173ec7f6 [1.535.*] Pre-release merge (#1155) 2026-04-30 14:30:07 +00:00
renovate[bot]andGitHub 2d88ebafa3 chore(deps): update plugin dependencyanalysis to v3.10.0 2026-04-30 04:35:12 +00:00
Harsh Shandilya a1f0c79172 fix: bump Haze to 2.0.0-alpha01 2026-04-30 10:04:00 +05:30
tramline-github[bot]andGitHub acc9b4571f [1.534.*] Pre-release merge (#1154) 2026-04-29 14:30:06 +00:00
tramline-github[bot]andGitHub 7d41a500e7 [1.533.*] Pre-release merge (#1153) 2026-04-28 14:30:06 +00:00
renovate[bot]andGitHub 039c084967 chore(deps): update gradle to v9.5.0 2026-04-28 14:01:48 +00:00
renovate[bot]andGitHub a1ae8af8a8 fix(deps): update metro to v1.0.0 2026-04-28 05:31:30 +00:00
renovate[bot]andGitHub ad269f9edc fix(deps): update kotlinx-coroutines monorepo to v1.11.0-rc02 2026-04-27 17:31:09 +00:00
tramline-github[bot]andGitHub 79d6f17eb6 [1.532.*] Pre-release merge (#1152) 2026-04-26 14:30:07 +00:00
tramline-github[bot]andGitHub 4b345c1d46 [1.531.*] Pre-release merge (#1151) 2026-04-25 14:30:06 +00:00
renovate[bot]andGitHub 7bf21ba23e fix(deps): update dependency org.junit:junit-bom to v6.1.0-rc1 2026-04-25 13:37:29 +00:00
renovate[bot]andGitHub 12226b48e8 chore(deps): update plugin poko to v0.22.1 2026-04-25 05:50:07 +00:00
renovate[bot]andGitHub 1d3ba7c0de fix(deps): update metro to v1.0.0-rc4 2026-04-25 00:46:14 +00:00
tramline-github[bot]andGitHub 98ab444c47 [1.530.*] Pre-release merge (#1150) 2026-04-24 14:30:08 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Harsh Shandilya
6e779cd438 fix(deps): update metro to v1.0.0-rc3 (#1148)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [dev.zacsweers.metro](https://redirect.github.com/ZacSweers/metro) |
`1.0.0-RC2` → `1.0.0-RC3` |
![age](https://developer.mend.io/api/mc/badges/age/maven/dev.zacsweers.metro:dev.zacsweers.metro.gradle.plugin/1.0.0-RC3?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/dev.zacsweers.metro:dev.zacsweers.metro.gradle.plugin/1.0.0-RC2/1.0.0-RC3?slim=true)
|
|
[dev.zacsweers.metro:metrox-viewmodel-compose](https://redirect.github.com/ZacSweers/metro)
| `1.0.0-RC2` → `1.0.0-RC3` |
![age](https://developer.mend.io/api/mc/badges/age/maven/dev.zacsweers.metro:metrox-viewmodel-compose/1.0.0-RC3?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/dev.zacsweers.metro:metrox-viewmodel-compose/1.0.0-RC2/1.0.0-RC3?slim=true)
|
|
[dev.zacsweers.metro:metrox-viewmodel](https://redirect.github.com/ZacSweers/metro)
| `1.0.0-RC2` → `1.0.0-RC3` |
![age](https://developer.mend.io/api/mc/badges/age/maven/dev.zacsweers.metro:metrox-viewmodel/1.0.0-RC3?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/dev.zacsweers.metro:metrox-viewmodel/1.0.0-RC2/1.0.0-RC3?slim=true)
|
|
[dev.zacsweers.metro:metrox-android](https://redirect.github.com/ZacSweers/metro)
| `1.0.0-RC2` → `1.0.0-RC3` |
![age](https://developer.mend.io/api/mc/badges/age/maven/dev.zacsweers.metro:metrox-android/1.0.0-RC3?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/dev.zacsweers.metro:metrox-android/1.0.0-RC2/1.0.0-RC3?slim=true)
|

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/266) for more information.

---

### Release Notes

<details>
<summary>ZacSweers/metro (dev.zacsweers.metro)</summary>

###
[`v1.0.0-RC3`](https://redirect.github.com/ZacSweers/metro/blob/HEAD/CHANGELOG.md#100-RC3)

[Compare
Source](https://redirect.github.com/ZacSweers/metro/compare/1.0.0-RC2...1.0.0-RC3)

*2026-04-23*

This is the third release candidate for Metro 1.0!

This means that its *runtime* APIs (`runtime`, `metrox` artifacts,
Gradle plugin, etc) will be API stable unless annotated with an
experimental annotation.

*Changes since RC2*

##### `enableFunctionProviders` enabled by default

This release promotes `enableFunctionProviders` (i.e. `() -> T` syntax
for providers) to stable, enables it by default, and marks usage of
Metro's native `Provider` as a warning.

This may require some migration in existing codebases. To help with
this, there's a [new section in
docs](https://zacsweers.github.io/metro/latest/adoption/#migrating-providert-to-function-syntax)
with information as well as a helper script: adoption docs guidance here
too.

##### New

- Add a new `desugaredProviderSeverity` option (default: `WARN`) that
reports a diagnostic when `Provider<T>` is used instead of the preferred
`() -> T` form. Set this to `NONE` to disable the warning during
migration, or `ERROR` to enforce the new style. Automatically treated as
`NONE` when `enableFunctionProviders` is disabled.
- **\[FIR]** Add diagnostic checks against providing intrinsic types
(`Provider`, `Lazy`, etc.) from `@Provides` declarations.
- **\[Gradle]** Introduce a new `compilerOptions {}` DSL for free Metro
compiler options and flags.
- **\[Gradle]** Add `IDE_WARN` and `IDE_ERROR` members to
`DiagnosticSeverity` to allow configuring some diagnostics to *only* run
in IDE sessions. Useful for diagnostics you only want to surface to
readers in the IDE without emitting compiler warnings in real (CLI)
compilations.

##### Enhancements

- **\[FIR]** Add a new diagnostic for ambiguous inject constructors.
Namely, cases where a class is annotated with `@Inject`, defines a
secondary constructor but no primary constructor. This is ambiguous,
metro now asks you to pick a lane.
- **\[FIR]** Add a new diagnostic for check against `private`
contributions.
- **\[FIR]** When rendering diagnostics in the IDE, use short names for
classes since they are shown in context already.
- **\[JVM/JS]** Generate `@JvmStatic` and `@JsStatic` annotations onto
static-ish functions for better staticization on those platforms.
- **\[docs]** Migrate doc site to Zensical. Works the same, fresh-ish
coat of paint!

##### Fixes

- **\[FIR]** Fix not recognizing `FirDeclarationOrigin.Precompiled`
origins when checking resolved default binding types. Previously we only
considered `FirDeclarationOrigin.Library`, but incremental compilation
uses `FirDeclarationOrigin.Precompiled` to differentiate. This would
result in misreads of default binding types in some cases during IC.
- **\[FIR]** When merging `@ContributesTo` types to graph supertypes,
add the original interface as well. This ensures they are visible in
ObjC exports, as the metro-generated contribution interfaces are
normally excluded.
- **\[IR]** Fix secondary inject constructors support when
`generateContributionProviders` is enabled.
- **\[IR]** Fix implicit class key lookup for map keys on
source-declared `@Binds` declarations.
- **\[IR]** Fix `implementsProviderType()` check in the compiler to only
exactly match `Function0` types when `enableFunctionProviders` is
enabled.
- **\[IR]** Set `thisGraphInstance` field types as the graph impl type
to avoid a Wasm issue.
- **\[interop]** Fix `@ContributesSubcomponent.Factory` interop with
square/anvil.
- **\[interop]** Fix `@MergeSubcomponent.Factory` interop with
zacsweers/anvil (anvil-ksp).
- **\[docs]** Fix source links in Dokka API docs.
- **\[docs]** Don't publish `**.internal.**` APIs in Dokka API docs.

##### Changes

- `enableFunctionProviders` (i.e. `() -> T` syntax for providers) is now
enabled by default. Previously this required opting in. The
function-syntax form is now the **recommended** way to declare provider
dependencies; `Provider<T>` is still supported but treated as a
desugared alternative and a **warning** by default, similar to if you
were to use `Function0` instead of `() -> T` syntax for functions. See
the [metro-intrinsics](docs/metro-intrinsics.md) docs for more details.
- **\[IR]** Remove deprecated `indexInOldValueParameters` use in IR for
better `2.4.0`+ support.
- **\[Gradle]** Promote `enableFunctionProviders` to stable.
- **\[Gradle]** Remove deprecated `useAssistedParamNamesAsIdentifiers`
property.
- **\[Gradle]** Remove `deduplicateInjectedParams` property.
- **\[Gradle]** Remove `enableKlibParamsCheck` property, use the new
`compilerOptions` API.
- **\[Gradle]** Remove `enableFullBindingGraphValidation` property, use
the new `compilerOptions` API.
- **\[Gradle]** Remove `enableGraphImplClassAsReturnType` property, use
the new `compilerOptions` API.
- **\[Gradle]** Remove `shrinkUnusedBindings` property, use the new
`compilerOptions` API.
- **\[metrox-android]** Change `MetroAppComponentProviders` accessor
multibindings to expose function types instead of `Provider` types.
- **\[metrox-viewmodel]** Change `MetroViewModelFactory` and
`MetroViewModelMultibindings` accessor multibindings to expose function
types instead of `Provider` types.
- Support Kotlin `2.4.0-Beta2`.
- Update embedded androidx.tracing to `2.0.0-alpha06`.

##### Contributors

Special thanks to the following contributors for contributing to this
release!

- [@&#8203;anddani](https://redirect.github.com/anddani)
- [@&#8203;jonamireh](https://redirect.github.com/jonamireh)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/msfjarvis/compose-lobsters).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMzkuNyIsInVwZGF0ZWRJblZlciI6IjQzLjEzOS43IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

---------

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
2026-04-23 18:12:19 +00:00
Harsh Shandilya 21ba2c3869 Merge remote-tracking branches 'origin/renovate/sentry' and 'origin/renovate/kotlin-monorepo'
* origin/renovate/sentry:
  fix(deps): update sentry

* origin/renovate/kotlin-monorepo:
  fix(deps): update kotlin monorepo to v2.3.21
2026-04-23 23:26:44 +05:30
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
61d8c4e4e0 fix(deps): update kotlin and compose to v2.3.21 (#1145)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [org.jetbrains.kotlin.plugin.serialization](https://kotlinlang.org/)
([source](https://redirect.github.com/JetBrains/kotlin)) | `2.3.20` →
`2.3.21` |
![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin/2.3.21?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin/2.3.20/2.3.21?slim=true)
|
| [org.jetbrains.kotlin.plugin.compose](https://kotlinlang.org/)
([source](https://redirect.github.com/JetBrains/kotlin)) | `2.3.20` →
`2.3.21` |
![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin/2.3.21?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin/2.3.20/2.3.21?slim=true)
|
| [org.jetbrains.kotlin:kotlin-gradle-plugin](https://kotlinlang.org/)
([source](https://redirect.github.com/JetBrains/kotlin)) | `2.3.20` →
`2.3.21` |
![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.kotlin:kotlin-gradle-plugin/2.3.21?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.kotlin:kotlin-gradle-plugin/2.3.20/2.3.21?slim=true)
|

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/266) for more information.

---

### Release Notes

<details>
<summary>JetBrains/kotlin
(org.jetbrains.kotlin.plugin.serialization)</summary>

###
[`v2.3.21`](https://redirect.github.com/JetBrains/kotlin/blob/HEAD/ChangeLog.md#2321)

##### Backend. Wasm

- [`KT-84610`](https://youtrack.jetbrains.com/issue/KT-84610) \[Wasm]
Failed to compile klibs in IC mode

##### Compiler

- [`KT-84566`](https://youtrack.jetbrains.com/issue/KT-84566) Prevent
launching Default dispatcher threads from IJ SDK in kotlin compiler
- [`KT-85358`](https://youtrack.jetbrains.com/issue/KT-85358) Native:
roll back the workaround for KT-84678 once MapLibre has been properly
fixed
- [`KT-85626`](https://youtrack.jetbrains.com/issue/KT-85626)
`@JvmRecord` in commonMain breaks compileCommonMainKotlinMetadata with
"Cannot access 'java.lang.Record'"
- [`KT-85405`](https://youtrack.jetbrains.com/issue/KT-85405)
Postpone/Revert `DontIgnoreUpperBoundViolatedOnImplicitArguments`
- [`KT-84678`](https://youtrack.jetbrains.com/issue/KT-84678) K/N:
Undefined symbol from SPM-added ObjC frameworks when linking iOS target
- [`KT-85021`](https://youtrack.jetbrains.com/issue/KT-85021) False
positive SUBCLASS\_CANT\_CALL\_COMPANION\_PROTECTED\_NON\_STATIC error
in multi-module project

##### JavaScript

- [`KT-82395`](https://youtrack.jetbrains.com/issue/KT-82395) Support
top-level declarations from compiler plugins in JS incremental
compilation
- [`KT-84475`](https://youtrack.jetbrains.com/issue/KT-84475) K/JS:
false-positive exportability warnings in multi-module project
- [`KT-84633`](https://youtrack.jetbrains.com/issue/KT-84633) Kotlin/JS:
"Serializer for class not found" error when IR output granularity is
`whole-program`
- [`KT-85047`](https://youtrack.jetbrains.com/issue/KT-85047) Kotlin/JS:
`@JsStatic` on suspend fun of class companion generates incorrect d.ts
- [`KT-84517`](https://youtrack.jetbrains.com/issue/KT-84517) K/JS: bad
mappings data in outputted Kotlin stdlib source map

##### Libraries

- [`KT-71848`](https://youtrack.jetbrains.com/issue/KT-71848)
Kotlinx.metadata: Add `CompilerPluginData` into Km API

##### Native. C and ObjC Import

- [`KT-85399`](https://youtrack.jetbrains.com/issue/KT-85399)
Kotlin/Native: TypeCastException when casting ObjC Protocol MetaClass
with genericSafeCasts enabled
- [`KT-85508`](https://youtrack.jetbrains.com/issue/KT-85508) K/N:
TypeCastException when using nw\_parameters\_create\_secure\_tcp block
parameter on 2.3.20

##### Tools. Gradle

- [`KT-84729`](https://youtrack.jetbrains.com/issue/KT-84729) Update
Gradle plugin-publish version to enable configuration cache badge on
Gradle plugins portal

##### Tools. Gradle. Compiler plugins

- [`KT-85257`](https://youtrack.jetbrains.com/issue/KT-85257)
AGP/Compose: MergeMappingFileTask clears R8 artifacts due to
`@OutputDirectory` annotation on AGP 9.1+

##### Tools. Scripts

- [`KT-85105`](https://youtrack.jetbrains.com/issue/KT-85105) Scripts:
JVM backend internal error (IR lowering) when scratch file contains
anonymous object
- [`KT-85103`](https://youtrack.jetbrains.com/issue/KT-85103) Exception
while generating code when explain destructuring decls
- [`KT-84842`](https://youtrack.jetbrains.com/issue/KT-84842)
scriptCompilationClasspathFromContext behavior changed from 2.3.10 to
2.3.20
- [`KT-85029`](https://youtrack.jetbrains.com/issue/KT-85029) Kotlin
Scripting: ScriptDiagnostic reports "at null" instead of error location

##### Tools. Statistics (FUS)

- [`KT-85628`](https://youtrack.jetbrains.com/issue/KT-85628) KGP:
composite build FUS metrics fail on access of 'configurationTimeMetrics'

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/msfjarvis/compose-lobsters).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMzkuNyIsInVwZGF0ZWRJblZlciI6IjQzLjEzOS43IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-23 17:10:53 +00:00
renovate[bot]andGitHub ccdc75f7a3 fix(deps): update sentry 2026-04-23 16:58:32 +00:00
renovate[bot]andGitHub f1ae3c1a44 fix(deps): update kotlin monorepo to v2.3.21 2026-04-23 16:58:25 +00:00
renovate[bot]andGitHub aa32599a59 chore(deps): update plugin ksp to v2.3.7 2026-04-23 01:55:47 +00:00
renovate[bot]andGitHub a58edda23e fix(deps): update androidx 2026-04-22 20:32:10 +00:00
tramline-github[bot]andGitHub f9eb966015 [1.529.*] Pre-release merge (#1144) 2026-04-22 14:30:09 +00:00
renovate[bot]andGitHub a833820be3 fix(deps): update agp to v9.2.0 2026-04-21 14:32:06 +00:00
tramline-github[bot]andGitHub e33e6a9d9a [1.528.*] Pre-release merge (#1143) 2026-04-21 14:30:11 +00:00
renovate[bot]andGitHub 16cc3097b7 chore(deps): update r0adkll/upload-google-play action to v1.1.5 2026-04-21 02:41:06 +00:00
renovate[bot]andGitHub 49318b2766 chore(deps): update plugin dependencyanalysis to v3.9.0 2026-04-20 21:57:06 +00:00
renovate[bot]andGitHub c9a22df572 chore(deps): update r0adkll/upload-google-play action to v1.1.4 2026-04-20 19:43:40 +00:00
tramline-github[bot]andGitHub 31db513564 [1.527.*] Pre-release merge (#1142) 2026-04-20 14:30:09 +00:00
renovate[bot]andGitHub 13d368b86a chore(deps): update plugin com.gradle.develocity to v4.4.1 2026-04-20 13:13:55 +00:00
renovate[bot]andGitHub 4c14890170 fix(deps): update dependency org.jsoup:jsoup to v1.22.2 2026-04-20 08:40:49 +00:00
tramline-github[bot]andGitHub 08063d7edc [1.526.*] Pre-release merge (#1141) 2026-04-19 14:30:04 +00:00
renovate[bot]andGitHub 43d1b51901 chore(deps): update plugin dependencyanalysis to v3.8.0 2026-04-19 09:09:49 +00:00
Harsh Shandilya 088fcf5a62 fix: bump dependencies 2026-04-18 21:26:19 +05:30
tramline-github[bot]andGitHub 48b779d6d1 [1.525.*] Pre-release merge (#1140) 2026-04-17 14:30:08 +00:00
renovate[bot]andGitHub e0bb04ce4c fix(deps): update sentry to v8.39.1 2026-04-17 09:20:42 +00:00
renovate[bot]andGitHub b621fb0480 fix(deps): update sentry to v8.39.0 2026-04-16 20:36:33 +00:00
tramline-github[bot]andGitHub a1e8bba16e [1.524.*] Pre-release merge (#1139) 2026-04-14 14:30:08 +00:00
renovate[bot]andGitHub def7d1a727 fix(deps): update agp to v9.2.0-rc01 2026-04-13 17:03:34 +00:00
Harsh ShandilyaandGitHub 9153880007 [1.523.*] Pre-release merge (#1138) 2026-04-13 20:07:12 +05:30
tramline-github[bot]andGitHub e5f0f91e8b [1.522.*] Pre-release merge (#1137) 2026-04-12 14:30:05 +00:00
renovate[bot]andGitHub 24abd4171a chore(deps): update plugin modulegraphassert to v2.9.1 2026-04-12 14:00:50 +00:00
Harsh Shandilya 31be02c521 refactor: replace if with when 2026-04-12 14:25:49 +05:30
Harsh Shandilya 79f2b5d01c chore: rename ProGuard files to R8 2026-04-12 14:25:49 +05:30
Harsh Shandilya ae047d8a6d fix: remove obsoleted ProGuard rule 2026-04-12 14:10:13 +05:30
Harsh Shandilya 864b023302 refactor: remove useless twiddling of Main dispatcher 2026-04-12 14:01:59 +05:30
Harsh Shandilya 502eb751eb chore: add changelog entry for login 2026-04-12 13:55:59 +05:30
tramline-github[bot]andGitHub 45c13d84af [1.521.*] Pre-release merge (#1136) 2026-04-11 14:30:07 +00:00
renovate[bot]andGitHub 210bc89346 chore(deps): update plugin dependencyanalysis to v3.7.0 (#1135) 2026-04-11 10:14:32 +05:30
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
9ecb0fa704 chore(deps): update actions/upload-artifact action to v7.0.1 (#1134)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[actions/upload-artifact](https://redirect.github.com/actions/upload-artifact)
| action | patch | `v7.0.0` → `v7.0.1` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/266) for more information.

---

### Release Notes

<details>
<summary>actions/upload-artifact (actions/upload-artifact)</summary>

###
[`v7.0.1`](https://redirect.github.com/actions/upload-artifact/compare/v7.0.0...v7.0.1)

[Compare
Source](https://redirect.github.com/actions/upload-artifact/compare/v7.0.0...v7.0.1)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/msfjarvis/compose-lobsters).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMTAuMiIsInVwZGF0ZWRJblZlciI6IjQzLjExMC4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-11 04:38:22 +00:00
Harsh Shandilya 635e33233b fix: move Lobsters URI creation out of class init
For some reason this is now too early and explodes
2026-04-11 08:17:54 +05:30
Harsh Shandilya 2cf53422bf fix: regenerate baseline profiles 2026-04-11 01:55:54 +05:30
Harsh Shandilya 80980af1c9 fix: move baseline profiles back to the emulators 2026-04-10 22:35:26 +05:30
Harsh Shandilya 3b5c4b3ddb fix(build): remove obsolete source set 2026-04-10 22:35:15 +05:30
Harsh Shandilya 84e36496d3 fix: downgrade Kotlin 2026-04-10 22:26:26 +05:30
Harsh Shandilya 3f714f41c3 fix: upgrade to latest everything 2026-04-10 21:44:31 +05:30
tramline-github[bot]andGitHub 7da7ec0467 [1.520.*] Pre-release merge (#1133) 2026-04-10 14:30:07 +00:00
Harsh ShandilyaandGitHub e20c90297a fix: remove now-empty core-ktx library 2026-04-10 00:13:11 +05:30
tramline-github[bot]andGitHub 1f815a613a [1.519.*] Pre-release merge (#1130) 2026-04-09 14:30:06 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
0ac8c91789 fix(deps): update androidx (#1128)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
|
[androidx.lint:lint-gradle](https://developer.android.com/jetpack/androidx/releases/lint#1.0.0-alpha06)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.0.0-alpha05` → `1.0.0-alpha06` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.lint:lint-gradle/1.0.0-alpha06?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.lint:lint-gradle/1.0.0-alpha05/1.0.0-alpha06?slim=true)
|
|
[androidx.sqlite:sqlite](https://developer.android.com/jetpack/androidx/releases/sqlite#2.6.2)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `2.7.0-alpha02` → `2.7.0-alpha03` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.sqlite:sqlite/2.7.0-alpha03?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.sqlite:sqlite/2.7.0-alpha02/2.7.0-alpha03?slim=true)
|
|
[androidx.compose.runtime:runtime-saveable](https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.10.6)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.11.0-beta02` → `1.11.0-rc01` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.runtime:runtime-saveable/1.11.0-rc01?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.runtime:runtime-saveable/1.11.0-beta02/1.11.0-rc01?slim=true)
|
|
[androidx.paging:paging-compose](https://developer.android.com/jetpack/androidx/releases/paging#3.4.2)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `3.5.0-alpha01` → `3.5.0-beta01` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.paging:paging-compose/3.5.0-beta01?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.paging:paging-compose/3.5.0-alpha01/3.5.0-beta01?slim=true)
|
|
[androidx.paging:paging-common](https://developer.android.com/jetpack/androidx/releases/paging#3.4.2)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `3.5.0-alpha01` → `3.5.0-beta01` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.paging:paging-common/3.5.0-beta01?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.paging:paging-common/3.5.0-alpha01/3.5.0-beta01?slim=true)
|
|
[androidx.compose.material3.adaptive:adaptive-navigation3](https://developer.android.com/jetpack/androidx/releases/compose-material3-adaptive#1.3.0-alpha10)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.3.0-alpha09` → `1.3.0-alpha10` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.material3.adaptive:adaptive-navigation3/1.3.0-alpha10?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.material3.adaptive:adaptive-navigation3/1.3.0-alpha09/1.3.0-alpha10?slim=true)
|
|
[androidx.compose.material3.adaptive:adaptive-layout](https://developer.android.com/jetpack/androidx/releases/compose-material3-adaptive#1.2.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.3.0-alpha09` → `1.3.0-alpha10` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.material3.adaptive:adaptive-layout/1.3.0-alpha10?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.material3.adaptive:adaptive-layout/1.3.0-alpha09/1.3.0-alpha10?slim=true)
|
|
[androidx.compose.material3.adaptive:adaptive](https://developer.android.com/jetpack/androidx/releases/compose-material3-adaptive#1.2.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.3.0-alpha09` → `1.3.0-alpha10` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.material3.adaptive:adaptive/1.3.0-alpha10?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.material3.adaptive:adaptive/1.3.0-alpha09/1.3.0-alpha10?slim=true)
|
|
[androidx.navigation3:navigation3-ui](https://developer.android.com/jetpack/androidx/releases/navigation3#1.1.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.1.0-rc01` → `1.1.0` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.navigation3:navigation3-ui/1.1.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.navigation3:navigation3-ui/1.1.0-rc01/1.1.0?slim=true)
|
|
[androidx.navigation3:navigation3-runtime](https://developer.android.com/jetpack/androidx/releases/navigation3#1.1.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.1.0-rc01` → `1.1.0` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.navigation3:navigation3-runtime/1.1.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.navigation3:navigation3-runtime/1.1.0-rc01/1.1.0?slim=true)
|
|
[androidx.compose.ui:ui-unit](https://developer.android.com/jetpack/androidx/releases/compose-ui#1.10.6)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.11.0-beta02` → `1.11.0-rc01` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.ui:ui-unit/1.11.0-rc01?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.ui:ui-unit/1.11.0-beta02/1.11.0-rc01?slim=true)
|
|
[androidx.compose.ui:ui-tooling-preview](https://developer.android.com/jetpack/androidx/releases/compose-ui#1.10.6)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.11.0-beta02` → `1.11.0-rc01` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.ui:ui-tooling-preview/1.11.0-rc01?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.ui:ui-tooling-preview/1.11.0-beta02/1.11.0-rc01?slim=true)
|
|
[androidx.compose.ui:ui-tooling](https://developer.android.com/jetpack/androidx/releases/compose-ui#1.10.6)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.11.0-beta02` → `1.11.0-rc01` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.ui:ui-tooling/1.11.0-rc01?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.ui:ui-tooling/1.11.0-beta02/1.11.0-rc01?slim=true)
|
|
[androidx.compose.ui:ui-text](https://developer.android.com/jetpack/androidx/releases/compose-ui#1.10.6)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.11.0-beta02` → `1.11.0-rc01` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.ui:ui-text/1.11.0-rc01?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.ui:ui-text/1.11.0-beta02/1.11.0-rc01?slim=true)
|
|
[androidx.compose.ui:ui-graphics](https://developer.android.com/jetpack/androidx/releases/compose-ui#1.10.6)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.11.0-beta02` → `1.11.0-rc01` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.ui:ui-graphics/1.11.0-rc01?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.ui:ui-graphics/1.11.0-beta02/1.11.0-rc01?slim=true)
|
|
[androidx.compose.ui:ui](https://developer.android.com/jetpack/androidx/releases/compose-ui#1.10.6)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.11.0-beta02` → `1.11.0-rc01` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.ui:ui/1.11.0-rc01?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.ui:ui/1.11.0-beta02/1.11.0-rc01?slim=true)
|
|
[androidx.compose.runtime:runtime-annotation](https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.10.6)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.11.0-beta02` → `1.11.0-rc01` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.runtime:runtime-annotation/1.11.0-rc01?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.runtime:runtime-annotation/1.11.0-beta02/1.11.0-rc01?slim=true)
|
|
[androidx.compose.runtime:runtime](https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.10.6)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.11.0-beta02` → `1.11.0-rc01` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.runtime:runtime/1.11.0-rc01?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.runtime:runtime/1.11.0-beta02/1.11.0-rc01?slim=true)
|
|
[androidx.compose.material3:material3-window-size-class](https://developer.android.com/jetpack/androidx/releases/compose-material3#1.4.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.5.0-alpha16` → `1.5.0-alpha17` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.material3:material3-window-size-class/1.5.0-alpha17?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.material3:material3-window-size-class/1.5.0-alpha16/1.5.0-alpha17?slim=true)
|
|
[androidx.compose.material3:material3](https://developer.android.com/jetpack/androidx/releases/compose-material3#1.4.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.5.0-alpha16` → `1.5.0-alpha17` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.material3:material3/1.5.0-alpha17?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.material3:material3/1.5.0-alpha16/1.5.0-alpha17?slim=true)
|
|
[androidx.compose.foundation:foundation-layout](https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.10.6)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.11.0-beta02` → `1.11.0-rc01` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.foundation:foundation-layout/1.11.0-rc01?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.foundation:foundation-layout/1.11.0-beta02/1.11.0-rc01?slim=true)
|
|
[androidx.compose.foundation:foundation](https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.10.6)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.11.0-beta02` → `1.11.0-rc01` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.foundation:foundation/1.11.0-rc01?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.foundation:foundation/1.11.0-beta02/1.11.0-rc01?slim=true)
|
|
[androidx.compose.animation:animation-core](https://developer.android.com/jetpack/androidx/releases/compose-animation#1.10.6)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.11.0-beta02` → `1.11.0-rc01` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.animation:animation-core/1.11.0-rc01?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.animation:animation-core/1.11.0-beta02/1.11.0-rc01?slim=true)
|
|
[androidx.compose.animation:animation](https://developer.android.com/jetpack/androidx/releases/compose-animation#1.10.6)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.11.0-beta02` → `1.11.0-rc01` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.compose.animation:animation/1.11.0-rc01?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.compose.animation:animation/1.11.0-beta02/1.11.0-rc01?slim=true)
|
|
[androidx.annotation:annotation](https://developer.android.com/jetpack/androidx/releases/annotation#1.10.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.10.0-rc01` → `1.10.0` |
![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.annotation:annotation/1.10.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.annotation:annotation/1.10.0-rc01/1.10.0?slim=true)
|

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/266) for more information.

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/msfjarvis/compose-lobsters).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMTAuMiIsInVwZGF0ZWRJblZlciI6IjQzLjExMC4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 12:16:38 +00:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>msfjarvisHarsh Shandilya
d74b0c3f40 Centralize Android SDK version constants and wire benchmark through build-logic (#1129)
`benchmark` was hardcoding `compileSdk` because it applies
`com.android.test` + `kotlin-android` — neither of which pulls in
`AndroidCommonPlugin`. The SDK values were also duplicated across two
plugin files with no shared constant.

## Changes

- **New `AndroidConstants.kt`** — single source of truth for
`COMPILE_SDK = 37` and `TARGET_SDK = 37`
- **`AndroidCommonPlugin`** — replace literal `37` with `COMPILE_SDK`
- **`ApplicationPlugin`** — replace literal `37` with `TARGET_SDK`
- **`benchmark/build.gradle.kts`** — apply
`dev.msfjarvis.claw.android-common` so `compileSdk` is set by the
plugin; drop the hardcoded value. `TestExtension` implements
`CommonExtension`, so the plugin's `configure<CommonExtension>` block
applies cleanly.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: msfjarvis <13348378+msfjarvis@users.noreply.github.com>
Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
2026-04-09 12:05:14 +00:00
renovate[bot]andGitHub 7040518fc7 fix(deps): update dependency io.sentry.android.gradle:io.sentry.android.gradle.gradle.plugin to v6.4.0 2026-04-09 11:34:17 +00:00
renovate[bot]andGitHub e27a3582f8 fix(deps): update sentry to v8.38.0 2026-04-08 19:31:23 +00:00
tramline-github[bot]andGitHub 05b33e67bc [1.518.*] Pre-release merge (#1127) 2026-04-07 14:30:06 +00:00
renovate[bot]andGitHub f4b3f936ae fix(deps): update metro to v0.13.2 2026-04-07 01:29:44 +00:00
renovate[bot]andGitHub 199ddc9ca8 fix(deps): update aboutlibraries to v14.0.0 2026-04-06 20:39:38 +00:00
tramline-github[bot]andGitHub 6be3d954cd [1.517.*] Pre-release merge (#1126) 2026-04-06 14:30:07 +00:00
renovate[bot]andGitHub 792b8b343b fix(deps): update metro to v0.13.1 2026-04-06 08:50:46 +00:00
tramline-github[bot]andGitHub daf4611844 [1.516.*] Pre-release merge (#1125) 2026-04-05 14:30:09 +00:00
renovate[bot]andGitHub b10c87c33d fix(deps): update metro to v0.13.0 2026-04-05 05:17:12 +00:00
tramline-github[bot]andGitHub 7aedefa1b5 [1.515.*] Pre-release merge (#1124) 2026-04-04 14:30:10 +00:00
renovate[bot]andGitHub b6ed8e914e chore(deps): update gradle/actions digest to 50e97c2 2026-04-03 16:40:31 +00:00
Harsh Shandilya 3c6145c1f3 chore: refresh Lint baseline 2026-04-03 22:01:41 +05:30
Harsh Shandilya e1ca964185 feat(android): wire Login destination into nav graph and Settings 2026-04-03 21:52:39 +05:30
Harsh Shandilya 2e84ada707 feat(android): add login/logout rows to SettingsScreen 2026-04-03 21:52:39 +05:30
Harsh Shandilya 758f88e02f feat(android): add SettingsViewModel with login state 2026-04-03 21:52:39 +05:30
Harsh Shandilya eef58ab267 feat(android): add LoginScreen WebView composable 2026-04-03 21:52:39 +05:30
Harsh Shandilya 9d34cf6c2e feat(android): add Login nav destination 2026-04-03 21:52:38 +05:30
Harsh Shandilya bfb402d4ae feat(core): add SessionExpiryInterceptor to handle expired sessions 2026-04-03 21:52:38 +05:30
Harsh Shandilya 66c4dc78f4 feat(core): add LobstersCookieJar and wire into OkHttpModule 2026-04-03 21:52:38 +05:30
Harsh Shandilya 34e4836f2d feat(common): implement SessionCookieStore backed by SQLDelight 2026-04-03 21:52:38 +05:30
Harsh Shandilya c77d10942f feat(database): add SessionCookie table and queries 2026-04-03 21:52:38 +05:30
Harsh Shandilya 9c6c5e82cb feat(core): add SessionCookieStore interface 2026-04-03 21:45:52 +05:30
tramline-github[bot]andGitHub 30563a822d [1.514.*] Pre-release merge (#1123) 2026-04-03 14:30:07 +00:00
Harsh Shandilya dac23176ff chore: document tag migration removal 2026-04-03 18:15:26 +05:30
Harsh Shandilya 5c8174879f chore: ignore local Git worktrees 2026-04-03 18:15:26 +05:30
Harsh Shandilya 30a5bcc687 refactor(test): remove unnecessary advanceUntilIdle calls 2026-04-03 18:15:26 +05:30
Harsh Shandilya 30293d29f6 fix(build): remove datastore dependencies 2026-04-03 01:06:10 +05:30
Harsh Shandilya 0269a284ef refactor(test): use deterministic tag block dispatchers 2026-04-03 01:06:10 +05:30
Harsh Shandilya 19103c19b9 refactor(test): cover reactive tag block flows 2026-04-03 01:06:10 +05:30
Harsh Shandilya 96f2c78a4e refactor: remove tag datastore migration 2026-04-03 01:06:10 +05:30
tramline-github[bot]andGitHub 8c34a5bcde [1.513.*] Pre-release merge (#1122) 2026-04-02 14:30:05 +00:00
GitHub Actions 4e44758c39 feat(release): start next development iteration 2026-04-02 09:29:53 +00:00
tramline-github[bot]andGitHub 3b31e5fe57 [1.512.*] Pre-release merge (#1121) 2026-03-31 14:30:09 +00:00
tramline-github[bot]andGitHub 8bd250d7aa [1.511.*] Pre-release merge (#1120) 2026-03-28 14:30:06 +00:00
tramline-github[bot]andGitHub c3e268dabb [1.510.*] Pre-release merge (#1119) 2026-03-27 14:30:09 +00:00
tramline-github[bot]andGitHub b2da38319c [1.509.*] Pre-release merge (#1118) 2026-03-26 14:30:07 +00:00
tramline-github[bot]andGitHub 7c6ee1728a [1.508.*] Pre-release merge (#1117) 2026-03-25 14:30:05 +00:00
tramline-github[bot]andGitHub 8bb926eeea [1.507.*] Pre-release merge (#1115) 2026-03-24 14:30:18 +00:00
tramline-github[bot]andGitHub 2abaa7435d [1.506.*] Pre-release merge (#1114) 2026-03-23 14:30:06 +00:00
tramline-github[bot]andGitHub 0c31dd3dfd [1.505.*] Pre-release merge (#1113) 2026-03-21 20:17:20 +00:00
tramline-github[bot]andGitHub e4654030ea [1.504.*] Pre-release merge (#1111) 2026-03-20 14:30:10 +00:00
tramline-github[bot]andGitHub d8338daade [1.503.*] Pre-release merge (#1110) 2026-03-19 14:30:05 +00:00
tramline-github[bot]andGitHub e21245df0a [1.502.*] Pre-release merge (#1109) 2026-03-18 14:30:11 +00:00
tramline-github[bot]andGitHub 0aebcf0d65 [1.501.*] Pre-release merge (#1104) 2026-03-15 14:30:07 +00:00
tramline-github[bot]andGitHub 159954bfff [1.500.*] Pre-release merge (#1103) 2026-03-14 14:30:06 +00:00
tramline-github[bot]andGitHub fa039af173 [1.499.*] Pre-release merge (#1102) 2026-03-13 14:30:10 +00:00
tramline-github[bot]andGitHub de7bf7e869 [1.498.*] Pre-release merge (#1101) 2026-03-12 14:30:08 +00:00
tramline-github[bot]andGitHub 0d103c7b71 [1.497.*] Pre-release merge (#1097) 2026-03-10 14:30:09 +00:00
tramline-github[bot]andGitHub c801dd8693 [1.496.*] Pre-release merge (#1096) 2026-03-09 14:30:14 +00:00
tramline-github[bot]andGitHub 54cf657bc9 [1.495.*] Pre-release merge (#1095) 2026-03-09 05:50:26 +00:00
tramline-github[bot]andGitHub ececac2ff9 [1.495.*] Pre-release merge (#1094) 2026-03-06 14:30:10 +00:00
tramline-github[bot]andGitHub a0bfa80b37 [1.495.*] Pre-release merge (#1091) 2026-03-05 14:30:06 +00:00
tramline-github[bot]andGitHub 88397b7945 [1.494.*] Pre-release merge (#1089) 2026-03-04 14:30:09 +00:00
tramline-github[bot]andGitHub f0cb29e635 [1.493.*] Pre-release merge (#1088) 2026-03-03 14:30:08 +00:00
tramline-github[bot]andGitHub 97fe449e8f [1.492.*] Pre-release merge (#1087) 2026-03-02 14:30:10 +00:00
tramline-github[bot]andGitHub 47d87575ef [1.491.*] Pre-release merge (#1086) 2026-02-28 14:30:08 +00:00
tramline-github[bot]andGitHub 93329a9ad5 [1.490.*] Pre-release merge (#1085) 2026-02-27 14:30:08 +00:00
tramline-github[bot]andGitHub 9b5f463972 [1.489.*] Pre-release merge (#1084) 2026-02-26 14:30:12 +00:00
tramline-github[bot]andGitHub 98fe6dbddd [1.488.*] Pre-release merge (#1082) 2026-02-25 14:30:10 +00:00
tramline-github[bot]andGitHub a67c29403a [1.487.*] Pre-release merge (#1081) 2026-02-24 19:13:29 +00:00
tramline-github[bot]andGitHub 5d8ab9d216 [1.486.*] Pre-release merge (#1080) 2026-02-24 14:30:08 +00:00
tramline-github[bot]andGitHub 8877885906 [1.485.*] Pre-release merge (#1079) 2026-02-22 14:30:06 +00:00
tramline-github[bot]andGitHub 27c4dde264 [1.484.*] Pre-release merge (#1078) 2026-02-20 14:30:08 +00:00
tramline-github[bot]andGitHub 171a29c7b9 [1.483.*] Pre-release merge (#1077) 2026-02-19 14:30:13 +00:00
tramline-github[bot]andGitHub dec3967dc8 [1.482.*] Pre-release merge (#1076) 2026-02-18 14:30:06 +00:00
tramline-github[bot]andGitHub ea72ce7fb7 [1.481.*] Pre-release merge (#1075) 2026-02-16 14:30:08 +00:00
tramline-github[bot]andGitHub a72cb847be [1.480.*] Pre-release merge (#1074) 2026-02-15 14:30:06 +00:00
tramline-github[bot]andGitHub 51d7b414ce [1.479.*] Pre-release merge (#1073) 2026-02-14 14:30:04 +00:00
tramline-github[bot]andGitHub 273d9fdcb5 [1.478.*] Pre-release merge (#1072) 2026-02-13 14:30:06 +00:00
tramline-github[bot]andGitHub 144a7f1023 [1.477.*] Pre-release merge (#1071) 2026-02-12 14:30:09 +00:00
tramline-github[bot]andGitHub edcde61607 [1.476.*] Pre-release merge (#1070) 2026-02-11 14:30:08 +00:00
tramline-github[bot]andGitHub eb3c216166 [1.475.*] Pre-release merge (#1068) 2026-02-10 14:30:10 +00:00
tramline-github[bot]andGitHub dbd1fc0dc3 [1.474.*] Pre-release merge (#1065) 2026-02-08 14:30:06 +00:00
tramline-github[bot]andGitHub 2a3f0b7a64 [1.473.*] Pre-release merge (#1064) 2026-02-06 14:30:07 +00:00
tramline-github[bot]andGitHub efcc0f3b9b [1.472.*] Pre-release merge (#1063) 2026-02-05 14:30:11 +00:00
tramline-github[bot]andGitHub 7bcff6ac9c [1.471.*] Pre-release merge (#1062) 2026-02-03 14:30:07 +00:00
tramline-github[bot]andGitHub 72d2f3f0a9 [1.470.*] Pre-release merge (#1061) 2026-02-01 10:13:04 +00:00
tramline-github[bot]andGitHub 9c1e8e93eb [1.469.*] Pre-release merge (#1058) 2026-01-31 14:30:05 +00:00
tramline-github[bot]andGitHub 4c4c6f3e5c [1.468.*] Pre-release merge (#1057) 2026-01-30 14:30:06 +00:00
tramline-github[bot]andGitHub 8ce684322c [1.467.*] Pre-release merge (#1056) 2026-01-29 14:30:05 +00:00
tramline-github[bot]andGitHub dd29e4488c [1.466.*] Pre-release merge (#1055) 2026-01-28 14:30:06 +00:00
tramline-github[bot]andGitHub 0d2722a9de [1.465.*] Pre-release merge (#1054) 2026-01-27 14:30:09 +00:00
tramline-github[bot]andGitHub d1a39fcd69 [1.464.*] Pre-release merge (#1053) 2026-01-25 14:30:09 +00:00
tramline-github[bot]andGitHub f0e943a689 [1.463.*] Pre-release merge (#1052) 2026-01-24 14:30:06 +00:00
tramline-github[bot]andGitHub d398fc4a1e [1.462.*] Pre-release merge (#1051) 2026-01-23 14:30:04 +00:00
tramline-github[bot]andGitHub edf37a79e1 [1.461.*] Pre-release merge (#1050) 2026-01-22 14:30:07 +00:00
tramline-github[bot]andGitHub fac555f7ed [1.460.*] Pre-release merge (#1049) 2026-01-20 14:30:11 +00:00
tramline-github[bot]andGitHub 5e36be034a [1.460.*] Pre-release merge (#1048) 2026-01-19 14:30:06 +00:00
tramline-github[bot]andGitHub 65400697d2 [1.459.*] Pre-release merge (#1046) 2026-01-18 14:30:09 +00:00
tramline-github[bot]andGitHub 37836b0116 [1.458.*] Pre-release merge (#1044) 2026-01-17 14:30:04 +00:00
tramline-github[bot]andGitHub 50bf21802d [1.457.*] Pre-release merge (#1043) 2026-01-16 14:30:08 +00:00
tramline-github[bot]andGitHub b5a379b165 [1.456.*] Pre-release merge (#1042) 2026-01-15 14:30:06 +00:00
tramline-github[bot]andGitHub e7579bbcf5 [1.455.*] Pre-release merge (#1040) 2026-01-14 14:30:11 +00:00
tramline-github[bot]andGitHub f47bc09cd6 [1.454.*] Pre-release merge (#1039) 2026-01-13 14:30:08 +00:00
tramline-github[bot]andGitHub 38def3d71f [1.453.*] Pre-release merge (#1038) 2026-01-12 16:07:54 +00:00
tramline-github[bot]andGitHub 20c447aefc [1.452.*] Pre-release merge (#1037) 2026-01-11 14:30:05 +00:00
tramline-github[bot]andGitHub f17ca7c077 [1.451.*] Pre-release merge (#1036) 2026-01-10 14:30:05 +00:00
tramline-github[bot]andGitHub d573c8b01a [1.450.*] Pre-release merge (#1035) 2026-01-09 14:30:05 +00:00
tramline-github[bot]andGitHub 105e658681 [1.449.*] Pre-release merge (#1034) 2026-01-08 14:30:07 +00:00
tramline-github[bot]andGitHub 2b25607d2a [1.448.*] Pre-release merge (#1033) 2026-01-07 14:30:07 +00:00
tramline-github[bot]andGitHub c9b572f487 [1.447.*] Pre-release merge (#1032) 2026-01-06 17:59:26 +00:00
tramline-github[bot]andGitHub fc0d0a38fa [1.446.*] Pre-release merge (#1031) 2026-01-06 16:07:11 +00:00
tramline-github[bot]andGitHub 88252301aa [1.446.*] Pre-release merge (#1030) 2026-01-06 14:30:06 +00:00
tramline-github[bot]andGitHub 290d41fd2b [1.445.*] Pre-release merge (#1029) 2026-01-05 14:30:06 +00:00
tramline-github[bot]andGitHub 22b2b4e211 [1.444.*] Pre-release merge (#1028) 2026-01-04 14:30:10 +00:00
tramline-github[bot]andGitHub 73a7261893 [1.443.*] Pre-release merge (#1027) 2026-01-04 07:52:43 +00:00
tramline-github[bot]andGitHub 22ea6df4c0 [1.443.*] Pre-release merge (#1026) 2026-01-04 07:47:02 +00:00
tramline-github[bot]andGitHub 269c28c7f9 [1.443.*] Pre-release merge (#1025) 2026-01-04 07:41:15 +00:00
tramline-github[bot]andGitHub c316e55b14 [1.443.*] Pre-release merge (#1024) 2026-01-04 07:36:26 +00:00
tramline-github[bot]andGitHub f2a0ab45ee [1.443.*] Pre-release merge (#1022) 2026-01-03 17:15:13 +00:00
tramline-github[bot]andGitHub b24ddcb0b0 [1.442.*] Pre-release merge (#1021) 2026-01-03 14:30:05 +00:00
tramline-github[bot]andGitHub bc3c7a3c48 [1.441.*] Pre-release merge (#1020) 2026-01-02 15:06:49 +00:00
tramline-github[bot]andGitHub e4cd3add92 [1.441.*] Pre-release merge (#1019) 2026-01-01 14:30:06 +00:00
tramline-github[bot]andGitHub 2e4fe65e4d [1.441.*] Pre-release merge (#1018) 2025-12-31 14:30:07 +00:00
tramline-github[bot]andGitHub 212c1a2a2a [1.440.*] Pre-release merge (#1017) 2025-12-30 18:11:49 +00:00
tramline-github[bot]andGitHub 8bcc5686ed [1.439.*] Pre-release merge (#1016) 2025-12-30 14:30:07 +00:00
tramline-github[bot]andGitHub 5efcdf40a6 [1.438.*] Pre-release merge (#1015) 2025-12-29 14:30:06 +00:00
tramline-github[bot]andGitHub d429a9ddaf [1.437.*] Pre-release merge (#1014) 2025-12-28 14:30:11 +00:00
tramline-github[bot]andGitHub b03809196c [1.436.*] Pre-release merge (#1013) 2025-12-27 21:03:37 +00:00
tramline-github[bot]andGitHub 5c6bca960a [1.435.*] Pre-release merge (#1012) 2025-12-27 14:30:06 +00:00
tramline-github[bot]andGitHub 3dfdb8f4fa [1.434.*] Pre-release merge (#1010) 2025-12-26 17:26:27 +00:00
tramline-github[bot]andGitHub 63a977965e [1.433.*] Pre-release merge (#1009) 2025-12-26 14:30:04 +00:00
tramline-github[bot]andGitHub a0b5521051 [1.432.*] Pre-release merge (#1007) 2025-12-25 14:30:06 +00:00
tramline-github[bot]andGitHub 802336ac0d [1.431.*] Pre-release merge (#1004) 2025-12-25 08:28:17 +00:00
tramline-github[bot]andGitHub 634773d922 [1.430.*] Pre-release merge (#1002) 2025-12-22 14:30:06 +00:00
tramline-github[bot]andGitHub 4cc9b28c7d [1.429.*] Pre-release merge (#1001) 2025-12-20 14:30:08 +00:00
tramline-github[bot]andGitHub 1b997a76e5 [1.428.*] Pre-release merge (#999) 2025-12-18 14:30:08 +00:00
tramline-github[bot]andGitHub 61e090d912 [1.427.*] Pre-release merge (#996) 2025-12-16 14:30:05 +00:00
tramline-github[bot]andGitHub f648d57b3e [1.426.*] Pre-release merge (#995) 2025-12-13 14:30:09 +00:00
tramline-github[bot]andGitHub 8019976d84 [1.425.*] Pre-release merge (#994) 2025-12-11 14:30:09 +00:00
tramline-github[bot]andGitHub 64689eb34b [1.424.*] Pre-release merge (#993) 2025-12-10 14:30:10 +00:00
tramline-github[bot]andGitHub f9aecea920 [1.423.*] Pre-release merge (#992) 2025-12-08 14:30:09 +00:00
tramline-github[bot]andGitHub d08107c146 [1.422.*] Pre-release merge (#990) 2025-11-18 14:30:07 +00:00
tramline-github[bot]andGitHub a4c6c576b3 [1.421.*] Pre-release merge (#989) 2025-11-17 14:30:09 +00:00
tramline-github[bot]andGitHub 1b8a30c528 [1.420.*] Pre-release merge (#988) 2025-11-14 14:30:10 +00:00
tramline-github[bot]andGitHub 3354dbee98 [1.419.*] Pre-release merge (#987) 2025-11-12 14:30:07 +00:00
tramline-github[bot]andGitHub 407e11eb53 [1.418.*] Pre-release merge (#986) 2025-11-11 14:30:05 +00:00
tramline-github[bot]andGitHub 4e6ce33cf7 [1.417.*] Pre-release merge (#985) 2025-11-07 14:30:11 +00:00
tramline-github[bot]andGitHub 0222e78f9f [1.416.*] Pre-release merge (#984) 2025-11-06 14:30:07 +00:00
tramline-github[bot]andGitHub e56c5c55a2 [1.415.*] Pre-release merge (#983) 2025-11-04 14:30:09 +00:00
tramline-github[bot]andGitHub e7dbb56e34 [1.414.*] Pre-release merge (#982) 2025-11-01 14:30:07 +00:00
tramline-github[bot]andGitHub b00dbd292c [1.413.*] Pre-release merge (#980) 2025-10-31 14:30:09 +00:00
tramline-github[bot]andGitHub 2824c0a472 [1.412.*] Pre-release merge (#979) 2025-10-30 14:30:12 +00:00
tramline-github[bot]andGitHub 2d6e03ea76 [1.411.*] Pre-release merge (#978) 2025-10-29 14:30:08 +00:00
tramline-github[bot]andGitHub 3bfe515a09 [1.410.*] Pre-release merge (#977) 2025-10-27 21:04:34 +00:00
tramline-github[bot]andGitHub 5e8979ff6e [1.409.*] Pre-release merge (#976) 2025-10-25 14:30:05 +00:00
tramline-github[bot]andGitHub 44d85e82dc [1.408.*] Pre-release merge (#975) 2025-10-24 14:30:06 +00:00
tramline-github[bot]andGitHub 25a8e86002 [1.407.*] Pre-release merge (#974) 2025-10-23 14:30:08 +00:00
tramline-github[bot]andGitHub 8d18902d31 [1.406.*] Pre-release merge (#973) 2025-10-21 14:30:07 +00:00
tramline-github[bot]andGitHub 1883455722 [1.405.*] Pre-release merge (#972) 2025-10-20 14:30:11 +00:00
tramline-github[bot]andGitHub 2d2a6fec7c [1.404.*] Pre-release merge (#968) 2025-10-17 14:30:07 +00:00
tramline-github[bot]andGitHub 91c58d54e5 [1.403.*] Pre-release merge (#966) 2025-10-14 14:30:07 +00:00
tramline-github[bot]andGitHub adb7632d61 [1.402.*] Pre-release merge (#965) 2025-10-11 14:30:06 +00:00
tramline-github[bot]andGitHub 6676429dcd [1.401.*] Pre-release merge (#963) 2025-10-10 14:30:10 +00:00
tramline-github[bot]andGitHub 51fc911f39 [1.400.*] Pre-release merge (#962) 2025-10-09 14:30:07 +00:00
tramline-github[bot]andGitHub 3ed7615868 [1.399.*] Pre-release merge (#959) 2025-10-08 14:30:11 +00:00
tramline-github[bot]andGitHub 91869b1af3 [1.398.*] Pre-release merge (#958) 2025-10-06 14:30:05 +00:00
tramline-github[bot]andGitHub fbd28d3be6 [1.397.*] Pre-release merge (#957) 2025-10-05 14:30:05 +00:00
tramline-github[bot]andGitHub e32e29ddb3 [1.396.*] Pre-release merge (#956) 2025-10-02 14:30:08 +00:00
tramline-github[bot]andGitHub 91575f9e0a [1.395.*] Pre-release merge (#955) 2025-10-01 15:21:35 +00:00
tramline-github[bot]andGitHub 1931730abb [1.394.*] Pre-release merge (#954) 2025-10-01 14:30:07 +00:00
tramline-github[bot]andGitHub 7f62039cd4 [1.393.*] Pre-release merge (#953) 2025-09-30 14:30:10 +00:00
tramline-github[bot]andGitHub ad4784e025 [1.392.*] Pre-release merge (#952) 2025-09-29 14:30:09 +00:00
tramline-github[bot]andGitHub 4f908a932d [1.391.*] Pre-release merge (#951) 2025-09-26 14:30:07 +00:00
tramline-github[bot]andGitHub 26029e9cf0 [1.390.*] Pre-release merge (#950) 2025-09-25 20:00:35 +00:00
tramline-github[bot]andGitHub 6126818cbf [1.389.*] Pre-release merge (#949) 2025-09-25 14:30:08 +00:00
tramline-github[bot]andGitHub d41f343735 [1.388.*] Pre-release merge (#947) 2025-09-24 14:30:11 +00:00
tramline-github[bot]andGitHub fac6212e2d [1.387.*] Pre-release merge (#946) 2025-09-23 14:30:07 +00:00
tramline-github[bot]andGitHub 1403c341cd [1.386.*] Pre-release merge (#938) 2025-09-19 14:30:12 +00:00
tramline-github[bot]andGitHub a8cda2ca4b [1.385.*] Pre-release merge (#937) 2025-09-18 14:30:07 +00:00
tramline-github[bot]andGitHub 6f79d3e03a [1.384.*] Pre-release merge (#936) 2025-09-17 14:30:13 +00:00
tramline-github[bot]andGitHub 9a15249817 [1.383.*] Pre-release merge (#935) 2025-09-15 14:30:08 +00:00
tramline-github[bot]andGitHub 627923223e [1.382.*] Pre-release merge (#934) 2025-09-13 14:30:06 +00:00
tramline-github[bot]andGitHub 5aa9ff68bf [1.381.*] Pre-release merge (#933) 2025-09-12 14:30:06 +00:00
tramline-github[bot]andGitHub 5d35d83d6d [1.380.*] Pre-release merge (#932) 2025-09-11 14:30:09 +00:00
tramline-github[bot]andGitHub 190bfb4406 [1.379.*] Pre-release merge (#931) 2025-09-10 14:30:11 +00:00
tramline-github[bot]andGitHub 1190e9ee72 [1.378.*] Pre-release merge (#930) 2025-09-09 14:30:09 +00:00
tramline-github[bot]andGitHub c33e6b34bd [1.377.*] Pre-release merge (#928) 2025-09-03 14:30:07 +00:00
tramline-github[bot]andGitHub 05066bd3f7 [1.376.*] Pre-release merge (#927) 2025-08-30 14:30:07 +00:00
tramline-github[bot]andGitHub ed87e36942 [1.375.*] Pre-release merge (#924) 2025-08-28 20:37:11 +00:00
tramline-github[bot]andGitHub 36faba6353 [1.374.*] Pre-release merge (#923) 2025-08-28 14:30:09 +00:00
tramline-github[bot]andGitHub cdcb39c573 [1.373.*] Pre-release merge (#922) 2025-08-27 14:30:10 +00:00
tramline-github[bot]andGitHub 7cbf862a90 [1.372.*] Pre-release merge (#921) 2025-08-26 14:30:07 +00:00
tramline-github[bot]andGitHub e0fd165f8a [1.371.*] Pre-release merge (#919) 2025-08-25 14:48:16 +00:00
tramline-github[bot]andGitHub 2a572d70f1 [1.371.*] Pre-release merge (#916) 2025-08-14 14:30:07 +00:00
tramline-github[bot]andGitHub 73c6902c1d [1.370.*] Pre-release merge (#915) 2025-08-13 14:30:11 +00:00
tramline-github[bot]andGitHub 63de869fac [1.369.*] Pre-release merge (#914) 2025-08-12 14:30:07 +00:00
tramline-github[bot]andGitHub ff73293d8c [1.368.*] Pre-release merge (#913) 2025-08-07 14:30:06 +00:00
tramline-github[bot]andGitHub 696bc49b4e [1.367.*] Pre-release merge (#912) 2025-08-06 14:30:10 +00:00
tramline-github[bot]andGitHub eb100961fa [1.367.*] Pre-release merge (#911) 2025-08-05 14:30:09 +00:00
tramline-github[bot]andGitHub 5742c707e6 [1.366.*] Pre-release merge (#910) 2025-08-04 14:30:06 +00:00
tramline-github[bot]andGitHub f651f85b14 [1.365.*] Pre-release merge (#909) 2025-08-02 14:30:09 +00:00
tramline-github[bot]andGitHub 03d6834e17 [1.364.*] Pre-release merge (#908) 2025-07-26 14:30:07 +00:00
tramline-github[bot]andGitHub 7f2d5a7fcd [1.363.*] Pre-release merge (#907) 2025-07-23 14:30:06 +00:00
tramline-github[bot]andGitHub a8e0c7a98c [1.362.*] Pre-release merge (#906) 2025-07-22 09:36:36 +00:00
tramline-github[bot]andGitHub 9613533630 [1.361.*] Pre-release merge (#905) 2025-07-21 14:30:08 +00:00
tramline-github[bot]andGitHub 2eee7edb01 [1.360.*] Pre-release merge (#902) 2025-07-19 14:30:06 +00:00
tramline-github[bot]andGitHub 7c2d1d6144 [1.359.*] Pre-release merge (#896) 2025-07-18 14:30:04 +00:00
tramline-github[bot]andGitHub 77d586a3ec [1.358.*] Pre-release merge (#895) 2025-07-17 14:30:09 +00:00
tramline-github[bot]andGitHub 2bcdca2647 [1.357.*] Pre-release merge (#894) 2025-07-16 14:30:06 +00:00
tramline-github[bot]andGitHub 29b778066d [1.356.*] Pre-release merge (#893) 2025-07-15 14:30:06 +00:00
tramline-github[bot]andGitHub 3c54c53ef2 [1.355.*] Pre-release merge (#891) 2025-07-11 14:30:09 +00:00
tramline-github[bot]andGitHub 175d9211e5 [1.354.*] Pre-release merge (#890) 2025-07-09 14:30:06 +00:00
tramline-github[bot]andGitHub 228ca6f683 [1.353.*] Pre-release merge (#889) 2025-07-08 14:30:08 +00:00
tramline-github[bot]andGitHub e4666ac2ce [1.352.*] Pre-release merge (#888) 2025-07-07 14:30:10 +00:00
tramline-github[bot]andGitHub ed02ee00d0 [1.351.*] Pre-release merge (#887) 2025-07-06 14:30:07 +00:00
tramline-github[bot]andGitHub b1c9a9c8d0 [1.350.*] Pre-release merge (#886) 2025-07-05 14:30:04 +00:00
tramline-github[bot]andGitHub 28969b9aa0 [1.349.*] Pre-release merge (#885) 2025-07-04 14:30:04 +00:00
tramline-github[bot]andGitHub fdb612709a [1.348.*] Pre-release merge (#884) 2025-07-03 14:30:09 +00:00
tramline-github[bot]andGitHub 2b12bf7797 [1.347.*] Pre-release merge (#883) 2025-07-02 14:30:09 +00:00
tramline-github[bot]andGitHub ffe9b5a9ac [1.346.*] Pre-release merge (#881) 2025-06-29 14:30:08 +00:00
tramline-github[bot]andGitHub 0804571496 [1.345.*] Pre-release merge (#880) 2025-06-28 14:30:03 +00:00
tramline-github[bot]andGitHub 6bb12586d9 [1.344.*] Pre-release merge (#879) 2025-06-27 14:30:07 +00:00
tramline-github[bot]andGitHub ec28df5242 [1.344.*] Pre-release merge (#875) 2025-06-26 14:30:05 +00:00
tramline-github[bot]andGitHub 83a9b91090 [1.343.*] Pre-release merge (#874) 2025-06-25 14:30:08 +00:00
tramline-github[bot]andGitHub 6b961dd7b2 [1.342.*] Pre-release merge (#873) 2025-06-24 14:30:08 +00:00
tramline-github[bot]andGitHub 8d5d875faf [1.341.*] Pre-release merge (#871) 2025-06-23 14:30:10 +00:00
tramline-github[bot]andGitHub 5995bfcb27 [1.340.*] Pre-release merge (#870) 2025-06-21 14:30:10 +00:00
tramline-github[bot]andGitHub 5d9c489301 [1.339.*] Pre-release merge (#868) 2025-06-19 14:30:09 +00:00
tramline-github[bot]andGitHub d52c928f93 [1.338.*] Pre-release merge (#867) 2025-06-18 14:30:08 +00:00
tramline-github[bot]andGitHub 40804c8362 [1.337.*] Pre-release merge (#866) 2025-06-17 14:30:07 +00:00
tramline-github[bot]andGitHub 067716b22d [1.336.*] Pre-release merge (#865) 2025-06-16 14:30:10 +00:00
tramline-github[bot]andGitHub 95aa846a43 [1.335.*] Pre-release merge (#864) 2025-06-14 14:30:09 +00:00
tramline-github[bot]andGitHub 50a82e5396 [1.334.*] Pre-release merge (#862) 2025-06-13 14:30:11 +00:00
tramline-github[bot]andGitHub 8b51633e2b [1.333.*] Pre-release merge (#860) 2025-06-12 14:30:09 +00:00
tramline-github[bot]andGitHub 61f956b873 [1.332.*] Pre-release merge (#857) 2025-06-11 14:30:06 +00:00
Harsh ShandilyaandGitHub 2d10be3bd7 [1.331.*] Pre-release merge (#855) 2025-06-10 20:31:40 +05:30
tramline-github[bot]andGitHub 2f18a9b424 [1.330.*] Pre-release merge (#854) 2025-06-07 14:30:07 +00:00
Harsh Shandilya 650eb986aa Merge branch 'main' into nightly
* main:
  fix(deps): update agp to v8.12.0-alpha04 (#851)
  fix: remove benchmark project
  refactor: move datastore module to `common`
  fix: improve TagList UI
  fix(deps): update haze to v1.6.4
  fix(deps): update aboutlibraries to v12.2.1
  refactor(core): reorganize to feature packages
  chore(deps): update dependency gradle to v8.14.2
  common: add TagList
  feat: add get tags endpoint
  fix: upgrade AndroidX snapshot
2025-06-07 17:53:03 +05:30
tramline-github[bot]andGitHub 5c85a97735 [1.328.*] Pre-release merge (#850) 2025-06-06 14:30:09 +00:00
renovate[bot]andHarsh Shandilya d8d62279f4 chore(deps): update dependency gradle to v8.14.2 2025-06-06 00:15:39 +05:30
Harsh Shandilya c08ca156fe common: add TagList 2025-06-06 00:12:29 +05:30
Harsh Shandilya fb1b1d23ec feat: add get tags endpoint 2025-06-06 00:12:29 +05:30
tramline-github[bot]andGitHub f17ea7cac2 [1.327.*] Pre-release merge (#849) 2025-06-05 14:30:07 +00:00
Harsh Shandilya 58ed13cfa8 fix: upgrade AndroidX snapshot 2025-06-05 18:37:37 +05:30
tramline-github[bot]andGitHub 99fa3f4de3 [1.326.*] Pre-release merge (#848) 2025-06-04 14:30:07 +00:00
tramline-github[bot]andGitHub b86d3a105b [1.325.*] Pre-release merge (#846) 2025-06-02 20:00:07 +00:00
tramline-github[bot]andGitHub b07357e585 [1.324.*] Pre-release merge (#844) 2025-06-02 16:37:54 +00:00
tramline-github[bot]andGitHub c0fbbd3800 [1.323.*] Pre-release merge (#843) 2025-06-01 14:30:09 +00:00
tramline-github[bot]andGitHub f1f06adb76 [1.322.*] Pre-release merge (#842) 2025-06-01 09:31:27 +00:00
tramline-github[bot]andGitHub fe4dee3efb [1.321.*] Pre-release merge (#840) 2025-05-30 14:30:09 +00:00
tramline-github[bot]andGitHub 2d99dc6aac [1.320.*] Pre-release merge (#839) 2025-05-29 20:06:23 +00:00
tramline-github[bot]andGitHub 6a8f181928 [1.319.*] Pre-release merge (#838) 2025-05-29 14:30:09 +00:00
tramline-github[bot]andGitHub a02760a585 [1.318.*] Pre-release merge (#837) 2025-05-28 14:30:07 +00:00
tramline-github[bot]andGitHub b23878882d [1.317.*] Pre-release merge (#836) 2025-05-27 18:13:42 +00:00
tramline-github[bot]andGitHub 68472933b5 [1.316.*] Pre-release merge (#832) 2025-05-25 17:45:27 +00:00
tramline-github[bot]andGitHub d9fad8af46 [1.315.*] Pre-release merge (#831) 2025-05-25 14:30:13 +00:00
tramline-github[bot]andGitHub e916282ccc [1.314.*] Pre-release merge (#829) 2025-05-25 04:10:53 +00:00
tramline-github[bot]andGitHub 164790eac4 [1.313.*] Pre-release merge (#828) 2025-05-24 18:22:17 +00:00
tramline-github[bot]andGitHub 7bb2dc5fde [1.312.*] Pre-release merge (#827) 2025-05-22 19:18:10 +00:00
tramline-github[bot]andGitHub 5c225799e6 [1.312.*] Pre-release merge (#814) 2025-04-01 14:30:05 +00:00
tramline-github[bot]andGitHub a76a2da7f1 [1.311.*] Pre-release merge (#813) 2025-03-29 14:30:07 +00:00
tramline-github[bot]andGitHub e1fd6d401e [1.310.*] Pre-release merge (#811) 2025-03-28 14:30:07 +00:00
tramline-github[bot]andGitHub c7297624da [1.309.*] Pre-release merge (#810) 2025-03-27 15:49:30 +00:00
tramline-github[bot]andGitHub 40323e2be9 [1.308.*] Pre-release merge (#808) 2025-03-26 14:30:16 +00:00
tramline-github[bot]andGitHub 4cba61bf74 [1.307.*] Pre-release merge (#805) 2025-03-25 14:30:10 +00:00
tramline-github[bot]andGitHub 63821b49cd [1.306.*] Pre-release merge (#803) 2025-03-24 14:30:08 +00:00
tramline-github[bot]andGitHub 9f6fddb4bf [1.305.*] Pre-release merge (#802) 2025-03-21 14:30:06 +00:00
tramline-github[bot]andGitHub dfb5897120 [1.304.*] Pre-release merge (#800) 2025-03-20 14:30:08 +00:00
tramline-github[bot]andGitHub 8f5a0844ac [1.303.*] Pre-release merge (#798) 2025-03-19 14:30:09 +00:00
Harsh ShandilyaandGitHub 116805f5b5 [1.302.*] Pre-release merge (#796) 2025-03-15 00:00:56 +05:30
tramline-github[bot]andGitHub 8fa4cc4559 [1.301.*] Pre-release merge (#795) 2025-03-13 14:30:10 +00:00
tramline-github[bot]andGitHub c5937a607e [1.300.*] Pre-release merge (#794) 2025-03-12 14:30:07 +00:00
tramline-github[bot]andGitHub 3fed87483c [1.299.*] Pre-release merge (#793) 2025-03-11 14:30:11 +00:00
tramline-github[bot]andGitHub 35aae09c8d [1.298.*] Pre-release merge (#792) 2025-03-07 14:30:09 +00:00
Harsh ShandilyaandGitHub 657a92ea7f [1.297.*] Pre-release merge (#791) 2025-03-06 20:47:07 +05:30
tramline-github[bot]andGitHub 5c9a7549f3 [1.296.*] Pre-release merge (#788) 2025-03-03 14:30:15 +00:00
tramline-github[bot]andGitHub 2d0a3cb27b [1.295.*] Pre-release merge (#787) 2025-03-01 14:30:05 +00:00
tramline-github[bot]andGitHub 064061bea8 [1.294.*] Pre-release merge (#786) 2025-02-28 14:30:07 +00:00
tramline-github[bot]andGitHub ee60b7897f [1.293.*] Pre-release merge (#785) 2025-02-27 14:30:10 +00:00
tramline-github[bot]andGitHub a6b91eea43 [1.292.*] Pre-release merge (#784) 2025-02-26 14:30:07 +00:00
tramline-github[bot]andGitHub 2016335f04 [1.291.*] Pre-release merge (#783) 2025-02-22 14:30:05 +00:00
tramline-github[bot]andGitHub 0cb61451d1 [1.290.*] Pre-release merge (#781) 2025-02-21 14:30:10 +00:00
tramline-github[bot]andGitHub a1e16fb7c7 [1.289.*] Pre-release merge (#780) 2025-02-19 14:30:05 +00:00
tramline-github[bot]andGitHub 9e29d45d24 [1.288.*] Pre-release merge (#779) 2025-02-16 14:30:08 +00:00
tramline-github[bot]andGitHub 707f02e062 [1.287.*] Pre-release merge (#778) 2025-02-15 14:30:07 +00:00
tramline-github[bot]andGitHub 7e0e0643c6 [1.286.*] Pre-release merge (#777) 2025-02-14 14:30:11 +00:00
tramline-github[bot]andGitHub 4687b0239c [1.285.*] Pre-release merge (#776) 2025-02-13 22:06:07 +00:00
tramline-github[bot]andGitHub a7a51124e7 [1.284.*] Pre-release merge (#774) 2025-02-13 14:30:08 +00:00
tramline-github[bot]andGitHub b4bebee06c [1.283.*] Pre-release merge (#771) 2025-01-30 14:30:15 +00:00
tramline-github[bot]andGitHub c2714c4e6a [1.282.*] Pre-release merge (#770) 2025-01-29 14:30:09 +00:00
tramline-github[bot]andGitHub a96689b30e [1.281.*] Pre-release merge (#769) 2025-01-28 14:30:08 +00:00
tramline-github[bot]andGitHub 9c617455ea [1.280.*] Pre-release merge (#768) 2025-01-27 14:30:07 +00:00
tramline-github[bot]andGitHub da8ea646c9 [1.279.*] Pre-release merge (#767) 2025-01-25 14:30:08 +00:00
tramline-github[bot]andGitHub c02e324cb0 [1.278.*] Pre-release merge (#766) 2025-01-24 14:30:07 +00:00
tramline-github[bot]andGitHub 65b0429de3 [1.277.*] Pre-release merge (#764) 2025-01-22 14:30:07 +00:00
tramline-github[bot]andGitHub 3257a3f242 [1.276.*] Pre-release merge (#763) 2025-01-21 14:30:09 +00:00
tramline-github[bot]andGitHub a75aadea6b [1.275.*] Pre-release merge (#762) 2025-01-19 14:30:06 +00:00
tramline-github[bot]andGitHub cab246beb5 [1.274.*] Pre-release merge (#761) 2025-01-17 14:30:07 +00:00
tramline-github[bot]andGitHub a7bb1d08b5 [1.273.*] Pre-release merge (#760) 2025-01-16 14:30:13 +00:00
tramline-github[bot]andGitHub bae72e09cd [1.272.*] Pre-release merge (#758) 2025-01-15 14:30:09 +00:00
tramline-github[bot]andGitHub ad84feff70 [1.271.*] Pre-release merge (#757) 2025-01-14 14:30:06 +00:00
tramline-github[bot]andGitHub e778730456 [1.270.*] Pre-release merge (#755) 2025-01-11 14:30:10 +00:00
Harsh ShandilyaandGitHub c5d4693ede [1.269.*] Pre-release merge (#753) 2025-01-10 20:02:23 +05:30
tramline-github[bot]andGitHub 60c0e34fce [1.268.*] Pre-release merge (#751) 2025-01-09 14:30:05 +00:00
Harsh ShandilyaandGitHub 58b44d21b7 [1.267.*] Pre-release merge (#750) 2025-01-07 20:01:27 +05:30
Harsh ShandilyaandGitHub 57560a3a37 [1.266.*] Pre-release merge (#749) 2025-01-04 21:06:45 +05:30
Harsh ShandilyaandGitHub 363c5ec27f [1.265.*] Pre-release merge (#748) 2025-01-04 20:27:10 +05:30
Harsh ShandilyaandGitHub 25ab8d1b44 [1.265.*] Pre-release merge (#747) 2025-01-03 21:50:34 +05:30
Harsh ShandilyaandGitHub b9ec58271c [1.264.*] Pre-release merge (#745) 2025-01-03 04:08:14 +05:30
tramline-github[bot]andGitHub d763d0f47d [1.263.*] Pre-release merge (#744) 2025-01-01 14:30:06 +00:00
tramline-github[bot]andGitHub 5e0cce8e62 [1.262.*] Pre-release merge (#743) 2024-12-28 14:30:05 +00:00
tramline-github[bot]andGitHub 469a99f315 [1.261.*] Pre-release merge (#742) 2024-12-24 14:30:07 +00:00
tramline-github[bot]andGitHub fc7c0be912 [1.260.*] Pre-release merge (#741) 2024-12-23 14:30:06 +00:00
tramline-github[bot]andGitHub 9ec4fd5be6 [1.259.*] Pre-release merge (#740) 2024-12-21 14:30:06 +00:00
tramline-github[bot]andGitHub eaf305f8dc [1.258.*] Pre-release merge (#738) 2024-12-20 14:30:07 +00:00
tramline-github[bot]andGitHub c2820c7fba [1.257.*] Pre-release merge (#737) 2024-12-18 14:30:12 +00:00
tramline-github[bot]andGitHub 4360bdbbe4 [1.256.*] Pre-release merge (#736) 2024-12-17 14:30:08 +00:00
tramline-github[bot]andGitHub 2b4621d2bd [1.255.*] Pre-release merge (#735) 2024-12-15 14:30:10 +00:00
Harsh ShandilyaandGitHub 0fc0ee30a9 [1.254.*] Pre-release merge (#734) 2024-12-14 20:36:36 +05:30
tramline-github[bot]andGitHub ad6f0910f3 [1.252.*] Pre-release merge (#730) 2024-12-05 14:30:07 +00:00
tramline-github[bot]andGitHub 9f70d374d5 [1.251.*] Pre-release merge (#729) 2024-12-04 14:30:05 +00:00
tramline-github[bot]andGitHub 78f3739fe9 [1.250.*] Pre-release merge (#728) 2024-12-03 14:30:06 +00:00
tramline-github[bot]andGitHub 0975582712 [1.249.*] Pre-release merge (#727) 2024-12-02 14:30:06 +00:00
tramline-github[bot]andGitHub 9d9c27d98a [1.248.*] Pre-release merge (#725) 2024-11-30 14:30:08 +00:00
tramline-github[bot]andGitHub 35a7ca1463 [1.247.*] Pre-release merge (#724) 2024-11-30 08:26:15 +00:00
tramline-github[bot]andGitHub d6990f141e [1.246.*] Pre-release merge (#722) 2024-11-29 14:30:05 +00:00
tramline-github[bot]andGitHub 57a906602e [1.245.*] Pre-release merge (#721) 2024-11-28 14:30:12 +00:00
tramline-github[bot]andGitHub 8bdba47ced [1.244.*] Pre-release merge (#720) 2024-11-27 14:30:08 +00:00
tramline-github[bot]andGitHub b80eb70bf1 [1.243.*] Pre-release merge (#719) 2024-11-26 14:30:08 +00:00
tramline-github[bot]andGitHub f86458964e [1.242.*] Pre-release merge (#718) 2024-11-21 14:30:06 +00:00
tramline-github[bot]andGitHub af09c64a18 [1.241.*] Pre-release merge (#717) 2024-11-19 14:30:07 +00:00
tramline-github[bot]andGitHub e98da42be8 [1.240.*] Pre-release merge (#716) 2024-11-18 14:30:11 +00:00
tramline-github[bot]andGitHub d12eb1da14 [1.239.*] Pre-release merge (#715) 2024-11-17 14:30:04 +00:00
tramline-github[bot]andGitHub dc5d7235fe [1.238.*] Pre-release merge (#713) 2024-11-16 14:30:07 +00:00
tramline-github[bot]andGitHub 4f31ef7910 [1.237.*] Pre-release merge (#712) 2024-11-15 14:30:08 +00:00
tramline-github[bot]andGitHub 213d78cf68 [1.236.*] Pre-release merge (#711) 2024-11-14 14:30:08 +00:00
tramline-github[bot]andGitHub 33aef32fd3 [1.235.*] Pre-release merge (#710) 2024-11-13 14:30:11 +00:00
tramline-github[bot]andGitHub 76ca5cdda4 [1.234.*] Pre-release merge (#709) 2024-11-12 14:30:12 +00:00
tramline-github[bot]andGitHub 79184253a8 [1.233.*] Pre-release merge (#708) 2024-11-10 14:30:09 +00:00
tramline-github[bot]andGitHub e8c86f670f [1.232.*] Pre-release merge (#707) 2024-11-08 14:30:07 +00:00
tramline-github[bot]andGitHub 600102e9c4 [1.231.*] Pre-release merge (#706) 2024-11-05 14:30:05 +00:00
tramline-github[bot]andGitHub 1c3a06744e [1.230.*] Pre-release merge (#703) 2024-11-04 14:30:08 +00:00
tramline-github[bot]andGitHub 0373177fc5 [1.229.*] Pre-release merge (#702) 2024-11-03 14:30:07 +00:00
tramline-github[bot]andGitHub 771940e2d2 [1.228.*] Pre-release merge (#701) 2024-11-02 14:30:07 +00:00
tramline-github[bot]andGitHub 34c2241c71 [1.227.*] Pre-release merge (#700) 2024-11-01 14:30:07 +00:00
tramline-github[bot]andGitHub 965233a007 [1.226.*] Pre-release merge (#699) 2024-10-31 14:30:11 +00:00
tramline-github[bot]andGitHub a709c14b02 [1.225.*] Pre-release merge (#698) 2024-10-29 14:30:06 +00:00
tramline-github[bot]andGitHub f4e9713beb [1.224.*] Pre-release merge (#697) 2024-10-28 14:30:07 +00:00
tramline-github[bot]andGitHub 93d2e8f7fd [1.224.*] Pre-release merge (#696) 2024-10-27 14:30:08 +00:00
tramline-github[bot]andGitHub 09439315cb [1.223.*] Pre-release merge (#695) 2024-10-25 14:30:06 +00:00
Harsh ShandilyaandGitHub c7e0866a52 [1.222.*] Pre-release merge (#694) 2024-10-25 01:36:41 +05:30
tramline-github[bot]andGitHub 018e8d43ee [1.221.*] Pre-release merge (#693) 2024-10-23 14:30:15 +00:00
tramline-github[bot]andGitHub 8f1e489da6 [1.220.*] Pre-release merge (#691) 2024-10-22 14:30:07 +00:00
tramline-github[bot]andGitHub 81858e9534 [1.219.*] Pre-release merge (#689) 2024-10-21 14:30:09 +00:00
tramline-github[bot]andGitHub 0e7d595430 [1.218.*] Pre-release merge (#688) 2024-10-19 14:30:05 +00:00
tramline-github[bot]andGitHub 6e3ba6fe5f [1.217.*] Pre-release merge (#685) 2024-10-17 14:30:06 +00:00
tramline-github[bot]andGitHub 01ce6d9506 [1.216.*] Pre-release merge (#684) 2024-10-16 14:30:08 +00:00
tramline-github[bot]andGitHub 5a1f9f1c65 [1.215.*] Pre-release merge (#683) 2024-10-15 14:30:06 +00:00
tramline-github[bot]andGitHub e994b0f93b [1.214.*] Pre-release merge (#682) 2024-10-12 14:30:05 +00:00
tramline-github[bot]andGitHub 424b88813b [1.213.*] Pre-release merge (#681) 2024-10-11 13:34:33 +00:00
tramline-github[bot]andGitHub e84d08a3bf [1.212.*] Pre-release merge (#677) 2024-10-08 14:30:09 +00:00
tramline-github[bot]andGitHub 79e874926e [1.211.*] Pre-release merge (#676) 2024-10-05 14:30:06 +00:00
tramline-github[bot]andGitHub dc965abf44 [1.210.*] Pre-release merge (#675) 2024-10-04 14:30:10 +00:00
tramline-github[bot]andGitHub 58c8376e46 [1.209.*] Pre-release merge (#672) 2024-10-03 14:30:11 +00:00
tramline-github[bot]andGitHub c2d9026162 [1.208.*] Pre-release merge (#667) 2024-10-01 14:30:05 +00:00
tramline-github[bot]andGitHub 89028bcda0 [1.207.*] Pre-release merge (#666) 2024-09-28 14:30:05 +00:00
tramline-github[bot]andGitHub 1bf67e1323 [1.206.*] Pre-release merge (#665) 2024-09-26 14:30:06 +00:00
tramline-github[bot]andGitHub 507973d39e [1.205.*] Pre-release merge (#664) 2024-09-25 14:30:05 +00:00
tramline-github[bot]andGitHub a228dd428b [1.204.*] Pre-release merge (#663) 2024-09-24 14:30:07 +00:00
tramline-github[bot]andGitHub 6209f8622e [1.203.*] Pre-release merge (#662) 2024-09-23 14:30:07 +00:00
tramline-github[bot]andGitHub f6ce0e5bd4 [1.202.*] Pre-release merge (#661) 2024-09-22 14:30:08 +00:00
tramline-github[bot]andGitHub 9865a46fbd [1.201.*] Pre-release merge (#660) 2024-09-20 14:30:09 +00:00
tramline-github[bot]andGitHub 1cbf310236 [1.200.*] Pre-release merge (#659) 2024-09-19 14:30:06 +00:00
tramline-github[bot]andGitHub d4ff6a8c6b [1.199.*] Pre-release merge (#658) 2024-09-18 14:30:09 +00:00
tramline-github[bot]andGitHub 33385e33d3 [1.198.*] Pre-release merge (#657) 2024-09-16 14:39:19 +00:00
tramline-github[bot]andGitHub 7aa5b6303d [1.198.*] Pre-release merge (#655) 2024-09-13 14:30:08 +00:00
tramline-github[bot]andGitHub 547773f7f1 [1.197.*] Pre-release merge (#654) 2024-09-11 14:30:05 +00:00
tramline-github[bot]andGitHub a57a5daf57 [1.196.*] Pre-release merge (#653) 2024-09-10 14:30:10 +00:00
tramline-github[bot]andGitHub d5c9294bcf [1.195.*] Pre-release merge (#652) 2024-09-09 14:30:10 +00:00
tramline-github[bot]andGitHub eb29fbfddb [1.194.*] Pre-release merge (#650) 2024-09-06 14:30:05 +00:00
tramline-github[bot]andGitHub 9450272e5e [1.193.*] Pre-release merge (#648) 2024-09-05 14:30:06 +00:00
tramline-github[bot]andGitHub 415640b5a2 [1.192.*] Pre-release merge (#647) 2024-09-02 14:30:06 +00:00
tramline-github[bot]andGitHub c3479c1573 [1.191.*] Pre-release merge (#646) 2024-08-30 14:30:09 +00:00
tramline-github[bot]andGitHub 345ec759ab [1.190.*] Pre-release merge (#645) 2024-08-29 14:30:10 +00:00
tramline-github[bot]andGitHub 3365080736 [1.189.*] Pre-release merge (#643) 2024-08-28 14:30:08 +00:00
tramline-github[bot]andGitHub 246a947ea7 [1.188.*] Pre-release merge (#640) 2024-08-27 14:30:06 +00:00
tramline-github[bot]andGitHub d9a208cc7a [1.187.*] Pre-release merge (#639) 2024-08-26 14:30:08 +00:00
tramline-github[bot]andGitHub 52ebab5ef6 [1.186.*] Pre-release merge (#638) 2024-08-24 14:30:07 +00:00
tramline-github[bot]andGitHub 76864994a0 [1.185.*] Pre-release merge (#637) 2024-08-23 14:30:05 +00:00
tramline-github[bot]andGitHub 7aedc673db [1.184.*] Pre-release merge (#636) 2024-08-22 14:30:05 +00:00
tramline-github[bot]andGitHub 41072d11b3 [1.183.*] Pre-release merge (#633) 2024-08-21 14:50:37 +00:00
tramline-github[bot]andGitHub 7a67749d1c [1.183.*] Pre-release merge (#632) 2024-08-09 14:30:11 +00:00
tramline-github[bot]andGitHub eac9f77a73 [1.182.*] Pre-release merge (#631) 2024-08-08 14:30:05 +00:00
tramline-github[bot]andGitHub 5f78cbe4f0 [1.181.*] Pre-release merge (#630) 2024-08-07 14:30:16 +00:00
tramline-github[bot]andGitHub 7a2a34c351 [1.180.*] Pre-release merge (#629) 2024-08-06 14:30:06 +00:00
tramline-github[bot]andGitHub 28afbaf2a8 [1.179.*] Pre-release merge (#628) 2024-08-04 14:30:05 +00:00
tramline-github[bot]andGitHub 8a51b45bbc [1.178.*] Pre-release merge (#627) 2024-08-03 14:30:04 +00:00
tramline-github[bot]andGitHub 317554fb65 [1.177.*] Pre-release merge (#626) 2024-08-02 14:30:05 +00:00
tramline-github[bot]andGitHub 0a96004b65 [1.176.*] Pre-release merge (#625) 2024-07-31 14:30:06 +00:00
tramline-github[bot]andGitHub 4d83957296 [1.175.*] Pre-release merge (#624) 2024-07-30 14:30:06 +00:00
tramline-github[bot]andGitHub 46019c7006 [1.174.*] Pre-release merge (#623) 2024-07-28 14:30:04 +00:00
tramline-github[bot]andGitHub 5d14c5a3d2 [1.173.*] Pre-release merge (#622) 2024-07-27 14:30:06 +00:00
tramline-github[bot]andGitHub 8ce425d8d8 [1.172.*] Pre-release merge (#621) 2024-07-26 14:30:08 +00:00
tramline-github[bot]andGitHub 967bb0ab9e [1.171.*] Pre-release merge (#620) 2024-07-25 14:30:09 +00:00
tramline-github[bot]andGitHub 23c28983a6 [1.170.*] Pre-release merge (#619) 2024-07-24 14:30:05 +00:00
tramline-github[bot]andGitHub a4ca373bb8 [1.169.*] Pre-release merge (#618) 2024-07-23 14:30:09 +00:00
tramline-github[bot]andGitHub e648e752e5 [1.168.*] Pre-release merge (#617) 2024-07-22 14:30:08 +00:00
tramline-github[bot]andGitHub e91d2bbc87 [1.167.*] Pre-release merge (#616) 2024-07-20 14:30:08 +00:00
tramline-github[bot]andGitHub 7052081b8b [1.166.*] Pre-release merge (#615) 2024-07-18 14:30:05 +00:00
tramline-github[bot]andGitHub 750d1a69b6 [1.165.*] Pre-release merge (#614) 2024-07-16 14:30:09 +00:00
tramline-github[bot]andGitHub 4bf17fae2f [1.164.*] Pre-release merge (#613) 2024-07-13 14:30:04 +00:00
tramline-github[bot]andGitHub 0bda136274 [1.163.*] Pre-release merge (#612) 2024-07-12 14:30:08 +00:00
tramline-github[bot]andGitHub bd33c9e755 [1.162.*] Pre-release merge (#606) 2024-07-11 14:30:10 +00:00
tramline-github[bot]andGitHub fd5f6c07f6 [1.161.*] Pre-release merge (#605) 2024-07-10 14:30:08 +00:00
tramline-github[bot]andGitHub bd3e179776 [1.160.*] Pre-release merge (#604) 2024-07-08 14:30:05 +00:00
tramline-github[bot]andGitHub 387825cb97 [1.159.*] Pre-release merge (#602) 2024-07-06 14:30:05 +00:00
tramline-github[bot]andGitHub 63d3147ae1 [1.158.*] Pre-release merge (#601) 2024-07-03 14:30:06 +00:00
tramline-github[bot]andGitHub 73c22d526e [1.157.*] Pre-release merge (#600) 2024-07-02 14:30:10 +00:00
tramline-github[bot]andGitHub eff85fe7c2 [1.156.*] Pre-release merge (#599) 2024-07-01 14:30:06 +00:00
tramline-github[bot]andGitHub 0f7be01d4d [1.155.*] Pre-release merge (#598) 2024-06-30 14:30:07 +00:00
tramline-github[bot]andGitHub d302aedca5 [1.154.*] Pre-release merge (#597) 2024-06-29 14:30:03 +00:00
tramline-github[bot]andGitHub 6cf801e6ba [1.153.*] Pre-release merge (#596) 2024-06-28 14:30:08 +00:00
tramline-github[bot]andGitHub 89b73cbc05 [1.152.*] Pre-release merge (#595) 2024-06-27 14:30:04 +00:00
tramline-github[bot]andGitHub 07dff720e6 [1.151.*] Pre-release merge (#594) 2024-06-26 14:30:06 +00:00
tramline-github[bot]andGitHub f4224f29fd [1.150.*] Pre-release merge (#593) 2024-06-25 14:30:09 +00:00
tramline-github[bot]andGitHub 04deb762ed [1.149.*] Pre-release merge (#592) 2024-06-22 14:30:04 +00:00
tramline-github[bot]andGitHub 8b53cee130 [1.148.*] Pre-release merge (#591) 2024-06-20 14:30:08 +00:00
tramline-github[bot]andGitHub 29bdfa9949 [1.147.*] Pre-release merge (#590) 2024-06-18 14:30:05 +00:00
tramline-github[bot]andGitHub 1f934e3a79 [1.146.*] Pre-release merge (#589) 2024-06-15 14:30:09 +00:00
tramline-github[bot]andGitHub a8ac8ef24b [1.145.*] Pre-release merge (#588) 2024-06-14 14:30:09 +00:00
tramline-github[bot]andGitHub b2c37c7d23 [1.144.*] Pre-release merge (#587) 2024-06-13 14:30:05 +00:00
tramline-github[bot]andGitHub 2c222b7454 [1.143.*] Pre-release merge (#586) 2024-06-11 14:30:07 +00:00
tramline-github[bot]andGitHub aceb40b811 [1.142.*] Pre-release merge (#585) 2024-06-08 14:30:04 +00:00
tramline-github[bot]andGitHub f9ffdff1e7 [1.141.*] Pre-release merge (#583) 2024-06-07 14:30:10 +00:00
tramline-github[bot]andGitHub ae8aa7b657 [1.140.*] Pre-release merge (#582) 2024-06-06 14:30:06 +00:00
tramline-github[bot]andGitHub 0b140ba608 [1.139.*] Pre-release merge (#581) 2024-06-05 14:30:05 +00:00
tramline-github[bot]andGitHub f30633dbe2 [1.138.*] Pre-release merge (#580) 2024-06-04 14:30:05 +00:00
tramline-github[bot]andGitHub 232748e22b [1.137.*] Pre-release merge (#579) 2024-06-01 14:30:09 +00:00
tramline-github[bot]andGitHub eac3f78d51 [1.136.*] Pre-release merge (#578) 2024-05-31 14:30:04 +00:00
tramline-github[bot]andGitHub 2abb3089d6 [1.135.*] Pre-release merge (#576) 2024-05-30 19:38:40 +00:00
tramline-github[bot]andGitHub 0db0f48688 [1.134.*] Pre-release merge (#575) 2024-05-30 14:30:08 +00:00
tramline-github[bot]andGitHub 3a281801a5 [1.133.*] Pre-release merge (#573) 2024-05-29 14:30:05 +00:00
tramline-github[bot]andGitHub 73682459fe [1.132.*] Pre-release merge (#572) 2024-05-28 14:30:06 +00:00
tramline-github[bot]andGitHub 990b9cd171 [1.131.*] Pre-release merge (#571) 2024-05-27 14:30:06 +00:00
tramline-github[bot]andGitHub 8f2699ff8d [1.130.*] Pre-release merge (#570) 2024-05-24 14:30:04 +00:00
tramline-github[bot]andGitHub 0aff814cb1 [1.129.*] Pre-release merge (#567) 2024-05-23 14:30:05 +00:00
tramline-github[bot]andGitHub c78342f148 [1.128.*] Pre-release merge (#565) 2024-05-22 14:30:06 +00:00
tramline-github[bot]andGitHub 713b671cc4 [1.127.*] Pre-release merge (#564) 2024-05-21 14:30:05 +00:00
tramline-github[bot]andGitHub 3cd5b7c85a [1.126.*] Pre-release merge (#563) 2024-05-20 15:02:53 +00:00
tramline-github[bot]andGitHub 25c4c335bd [1.126.*] Pre-release merge (#561) 2024-05-15 14:30:05 +00:00
Harsh Shandilya 1876884235 fix(deps): update Compose Compiler to 1.5.14 2024-05-15 18:21:34 +05:30
tramline-github[bot]andGitHub 246faf967f [1.125.*] Pre-release merge (#560) 2024-05-14 14:30:05 +00:00
tramline-github[bot]andGitHub 4f04b74c6f [1.124.*] Pre-release merge (#559) 2024-05-13 14:30:06 +00:00
tramline-github[bot]andGitHub 2ec47e7188 [1.123.*] Pre-release merge (#557) 2024-05-11 14:30:03 +00:00
tramline-github[bot]andGitHub bf45be6e1f [1.122.*] Pre-release merge (#556) 2024-05-10 14:30:08 +00:00
tramline-github[bot]andGitHub f340fde950 [1.121.*] Pre-release merge (#555) 2024-05-09 14:30:04 +00:00
tramline-github[bot]andGitHub 3ebd0d6b95 [1.120.*] Pre-release merge (#552) 2024-05-08 14:30:06 +00:00
tramline-github[bot]andGitHub acaf31b68f [1.119.*] Pre-release merge (#550) 2024-05-07 14:30:05 +00:00
tramline-github[bot]andGitHub c59fdf6f51 [1.118.*] Pre-release merge (#548) 2024-05-04 14:30:09 +00:00
tramline-github[bot]andGitHub 7d6d7ac0f4 [1.117.*] Pre-release merge (#547) 2024-05-03 14:30:06 +00:00
tramline-github[bot]andGitHub 1ba6585984 [1.116.*] Pre-release merge (#546) 2024-05-02 14:30:04 +00:00
tramline-github[bot]andGitHub 9cfb3d21dd [1.115.*] Pre-release merge (#543) 2024-05-01 14:30:04 +00:00
tramline-github[bot]andGitHub c8aa440607 [1.114.*] Pre-release merge (#542) 2024-04-30 14:30:06 +00:00
tramline-github[bot]andGitHub f619fca0eb [1.113.*] Pre-release merge (#541) 2024-04-29 20:12:14 +00:00
tramline-github[bot]andGitHub 239b584f6c [1.112.*] Pre-release merge (#539) 2024-04-29 14:30:09 +00:00
tramline-github[bot]andGitHub 7fa3821d7c [1.111.*] Pre-release merge (#538) 2024-04-28 14:30:07 +00:00
tramline-github[bot]andGitHub 0db317a07a [1.110.*] Pre-release merge (#537) 2024-04-27 15:05:53 +00:00
tramline-github[bot]andGitHub 0668c1e64d [1.109.*] Pre-release merge (#534) 2024-04-26 14:30:07 +00:00
tramline-github[bot]andGitHub 59f1809fdf [1.108.*] Pre-release merge (#533) 2024-04-24 18:43:57 +00:00
tramline-github[bot]andGitHub 07000fae70 [1.107.*] Pre-release merge (#530) 2024-04-23 14:30:05 +00:00
tramline-github[bot]andGitHub 5965374a3e [1.106.*] Pre-release merge (#529) 2024-04-22 14:30:08 +00:00
tramline-github[bot]andGitHub 04c39d4a86 [1.105.*] Pre-release merge (#528) 2024-04-20 14:30:04 +00:00
tramline-github[bot]andGitHub cc77e27b0c [1.104.*] Pre-release merge (#525) 2024-04-19 14:30:05 +00:00
tramline-github[bot]andGitHub 5a2c7f1d12 [1.103.*] Pre-release merge (#524) 2024-04-18 14:30:06 +00:00
tramline-github[bot]andGitHub eed1824d86 [1.102.*] Pre-release merge (#522) 2024-04-17 14:30:08 +00:00
tramline-github[bot]andGitHub 64afde07bd [1.101.*] Pre-release merge (#521) 2024-04-16 14:30:05 +00:00
tramline-github[bot]andGitHub 6b07dc3648 [1.100.*] Pre-release merge (#520) 2024-04-15 14:30:07 +00:00
tramline-github[bot]andGitHub f320427786 [1.99.*] Pre-release merge (#519) 2024-04-13 14:30:04 +00:00
tramline-github[bot]andGitHub f35addee1b [1.98.*] Pre-release merge (#518) 2024-04-12 14:30:04 +00:00
tramline-github[bot]andGitHub 0c5773dd89 [1.97.*] Pre-release merge (#517) 2024-04-10 14:30:05 +00:00
tramline-github[bot]andGitHub 63d41f7b8d [1.96.*] Pre-release merge (#516) 2024-04-09 19:20:02 +00:00
tramline-github[bot]andGitHub 04932ef60c [1.95.*] Pre-release merge (#515) 2024-04-09 14:30:06 +00:00
tramline-github[bot]andGitHub e529cab77e [1.94.*] Pre-release merge (#513) 2024-04-06 14:30:03 +00:00
tramline-github[bot]andGitHub 4098e4b584 [1.93.*] Pre-release merge (#512) 2024-04-05 14:30:05 +00:00
tramline-github[bot]andGitHub 9c8995cacb [1.92.*] Pre-release merge (#511) 2024-04-04 00:22:39 +00:00
tramline-github[bot]andGitHub cd86d8daf7 [1.91.*] Pre-release merge (#510) 2024-04-03 14:30:08 +00:00
tramline-github[bot]andGitHub 8053968b4b [1.90.*] Pre-release merge (#508) 2024-03-31 14:30:06 +00:00
tramline-github[bot]andGitHub 5aa52285e2 [1.89.*] Pre-release merge (#507) 2024-03-30 14:30:06 +00:00
tramline-github[bot]andGitHub e996b80299 [1.88.*] Pre-release merge (#505) 2024-03-29 14:30:05 +00:00
tramline-github[bot]andGitHub e06356dd2c [1.87.*] Pre-release merge (#504) 2024-03-28 14:30:07 +00:00
tramline-github[bot]andGitHub 115c9def1f [1.86.*] Pre-release merge (#503) 2024-03-27 14:30:06 +00:00
tramline-github[bot]andGitHub ec70b6c581 [1.85.*] Pre-release merge (#502) 2024-03-26 14:30:08 +00:00
tramline-github[bot]andGitHub a92175ec3f [1.84.*] Pre-release merge (#501) 2024-03-25 14:30:08 +00:00
tramline-github[bot]andGitHub 3249f0673a [1.83.*] Pre-release merge (#500) 2024-03-24 14:30:08 +00:00
tramline-github[bot]andGitHub 938a01fa1a [1.82.*] Pre-release merge (#498) 2024-03-23 14:30:03 +00:00
tramline-github[bot]andGitHub c406675542 [1.81.*] Pre-release merge (#497) 2024-03-22 14:30:05 +00:00
tramline-github[bot]andGitHub 5efac989e4 [1.80.*] Pre-release merge (#496) 2024-03-21 14:30:08 +00:00
tramline-github[bot]andGitHub 63cca980b5 [1.79.*] Pre-release merge (#495) 2024-03-20 14:30:05 +00:00
tramline-github[bot]andGitHub 2f2f376fa4 [1.78.*] Pre-release merge (#494) 2024-03-19 14:30:05 +00:00
tramline-github[bot]andGitHub 2465c56b1a [1.77.*] Pre-release merge (#493) 2024-03-17 14:30:08 +00:00
tramline-github[bot]andGitHub 57d79c42fa [1.76.*] Pre-release merge (#492) 2024-03-16 14:30:08 +00:00
tramline-github[bot]andGitHub 1c07d1f690 [1.76.*] Pre-release merge (#490) 2024-03-16 04:46:04 +00:00
tramline-github[bot]andGitHub 6222616056 [1.75.*] Pre-release merge (#489) 2024-03-15 14:30:06 +00:00
tramline-github[bot]andGitHub cc0732702a [1.74.*] Pre-release merge (#487) 2024-03-14 14:30:06 +00:00
tramline-github[bot]andGitHub 740b36363b [1.73.*] Pre-release merge (#486) 2024-03-13 14:30:06 +00:00
tramline-github[bot]andGitHub 879f9895a3 [1.72.*] Pre-release merge (#485) 2024-03-12 14:30:04 +00:00
tramline-github[bot]andGitHub 80efa91720 [1.71.*] Pre-release merge (#483) 2024-03-11 14:30:08 +00:00
tramline-github[bot]andGitHub 083f12e35e [1.70.*] Pre-release merge (#480) 2024-03-08 14:30:05 +00:00
tramline-github[bot]andGitHub 2e6d62af74 [1.70.*] Pre-release merge (#473) 2024-03-07 12:04:22 +00:00
tramline-github[bot]andGitHub 38139e0e6e [1.69.*] Pre-release merge (#472) 2024-03-06 14:30:09 +00:00
tramline-github[bot]andGitHub 0658fcfd56 [1.68.*] Pre-release merge (#471) 2024-03-05 14:30:06 +00:00
tramline-github[bot]andGitHub d3b6328048 [1.67.*] Pre-release merge (#470) 2024-03-04 14:30:06 +00:00
tramline-github[bot]andGitHub c60b4d05ae [1.66.*] Pre-release merge (#469) 2024-03-03 14:30:08 +00:00
tramline-github[bot]andGitHub e121ae97e6 [1.65.*] Pre-release merge (#468) 2024-03-02 04:35:35 +00:00
tramline-github[bot]andGitHub 663cfc581d [1.63.*] Pre-release merge (#464) 2024-02-26 14:30:05 +00:00
tramline-github[bot]andGitHub aa5b48d2c9 [1.62.*] Pre-release merge (#463) 2024-02-24 14:30:08 +00:00
tramline-github[bot]andGitHub b23db41d2c [1.61.*] Pre-release merge (#462) 2024-02-23 14:30:05 +00:00
tramline-github[bot]andGitHub dc3d259a65 [1.60.*] Pre-release merge (#460) 2024-02-22 14:30:06 +00:00
tramline-github[bot]andGitHub 613f8a8f9a [1.59.*] Pre-release merge (#458) 2024-02-21 14:30:09 +00:00
tramline-github[bot]andGitHub f38c6bab21 [1.58.*] Pre-release merge (#457) 2024-02-20 14:30:04 +00:00
tramline-github[bot]andGitHub f9e6b2045a [1.57.*] Pre-release merge (#456) 2024-02-19 14:30:03 +00:00
tramline-github[bot]andGitHub 724e0791d8 [1.56.*] Pre-release merge (#454) 2024-02-17 14:30:04 +00:00
tramline-github[bot]andGitHub 3998f6c304 [1.55.*] Pre-release merge (#453) 2024-02-16 14:30:05 +00:00
tramline-github[bot]andGitHub b26b753c54 [1.54.*] Pre-release merge (#452) 2024-02-14 14:30:07 +00:00
tramline-github[bot]andGitHub 9983bf4781 [1.53.*] Pre-release merge (#450) 2024-02-13 16:32:29 +00:00
tramline-github[bot]andGitHub 8d841afa34 [1.52.*] Pre-release merge (#449) 2024-02-13 14:30:10 +00:00
tramline-github[bot]andGitHub 2860da2710 [1.51.*] Pre-release merge (#448) 2024-02-12 14:30:07 +00:00
tramline-github[bot]andGitHub 48214cefd9 [1.50.*] Pre-release merge (#447) 2024-02-10 14:30:03 +00:00
tramline-github[bot]andGitHub 20e19446e1 [1.49.*] Pre-release merge (#445) 2024-02-09 14:30:06 +00:00
tramline-github[bot]andGitHub 4aa9341de4 [1.48.*] Pre-release merge (#444) 2024-02-08 14:30:07 +00:00
tramline-github[bot]andGitHub 15e8e59111 [1.47.*] Pre-release merge (#443) 2024-02-07 14:30:06 +00:00
tramline-github[bot]andGitHub d28dc0e847 [1.46.*] Pre-release merge (#442) 2024-02-06 14:30:08 +00:00
tramline-github[bot]andGitHub bf1aaa9e17 [1.45.*] Pre-release merge (#441) 2024-02-05 14:30:11 +00:00
tramline-github[bot]andGitHub e623a274a6 [1.44.*] Pre-release merge (#440) 2024-02-04 14:30:06 +00:00
tramline-github[bot]andGitHub 5357f85802 [1.43.*] Pre-release merge (#439) 2024-02-03 14:30:06 +00:00
tramline-github[bot]andGitHub b91b966ce7 [1.42.*] Pre-release merge (#438) 2024-02-01 14:30:09 +00:00
tramline-github[bot]andGitHub f3e19adca1 [1.41.*] Pre-release merge (#437) 2024-01-30 14:30:07 +00:00
Harsh ShandilyaandGitHub 7877f8c62f [1.40.*] Pre-release merge (#436)
[1.40.*] Pre-release merge
2024-01-29 20:36:06 +05:30
tramline-github[bot]andGitHub 8fd9d04d73 [1.39.*] Pre-release merge (#435) 2024-01-28 08:41:21 +00:00
260 changed files with 24407 additions and 5562 deletions
+18 -6
View File
@@ -6,6 +6,9 @@
"helpers:pinGitHubActionDigests",
"github>msfjarvis/shared-workflows//renovate/automerge"
],
"ignorePresets": [
"group:monorepos"
],
"branchConcurrentLimit": 15,
"github-actions": {
"managerFilePatterns": [
@@ -24,6 +27,20 @@
"https://jitpack.io",
],
},
{
"extends": [
"monorepo:kotlin"
],
"groupName": "Kotlin and Compose",
"groupSlug": "kotlin-and-compose",
"matchUpdateTypes": [
"digest",
"patch",
"minor",
"major",
],
"enabled": false,
},
{
"matchDepNames": [
"com.google.devtools.ksp",
@@ -33,6 +50,7 @@
"org.jetbrains.kotlin.plugin.serialization",
],
"groupName": "Kotlin and Compose",
"groupSlug": "kotlin-and-compose",
},
{
"matchPackageNames": [
@@ -47,12 +65,6 @@
],
"groupName": "Sentry"
},
{
"matchPackageNames": [
"/^com.squareup.anvil/"
],
"allowedVersions": "!/.*-1-8$/",
},
{
"matchManagers": [
"github-actions"
@@ -25,7 +25,7 @@ runs:
using: "composite"
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: ${{ inputs.fetch-depth }}
token: ${{ inputs.token }}
@@ -33,10 +33,10 @@ runs:
persist-credentials: ${{ inputs.persist-credentials }}
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
- name: Set up JDK
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: temurin
java-version: 25
@@ -53,7 +53,7 @@ runs:
echo "android.testoptions.manageddevices.emulator.gpu=swiftshader_indirect" >> ~/.gradle/gradle.properties
- name: Setup Gradle
uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
add-job-summary: always
cache-read-only: ${{ inputs.cache-read-only }}
+6 -6
View File
@@ -19,14 +19,14 @@ jobs:
runs-on: macos-26
steps:
- name: Setup build environment
uses: msfjarvis/compose-lobsters/.github/reusable-workflows/setup-gradle@92aae743ab8e1c98741128f1bfda3715750f5cf6 # main
uses: $/.github/reusable-workflows/setup-gradle
with:
fetch-depth: 0
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
dependency-graph: generate-and-submit
- name: Check changelog format
uses: mindsers/changelog-reader-action@32aa5b4c155d76c94e4ec883a223c947b2f02656 # v2.2.3
uses: mindsers/changelog-reader-action@1faaf50aa09d5793d9a100819973df801febfb31 # v2.4.0
with:
path: ./CHANGELOG.md
validation_level: error
@@ -38,7 +38,7 @@ jobs:
- name: (Fail-only) Upload test report
if: failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Test report
path: '**/build/reports/tests/**'
@@ -50,12 +50,12 @@ jobs:
- check
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Dependency Review
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
base-ref: refs/heads/main
head-ref: ${{ github.ref }}
@@ -66,7 +66,7 @@ jobs:
- check
steps:
- name: Setup build environment
uses: msfjarvis/compose-lobsters/.github/reusable-workflows/setup-gradle@92aae743ab8e1c98741128f1bfda3715750f5cf6 # main
uses: $/.github/reusable-workflows/setup-gradle
with:
cache-read-only: true
+4 -3
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: macos-26
steps:
- name: Setup build environment
uses: msfjarvis/compose-lobsters/.github/reusable-workflows/setup-gradle@92aae743ab8e1c98741128f1bfda3715750f5cf6 # main
uses: $/.github/reusable-workflows/setup-gradle
with:
token: ${{ secrets.POST_RELEASE_GH_TOKEN }}
fetch-depth: 0
@@ -53,7 +53,7 @@ jobs:
- name: Get changelog entry
id: changelog_reader
uses: mindsers/changelog-reader-action@32aa5b4c155d76c94e4ec883a223c947b2f02656 # v2.2.3
uses: mindsers/changelog-reader-action@1faaf50aa09d5793d9a100819973df801febfb31 # v2.4.0
with:
version: ${{ env.VERSION }}
path: ./CHANGELOG.md
@@ -102,6 +102,7 @@ jobs:
- name: Build release assets
shell: bash
env:
ZIPLINE_SIGNING_KEY: ${{ secrets.ZIPLINE_SIGNING_KEY }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_VCS_HEAD_SHA: ${{ env.SENTRY_VCS_HEAD_SHA }}
@@ -122,7 +123,7 @@ jobs:
printf '%s' "${CHANGELOG}" >> distribution/whatsnew/whatsnew-en-GB
- name: Publish bundle to Google Play
uses: r0adkll/upload-google-play@935ef9c68bb393a8e6116b1575626a7f5be3a7fb # v1.1.3
uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5
with:
mappingFile: android/bundle/mapping.txt
packageName: dev.msfjarvis.claw.android
+3 -2
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup build environment
uses: msfjarvis/compose-lobsters/.github/reusable-workflows/setup-gradle@92aae743ab8e1c98741128f1bfda3715750f5cf6 # main
uses: $/.github/reusable-workflows/setup-gradle
with:
cache-read-only: true
fetch-depth: 0
@@ -57,6 +57,7 @@ jobs:
./gradlew --no-configuration-cache --stacktrace assembleRelease uploadSentryProguardMappingsRelease collectReleaseBundle -PenableSentry
./scripts/signing-cleanup.sh
env:
ZIPLINE_SIGNING_KEY: ${{ secrets.ZIPLINE_SIGNING_KEY }}
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@@ -66,7 +67,7 @@ jobs:
ORG_GRADLE_PROJECT_VERSION_CODE: ${{ steps.tramline.outputs.version_code }}
- name: Upload app bundle
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-aab
path: android/build/outputs/bundle/release/android-release.aab
+1
View File
@@ -248,3 +248,4 @@ distribution/
screenshots/
claw-export.json
.kotlin/
.worktrees/
-1
View File
@@ -1,4 +1,3 @@
*.json
baseline-prof.txt
startup-prof.txt
**/test/resources/**
+71 -1
View File
@@ -7,6 +7,71 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.68.0] - 2026-07-18
### Fixed
- Comment votes and tags in search results are now parsed correctly again (Already deployed via OTA)
- Failing to fetch the remote HTML parser manifest now falls back to the embedded copy
- Fixed tags failing to be fetched from the server
### Added
- Direct links to comments can now be shared via the context menu, tap on a comment to reveal it
## [1.67.0] - 2026-07-10
### Added
- Permanent tag blocks are now synced to Lobsters if you are logged in.
### Changed
- Search UX has been redone to match Material guidelines
- Comments now indicated the OP like the site does, and the animations are faster.
- The HTML parser has been reimplemented so it can be updated out-of-band from
app updates, ensuring fixes for site markup changes can be rolled out faster.
### Fixed
- Tags are now parsed properly again.
## [1.66.0] - 2026-06-02
### Added
- You can now upvote and reply to comments from the app, if you're logged in
### Changed
- The comments UI now tries to mimic Boost for Reddit
### Fixed
- Certificate pinning was removed to avoid errors when certificates were rotated
## [1.65.0] - 2026-05-29
### Fixed
- Address fallout from Lobste.rs changing its HTML markup. This will keep happening :(
### Changed
- The comment expand/collapse indicators now match the Lobsters UI
## [1.64.0] - 2026-05-26
### Added
- Allow logging in to avoid rate limiting problems.
### Changed
- The migration path for tag data format changes in 1.61.0 has been removed.
- The app no longer uses the unsupported JSON endpoints on lobste.rs, instead relying
on parsing the HTML pages instead.
## [1.63.0] - 2026-04-02
### Fixed
@@ -599,7 +664,12 @@ Thanks to Charles Lombardo for contributing the action button UI changes.
- Initial Play Store release
[Unreleased]: https://github.com/msfjarvis/compose-lobsters/compare/v1.63.0...HEAD
[Unreleased]: https://github.com/msfjarvis/compose-lobsters/compare/v1.68.0...HEAD
[1.68.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.67.0...v1.68.0
[1.67.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.66.0...v1.67.0
[1.66.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.65.0...v1.66.0
[1.65.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.64.0...v1.65.0
[1.64.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.63.0...v1.64.0
[1.63.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.62.0...v1.63.0
[1.62.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.61.0...v1.62.0
[1.61.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.60.0...v1.61.0
+7 -3
View File
@@ -1,9 +1,13 @@
# Claw for [lobste.rs](https://lobste.rs) [![CI](https://github.com/msfjarvis/compose-lobsters/actions/workflows/ci.yml/badge.svg)](https://github.com/msfjarvis/compose-lobsters/actions/workflows/ci.yml)
Unofficial Android app for read-only access to [lobste.rs](https://lobste.rs), built with [Jetpack Compose](https://developer.android.com/jetpack/compose).
Unofficial Android app for the [lobste.rs](https://lobste.rs) linkblogging community, built with [Jetpack Compose](https://developer.android.com/jetpack/compose).
> [!WARNING]
> This app uses a JSON API that Lobsters does not expose by choice and [the maintainer has no interest in having](https://github.com/lobsters/lobsters/issues/1663#issuecomment-3074472781), so it may break in any manner at any point in time. File an issue [here](https://github.com/msfjarvis/compose-lobsters/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen) if you come across any bugs, do not bother the upstream developers about a feature they don't support.
## Features
- Browse posts in the frontpage (Hottest) and Newest feeds
- Save posts for future reading in-app
- Upvote and reply to comments
- Share post URLs
<a href="https://play.google.com/store/apps/details?id=dev.msfjarvis.claw.android">
<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"
+130 -12
View File
@@ -5,9 +5,15 @@
* https://opensource.org/licenses/MIT.
*/
@file:Suppress("UnstableApiUsage")
@file:OptIn(ExperimentalTime::class)
import dev.msfjarvis.claw.gradle.addTestDependencies
import dev.zacsweers.metro.gradle.DiagnosticSeverity
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import java.io.File
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
plugins {
id("dev.msfjarvis.claw.android-application")
@@ -16,33 +22,129 @@ plugins {
id("dev.msfjarvis.claw.sentry")
id("dev.msfjarvis.claw.versioning-plugin")
id("kotlin-parcelize")
alias(libs.plugins.android.junit5)
alias(libs.plugins.aboutlibraries)
alias(libs.plugins.modulegraphassert)
alias(libs.plugins.baselineprofile)
alias(libs.plugins.licensee)
alias(libs.plugins.screenshot)
alias(libs.plugins.kotlin.composeCompiler)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.dependencyAnalysis)
alias(libs.plugins.metro)
alias(libs.plugins.zipline)
}
val ziplineDevManifestUrl =
providers
.gradleProperty("claw.ziplineParserManifestUrl")
.orElse("http://10.0.2.2:8080/manifest.zipline.json")
.get()
val ziplineProdManifestUrl = "https://claw.msfjarvis.dev/current/v2/manifest.zipline.json"
val embeddedZiplineAssetsRoot = layout.buildDirectory.dir("generated/ziplineEmbeddedAssets")
val embeddedZiplineAssetsRootFile =
layout.buildDirectory.get().dir("generated/ziplineEmbeddedAssets").asFile
val prepareEmbeddedZiplineParser =
tasks.register("prepareEmbeddedZiplineParser") {
val sourceDir =
rootProject.layout.projectDirectory.dir("zipline-parser/build/zipline/Production")
val outputRoot = embeddedZiplineAssetsRoot
inputs.dir(sourceDir)
outputs.dir(outputRoot)
dependsOn(":zipline-parser:compileProductionExecutableKotlinJsZipline")
description = "Embed compiled Zipline artifacts into the app"
doLast {
val outputDir = outputRoot.get().asFile
val ziplineDir = File(outputDir, "zipline")
ziplineDir.deleteRecursively()
ziplineDir.mkdirs()
val sourceManifestFile = File(sourceDir.asFile, "manifest.zipline.json")
check(sourceManifestFile.exists()) { "Missing Zipline manifest: $sourceManifestFile" }
@Suppress("UNCHECKED_CAST")
val parsed = JsonSlurper().parse(sourceManifestFile) as MutableMap<String, Any?>
val modules = parsed["modules"] as? Map<*, *>
check(!modules.isNullOrEmpty()) {
"Zipline manifest contains no modules: $sourceManifestFile"
}
for ((_, moduleValue) in modules) {
val module = moduleValue as Map<*, *>
val sourceName = module["url"] as String
val targetName = module["sha256"] as String
val sourceFile = File(sourceDir.asFile, sourceName)
check(sourceFile.exists()) { "Missing Zipline module: $sourceFile" }
sourceFile.copyTo(File(ziplineDir, targetName), overwrite = true)
}
val unsigned =
((parsed["unsigned"] as? Map<*, *>)?.toMutableMap() ?: mutableMapOf()).apply {
put("freshAtEpochMs", Clock.System.now().toEpochMilliseconds())
}
parsed["unsigned"] = unsigned
val embeddedManifestFile = File(ziplineDir, "zipline-parser.manifest.zipline.json")
embeddedManifestFile.writeText(JsonOutput.toJson(parsed))
}
}
android {
namespace = "dev.msfjarvis.claw.android"
defaultConfig.applicationId = "dev.msfjarvis.claw.android"
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildFeatures.compose = true
buildTypes.create("internal") {
matchingFallbacks += "release"
signingConfig = signingConfigs["debug"]
applicationIdSuffix = ".internal"
isDebuggable = true
experimentalProperties["android.experimental.enableScreenshotTest"] = true
sourceSets.getByName("main").assets.directories.add(embeddedZiplineAssetsRootFile.path)
buildTypes {
getByName("debug") {
buildConfigField(
"String",
"ZIPLINE_PARSER_MANIFEST_URL",
"\"$ziplineDevManifestUrl\"",
)
buildConfigField("boolean", "ZIPLINE_PARSER_VERIFY_SIGNATURES", "false")
}
getByName("release") {
buildConfigField(
"String",
"ZIPLINE_PARSER_MANIFEST_URL",
"\"$ziplineProdManifestUrl\"",
)
buildConfigField("boolean", "ZIPLINE_PARSER_VERIFY_SIGNATURES", "true")
}
create("internal") {
matchingFallbacks += "release"
signingConfig = signingConfigs["debug"]
applicationIdSuffix = ".internal"
isDebuggable = true
buildConfigField(
"String",
"ZIPLINE_PARSER_MANIFEST_URL",
"\"$ziplineProdManifestUrl\"",
)
buildConfigField("boolean", "ZIPLINE_PARSER_VERIFY_SIGNATURES", "true")
}
}
buildTypes.getByName("release") {
baselineProfile.automaticGenerationDuringBuild =
project.providers.gradleProperty("genProf").isPresent
testOptions {
managedDevices {
localDevices {
create("pixel9api36") {
device = "Pixel 9"
apiLevel = 36
systemImageSource = "aosp"
}
}
}
}
}
tasks.named("preBuild").configure { dependsOn(prepareEmbeddedZiplineParser) }
aboutLibraries.collect.gitHubApiToken = providers.environmentVariable("GITHUB_TOKEN").orNull
baselineProfile {
@@ -57,6 +159,10 @@ licensee {
ignoreDependencies("com.michael-bull.kotlin-result") { because("kotlin-result is ISC licensed") }
ignoreDependencies("org.commonmark") { because("Commonmark is BSD licensed") }
allowUrl("https://jsoup.org/license") { because("Jsoup is MIT licensed") }
allowUrl("https://opensource.org/license/MIT") { because("Ksoup is MIT licensed") }
allowUrl("https://github.com/Osmerion/sqlite-android/blob/master/LICENSE") {
because("sqlite-android is MIT licensed")
}
}
metro { unusedGraphInputsSeverity = DiagnosticSeverity.ERROR }
@@ -97,9 +203,8 @@ dependencies {
implementation(libs.androidx.compose.ui.unit)
implementation(libs.androidx.annotation)
implementation(libs.androidx.collection)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.core)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.datastore.preferences.core)
implementation(libs.androidx.lifecycle.common)
implementation(libs.androidx.lifecycle.compose)
implementation(libs.androidx.lifecycle.runtime.compose)
@@ -110,11 +215,15 @@ dependencies {
implementation(libs.androidx.paging.common)
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.work.runtime)
implementation(libs.coil3.compose)
implementation(libs.coil3.core)
implementation(libs.eithernet)
implementation(libs.haze)
implementation(libs.haze.blur)
implementation(libs.kotlin.parcelize.runtime)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.core)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlin.stdlib)
@@ -129,6 +238,8 @@ dependencies {
implementation(libs.sqldelight.runtime)
implementation(libs.swipe)
implementation(libs.unfurl)
implementation(libs.zipline)
implementation(libs.zipline.loader)
implementation(projects.api)
implementation(projects.common)
implementation(projects.core)
@@ -138,9 +249,16 @@ dependencies {
compileOnly(libs.androidx.compose.glance.preview)
debugRuntimeOnly(libs.androidx.compose.glance)
runtimeOnly(libs.androidx.profileinstaller)
screenshotTestImplementation(libs.screenshot.validation.api)
screenshotTestImplementation(libs.androidx.compose.ui.tooling)
testImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.test.rules)
androidTestImplementation(libs.androidx.test.runner)
addTestDependencies(project)
}
+101 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 9.1.0" type="baseline" client="gradle" dependencies="true" name="AGP (9.1.0)" variant="all" version="9.1.0">
<issues format="6" by="lint 9.4.0-alpha03" type="baseline" client="gradle" dependencies="true" name="AGP (9.4.0-alpha03)" variant="all" version="9.4.0-alpha03">
<issue
id="Instantiatable"
@@ -12,6 +12,105 @@
column="21"/>
</issue>
<issue
id="DeprecatedCall"
message="android.webkit.WebView.loadUrl is deprecated; consider using an alternative."
errorLine1=" webViewRef?.loadUrl(LOGIN_URL)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
line="69"
column="15"/>
</issue>
<issue
id="DeprecatedCall"
message="android.webkit.WebView.loadUrl is deprecated; consider using an alternative."
errorLine1=" webViewRef?.loadUrl(LOGIN_URL)"
errorLine2=" ~~~~~~~~~">
<location
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
line="69"
column="35"/>
</issue>
<issue
id="DeprecatedCall"
message="android.webkit.WebView.WebView is deprecated; consider using an alternative."
errorLine1=" WebView(context)"
errorLine2=" ~~~~~~~~~~~~~~~~">
<location
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
line="78"
column="13"/>
</issue>
<issue
id="DeprecatedCall"
message="android.webkit.WebView.WebView is deprecated; consider using an alternative."
errorLine1=" WebView(context)"
errorLine2=" ~~~~~~~">
<location
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
line="78"
column="21"/>
</issue>
<issue
id="DeprecatedCall"
message="android.webkit.WebView.getSettings is deprecated; consider using an alternative."
errorLine1=" settings.javaScriptEnabled = true"
errorLine2=" ~~~~~~~~">
<location
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
line="81"
column="17"/>
</issue>
<issue
id="DeprecatedCall"
message="android.webkit.WebView.setWebViewClient is deprecated; consider using an alternative."
errorLine1=" webViewClient ="
errorLine2=" ^">
<location
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
line="82"
column="17"/>
</issue>
<issue
id="DeprecatedCall"
message="android.webkit.WebView.setWebViewClient is deprecated; consider using an alternative."
errorLine1=" object : WebViewClient() {"
errorLine2=" ^">
<location
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
line="83"
column="19"/>
</issue>
<issue
id="DeprecatedCall"
message="android.webkit.WebView.loadUrl is deprecated; consider using an alternative."
errorLine1=" loadUrl(LOGIN_URL)"
errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
line="118"
column="17"/>
</issue>
<issue
id="DeprecatedCall"
message="android.webkit.WebView.loadUrl is deprecated; consider using an alternative."
errorLine1=" loadUrl(LOGIN_URL)"
errorLine2=" ~~~~~~~~~">
<location
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
line="118"
column="25"/>
</issue>
<issue
id="MemberExtensionConflict"
message="`KeepUntilTransitionsFinished` is defined both as a member in class `androidx.compose.animation.ExitTransition.Companion` and an extension in package `androidx.compose.animation`. The defined behavior for this is to use the member, but since the extension is explicitly imported into this file, there&apos;s a chance that this was not expected. (One common way this happens is for members to be added to a class after code was already written to use an extension)."
@@ -19,7 +118,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LobstersPostsScreen.kt"
line="253"
line="421"
column="49"/>
</issue>
-7
View File
@@ -1,7 +0,0 @@
-dontobfuscate
-keepattributes SourceFile, LineNumberTable
# Keep Glance ActionCallback classes for reflection
-keep class * implements androidx.glance.action.ActionCallback {
public <init>();
}
+2
View File
@@ -0,0 +1,2 @@
-dontobfuscate
-keepattributes SourceFile, LineNumberTable
@@ -0,0 +1,63 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.android.zipline
import androidx.test.platform.app.InstrumentationRegistry
import dev.msfjarvis.claw.android.ClawApplication
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.fail
@OptIn(DelicateCoroutinesApi::class)
class AndroidZiplineParserClientTest {
@Test
fun ziplineLoadingSucceeds() {
val context =
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
as ClawApplication
val parserClient = context.appGraph.ziplineClient
GlobalScope.launch {
try {
parserClient
.service()
.parseReplyForm(
"""
<div class="comment_form_container" data-shortid="">
<form action="/comments" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="AI0414bnzi152-mE0JTWEtwq5B0ZhALBW1W7rGiG5zR-sFaJjWzdARXFM7w_DbPQqWNjzh9bufWZbXG39v5T6g" autocomplete="off">
<input value="znlkib" autocomplete="off" type="hidden" name="story_id" id="story_id" />
<input value="post" autocomplete="off" type="hidden" name="_method" id="_method" />
<input value="edtrox" autocomplete="off" type="hidden" name="parent_comment_short_id" id="parent_comment_short_id" />
<div style="width: 100%;">
<textarea rows="5" placeholder="" required="required" name="comment" id="comment">
</textarea>
<div class="controls">
<div class="buttons">
<input type="submit" name="commit" value="Post" class="comment-post" data-disable-with="Post">
</div>
</div>
</div>
</form>
</div>
"""
.trimIndent()
)
} catch (e: Exception) {
fail(e)
}
}
}
}
+1 -1
View File
@@ -5,7 +5,7 @@
~ https://opensource.org/licenses/MIT.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<application android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".MainActivity"
android:exported="true">
@@ -0,0 +1,12 @@
<!--
~ Copyright © Harsh Shandilya.
~ Use of this source code is governed by an MIT-style
~ license that can be found in the LICENSE file or at
~ https://opensource.org/licenses/MIT.
-->
<network-security-config xmlns:tools="http://schemas.android.com/tools">
<!-- Only allowed in debug builds -->
<base-config cleartextTrafficPermitted="true"
tools:ignore="InsecureBaseConfiguration">
</base-config>
</network-security-config>
+1
View File
@@ -28,6 +28,7 @@
android:name=".MainActivity"
android:configChanges="assetsPaths|colorMode|density|fontScale|fontWeightAdjustment|grammaticalGender|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|resourcesUnused|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:taskAffinity=""
android:exported="true">
<intent-filter>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -8,22 +8,18 @@ package dev.msfjarvis.claw.android
import android.app.Application
import android.os.Build
import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import dev.msfjarvis.claw.android.glance.SavedPostsWidgetReceiver
import dev.msfjarvis.claw.android.injection.AppGraph
import dev.msfjarvis.claw.android.work.GeneratedWidgetPreviewUpdateWorker
import dev.msfjarvis.claw.android.work.SavedPostUpdaterWorker
import dev.msfjarvis.claw.android.work.TagExpirationCleanupWorker
import dev.zacsweers.metro.createGraphFactory
import dev.zacsweers.metrox.android.MetroAppComponentProviders
import dev.zacsweers.metrox.android.MetroApplication
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class ClawApplication : Application(), MetroApplication {
@@ -32,7 +28,6 @@ class ClawApplication : Application(), MetroApplication {
override val appComponentProviders: MetroAppComponentProviders
get() = appGraph
@OptIn(DelicateCoroutinesApi::class)
override fun onCreate() {
super.onCreate()
appGraph.plugins.forEach { plugin -> plugin.apply(this) }
@@ -53,16 +48,7 @@ class ClawApplication : Application(), MetroApplication {
request = tagCleanupWorkRequest,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
GlobalScope.launch(appGraph.mainDispatcher) {
try {
GlanceAppWidgetManager(applicationContext)
.setWidgetPreviews(SavedPostsWidgetReceiver::class)
} catch (_: IllegalArgumentException) {
// Workaround for Motorola Android 15 bug where AppWidgetServiceImpl incorrectly
// reports registered widgets as invalid when setting previews.
// See: https://claw.sentry.io/share/issue/f8ee09821d8840b9b86293d3ffb627d3/
}
}
GeneratedWidgetPreviewUpdateWorker.enqueue(appGraph.workManager)
}
}
}
@@ -26,13 +26,13 @@ import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.ViewModelProvider
import androidx.navigation3.runtime.NavKey
import dev.msfjarvis.claw.android.injection.InjectedViewModelFactory
import dev.msfjarvis.claw.android.ui.navigation.Comments
import dev.msfjarvis.claw.android.ui.navigation.Hottest
import dev.msfjarvis.claw.android.ui.navigation.Newest
import dev.msfjarvis.claw.android.ui.navigation.Saved
import dev.msfjarvis.claw.android.ui.screens.LobstersPostsScreen
import dev.msfjarvis.claw.common.theme.LobstersTheme
import dev.msfjarvis.claw.core.injection.InjectedViewModelFactory
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
@@ -42,7 +42,6 @@ import dev.msfjarvis.claw.model.fromCachedRemotePost
import dev.msfjarvis.claw.model.toUIPost
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.first
class HottestPostsWidget : GlanceAppWidget() {
@@ -52,7 +51,7 @@ class HottestPostsWidget : GlanceAppWidget() {
val tagBlockRepository = appGraph.tagBlockRepository
val filteredTags =
try {
tagBlockRepository.getSavedTags().first()
tagBlockRepository.getSavedTagsSnapshot()
} catch (_: Exception) {
emptySet()
}
@@ -105,7 +104,9 @@ class HottestPostsWidget : GlanceAppWidget() {
}
override suspend fun providePreview(context: Context, widgetCategory: Int) {
provideContent { Content(context, samplePosts()) }
provideContent {
LobstersGlanceTheme { PreviewContent(context, generatedWidgetPreviewPosts()) }
}
}
@SuppressLint("ComposeUnstableReceiver")
@@ -114,39 +115,48 @@ class HottestPostsWidget : GlanceAppWidget() {
context: Context,
posts: ImmutableList<UIPost>,
modifier: GlanceModifier = GlanceModifier,
actionsEnabled: Boolean = true,
showFooterAction: Boolean = true,
) {
WidgetContainer(
"Hottest posts",
listContent = {
items(posts) { post ->
Box(GlanceModifier.padding(horizontal = 16.dp, vertical = 4.dp)) { WidgetPostEntry(post) }
Box(GlanceModifier.padding(horizontal = 16.dp, vertical = 4.dp)) {
WidgetPostEntry(post, actionsEnabled = actionsEnabled)
}
}
item {
Box(
contentAlignment = Alignment.Center,
modifier = GlanceModifier.fillMaxWidth().padding(16.dp),
) {
if (showFooterAction) {
item {
Box(
contentAlignment = Alignment.Center,
modifier =
GlanceModifier.background(GlanceTheme.colors.primary)
.cornerRadius(20.dp)
.padding(horizontal = 24.dp, vertical = 10.dp)
.clickable(
actionStartActivity(
Intent(Intent.ACTION_VIEW, "${BuildConfig.DEEPLINK_SCHEME}://hottest".toUri())
.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
setClass(LocalContext.current, MainActivity::class.java)
}
)
),
modifier = GlanceModifier.fillMaxWidth().padding(16.dp),
) {
Text(
text = context.getString(R.string.see_more_posts),
style =
TextStyle(color = GlanceTheme.colors.onPrimary, textAlign = TextAlign.Center),
)
Box(
contentAlignment = Alignment.Center,
modifier =
GlanceModifier.background(GlanceTheme.colors.primary)
.cornerRadius(20.dp)
.padding(horizontal = 24.dp, vertical = 10.dp)
.clickable(
actionStartActivity(
Intent(
Intent.ACTION_VIEW,
"${BuildConfig.DEEPLINK_SCHEME}://hottest".toUri(),
)
.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
setClass(LocalContext.current, MainActivity::class.java)
}
)
),
) {
Text(
text = context.getString(R.string.see_more_posts),
style =
TextStyle(color = GlanceTheme.colors.onPrimary, textAlign = TextAlign.Center),
)
}
}
}
}
@@ -154,4 +164,20 @@ class HottestPostsWidget : GlanceAppWidget() {
modifier = modifier,
)
}
@SuppressLint("ComposeUnstableReceiver")
@Composable
private fun PreviewContent(
context: Context,
posts: ImmutableList<UIPost>,
modifier: GlanceModifier = GlanceModifier,
) {
Content(
context = context,
posts = posts,
modifier = modifier,
actionsEnabled = false,
showFooterAction = false,
)
}
}
@@ -41,57 +41,65 @@ import dev.msfjarvis.claw.model.UIPost
import dev.msfjarvis.claw.model.fromSavedPost
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.first
class SavedPostsWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
val appGraph = (context.applicationContext as ClawApplication).appGraph
val posts = appGraph.savedPostsRepository.getRecentPosts(50)
val postWindow = posts.first().map(UIPost::fromSavedPost).toImmutableList()
val postWindow = posts.map { UIPost.fromSavedPost(it) }.toImmutableList()
provideContent { LobstersGlanceTheme { Content(postWindow) } }
}
override suspend fun providePreview(context: Context, widgetCategory: Int) {
provideContent { Content(samplePosts()) }
provideContent { LobstersGlanceTheme { PreviewContent(generatedWidgetPreviewPosts()) } }
}
@SuppressLint("ComposeUnstableReceiver")
@Composable
private fun Content(posts: ImmutableList<UIPost>, modifier: GlanceModifier = GlanceModifier) {
private fun Content(
posts: ImmutableList<UIPost>,
modifier: GlanceModifier = GlanceModifier,
actionsEnabled: Boolean = true,
showFooterAction: Boolean = true,
) {
val context = LocalContext.current
WidgetContainer(
title = context.getString(R.string.saved_posts),
listContent = {
items(posts) { post ->
Box(GlanceModifier.padding(horizontal = 16.dp, vertical = 4.dp)) { WidgetPostEntry(post) }
Box(GlanceModifier.padding(horizontal = 16.dp, vertical = 4.dp)) {
WidgetPostEntry(post, actionsEnabled = actionsEnabled)
}
}
item {
Box(
contentAlignment = Alignment.Center,
modifier = GlanceModifier.fillMaxWidth().padding(16.dp),
) {
if (showFooterAction) {
item {
Box(
contentAlignment = Alignment.Center,
modifier =
GlanceModifier.background(GlanceTheme.colors.primary)
.cornerRadius(20.dp)
.padding(horizontal = 24.dp, vertical = 10.dp)
.clickable(
actionStartActivity(
Intent(Intent.ACTION_VIEW, "${BuildConfig.DEEPLINK_SCHEME}://saved".toUri())
.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
setClass(LocalContext.current, MainActivity::class.java)
}
)
),
modifier = GlanceModifier.fillMaxWidth().padding(16.dp),
) {
Text(
text = context.getString(R.string.see_more_posts),
style =
TextStyle(color = GlanceTheme.colors.onPrimary, textAlign = TextAlign.Center),
)
Box(
contentAlignment = Alignment.Center,
modifier =
GlanceModifier.background(GlanceTheme.colors.primary)
.cornerRadius(20.dp)
.padding(horizontal = 24.dp, vertical = 10.dp)
.clickable(
actionStartActivity(
Intent(Intent.ACTION_VIEW, "${BuildConfig.DEEPLINK_SCHEME}://saved".toUri())
.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
setClass(LocalContext.current, MainActivity::class.java)
}
)
),
) {
Text(
text = context.getString(R.string.see_more_posts),
style =
TextStyle(color = GlanceTheme.colors.onPrimary, textAlign = TextAlign.Center),
)
}
}
}
}
@@ -100,12 +108,26 @@ class SavedPostsWidget : GlanceAppWidget() {
)
}
@SuppressLint("ComposeUnstableReceiver")
@Composable
private fun PreviewContent(
posts: ImmutableList<UIPost>,
modifier: GlanceModifier = GlanceModifier,
) {
Content(
posts = posts,
modifier = modifier,
actionsEnabled = false,
showFooterAction = false,
)
}
@SuppressLint("ComposeUnstableReceiver")
@OptIn(ExperimentalGlancePreviewApi::class)
@Preview
@GlanceComposable
@Composable
private fun Preview() {
Content(samplePosts())
LobstersGlanceTheme { PreviewContent(generatedWidgetPreviewPosts()) }
}
}
@@ -36,37 +36,51 @@ import dev.msfjarvis.claw.model.UIPost
@SuppressLint("ArgInFormattedQuantityStringRes")
@Composable
@GlanceComposable
fun WidgetPostEntry(post: UIPost, modifier: GlanceModifier = GlanceModifier) {
fun WidgetPostEntry(
post: UIPost,
modifier: GlanceModifier = GlanceModifier,
actionsEnabled: Boolean = true,
) {
val titleStyle = MaterialTheme.typography.bodyMedium
val context = LocalContext.current
val commentsAction =
actionStartActivity(
Intent(context, WidgetClickActivity::class.java).apply {
action = WidgetClickActivity.ACTION_OPEN_COMMENTS
putExtra(WidgetClickActivity.EXTRA_SHORT_ID, post.shortId)
}
)
if (actionsEnabled) {
actionStartActivity(
Intent(context, WidgetClickActivity::class.java).apply {
action = WidgetClickActivity.ACTION_OPEN_COMMENTS
putExtra(WidgetClickActivity.EXTRA_SHORT_ID, post.shortId)
}
)
} else {
null
}
val postAction =
if (post.url.startsWith('/') || post.url.isEmpty()) commentsAction
else
if (!actionsEnabled) {
null
} else if (post.url.startsWith('/') || post.url.isEmpty()) {
commentsAction
} else {
actionStartActivity(
Intent(context, WidgetClickActivity::class.java).apply {
putExtra(WidgetClickActivity.EXTRA_URL, post.url)
putExtra(WidgetClickActivity.EXTRA_SHORT_ID, post.shortId)
}
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
modifier.fillMaxWidth().cornerRadius(8.dp).background(GlanceTheme.colors.secondaryContainer),
) {
val titleModifier =
GlanceModifier.defaultWeight()
.padding(horizontal = 8.dp, vertical = 4.dp)
.padding(start = 4.dp)
val commentsModifier =
GlanceModifier.padding(horizontal = 8.dp, vertical = 4.dp).padding(horizontal = 4.dp)
Text(
text = post.title,
modifier =
GlanceModifier.defaultWeight()
.clickable(postAction)
.padding(horizontal = 8.dp, vertical = 4.dp)
.padding(start = 4.dp),
modifier = if (postAction != null) titleModifier.clickable(postAction) else titleModifier,
maxLines = 1,
style =
TextStyle(color = GlanceTheme.colors.onSecondaryContainer, fontSize = titleStyle.fontSize),
@@ -81,9 +95,8 @@ fun WidgetPostEntry(post: UIPost, modifier: GlanceModifier = GlanceModifier) {
),
colorFilter = ColorFilter.tint(GlanceTheme.colors.onSecondaryContainer),
modifier =
GlanceModifier.clickable(commentsAction)
.padding(horizontal = 8.dp, vertical = 4.dp)
.padding(horizontal = 4.dp),
if (commentsAction != null) commentsModifier.clickable(commentsAction)
else commentsModifier,
)
}
}
@@ -93,5 +106,5 @@ fun WidgetPostEntry(post: UIPost, modifier: GlanceModifier = GlanceModifier) {
@GlanceComposable
@Composable
private fun WidgetPostEntryPreview() {
WidgetPostEntry(samplePosts(1).first())
WidgetPostEntry(generatedWidgetPreviewPosts().first(), actionsEnabled = false)
}
@@ -70,3 +70,31 @@ fun samplePosts(
): PersistentList<UIPost> {
return buildList(count) { repeat(count) { add(TEST_POST.mutator(it)) } }.toPersistentList()
}
internal fun generatedWidgetPreviewPosts(): PersistentList<UIPost> {
return listOf(
TEST_POST.copy(
shortId = "widget-preview-1",
title = "Rust 2026 roadmap",
url = "/s/widget_preview_1",
commentCount = 18,
commentsUrl = "https://lobste.rs/s/widget_preview_1/rust_2026_roadmap",
submitter = "alice",
tags = listOf("rust", "roadmap"),
description = "",
),
TEST_POST.copy(
shortId = "widget-preview-2",
title = "SQLite tips for mobile apps",
url = "/s/widget_preview_2",
commentCount = 7,
commentsUrl = "https://lobste.rs/s/widget_preview_2/sqlite_tips_for_mobile_apps",
submitter = "bob",
tags = listOf("sqlite", "android"),
description = "",
),
)
.toPersistentList()
}
internal fun generatedWidgetPreviewDelayMinutes(): Long = 15L
@@ -11,15 +11,13 @@ import androidx.work.ListenableWorker
import androidx.work.WorkManager
import dev.msfjarvis.claw.android.viewmodel.CachedRemotePostsRepository
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
import dev.msfjarvis.claw.android.zipline.AndroidZiplineParserClient
import dev.msfjarvis.claw.api.LobstersApi
import dev.msfjarvis.claw.common.tags.TagBlockRepository
import dev.msfjarvis.claw.core.coroutines.MainDispatcher
import dev.msfjarvis.claw.core.injection.AppPlugin
import dev.msfjarvis.claw.core.injection.InjectedWorkerFactory
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.DependencyGraph
import dev.zacsweers.metro.Multibinds
import dev.zacsweers.metro.Provider
import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn
import dev.zacsweers.metrox.android.MetroAppComponentProviders
@@ -41,7 +39,7 @@ interface AppGraph : MetroAppComponentProviders, ViewModelGraph {
@Multibinds
val workerProviders:
Map<KClass<out ListenableWorker>, Provider<InjectedWorkerFactory.WorkerInstanceFactory<*>>>
Map<KClass<out ListenableWorker>, () -> InjectedWorkerFactory.WorkerInstanceFactory<*>>
val workerFactory: InjectedWorkerFactory
@@ -53,6 +51,8 @@ interface AppGraph : MetroAppComponentProviders, ViewModelGraph {
val tagBlockRepository: TagBlockRepository
val ziplineClient: AndroidZiplineParserClient
@MainDispatcher val mainDispatcher: CoroutineDispatcher
@DependencyGraph.Factory
@@ -4,7 +4,7 @@
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.core.injection
package dev.msfjarvis.claw.android.injection
import android.app.Application
@@ -4,7 +4,7 @@
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.common.injection
package dev.msfjarvis.claw.android.injection
import android.app.Application
import coil3.ImageLoader
@@ -13,14 +13,13 @@ import coil3.disk.DiskCache
import coil3.disk.directory
import coil3.memory.MemoryCache
import coil3.request.crossfade
import dev.msfjarvis.claw.core.injection.AppPlugin
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoSet
import dev.zacsweers.metro.Inject
@ContributesIntoSet(AppScope::class)
@Inject
class CoilAppPlugin() : AppPlugin {
class CoilAppPlugin : AppPlugin {
override fun apply(application: Application) {
SingletonImageLoader.setSafe {
ImageLoader.Builder(application)
@@ -33,14 +32,13 @@ class CoilAppPlugin() : AppPlugin {
.maxSizeBytes(DISK_CACHE_MAX_SIZE)
.build()
}
// Show a short crossfade when loading images asynchronously.
.crossfade(true)
.build()
}
}
private companion object {
private const val MEMORY_CACHE_RATIO = 0.10 // 10% of application heap
private const val DISK_CACHE_MAX_SIZE = 100L * 1024 * 1024 // 100 MB
private const val MEMORY_CACHE_RATIO = 0.10
private const val DISK_CACHE_MAX_SIZE = 100L * 1024 * 1024
}
}
@@ -4,12 +4,11 @@
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.core.injection
package dev.msfjarvis.claw.android.injection
import androidx.lifecycle.ViewModel
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Provider
import dev.zacsweers.metro.SingleIn
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory
import dev.zacsweers.metrox.viewmodel.MetroViewModelFactory
@@ -19,9 +18,8 @@ import kotlin.reflect.KClass
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class InjectedViewModelFactory(
override val viewModelProviders: Map<KClass<out ViewModel>, Provider<ViewModel>>,
override val assistedFactoryProviders:
Map<KClass<out ViewModel>, Provider<ViewModelAssistedFactory>>,
override val viewModelProviders: Map<KClass<out ViewModel>, () -> ViewModel>,
override val assistedFactoryProviders: Map<KClass<out ViewModel>, () -> ViewModelAssistedFactory>,
override val manualAssistedFactoryProviders:
Map<KClass<out ManualViewModelAssistedFactory>, Provider<ManualViewModelAssistedFactory>>,
Map<KClass<out ManualViewModelAssistedFactory>, () -> ManualViewModelAssistedFactory>,
) : MetroViewModelFactory()
@@ -4,7 +4,7 @@
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.core.injection
package dev.msfjarvis.claw.android.injection
import android.content.Context
import androidx.work.ListenableWorker
@@ -8,7 +8,6 @@ package dev.msfjarvis.claw.android.injection
import android.app.Application
import dev.msfjarvis.claw.api.LobstersApi
import dev.msfjarvis.claw.core.injection.AppPlugin
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoSet
import dev.zacsweers.metro.Inject
@@ -11,7 +11,6 @@ import android.util.Log
import androidx.work.Configuration
import androidx.work.WorkManager
import androidx.work.WorkerFactory
import dev.msfjarvis.claw.core.injection.AppPlugin
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoSet
import dev.zacsweers.metro.Inject
@@ -4,7 +4,7 @@
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.core.injection
package dev.msfjarvis.claw.android.injection
import androidx.work.ListenableWorker
import dev.zacsweers.metro.MapKey
@@ -4,7 +4,7 @@
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.core.network
package dev.msfjarvis.claw.android.network
import android.util.Log
import dev.zacsweers.metro.AppScope
@@ -13,7 +13,7 @@ import okhttp3.logging.HttpLoggingInterceptor
/** Implementation of [HttpLoggingInterceptor.Logger] backed by [Log]. */
@ContributesBinding(AppScope::class)
class OkHttpLogger : HttpLoggingInterceptor.Logger {
class OkHttpAndroidLogger : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
Log.d("LobstersApi", message)
}
@@ -0,0 +1,56 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.android.network
import android.content.Context
import android.net.TrafficStats
import dev.msfjarvis.claw.core.network.DelegatingSocketFactory
import dev.msfjarvis.claw.core.network.OkHttpClientConfigurator
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.IntoSet
import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn
import java.net.Socket
import javax.net.SocketFactory
import okhttp3.Cache
@BindingContainer
@ContributesTo(AppScope::class)
object OkHttpAndroidModule {
private const val CACHE_SIZE_MB = 10L * 1024 * 1024
private const val THREAD_STATS_TAG = 0x000090000
@Provides
@SingleIn(AppScope::class)
fun provideCache(context: Context): Cache {
return Cache(context.cacheDir, CACHE_SIZE_MB)
}
@Provides
fun provideSocketFactory(): SocketFactory {
return object : DelegatingSocketFactory(SocketFactory.getDefault()) {
override fun configureSocket(socket: Socket): Socket {
TrafficStats.setThreadStatsTag(THREAD_STATS_TAG)
return super.configureSocket(socket)
}
}
}
@Provides
@IntoSet
fun provideClientConfigurator(
cache: Cache,
socketFactory: SocketFactory,
): OkHttpClientConfigurator {
return OkHttpClientConfigurator { builder ->
builder.cache(cache)
builder.socketFactory(socketFactory)
}
}
}
@@ -7,75 +7,213 @@
package dev.msfjarvis.claw.android.ui.decorations
import android.app.Activity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Tune
import androidx.compose.material3.AppBarWithSearch
import androidx.compose.material3.ExpandedFullScreenContainedSearchBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberSearchBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.navigation3.runtime.NavKey
import dev.msfjarvis.claw.android.R
import dev.msfjarvis.claw.android.ui.navigation.Search
import dev.msfjarvis.claw.android.ui.navigation.Settings
import dev.msfjarvis.claw.common.theme.LobstersTheme
import dev.msfjarvis.claw.common.ui.preview.DevicePreviews
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
sealed interface ClawTopBarMode {
data object Browsing : ClawTopBarMode
class Searching(
val query: String,
val requestFocus: Boolean = true,
) : ClawTopBarMode
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ClawAppBar(
activity: Activity?,
isTopLevel: Boolean,
mode: ClawTopBarMode,
navigateTo: (NavKey) -> Unit,
popBackStack: () -> NavKey?,
onStartSearch: () -> Unit,
onDismissSearch: () -> Unit,
onQueryChange: (String) -> Unit,
onSearch: (String) -> Unit,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit,
) {
TopAppBar(
modifier = modifier.shadow(8.dp),
navigationIcon = {
if (!isTopLevel) {
IconButton(onClick = { if (popBackStack() == null) activity?.finish() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.go_back_to_previous_screen),
)
}
} else {
Icon(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = stringResource(R.string.the_app_icon_for_claw),
modifier = Modifier.size(48.dp),
when {
!isTopLevel -> {
TopAppBar(
modifier = modifier.shadow(8.dp),
navigationIcon = {
IconButton(onClick = { if (popBackStack() == null) activity?.finish() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.go_back_to_previous_screen),
)
}
},
title = {},
)
}
mode is ClawTopBarMode.Searching -> {
val textFieldState = rememberTextFieldState()
val searchBarState = rememberSearchBarState()
val focusRequester = remember { FocusRequester() }
val appBarWithSearchColors =
SearchBarDefaults.appBarWithSearchColors(
searchBarColors = SearchBarDefaults.containedColors(state = searchBarState)
)
}
},
title = {
if (isTopLevel) {
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
}
},
actions = {
if (isTopLevel) {
IconButton(onClick = { navigateTo(Search) }) {
Icon(
imageVector = Icons.Filled.Search,
contentDescription = stringResource(R.string.search_posts),
val inputField =
@Composable {
SearchBarDefaults.InputField(
textFieldState = textFieldState,
searchBarState = searchBarState,
onSearch = onSearch,
colors = appBarWithSearchColors.searchBarColors.inputFieldColors,
leadingIcon = {
Icon(
imageVector = Icons.Filled.Search,
contentDescription = null,
)
},
trailingIcon = {
IconButton(
onClick = {
if (mode.query.isNotEmpty()) {
onQueryChange("")
} else {
onDismissSearch()
}
}
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = stringResource(R.string.close_search),
)
}
},
)
}
IconButton(onClick = { navigateTo(Settings) }) {
Icon(
imageVector = Icons.Filled.Tune,
contentDescription = stringResource(R.string.settings),
)
Box(modifier = modifier) {
AppBarWithSearch(
state = searchBarState,
colors = appBarWithSearchColors,
inputField = inputField,
)
ExpandedFullScreenContainedSearchBar(
state = searchBarState,
inputField = inputField,
colors = appBarWithSearchColors.searchBarColors,
content = content,
)
LaunchedEffect(mode.requestFocus) {
if (mode.requestFocus) {
focusRequester.requestFocus()
}
}
}
},
)
}
else -> {
TopAppBar(
modifier = modifier.shadow(8.dp),
navigationIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = stringResource(R.string.the_app_icon_for_claw),
modifier = Modifier.size(48.dp),
)
},
title = {
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
},
actions = {
IconButton(onClick = onStartSearch) {
Icon(
imageVector = Icons.Filled.Search,
contentDescription = stringResource(R.string.search_posts),
)
}
IconButton(onClick = { navigateTo(Settings) }) {
Icon(
imageVector = Icons.Filled.Tune,
contentDescription = stringResource(R.string.settings),
)
}
},
)
}
}
}
@Composable
private fun FakeSearchResults() {
Text(text = "Fake result content")
}
@ThemePreviews
@DevicePreviews
@Composable
private fun ClawAppBarBrowsingPreview() {
LobstersTheme {
ClawAppBar(
activity = null,
isTopLevel = true,
mode = ClawTopBarMode.Browsing,
navigateTo = {},
popBackStack = { null },
onStartSearch = {},
onDismissSearch = {},
onQueryChange = {},
onSearch = {},
content = { FakeSearchResults() },
)
}
}
@ThemePreviews
@DevicePreviews
@Composable
private fun ClawAppBarSearchingPreview() {
LobstersTheme {
ClawAppBar(
activity = null,
isTopLevel = true,
mode = ClawTopBarMode.Searching(query = "compose", requestFocus = false),
navigateTo = {},
popBackStack = { null },
onStartSearch = {},
onDismissSearch = {},
onQueryChange = {},
onSearch = {},
content = { FakeSearchResults() },
)
}
}
@@ -27,9 +27,9 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import androidx.navigation3.runtime.NavKey
import dev.chrisbanes.haze.HazeDefaults
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.HazeStyle
import dev.chrisbanes.haze.blur.HazeBlurDefaults
import dev.chrisbanes.haze.blur.blurEffect
import dev.chrisbanes.haze.hazeEffect
import dev.msfjarvis.claw.android.ui.navigation.AppDestinations
import dev.msfjarvis.claw.common.ui.FloatingNavigationBar
@@ -62,6 +62,7 @@ fun ClawNavigationBar(
),
label = "",
content = {
val surfaceColor = MaterialTheme.colorScheme.surface
FloatingNavigationBar(
tonalElevation = 16.dp,
shape = MaterialTheme.shapes.extraLarge,
@@ -70,18 +71,15 @@ fun ClawNavigationBar(
.padding(horizontal = 16.dp)
.navigationBarsPadding()
.clip(MaterialTheme.shapes.extraLarge)
.hazeEffect(
hazeState,
style =
HazeStyle(
backgroundColor = MaterialTheme.colorScheme.surface,
tints = emptyList(),
blurRadius = 24.dp,
noiseFactor = 0f,
),
),
containerColor =
if (HazeDefaults.blurEnabled()) Color.Transparent else MaterialTheme.colorScheme.surface,
.hazeEffect(hazeState) {
blurEffect {
backgroundColor = surfaceColor
colorEffects = emptyList()
blurRadius = 24.dp
noiseFactor = 0f
}
},
containerColor = if (HazeBlurDefaults.blurEnabled()) Color.Transparent else surfaceColor,
) {
items.forEach { navItem ->
val isSelected = currentNavKey == navItem.navKey
@@ -40,15 +40,11 @@ fun PostActions(
}
override fun share(post: UIPost) {
val sendIntent: Intent =
Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, post.url.ifEmpty { post.commentsUrl })
putExtra(Intent.EXTRA_TITLE, post.title)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
context.startActivity(shareIntent)
shareUrl(post.url.ifEmpty { post.commentsUrl }, post.title)
}
override fun shareComment(commentId: String) {
shareUrl("https://lobste.rs/c/$commentId")
}
override fun isPostRead(post: UIPost): Boolean = viewModel.isPostRead(post)
@@ -58,5 +54,15 @@ fun PostActions(
override suspend fun getLinkMetadata(url: String): LinkMetadata {
return viewModel.getLinkMetadata(url)
}
private fun shareUrl(url: String, title: String? = null) {
val sendIntent =
Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_TEXT, url)
title?.let { putExtra(Intent.EXTRA_TITLE, it) }
type = "text/plain"
}
context.startActivity(Intent.createChooser(sendIntent, null))
}
}
}
@@ -7,8 +7,8 @@
package dev.msfjarvis.claw.android.ui.lists
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Reply
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.Web
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -32,9 +32,9 @@ fun LobstersListItem(
postActions: PostActions,
modifier: Modifier = Modifier,
) {
val commentsAction =
val webAction =
SwipeAction(
icon = rememberVectorPainter(Icons.AutoMirrored.Filled.Reply),
icon = rememberVectorPainter(Icons.Filled.Web),
background = MaterialTheme.colorScheme.tertiary,
onSwipe = { postActions.viewCommentsPage(item) },
)
@@ -46,7 +46,7 @@ fun LobstersListItem(
)
SwipeableActionsBox(
startActions = listOf(shareAction),
endActions = listOf(commentsAction),
endActions = listOf(webAction),
swipeThreshold = 80.dp,
backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceVariant,
) {
@@ -1,54 +0,0 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.android.ui.lists
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import androidx.paging.compose.LazyPagingItems
import dev.msfjarvis.claw.common.posts.PostActions
import dev.msfjarvis.claw.common.ui.SearchBar
import dev.msfjarvis.claw.model.UIPost
import kotlinx.collections.immutable.ImmutableSet
@Composable
fun SearchList(
lazyPagingItems: LazyPagingItems<UIPost>,
listState: LazyListState,
postActions: PostActions,
searchQuery: String,
setSearchQuery: (String) -> Unit,
contentPadding: PaddingValues,
filteredTags: ImmutableSet<String>,
modifier: Modifier = Modifier,
) {
val triggerSearch = { query: String ->
setSearchQuery(query)
lazyPagingItems.refresh()
}
Column(modifier.padding(contentPadding).fillMaxWidth()) {
SearchBar(
value = searchQuery,
onValueChange = setSearchQuery,
onSearch = { triggerSearch(it) },
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp).testTag("search_bar"),
)
NetworkPosts(
lazyPagingItems = lazyPagingItems,
listState = listState,
postActions = postActions,
contentPadding = PaddingValues(0.dp),
filteredTags = filteredTags,
)
}
}
@@ -0,0 +1,56 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.android.ui.lists
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import dev.msfjarvis.claw.android.R
import dev.msfjarvis.claw.common.posts.PostActions
import dev.msfjarvis.claw.model.UIPost
import kotlinx.collections.immutable.ImmutableSet
@Composable
fun SearchResultsList(
lazyPagingItems: LazyPagingItems<UIPost>,
listState: LazyListState,
postActions: PostActions,
filteredTags: ImmutableSet<String>,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
) {
val refreshLoadState by rememberUpdatedState(lazyPagingItems.loadState.refresh)
val hasNoResults = lazyPagingItems.itemCount == 0 && refreshLoadState is LoadState.NotLoading
Box(modifier = modifier.fillMaxSize()) {
if (hasNoResults) {
Text(
text = stringResource(R.string.no_search_results),
modifier = Modifier.align(Alignment.Center),
)
} else {
NetworkPosts(
lazyPagingItems = lazyPagingItems,
listState = listState,
postActions = postActions,
contentPadding = contentPadding,
filteredTags = filteredTags,
modifier = Modifier.fillMaxSize(),
)
}
}
}
@@ -33,7 +33,12 @@ interface NonStackable
@Parcelize @Serializable data class User(val username: String) : NavKey, Parcelable, NonStackable
@Parcelize @Serializable data object Search : NavKey, Parcelable
@Parcelize
@Serializable
class Reply(
val postId: String,
val commentId: String,
) : NavKey, Parcelable
@Parcelize @Serializable data object Settings : NavKey, Parcelable
@@ -41,6 +46,8 @@ interface NonStackable
@Parcelize @Serializable data object TagFiltering : NavKey, Parcelable
@Parcelize @Serializable data object Login : NavKey, Parcelable
enum class AppDestinations(
val icon: ImageVector,
val label: String,
@@ -16,8 +16,12 @@ import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
@@ -31,14 +35,18 @@ import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
@@ -55,34 +63,63 @@ import dev.msfjarvis.claw.android.ui.PostActions
import dev.msfjarvis.claw.android.ui.decorations.ClawAppBar
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationBar
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationRail
import dev.msfjarvis.claw.android.ui.decorations.ClawTopBarMode
import dev.msfjarvis.claw.android.ui.decorations.NavigationItem
import dev.msfjarvis.claw.android.ui.lists.DatabasePosts
import dev.msfjarvis.claw.android.ui.lists.NetworkPosts
import dev.msfjarvis.claw.android.ui.lists.SearchResultsList
import dev.msfjarvis.claw.android.ui.navigation.AboutLibraries
import dev.msfjarvis.claw.android.ui.navigation.AppDestinations
import dev.msfjarvis.claw.android.ui.navigation.ClawNavigationType
import dev.msfjarvis.claw.android.ui.navigation.Comments
import dev.msfjarvis.claw.android.ui.navigation.Hottest
import dev.msfjarvis.claw.android.ui.navigation.Login
import dev.msfjarvis.claw.android.ui.navigation.Newest
import dev.msfjarvis.claw.android.ui.navigation.NonStackable
import dev.msfjarvis.claw.android.ui.navigation.Reply
import dev.msfjarvis.claw.android.ui.navigation.Saved
import dev.msfjarvis.claw.android.ui.navigation.Search
import dev.msfjarvis.claw.android.ui.navigation.SentryNavigation3Traced
import dev.msfjarvis.claw.android.ui.navigation.Settings
import dev.msfjarvis.claw.android.ui.navigation.TagFiltering
import dev.msfjarvis.claw.android.ui.navigation.TopLevelDestination
import dev.msfjarvis.claw.android.ui.navigation.User
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
import dev.msfjarvis.claw.android.viewmodel.SettingsViewModel
import dev.msfjarvis.claw.common.comments.CommentsPage
import dev.msfjarvis.claw.common.comments.reply.ReplyScreen
import dev.msfjarvis.claw.common.tags.TagFilterViewModel
import dev.msfjarvis.claw.common.tags.TagList
import dev.msfjarvis.claw.common.user.UserProfile
import dev.zacsweers.metrox.viewmodel.metroViewModel
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
private const val SEARCH_DEBOUNCE_MILLIS = 300L
private const val SEARCH_WEB_URI = "https://lobste.rs/search"
private const val HOTTEST_WEB_URI = "https://lobste.rs/"
private const val NEWEST_WEB_URI = "https://lobste.rs/newest"
enum class TopLevelBackAction {
DismissSearch,
PopNavigation,
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun handleTopLevelBack(
isSearchActive: Boolean,
isCurrentDestinationTopLevel: Boolean,
): TopLevelBackAction {
return if (isSearchActive && isCurrentDestinationTopLevel) {
TopLevelBackAction.DismissSearch
} else {
TopLevelBackAction.PopNavigation
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun LobstersPostsScreen(
@@ -93,6 +130,7 @@ fun LobstersPostsScreen(
deepLinkDestination: NavKey? = null,
clearDeepLink: () -> Unit = {},
viewModel: ClawViewModel = metroViewModel(),
settingsViewModel: SettingsViewModel = metroViewModel(),
tagFilterViewModel: TagFilterViewModel = metroViewModel(key = "tag_filter"),
) {
val navigationType = ClawNavigationType.fromSize(windowSizeClass.widthSizeClass)
@@ -115,13 +153,22 @@ fun LobstersPostsScreen(
val savedPostsCount by viewModel.savedPostsCount.collectAsStateWithLifecycle(0L)
val filteredTags by tagFilterViewModel.filteredTags.collectAsStateWithLifecycle(persistentSetOf())
LaunchedEffect(deepLinkDestination) {
if (deepLinkDestination != null) {
navigateTo(backStack, deepLinkDestination, allowStacking = true)
clearDeepLink()
val isLoggedIn by settingsViewModel.isLoggedIn.collectAsStateWithLifecycle(false)
val username by settingsViewModel.username.collectAsStateWithLifecycle(null)
var isSearchActive by rememberSaveable { mutableStateOf(false) }
var searchQuery by rememberSaveable { mutableStateOf("") }
var lastExecutedSearchQuery by rememberSaveable { mutableStateOf<String?>(null) }
val searchResultsListState = rememberLazyListState()
val searchResults = viewModel.searchResults.collectAsLazyPagingItems()
val currentDestination = backStack.lastOrNull()
val currentDestinationIsTopLevel = currentDestination is TopLevelDestination
val normalizedSearchQuery = searchQuery.trim()
val searchResultsBottomPadding =
if (navigationType == ClawNavigationType.BOTTOM_NAVIGATION) {
80.dp + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
} else {
0.dp
}
}
val navItems =
persistentListOf(
@@ -137,31 +184,137 @@ fun LobstersPostsScreen(
)
// endregion
val postActions = remember {
PostActions(context, uriHandler, viewModel) {
navigateTo(backStack, Comments(it), allowStacking = false)
val executeSearch = { query: String ->
val normalizedQuery = query.trim()
lastExecutedSearchQuery = normalizedQuery
coroutineScope.launch { searchResultsListState.scrollToItem(0) }
viewModel.searchQuery = normalizedQuery
searchResults.refresh()
}
val dismissSearch = {
isSearchActive = false
searchQuery = ""
lastExecutedSearchQuery = ""
viewModel.searchQuery = ""
}
val navigateToDestination = { destination: NavKey, allowStacking: Boolean ->
if (isSearchActive) {
dismissSearch()
}
navigateTo(backStack, destination, allowStacking = allowStacking)
}
val startSearch = {
isSearchActive = true
}
val postActions =
remember(isSearchActive) {
PostActions(context, uriHandler, viewModel) {
navigateToDestination(Comments(it), false)
}
}
LaunchedEffect(deepLinkDestination) {
if (deepLinkDestination != null) {
navigateToDestination(deepLinkDestination, true)
clearDeepLink()
}
}
BackHandler(enabled = backStack.size > 1) { popBackStack(backStack) }
LaunchedEffect(currentDestination, isSearchActive) {
if (currentDestinationIsTopLevel) {
when {
isSearchActive -> setWebUri(SEARCH_WEB_URI)
currentDestination == Hottest -> setWebUri(HOTTEST_WEB_URI)
currentDestination == Newest -> setWebUri(NEWEST_WEB_URI)
currentDestination == Saved -> setWebUri(null)
}
}
}
LaunchedEffect(currentDestinationIsTopLevel, isSearchActive) {
if (!currentDestinationIsTopLevel && isSearchActive) {
dismissSearch()
}
}
LaunchedEffect(isSearchActive, normalizedSearchQuery) {
if (!isSearchActive) return@LaunchedEffect
delay(SEARCH_DEBOUNCE_MILLIS.milliseconds)
if (normalizedSearchQuery != lastExecutedSearchQuery) {
executeSearch(normalizedSearchQuery)
}
}
LaunchedEffect(
isSearchActive,
normalizedSearchQuery,
lastExecutedSearchQuery,
viewModel.searchQuery,
) {
if (!isSearchActive) return@LaunchedEffect
if (normalizedSearchQuery.isBlank()) return@LaunchedEffect
if (
normalizedSearchQuery == lastExecutedSearchQuery &&
viewModel.searchQuery != normalizedSearchQuery
) {
executeSearch(normalizedSearchQuery)
}
}
BackHandler(enabled = isSearchActive || backStack.size > 1) {
when (
handleTopLevelBack(
isSearchActive = isSearchActive,
isCurrentDestinationTopLevel = currentDestinationIsTopLevel,
)
) {
TopLevelBackAction.DismissSearch -> dismissSearch()
TopLevelBackAction.PopNavigation -> popBackStack(backStack)
}
}
Scaffold(
topBar = {
ClawAppBar(
activity = activity,
isTopLevel = backStack.lastOrNull() is TopLevelDestination,
navigateTo = { destination -> navigateTo(backStack, destination) },
isTopLevel = currentDestinationIsTopLevel,
mode =
if (currentDestinationIsTopLevel && isSearchActive) {
ClawTopBarMode.Searching(query = searchQuery, requestFocus = true)
} else {
ClawTopBarMode.Browsing
},
navigateTo = { destination -> navigateToDestination(destination, false) },
popBackStack = { popBackStack(backStack) },
)
onStartSearch = startSearch,
onDismissSearch = dismissSearch,
onQueryChange = { searchQuery = it },
onSearch = { query ->
searchQuery = query
executeSearch(query)
},
) {
if (
normalizedSearchQuery.isNotBlank() && normalizedSearchQuery == lastExecutedSearchQuery
) {
SearchResultsList(
lazyPagingItems = searchResults,
listState = searchResultsListState,
postActions = postActions,
filteredTags = filteredTags,
contentPadding = PaddingValues(bottom = searchResultsBottomPadding),
)
}
}
},
bottomBar = {
val currentDestination = backStack.lastOrNull()
AnimatedVisibility(visible = navigationType == ClawNavigationType.BOTTOM_NAVIGATION) {
ClawNavigationBar(
items = navItems,
currentNavKey = currentDestination,
navigateTo = { destination -> navigateTo(backStack, destination) },
isVisible = backStack.lastOrNull() is TopLevelDestination,
navigateTo = { destination -> navigateToDestination(destination, false) },
isVisible = currentDestinationIsTopLevel && !isSearchActive,
hazeState = hazeState,
)
}
@@ -172,12 +325,11 @@ fun LobstersPostsScreen(
SentryNavigation3Traced(backStack = backStack)
Row {
AnimatedVisibility(visible = navigationType == ClawNavigationType.NAVIGATION_RAIL) {
val currentDestination = backStack.lastOrNull()
ClawNavigationRail(
items = navItems,
currentNavKey = currentDestination,
navigateTo = { destination -> navigateTo(backStack, destination) },
isVisible = backStack.lastOrNull() is TopLevelDestination,
navigateTo = { destination -> navigateToDestination(destination, false) },
isVisible = currentDestinationIsTopLevel && !isSearchActive,
)
}
NavDisplay(
@@ -199,7 +351,7 @@ fun LobstersPostsScreen(
entry<Hottest>(
metadata = ListDetailSceneStrategy.listPane(detailPlaceholder = { Placeholder() })
) {
setWebUri("https://lobste.rs/")
setWebUri(HOTTEST_WEB_URI)
val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems()
NetworkPosts(
lazyPagingItems = hottestPosts,
@@ -212,7 +364,7 @@ fun LobstersPostsScreen(
entry<Newest>(
metadata = ListDetailSceneStrategy.listPane(detailPlaceholder = { Placeholder() })
) {
setWebUri("https://lobste.rs/")
setWebUri(NEWEST_WEB_URI)
val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems()
NetworkPosts(
lazyPagingItems = newestPosts,
@@ -234,16 +386,29 @@ fun LobstersPostsScreen(
)
}
entry<Comments>(metadata = ListDetailSceneStrategy.detailPane()) { dest ->
val commentsPostActions = remember {
PostActions(context, uriHandler, viewModel) {
navigateTo(backStack, Comments(it), allowStacking = true)
val commentsPostActions =
remember(isSearchActive) {
PostActions(context, uriHandler, viewModel) {
navigateToDestination(Comments(it), true)
}
}
}
CommentsPage(
postId = dest.postId,
postActions = commentsPostActions,
contentPadding = contentPadding,
openUserProfile = { navigateTo(backStack, User(it)) },
openUserProfile = { navigateToDestination(User(it), false) },
openReplyScreen = { postId, commentId ->
navigateToDestination(Reply(postId, commentId), true)
},
)
}
entry<Reply>(metadata = ListDetailSceneStrategy.extraPane()) { dest ->
ReplyScreen(
commentId = dest.commentId,
contentPadding = contentPadding,
postId = dest.postId,
onReplySubmitted = { popBackStack(backStack) },
modifier = Modifier.fillMaxSize(),
)
}
entry<User>(
@@ -264,18 +429,22 @@ fun LobstersPostsScreen(
UserProfile(
username = dest.username,
contentPadding = contentPadding,
openUserProfile = { navigateTo(backStack, User(it)) },
openUserProfile = { navigateToDestination(User(it), false) },
)
}
entry<Settings>(metadata = ListDetailSceneStrategy.extraPane()) {
SettingsScreen(
openInputStream = context.contentResolver::openInputStream,
openOutputStream = context.contentResolver::openOutputStream,
openLibrariesScreen = { navigateTo(backStack, AboutLibraries) },
openLibrariesScreen = { navigateToDestination(AboutLibraries, false) },
openRepository = {
uriHandler.openUri("https://github.com/msfjarvis/compose-lobsters")
},
openTagFiltering = { navigateTo(backStack, TagFiltering) },
openTagFiltering = { navigateToDestination(TagFiltering, false) },
openLoginScreen = { navigateToDestination(Login, false) },
onLogout = { settingsViewModel.logout() },
isLoggedIn = isLoggedIn,
username = username,
importPosts = viewModel::importPosts,
exportPostsAsJson = viewModel::exportPostsAsJson,
exportPostsAsHtml = viewModel::exportPostsAsHtml,
@@ -285,12 +454,11 @@ fun LobstersPostsScreen(
modifier = Modifier.fillMaxSize(),
)
}
entry<Search>(metadata = ListDetailSceneStrategy.extraPane()) {
setWebUri("https://lobste.rs/search")
SearchScreen(
viewModel = viewModel,
postActions = postActions,
contentPadding = contentPadding,
entry<Login>(metadata = ListDetailSceneStrategy.extraPane()) {
LoginScreen(
onLoginSuccess = settingsViewModel::saveCookie,
popBackStack = { popBackStack(backStack) },
modifier = Modifier.fillMaxSize(),
)
}
entry<AboutLibraries>(metadata = ListDetailSceneStrategy.extraPane()) {
@@ -300,7 +468,12 @@ fun LobstersPostsScreen(
modifier = Modifier.fillMaxSize(),
)
}
entry<TagFiltering> { TagList(contentPadding = contentPadding) }
entry<TagFiltering> {
TagList(
popBackStack = { popBackStack(backStack) },
contentPadding = contentPadding,
)
}
},
)
}
@@ -0,0 +1,131 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.android.ui.screens
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.webkit.CookieManager
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
private const val LOGIN_URL = "https://lobste.rs/login"
private const val LOBSTERS_URL = "https://lobste.rs"
internal fun parseAuthenticatedUsername(usernameResult: String): String? {
return usernameResult.removeSurrounding("\"").takeUnless { it.isBlank() || it == "null" }
}
@Composable
fun LoginScreen(
onLoginSuccess: (String, String) -> Unit,
popBackStack: () -> Unit,
modifier: Modifier = Modifier,
) {
var isLoading by remember { mutableStateOf(true) }
var hasError by remember { mutableStateOf(false) }
var webViewRef by remember { mutableStateOf<WebView?>(null) }
Scaffold(modifier = modifier) { contentPadding ->
Box(modifier = Modifier.padding(contentPadding).fillMaxSize()) {
if (hasError) {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text("Failed to load login page.")
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = {
hasError = false
isLoading = true
webViewRef?.loadUrl(LOGIN_URL)
}
) {
Text("Retry")
}
}
} else {
AndroidView(
factory = { context ->
WebView(context)
.apply {
@SuppressLint("SetJavaScriptEnabled")
settings.javaScriptEnabled = true
webViewClient =
object : WebViewClient() {
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
isLoading = true
}
override fun onPageFinished(view: WebView, url: String) {
isLoading = false
if (!url.contains("/login")) {
@SuppressLint("DeprecatedCall")
view.evaluateJavascript(
"document.body && document.body.getAttribute('data-username')"
) { usernameResult ->
val username = parseAuthenticatedUsername(usernameResult)
if (username != null) {
val cookie = CookieManager.getInstance().getCookie(LOBSTERS_URL)
if (cookie != null) {
onLoginSuccess(cookie, username)
popBackStack()
}
}
}
}
}
override fun onReceivedError(
view: WebView,
request: WebResourceRequest,
error: WebResourceError,
) {
if (request.isForMainFrame) {
isLoading = false
hasError = true
}
}
}
loadUrl(LOGIN_URL)
}
.also { webViewRef = it }
},
modifier = Modifier.fillMaxSize(),
)
}
if (isLoading && !hasError) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth().align(Alignment.TopCenter))
}
}
}
}
@@ -1,45 +0,0 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.android.ui.screens
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.compose.collectAsLazyPagingItems
import dev.msfjarvis.claw.android.ui.lists.SearchList
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
import dev.msfjarvis.claw.common.posts.PostActions
import dev.msfjarvis.claw.common.tags.TagFilterViewModel
import dev.zacsweers.metrox.viewmodel.metroViewModel
import kotlinx.collections.immutable.toPersistentSet
@Composable
fun SearchScreen(
postActions: PostActions,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
viewModel: ClawViewModel = metroViewModel(),
tagFilterViewModel: TagFilterViewModel = metroViewModel(key = "tag_filter"),
) {
val listState = rememberLazyListState()
val searchResults = viewModel.searchResults.collectAsLazyPagingItems()
val filteredTags by tagFilterViewModel.filteredTags.collectAsStateWithLifecycle(emptySet())
SearchList(
lazyPagingItems = searchResults,
listState = listState,
postActions = postActions,
searchQuery = viewModel.searchQuery,
contentPadding = contentPadding,
setSearchQuery = { query -> viewModel.searchQuery = query },
filteredTags = filteredTags.toPersistentSet(),
modifier = modifier,
)
}
@@ -17,8 +17,11 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.LibraryBooks
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Bookmarks
import androidx.compose.material.icons.filled.Code
import androidx.compose.material.icons.filled.FilterList
@@ -54,11 +57,23 @@ import kotlinx.coroutines.launch
private const val JSON_MIME_TYPE = "application/json"
private const val HTML_MIME_TYPE = "application/html"
internal fun loggedInAccountText(username: String?): String {
return if (username.isNullOrBlank()) {
"Sign out of your lobste.rs account"
} else {
"Logged in as $username"
}
}
@Composable
fun SettingsScreen(
openLibrariesScreen: () -> Unit,
openRepository: () -> Unit,
openTagFiltering: () -> Unit,
openLoginScreen: () -> Unit,
onLogout: () -> Unit,
isLoggedIn: Boolean,
username: String?,
snackbarHostState: SnackbarHostState,
openInputStream: (Uri) -> InputStream?,
openOutputStream: (Uri) -> OutputStream?,
@@ -70,11 +85,43 @@ fun SettingsScreen(
modifier: Modifier = Modifier,
) {
val coroutineScope = rememberCoroutineScope()
Column(modifier.padding(contentPadding)) {
Column(modifier.padding(contentPadding).verticalScroll(rememberScrollState())) {
// Account Section
SectionHeader(title = "Account")
if (isLoggedIn && !username.isNullOrBlank()) {
ListItem(
supportingContent = { Text(loggedInAccountText(username)) },
leadingContent = {
Icon(
imageVector = Icons.Filled.AccountCircle,
contentDescription = null,
modifier = Modifier.height(32.dp),
)
},
modifier = Modifier.clickable(onClick = onLogout),
) {
Text("Log out")
}
} else {
ListItem(
supportingContent = { Text("Sign in to your lobste.rs account") },
leadingContent = {
Icon(
imageVector = Icons.Filled.AccountCircle,
contentDescription = null,
modifier = Modifier.height(32.dp),
)
},
modifier = Modifier.clickable(onClick = openLoginScreen),
) {
Text("Log in with lobste.rs")
}
}
Spacer(modifier = Modifier.height(24.dp))
// Data Management Section
SectionHeader(title = stringResource(R.string.data_management))
ListItem(
headlineContent = { Text(stringResource(R.string.data_transfer)) },
supportingContent = { Text(stringResource(R.string.import_and_export_your_saved_posts)) },
leadingContent = {
Icon(
@@ -95,14 +142,15 @@ fun SettingsScreen(
)
}
},
)
) {
Text(stringResource(R.string.data_transfer))
}
Spacer(modifier = Modifier.height(24.dp))
// App Information Section
SectionHeader(title = stringResource(R.string.app_information))
ListItem(
headlineContent = { Text(stringResource(R.string.version)) },
supportingContent = {
Text(
stringResource(
@@ -119,9 +167,10 @@ fun SettingsScreen(
modifier = Modifier.height(32.dp),
)
},
)
) {
Text(stringResource(R.string.version))
}
ListItem(
headlineContent = { Text(stringResource(R.string.saved_posts)) },
supportingContent = { Text(stringResource(R.string.posts_saved_locally, savedPostsCount)) },
leadingContent = {
Icon(
@@ -130,9 +179,10 @@ fun SettingsScreen(
modifier = Modifier.height(32.dp),
)
},
)
) {
Text(stringResource(R.string.saved_posts))
}
ListItem(
headlineContent = { Text(stringResource(R.string.tag_filtering)) },
supportingContent = { Text(stringResource(R.string.filter_posts_by_tags)) },
leadingContent = {
Icon(
@@ -142,14 +192,15 @@ fun SettingsScreen(
)
},
modifier = Modifier.clickable(onClick = openTagFiltering),
)
) {
Text(stringResource(R.string.tag_filtering))
}
Spacer(modifier = Modifier.height(24.dp))
// About Section
SectionHeader(title = stringResource(R.string.about))
ListItem(
headlineContent = { Text(stringResource(R.string.libraries)) },
supportingContent = {
Text(stringResource(R.string.view_open_source_libraries_used_in_this_))
},
@@ -161,9 +212,10 @@ fun SettingsScreen(
)
},
modifier = Modifier.clickable(onClick = openLibrariesScreen),
)
) {
Text(stringResource(R.string.libraries))
}
ListItem(
headlineContent = { Text(stringResource(R.string.source_code)) },
supportingContent = { Text(stringResource(R.string.view_the_source_code_on_github)) },
leadingContent = {
Icon(
@@ -173,7 +225,9 @@ fun SettingsScreen(
)
},
modifier = Modifier.clickable(onClick = openRepository),
)
) {
Text(stringResource(R.string.source_code))
}
}
}
@@ -343,6 +397,10 @@ private fun SettingsScreenPreview() {
openLibrariesScreen = {},
openRepository = {},
openTagFiltering = {},
openLoginScreen = {},
onLogout = {},
isLoggedIn = false,
username = null,
snackbarHostState = SnackbarHostState(),
openInputStream = { null },
openOutputStream = { null },
@@ -7,8 +7,6 @@
package dev.msfjarvis.claw.android.viewmodel
import android.util.Log
import app.cash.sqldelight.coroutines.asFlow
import app.cash.sqldelight.coroutines.mapToList
import dev.msfjarvis.claw.android.BuildConfig
import dev.msfjarvis.claw.core.coroutines.DatabaseReadDispatcher
import dev.msfjarvis.claw.core.coroutines.DatabaseWriteDispatcher
@@ -24,15 +22,13 @@ class CachedRemotePostsRepository(
@param:DatabaseReadDispatcher private val readDispatcher: CoroutineDispatcher,
@param:DatabaseWriteDispatcher private val writeDispatcher: CoroutineDispatcher,
) {
val cachedPosts = cachedRemotePostQueries.selectAllPosts().asFlow().mapToList(readDispatcher)
fun getRecentPosts(limit: Long) =
cachedRemotePostQueries.selectRecentPosts(limit).asFlow().mapToList(readDispatcher)
suspend fun getCachedPosts(): List<CachedRemotePost> {
return withContext(readDispatcher) { cachedRemotePostQueries.selectAllPosts().executeAsList() }
}
suspend fun getRecentPosts(limit: Long): List<CachedRemotePost> =
withContext(readDispatcher) { cachedRemotePostQueries.selectRecentPosts(limit).executeAsList() }
suspend fun savePosts(posts: List<CachedRemotePost>) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Caching remote posts: ${posts.joinToString(",") { it.shortId }}")
@@ -38,9 +38,8 @@ import dev.zacsweers.metro.binding
import dev.zacsweers.metrox.viewmodel.ViewModelKey
import java.io.InputStream
import java.io.OutputStream
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import kotlin.time.Clock
import kotlin.time.Instant
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
@@ -48,6 +47,9 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
@Inject
@ViewModelKey
@@ -72,7 +74,9 @@ class ClawViewModel(
Pager(
config = PagingConfig(pageSize = PAGE_SIZE),
initialKey = STARTING_PAGE_INDEX,
pagingSourceFactory = { pagingSourceFactory.create(api::getHottestPosts) },
pagingSourceFactory = {
pagingSourceFactory.create { page -> api.getHottestPosts(page) }
},
)
.flow
.cachedIn(viewModelScope)
@@ -88,7 +92,9 @@ class ClawViewModel(
Pager(
config = PagingConfig(pageSize = PAGE_SIZE),
initialKey = STARTING_PAGE_INDEX,
pagingSourceFactory = { pagingSourceFactory.create(api::getNewestPosts) },
pagingSourceFactory = {
pagingSourceFactory.create { page -> api.getNewestPosts(page) }
},
)
.flow
.cachedIn(viewModelScope)
@@ -104,7 +110,7 @@ class ClawViewModel(
)
.flow
val savedPosts = savedPostsRepository.savedPosts.map { it.map(UIPost.Companion::fromSavedPost) }
val savedPostsCount = savedPostsRepository.savedPosts.map { it.size.toLong() }
val savedPostsCount = savedPostsRepository.savedPostsCount
val savedPostsByMonth
get() =
savedPostsRepository.savedPostsSortedByDate.map { posts ->
@@ -124,10 +130,11 @@ class ClawViewModel(
init {
viewModelScope.launch {
savedPosts.collectLatest { _savedPosts = it.map(UIPost::shortId).toSet() }
savedPostsRepository.savedPostIds.collectLatest { _savedPosts = it.toSet() }
}
viewModelScope.launch {
readPostsRepository.readPosts.collectLatest { _readPosts = it.toSet() }
readPostsRepository.initialize()
readPostsRepository.readPosts.collectLatest { _readPosts = it }
}
}
@@ -168,7 +175,7 @@ class ClawViewModel(
* solution.
*/
private fun String.toLocalDateTime(): LocalDateTime {
if (isEmpty()) return LocalDateTime.now(ZoneId.systemDefault())
return LocalDateTime.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(this))
if (isEmpty()) return Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
return Instant.parse(this).toLocalDateTime(TimeZone.currentSystemDefault())
}
}
@@ -12,10 +12,8 @@ import dev.msfjarvis.claw.database.local.SavedPost
import dev.zacsweers.metro.Inject
import java.io.InputStream
import java.io.OutputStream
import java.time.Instant
import java.time.format.DateTimeFormatter
import kotlin.time.Instant
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.builtins.ListSerializer
@@ -44,18 +42,15 @@ class DataTransferRepository(
}
suspend fun exportPostsAsJson(output: OutputStream) {
val posts = savedPostsRepository.savedPosts.first()
val posts = savedPostsRepository.getSavedPosts()
withContext(ioDispatcher) { json.encodeToStream(serializer, posts, output) }
}
suspend fun exportPostsAsHTML(output: OutputStream) {
fun computeTimestamp(post: SavedPost): Long {
val temporal = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(post.createdAt)
val instant = Instant.from(temporal)
return instant.toEpochMilli()
}
fun computeTimestamp(post: SavedPost): Long =
Instant.parse(post.createdAt).toEpochMilliseconds()
val posts = savedPostsRepository.savedPosts.first()
val posts = savedPostsRepository.getSavedPosts()
val header =
"""
<!DOCTYPE NETSCAPE-Bookmark-file-1>
@@ -6,24 +6,52 @@
*/
package dev.msfjarvis.claw.android.viewmodel
import app.cash.sqldelight.coroutines.asFlow
import app.cash.sqldelight.coroutines.mapToList
import dev.msfjarvis.claw.core.coroutines.DatabaseReadDispatcher
import dev.msfjarvis.claw.core.coroutines.DatabaseWriteDispatcher
import dev.msfjarvis.claw.database.local.ReadPostsQueries
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
@Inject
@SingleIn(AppScope::class)
class ReadPostsRepository(
private val readPostsQueries: ReadPostsQueries,
@param:DatabaseReadDispatcher private val readDispatcher: CoroutineDispatcher,
@param:DatabaseWriteDispatcher private val writeDispatcher: CoroutineDispatcher,
) {
val readPosts = readPostsQueries.selectAllPosts().asFlow().mapToList(readDispatcher)
private val initializationMutex = Mutex()
private val _readPosts = MutableStateFlow(emptySet<String>())
val readPosts: StateFlow<Set<String>> = _readPosts.asStateFlow()
private var initialized = false
suspend fun initialize() {
initializationMutex.withLock { initializeLocked() }
}
suspend fun markRead(postId: String) {
withContext(writeDispatcher) { readPostsQueries.markRead(postId) }
initializationMutex.withLock {
initializeLocked()
withContext(writeDispatcher) {
readPostsQueries.markRead(postId).executeAsOneOrNull()?.let { markedId ->
_readPosts.update { it + markedId }
}
}
}
}
private suspend fun initializeLocked() {
if (initialized) return
_readPosts.value =
withContext(readDispatcher) { readPostsQueries.selectAllPosts().executeAsList().toSet() }
initialized = true
}
}
@@ -9,6 +9,7 @@ package dev.msfjarvis.claw.android.viewmodel
import android.util.Log
import app.cash.sqldelight.coroutines.asFlow
import app.cash.sqldelight.coroutines.mapToList
import app.cash.sqldelight.coroutines.mapToOne
import dev.msfjarvis.claw.android.BuildConfig
import dev.msfjarvis.claw.core.coroutines.DatabaseReadDispatcher
import dev.msfjarvis.claw.core.coroutines.DatabaseWriteDispatcher
@@ -29,26 +30,31 @@ class SavedPostsRepository(
val savedPosts = savedPostQueries.selectAllPosts().asFlow().mapToList(readDispatcher)
val savedPostsSortedByDate =
savedPostQueries.selectAllPostsSortedByDate().asFlow().mapToList(readDispatcher)
val savedPostsCount = savedPostQueries.selectCount().asFlow().mapToOne(readDispatcher)
val savedPostIds = savedPostQueries.selectPostIds().asFlow().mapToList(readDispatcher)
fun getPostsFromLastNDays(days: Long) =
savedPostQueries.selectPostsFromLastNDays(days.toString()).asFlow().mapToList(readDispatcher)
suspend fun getSavedPosts() =
withContext(readDispatcher) { savedPostQueries.selectAllPosts().executeAsList() }
fun getRecentPosts(limit: Long) =
savedPostQueries.selectRecentPosts(limit).asFlow().mapToList(readDispatcher)
suspend fun getPostIdsFromLastNDays(days: Long) =
withContext(readDispatcher) {
savedPostQueries.selectPostIdsFromLastNDays(days.toString()).executeAsList()
}
suspend fun getRecentPosts(limit: Long) =
withContext(readDispatcher) { savedPostQueries.selectRecentPosts(limit).executeAsList() }
suspend fun toggleSave(post: UIPost) {
val exists =
withContext(readDispatcher) { savedPostQueries.postExists(post.shortId).executeAsOne() }
if (exists) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Removing post: ${post.shortId}")
withContext(writeDispatcher) {
savedPostQueries.transaction {
val removed = savedPostQueries.deletePost(post.shortId).executeAsOneOrNull() != null
if (removed) {
if (BuildConfig.DEBUG) Log.d(TAG, "Removing post: ${post.shortId}")
} else {
if (BuildConfig.DEBUG) Log.d(TAG, "Saving post: ${post.shortId}")
savedPostQueries.insertOrReplacePost(post.toSavedPost())
}
}
withContext(writeDispatcher) { savedPostQueries.deletePost(post.shortId) }
} else {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Saving post: ${post.shortId}")
}
withContext(writeDispatcher) { savedPostQueries.insertOrReplacePost(post.toSavedPost()) }
}
}
@@ -0,0 +1,54 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.msfjarvis.claw.core.network.SessionCookieStore
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.binding
import dev.zacsweers.metrox.viewmodel.ViewModelKey
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
@Inject
@ViewModelKey
@ContributesIntoMap(scope = AppScope::class, binding = binding<ViewModel>())
class SettingsViewModel(
private val sessionCookieStore: SessionCookieStore,
private val webViewCookieStore: WebViewCookieStore,
) : ViewModel() {
val isLoggedIn =
sessionCookieStore
.isLoggedIn()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = false,
)
val username =
sessionCookieStore
.username()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = null,
)
fun logout() {
sessionCookieStore.clear()
webViewCookieStore.clearLobstersCookies()
}
fun saveCookie(cookie: String, username: String) {
sessionCookieStore.set(cookie, username)
}
}
@@ -0,0 +1,25 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.android.viewmodel
import android.webkit.CookieManager
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
interface WebViewCookieStore {
fun clearLobstersCookies()
}
@Inject
@ContributesBinding(AppScope::class)
class AndroidWebViewCookieStore : WebViewCookieStore {
override fun clearLobstersCookies() {
CookieManager.getInstance().removeAllCookies(null)
CookieManager.getInstance().flush()
}
}
@@ -0,0 +1,80 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.android.work
import android.content.Context
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.work.CoroutineWorker
import androidx.work.OneTimeWorkRequest
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import dev.msfjarvis.claw.android.BuildConfig
import dev.msfjarvis.claw.android.glance.SavedPostsWidgetReceiver
import dev.msfjarvis.claw.android.glance.generatedWidgetPreviewDelayMinutes
import dev.msfjarvis.claw.android.injection.InjectedWorkerFactory
import dev.msfjarvis.claw.android.injection.WorkerKey
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.binding
import java.util.concurrent.TimeUnit
@AssistedInject
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
class GeneratedWidgetPreviewUpdateWorker(
context: Context,
@Assisted params: WorkerParameters,
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
try {
val result =
GlanceAppWidgetManager(applicationContext)
.setWidgetPreviews(SavedPostsWidgetReceiver::class)
if (BuildConfig.DEBUG) {
Log.d("WidgetPreviewUpdateWorker", "GlanceAppWidgetManager#setWidgetPreviews -> $result")
}
} catch (_: IllegalArgumentException) {
// Workaround for Motorola Android 15 bug where AppWidgetServiceImpl incorrectly
// reports registered widgets as invalid when setting previews.
// See: https://claw.sentry.io/share/issue/f8ee09821d8840b9b86293d3ffb627d3/
}
return Result.success()
}
companion object {
const val UNIQUE_WORK_NAME = "generatedWidgetPreviewUpdate"
fun enqueue(workManager: WorkManager) {
workManager.enqueueUniqueWork(
UNIQUE_WORK_NAME,
androidx.work.ExistingWorkPolicy.KEEP,
request(),
)
}
fun request(): OneTimeWorkRequest {
return OneTimeWorkRequestBuilder<GeneratedWidgetPreviewUpdateWorker>()
.setInitialDelay(generatedWidgetPreviewDelayMinutes(), TimeUnit.MINUTES)
.build()
}
}
@WorkerKey(GeneratedWidgetPreviewUpdateWorker::class)
@ContributesIntoMap(
AppScope::class,
binding = binding<InjectedWorkerFactory.WorkerInstanceFactory<*>>(),
)
@AssistedFactory
abstract class Factory :
InjectedWorkerFactory.WorkerInstanceFactory<GeneratedWidgetPreviewUpdateWorker>
}
@@ -12,10 +12,10 @@ import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.slack.eithernet.ApiResult.Success
import dev.msfjarvis.claw.android.glance.SavedPostsWidget
import dev.msfjarvis.claw.android.injection.InjectedWorkerFactory
import dev.msfjarvis.claw.android.injection.WorkerKey
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
import dev.msfjarvis.claw.api.LobstersApi
import dev.msfjarvis.claw.core.injection.InjectedWorkerFactory
import dev.msfjarvis.claw.core.injection.WorkerKey
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.toSavedPost
import dev.zacsweers.metro.AppScope
@@ -26,7 +26,6 @@ import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.binding
import kotlin.random.Random
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
/**
* WorkManager-backed [CoroutineWorker] that gets all the posts from [SavedPostsRepository] that
@@ -43,7 +42,7 @@ class SavedPostUpdaterWorker(
private val lobstersApi: LobstersApi,
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val postsToUpdate = savedPostsRepository.getPostsFromLastNDays(DAYS_TO_UPDATE).first()
val postsToUpdate = savedPostsRepository.getPostIdsFromLastNDays(DAYS_TO_UPDATE)
if (postsToUpdate.isEmpty()) {
return Result.success()
@@ -52,7 +51,7 @@ class SavedPostUpdaterWorker(
val updatedPosts = mutableListOf<SavedPost>()
for ((index, post) in postsToUpdate.withIndex()) {
when (val result = lobstersApi.getPostDetails(post.shortId)) {
when (val result = lobstersApi.getPostDetails(post)) {
is Success -> {
updatedPosts.add(result.value.toSavedPost())
}
@@ -9,9 +9,9 @@ package dev.msfjarvis.claw.android.work
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import dev.msfjarvis.claw.android.injection.InjectedWorkerFactory
import dev.msfjarvis.claw.android.injection.WorkerKey
import dev.msfjarvis.claw.common.tags.TagBlockRepository
import dev.msfjarvis.claw.core.injection.InjectedWorkerFactory
import dev.msfjarvis.claw.core.injection.WorkerKey
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
@@ -0,0 +1,232 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.android.zipline
import android.content.Context
import app.cash.zipline.Zipline
import app.cash.zipline.ZiplineManifest
import app.cash.zipline.loader.DefaultFreshnessCheckerNotFresh
import app.cash.zipline.loader.FreshnessChecker
import app.cash.zipline.loader.LoadResult
import app.cash.zipline.loader.ManifestVerifier
import app.cash.zipline.loader.ZiplineLoader
import dev.msfjarvis.claw.android.BuildConfig
import dev.msfjarvis.claw.api.LobstersParserClient
import dev.msfjarvis.claw.parser.LobstersParserService
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.binding
import java.io.File
import java.io.IOException
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicReference
import kotlinx.coroutines.ExecutorCoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okio.ByteString.Companion.decodeHex
import okio.FileSystem
import okio.Path.Companion.toPath
internal object EmbeddedAssetsFreshnessChecker : FreshnessChecker {
override fun isFresh(
manifest: ZiplineManifest,
freshAtEpochMs: Long,
): Boolean = true
}
internal suspend fun loadWithEmbeddedFallback(
initialFreshnessChecker: FreshnessChecker,
load: suspend (FreshnessChecker) -> LoadResult,
): LoadResult {
val initialResult = load(initialFreshnessChecker)
return if (initialResult is LoadResult.Failure && initialResult.exception is IOException) {
load(EmbeddedAssetsFreshnessChecker)
} else {
initialResult
}
}
@Inject
@ContributesBinding(AppScope::class, binding = binding<LobstersParserClient>())
class AndroidZiplineParserClient(
private val context: Context,
private val httpClient: OkHttpClient,
private val manifestUrl: String = BuildConfig.ZIPLINE_PARSER_MANIFEST_URL,
private val verifySignatures: Boolean = BuildConfig.ZIPLINE_PARSER_VERIFY_SIGNATURES,
) : LobstersParserClient, AutoCloseable {
private val ziplineThread = AtomicReference<Thread>()
private val dispatcher: ExecutorCoroutineDispatcher =
Executors.newSingleThreadExecutor { runnable ->
Thread(
null,
{
ziplineThread.set(Thread.currentThread())
runnable.run()
},
"Claw-ZiplineParser",
ZIPLINE_THREAD_STACK_SIZE_BYTES,
)
}
.asCoroutineDispatcher()
private val mutex = Mutex()
private var loadedZipline: Zipline? = null
private var loadedService: LobstersParserService? = null
override suspend fun service(): LobstersParserService {
loadedService?.let {
return it
}
return mutex.withLock { loadedService ?: loadService().also { loadedService = it } }
}
override fun close() {
loadedService?.close()
loadedService = null
loadedZipline?.let { zipline -> onZiplineThread { zipline.close() } }
loadedZipline = null
dispatcher.close()
}
private suspend fun loadService(): LobstersParserService {
extractEmbeddedArtifacts()
val embeddedDir = File(context.codeCacheDir, "zipline-embedded/zipline")
val embeddedManifestFile = File(embeddedDir, "zipline-parser.manifest.zipline.json")
evictManifestCacheEntry()
val manifestVerifier =
if (verifySignatures) {
ManifestVerifier.Builder()
.addEd25519(
"key0",
"c960a123f76e0312cc05d9e358377b82d1e655d9569e9cff5f4834b1c4f228ce".decodeHex(),
)
.build()
} else {
ManifestVerifier.NO_SIGNATURE_CHECKS
}
val loader =
ZiplineLoader(
dispatcher = dispatcher,
manifestVerifier = manifestVerifier,
httpClient = httpClient,
)
.withEmbedded(
embeddedFileSystem = FileSystem.SYSTEM,
embeddedDir = embeddedDir.absolutePath.toPath(),
)
val freshnessChecker =
if (verifySignatures) {
DefaultFreshnessCheckerNotFresh
} else {
EmbeddedAssetsFreshnessChecker
}
val effectiveManifestUrl =
if (!verifySignatures && embeddedManifestFile.exists()) {
embeddedManifestFile.toURI().toString()
} else {
manifestUrl
}
return withContext(dispatcher) {
when (
val result =
loadWithEmbeddedFallback(freshnessChecker) { attemptFreshnessChecker ->
loader.loadOnce(
applicationName = "zipline-parser",
freshnessChecker = attemptFreshnessChecker,
manifestUrl = effectiveManifestUrl,
)
}
) {
is LoadResult.Success -> {
loadedZipline = result.zipline
DispatcherConfinedLobstersParserService(result.zipline.take("LobstersParserService"))
}
is LoadResult.Failure -> throw result.exception
}
}
}
internal fun <T> onZiplineThread(block: () -> T): T {
return if (Thread.currentThread() === ziplineThread.get()) {
block()
} else {
runBlocking(dispatcher) { block() }
}
}
private inner class DispatcherConfinedLobstersParserService(
private val delegate: LobstersParserService
) : LobstersParserService {
override fun parsePostsPage(html: String) = onZiplineThread { delegate.parsePostsPage(html) }
override fun parsePostDetails(html: String) = onZiplineThread {
delegate.parsePostDetails(html)
}
override fun parseUser(html: String) = onZiplineThread { delegate.parseUser(html) }
override fun parseTagsPage(html: String) = onZiplineThread { delegate.parseTagsPage(html) }
override fun parseSearchResults(html: String) = onZiplineThread {
delegate.parseSearchResults(html)
}
override fun parseCsrfToken(html: String) = onZiplineThread { delegate.parseCsrfToken(html) }
override fun parseReplyForm(html: String) = onZiplineThread { delegate.parseReplyForm(html) }
override fun parseFiltersPage(html: String) = onZiplineThread {
delegate.parseFiltersPage(html)
}
override fun close() = onZiplineThread { delegate.close() }
}
// Workaround for the pinned /current manifest being served without the right
// Cache-Control headers.
private fun evictManifestCacheEntry() {
val cache = httpClient.cache ?: return
val urls = cache.urls()
while (urls.hasNext()) {
if (urls.next() == manifestUrl) {
urls.remove()
return
}
}
}
private fun extractEmbeddedArtifacts() {
val assets = context.assets
val ziplineAssets = assets.list("zipline").orEmpty()
if (ziplineAssets.isEmpty()) return
val embeddedDir = File(context.codeCacheDir, "zipline-embedded/zipline")
embeddedDir.mkdirs()
for (assetName in ziplineAssets) {
assets.open("zipline/$assetName").use { input ->
File(embeddedDir, assetName).outputStream().use { output ->
input.copyTo(output)
}
}
}
}
private companion object {
private const val ZIPLINE_THREAD_STACK_SIZE_BYTES = 8L * 1024L * 1024L
}
}
+2
View File
@@ -38,7 +38,9 @@
<string name="go_back_to_previous_screen">Go back to previous screen</string>
<string name="the_app_icon_for_claw">The app icon for Claw</string>
<string name="search_posts">Search posts</string>
<string name="close_search">Close search</string>
<string name="settings">Settings</string>
<string name="empty_inbox_icon">Empty inbox icon</string>
<string name="no_saved_posts">No saved posts</string>
<string name="no_search_results">No search results</string>
</resources>
@@ -0,0 +1,79 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.android.ui.decorations
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.android.tools.screenshot.PreviewTest
import dev.msfjarvis.claw.common.theme.LobstersTheme
@PreviewTest
@Preview(name = "Browsing")
@Composable
private fun ClawAppBarBrowsingPreviewScreenshot() {
LobstersTheme {
ClawAppBar(
activity = null,
isTopLevel = true,
mode = ClawTopBarMode.Browsing,
navigateTo = {},
popBackStack = { null },
onStartSearch = {},
onDismissSearch = {},
onQueryChange = {},
onSearch = {},
content = { Text("Fake result content") },
)
}
}
@PreviewTest
@Preview(name = "Searching")
@Composable
private fun ClawAppBarSearchingPreviewScreenshot() {
LobstersTheme {
ClawAppBar(
activity = null,
isTopLevel = true,
mode = ClawTopBarMode.Searching(query = "compose", requestFocus = false),
navigateTo = {},
popBackStack = { null },
onStartSearch = {},
onDismissSearch = {},
onQueryChange = {},
onSearch = {},
content = { Text("Fake result content") },
)
}
}
@PreviewTest
@Preview(name = "SearchingWithResults")
@Composable
private fun ClawAppBarSearchingWithResultsPreviewScreenshot() {
LobstersTheme {
ClawAppBar(
activity = null,
isTopLevel = true,
mode = ClawTopBarMode.Searching(query = "compose", requestFocus = false),
navigateTo = {},
popBackStack = { null },
onStartSearch = {},
onDismissSearch = {},
onQueryChange = {},
onSearch = {},
content = {
Column {
Text("Result 1")
Text("Result 2")
}
},
)
}
}
@@ -0,0 +1,27 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.android.glance
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Test
class GeneratedWidgetPreviewSupportTest {
@Test
fun `generated widget preview posts stay lightweight`() {
val posts = generatedWidgetPreviewPosts()
assertThat(posts).hasSize(2)
assertThat(posts.all { it.title.length <= 48 }).isTrue()
assertThat(posts.all { it.description.isEmpty() }).isTrue()
}
@Test
fun `generated widget preview registration is deferred`() {
assertThat(generatedWidgetPreviewDelayMinutes()).isGreaterThan(0)
}
}
@@ -68,4 +68,25 @@ class BackStackBehaviorTest {
.containsExactly(Hottest, Comments("abc123"), Comments("def456"), Comments("abc123"))
.inOrder()
}
@Test
fun `back closes active top level search before popping navigation`() {
val result = handleTopLevelBack(isSearchActive = true, isCurrentDestinationTopLevel = true)
assertThat(result).isEqualTo(TopLevelBackAction.DismissSearch)
}
@Test
fun `back pops navigation when search is inactive`() {
val result = handleTopLevelBack(isSearchActive = false, isCurrentDestinationTopLevel = true)
assertThat(result).isEqualTo(TopLevelBackAction.PopNavigation)
}
@Test
fun `back pops navigation when hidden search state exists on non top level destination`() {
val result = handleTopLevelBack(isSearchActive = true, isCurrentDestinationTopLevel = false)
assertThat(result).isEqualTo(TopLevelBackAction.PopNavigation)
}
}
@@ -0,0 +1,72 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.android.viewmodel
import com.google.common.truth.Truth.assertThat
import dev.msfjarvis.claw.core.network.SessionCookieStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.junit.jupiter.api.Test
class SettingsViewModelTest {
@Test
fun `logout clears persisted and webview sessions`() {
val sessionCookieStore = FakeSessionCookieStore()
val webViewCookieStore = FakeWebViewCookieStore()
val viewModel = SettingsViewModel(sessionCookieStore, webViewCookieStore)
viewModel.logout()
assertThat(sessionCookieStore.cleared).isTrue()
assertThat(webViewCookieStore.cleared).isTrue()
}
@Test
fun `saveCookie persists username`() {
val sessionCookieStore = FakeSessionCookieStore()
val viewModel = SettingsViewModel(sessionCookieStore, FakeWebViewCookieStore())
viewModel.saveCookie("cookie=value", "alice")
assertThat(sessionCookieStore.savedCookie).isEqualTo("cookie=value")
assertThat(sessionCookieStore.savedUsername).isEqualTo("alice")
}
private class FakeSessionCookieStore : SessionCookieStore {
var cleared = false
var savedCookie: String? = null
var savedUsername: String? = null
override fun get(): String? = savedCookie
override fun getUsername(): String? = savedUsername
override fun set(cookie: String, username: String) {
savedCookie = cookie
savedUsername = username
}
override fun clear() {
cleared = true
savedCookie = null
savedUsername = null
}
override fun isLoggedIn(): Flow<Boolean> = flowOf(savedUsername != null)
override fun username(): Flow<String?> = flowOf(savedUsername)
}
private class FakeWebViewCookieStore : WebViewCookieStore {
var cleared = false
override fun clearLobstersCookies() {
cleared = true
}
}
}
@@ -0,0 +1,41 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.android.zipline
import app.cash.zipline.loader.DefaultFreshnessCheckerNotFresh
import app.cash.zipline.loader.FreshnessChecker
import app.cash.zipline.loader.LoadResult
import com.google.common.truth.Truth.assertThat
import java.io.IOException
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class ZiplineLoadFallbackTest {
@Test
fun `retries from embedded assets after a network failure`() = runTest {
val attemptedFreshnessCheckers = mutableListOf<FreshnessChecker>()
val embeddedFailure =
LoadResult.Failure(IllegalStateException("embedded bundle is unavailable"))
val result =
loadWithEmbeddedFallback(DefaultFreshnessCheckerNotFresh) { freshnessChecker ->
attemptedFreshnessCheckers += freshnessChecker
if (freshnessChecker === DefaultFreshnessCheckerNotFresh) {
LoadResult.Failure(IOException("manifest server is inaccessible"))
} else {
embeddedFailure
}
}
assertEquals(embeddedFailure, result)
assertThat(attemptedFreshnessCheckers)
.containsExactly(DefaultFreshnessCheckerNotFresh, EmbeddedAssetsFreshnessChecker)
.inOrder()
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
#
# This file was automatically generated by 'versioning-plugin'. DO NOT EDIT MANUALLY.
#
versioning-plugin.versionCode=16300
versioning-plugin.versionName=1.63.0
versioning-plugin.versionCode=16900
versioning-plugin.versionName=1.69.0-SNAPSHOT
+6 -3
View File
@@ -8,23 +8,26 @@ import dev.msfjarvis.claw.gradle.addTestDependencies
plugins {
id("dev.msfjarvis.claw.kotlin-jvm")
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.dependencyAnalysis)
alias(libs.plugins.metro)
alias(libs.plugins.zipline)
}
dependencies {
api(libs.eithernet)
api(libs.kotlinx.serialization.core)
api(libs.okhttp.core)
api(libs.retrofit)
api(projects.model)
api(projects.ziplineParser)
implementation(libs.eithernet.integration.retrofit)
implementation(libs.jsoup)
implementation(libs.zipline.loader)
testImplementation(libs.eithernet.test.fixtures)
testImplementation(libs.kotlin.reflect)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.kotlinx.serialization.core)
testImplementation(libs.kotlinx.serialization.json)
testImplementation(libs.kotlinx.datetime)
addTestDependencies(project)
}
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 9.1.0" type="baseline" client="gradle" dependencies="true" name="AGP (9.1.0)" variant="all" version="9.1.0">
<issues format="6" by="lint 9.4.0-alpha03" type="baseline" client="gradle" dependencies="true" name="AGP (9.4.0-alpha03)" variant="all" version="9.4.0-alpha03">
</issues>
@@ -0,0 +1,123 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.api
import com.slack.eithernet.ApiResult
import com.slack.eithernet.ApiResult.Failure
import com.slack.eithernet.ApiResult.Success
import dev.msfjarvis.claw.model.FiltersPage
import dev.msfjarvis.claw.model.ReplyForm
import dev.zacsweers.metro.Inject
import java.io.IOException
import okhttp3.MultipartBody
private const val XML_HTTP_REQUEST = "XMLHttpRequest"
private const val XML_HTTP_REQUEST_DUPLICATE = "XMLHttpRequest, XMLHttpRequest"
private const val ACCEPT_ANY = "*/*"
@Inject
class AuthenticatedLobstersApi(private val api: LobstersApi) {
suspend fun upvoteComment(commentId: String): ApiResult<Unit, Unit> {
return withCSRFToken { csrfToken ->
api.upvoteComment(commentId, csrfToken, XML_HTTP_REQUEST, part("reason", ""))
}
}
suspend fun unvoteComment(commentId: String): ApiResult<Unit, Unit> {
return withCSRFToken { csrfToken ->
api.unvoteComment(commentId, csrfToken, XML_HTTP_REQUEST, part("reason", ""))
}
}
suspend fun reply(commentId: String, storyId: String, comment: String): ApiResult<Unit, Unit> {
val referer = "${LobstersApi.BASE_URL}/s/$storyId"
return withCSRFToken { csrfToken ->
when (val formResponse = api.getReplyForm(commentId, csrfToken, XML_HTTP_REQUEST, referer)) {
is Success -> {
val form = formResponse.value
if (!form.isValid()) {
ApiResult.unknownFailure(IOException("Lobsters reply form was missing required fields"))
} else {
api.postReply(
csrfToken = csrfToken,
requestedWith = XML_HTTP_REQUEST_DUPLICATE,
referer = referer,
origin = LobstersApi.BASE_URL,
accept = ACCEPT_ANY,
authenticityToken = part("authenticity_token", form.authenticityToken),
storyId = part("story_id", form.storyId),
method = part("_method", form.method),
parentCommentShortId = part("parent_comment_short_id", form.parentCommentShortId),
comment = part("comment", comment),
commit = part("commit", "Post"),
)
}
}
is Failure -> formResponse.toUnitFailure()
}
}
}
suspend fun getFiltersPage(): ApiResult<FiltersPage, Unit> = api.getFilters()
suspend fun saveBlockedTags(tags: Set<String>): ApiResult<Set<String>, Unit> {
return when (val page = getFiltersPage()) {
is Success -> {
val authenticityToken = page.value.authenticityToken
if (authenticityToken.isBlank()) {
ApiResult.unknownFailure(IOException("Lobsters filters page token was empty"))
} else {
val fields = tags.sorted().associate { tag -> "tags[$tag]" to "1" }
when (val save = api.saveFilters(authenticityToken, fields)) {
is Success ->
when (val refreshed = getFiltersPage()) {
is Success -> ApiResult.success(refreshed.value.blockedTags)
is Failure -> refreshed.toUnitFailure()
}
is Failure -> save.toUnitFailure()
}
}
}
is Failure -> page.toUnitFailure()
}
}
private suspend fun withCSRFToken(
block: suspend (String) -> ApiResult<Unit, Unit>
): ApiResult<Unit, Unit> {
return when (val token = api.getCSRFToken()) {
is Success -> {
val value = token.value.value
if (value.isBlank()) {
ApiResult.unknownFailure(IOException("Lobsters CSRF token was empty"))
} else {
block(value)
}
}
is Failure -> token.toUnitFailure()
}
}
private fun ReplyForm.isValid(): Boolean =
authenticityToken.isNotBlank() &&
storyId.isNotBlank() &&
method.isNotBlank() &&
parentCommentShortId.isNotBlank()
private fun part(name: String, value: String): MultipartBody.Part =
MultipartBody.Part.createFormData(name, value)
private fun Failure<*>.toUnitFailure(): Failure<Unit> =
when (this) {
is Failure.ApiFailure -> ApiResult.apiFailure(error as? Unit)
is Failure.HttpFailure -> ApiResult.httpFailure(code, error as? Unit)
is Failure.NetworkFailure -> ApiResult.networkFailure(error)
is Failure.UnknownFailure -> ApiResult.unknownFailure(error)
}
}
@@ -7,31 +7,93 @@
package dev.msfjarvis.claw.api
import com.slack.eithernet.ApiResult
import dev.msfjarvis.claw.model.CSRFToken
import dev.msfjarvis.claw.model.FiltersPage
import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.ReplyForm
import dev.msfjarvis.claw.model.Tag
import dev.msfjarvis.claw.model.User
import okhttp3.MultipartBody
import retrofit2.http.Field
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Path
/** Simple interface defining an API for lobste.rs */
interface LobstersApi {
@GET("page/{page}.json")
@GET("page/{page}")
suspend fun getHottestPosts(@Path("page") page: Int): ApiResult<List<LobstersPost>, Unit>
@GET("newest/page/{page}.json")
@GET("newest/page/{page}")
suspend fun getNewestPosts(@Path("page") page: Int): ApiResult<List<LobstersPost>, Unit>
@GET("s/{postId}.json")
@GET("s/{postId}")
suspend fun getPostDetails(@Path("postId") postId: String): ApiResult<LobstersPostDetails, Unit>
@GET("~{username}.json")
suspend fun getUser(@Path("username") username: String): ApiResult<User, Unit>
@GET("~{username}") suspend fun getUser(@Path("username") username: String): ApiResult<User, Unit>
@GET("/") suspend fun getCSRFToken(): ApiResult<CSRFToken, Unit>
@GET("tags.json") suspend fun getTags(): ApiResult<List<Tag>, Unit>
@GET("tags") suspend fun getTags(): ApiResult<List<Tag>, Unit>
@GET("filters") suspend fun getFilters(): ApiResult<FiltersPage, Unit>
@FormUrlEncoded
@POST("filters")
suspend fun saveFilters(
@Field("authenticity_token") authenticityToken: String,
@FieldMap(encoded = true) tags: Map<String, String>,
@Field("commit") commit: String = "Save Filters",
): ApiResult<Unit, Unit>
@Multipart
@POST("comments/{commentId}/upvote")
suspend fun upvoteComment(
@Path("commentId") commentId: String,
@Header("x-csrf-token") csrfToken: String,
@Header("x-requested-with") requestedWith: String,
@Part reason: MultipartBody.Part,
): ApiResult<Unit, Unit>
@Multipart
@POST("comments/{commentId}/unvote")
suspend fun unvoteComment(
@Path("commentId") commentId: String,
@Header("x-csrf-token") csrfToken: String,
@Header("x-requested-with") requestedWith: String,
@Part reason: MultipartBody.Part,
): ApiResult<Unit, Unit>
@GET("comments/{commentId}/reply")
suspend fun getReplyForm(
@Path("commentId") commentId: String,
@Header("x-csrf-token") csrfToken: String,
@Header("x-requested-with") requestedWith: String,
@Header("Referer") referer: String,
): ApiResult<ReplyForm, Unit>
@Multipart
@POST("comments")
suspend fun postReply(
@Header("x-csrf-token") csrfToken: String,
@Header("x-requested-with") requestedWith: String,
@Header("Referer") referer: String,
@Header("Origin") origin: String,
@Header("Accept") accept: String,
@Part authenticityToken: MultipartBody.Part,
@Part storyId: MultipartBody.Part,
@Part method: MultipartBody.Part,
@Part parentCommentShortId: MultipartBody.Part,
@Part comment: MultipartBody.Part,
@Part commit: MultipartBody.Part,
): ApiResult<Unit, Unit>
companion object {
const val BASE_URL = "https://lobste.rs"
@@ -0,0 +1,13 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.api
import dev.msfjarvis.claw.parser.LobstersParserService
interface LobstersParserClient {
suspend fun service(): LobstersParserService
}
@@ -1,73 +0,0 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.api.converters
import dev.msfjarvis.claw.api.LobstersApi
import dev.msfjarvis.claw.model.LobstersPost
import java.lang.reflect.Type
import okhttp3.ResponseBody
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.jsoup.select.Elements
import retrofit2.Converter
import retrofit2.Retrofit
object SearchConverter : Converter<ResponseBody, List<LobstersPost>> {
override fun convert(value: ResponseBody): List<LobstersPost> {
return value.byteStream().use { stream ->
Jsoup.parse(stream, "UTF-8", LobstersApi.BASE_URL)
.select("div.story_liner.h-entry")
.map(::parsePost)
}
}
private fun parsePost(elem: Element): LobstersPost {
val parent = elem.parent() ?: error("$elem must have a parent")
val shortId = parent.attr("data-shortid")
val titleElement = elem.select("span.link.h-cite > a")
val title = titleElement.text()
val url = titleElement.attr("href")
val tags = elem.select("span.tags > a").map(Element::text)
val (commentCount, commentsUrl) = getCommentsData(elem.select("span.comments_label"))
val submitter = elem.select("div.byline > a.u-author").text()
val userIsAuthor =
(elem.select("div.byline > span").first()?.text() ?: "").contains(
"authored",
ignoreCase = true,
)
return LobstersPost(
shortId = shortId,
title = title,
url = url,
commentCount = commentCount,
commentsUrl = commentsUrl,
tags = tags,
submitter = submitter,
// The value of these fields is irrelevant for our use case
createdAt = "",
description = "",
userIsAuthor = userIsAuthor,
)
}
private fun getCommentsData(elem: Elements): Pair<Int, String> {
val linkElement = elem.select("a")
val countString = linkElement.text().trimStart().substringBefore(" ")
val commentsUrl = LobstersApi.BASE_URL + linkElement.attr("href")
return (countString.toIntOrNull() ?: 0) to commentsUrl
}
object Factory : Converter.Factory() {
override fun responseBodyConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit,
): Converter<ResponseBody, List<LobstersPost>> {
return SearchConverter
}
}
}
@@ -6,23 +6,15 @@
*/
package dev.msfjarvis.claw.api.converters
import dev.msfjarvis.claw.api.CSRFToken
import dev.msfjarvis.claw.api.LobstersApi
import java.lang.reflect.Type
import kotlin.Unit
import okhttp3.ResponseBody
import org.jsoup.Jsoup
import retrofit2.Converter
import retrofit2.Retrofit
object CSRFTokenConverter : Converter<ResponseBody, CSRFToken> {
override fun convert(value: ResponseBody): CSRFToken {
val token =
Jsoup.parse(value.string(), LobstersApi.BASE_URL)
.select("meta[name=\"csrf-token\"]")
.firstOrNull()
?.attr("content")
.orEmpty()
return CSRFToken(token)
object UnitConverter : Converter<ResponseBody, Unit> {
override fun convert(value: ResponseBody) {
value.close()
}
object Factory : Converter.Factory() {
@@ -30,8 +22,8 @@ object CSRFTokenConverter : Converter<ResponseBody, CSRFToken> {
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit,
): Converter<ResponseBody, CSRFToken> {
return CSRFTokenConverter
): Converter<ResponseBody, *>? {
return if (type == Unit::class.java) UnitConverter else null
}
}
}
@@ -0,0 +1,81 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.api.converters
import dev.msfjarvis.claw.api.LobstersParserClient
import dev.msfjarvis.claw.model.CSRFToken
import dev.msfjarvis.claw.model.FiltersPage
import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.ReplyForm
import dev.msfjarvis.claw.model.Tag
import dev.msfjarvis.claw.model.User
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import java.lang.reflect.WildcardType
import kotlinx.coroutines.runBlocking
import okhttp3.ResponseBody
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.http.GET
class ZiplineHtmlConverterFactory(private val parserClient: LobstersParserClient) :
Converter.Factory() {
override fun responseBodyConverter(
type: Type,
annotations: Array<Annotation>,
retrofit: Retrofit,
): Converter<ResponseBody, *>? {
val normalizedType = type.unwrapWildcard()
val getPath = annotations.filterIsInstance<GET>().singleOrNull()?.value
return when {
normalizedType.isListOf(LobstersPost::class.java) &&
getPath?.startsWith("/search") == true -> {
HtmlConverter { html ->
parserClient.service().parseSearchResults(html)
}
}
normalizedType.isListOf(LobstersPost::class.java) -> {
HtmlConverter { html -> parserClient.service().parsePostsPage(html) }
}
normalizedType.isListOf(Tag::class.java) -> {
HtmlConverter { html -> parserClient.service().parseTagsPage(html) }
}
normalizedType == CSRFToken::class.java ->
HtmlConverter { html -> parserClient.service().parseCsrfToken(html) }
normalizedType == LobstersPostDetails::class.java ->
HtmlConverter { html -> parserClient.service().parsePostDetails(html) }
normalizedType == ReplyForm::class.java ->
HtmlConverter { html -> parserClient.service().parseReplyForm(html) }
normalizedType == FiltersPage::class.java ->
HtmlConverter { html -> parserClient.service().parseFiltersPage(html) }
normalizedType == User::class.java ->
HtmlConverter { html -> parserClient.service().parseUser(html) }
else -> null
}
}
private fun Type.unwrapWildcard(): Type {
return if (this is WildcardType) this.upperBounds.singleOrNull() ?: this else this
}
private fun Type.isListOf(raw: Class<*>): Boolean {
val normalized = unwrapWildcard()
if (normalized !is ParameterizedType) return false
val parameter = getParameterUpperBound(0, normalized).unwrapWildcard()
return getRawType(normalized) == List::class.java && parameter == raw
}
private class HtmlConverter<T>(private val parse: suspend (String) -> T) :
Converter<ResponseBody, T> {
override fun convert(value: ResponseBody): T {
value.use { body ->
return runBlocking { parse(body.string()) }
}
}
}
}
@@ -8,10 +8,12 @@ package dev.msfjarvis.claw.api.injection
import com.slack.eithernet.integration.retrofit.ApiResultCallAdapterFactory
import com.slack.eithernet.integration.retrofit.ApiResultConverterFactory
import dev.msfjarvis.claw.api.AuthenticatedLobstersApi
import dev.msfjarvis.claw.api.LobstersApi
import dev.msfjarvis.claw.api.LobstersParserClient
import dev.msfjarvis.claw.api.LobstersSearchApi
import dev.msfjarvis.claw.api.converters.CSRFTokenConverter
import dev.msfjarvis.claw.api.converters.SearchConverter
import dev.msfjarvis.claw.api.converters.UnitConverter
import dev.msfjarvis.claw.api.converters.ZiplineHtmlConverterFactory
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.ContributesTo
@@ -28,8 +30,8 @@ import retrofit2.Retrofit
import retrofit2.create
/**
* Ideally the multibindings used here would only use [dagger.multibindings.IntoSet], but its lack
* of ordering guarantees means that we roll a die on each app launch that [Converter]s and
* Ideally the multibindings used here would only use [dev.zacsweers.metro.IntoSet], but its lack of
* ordering guarantees means that we roll a die on each app launch that [Converter]s and
* [CallAdapter]s are in the correct order to be able to deserialize responses. Thus, the module
* uses [IntoMap] with [IntKey]s to fake the presence of a fixed order by sorting on the key of the
* injected [Map]s when injecting them into [Retrofit].
@@ -67,7 +69,6 @@ object RetrofitModule {
.baseUrl(baseUrl)
.apply { converterFactories.forEach(this::addConverterFactory) }
.apply { callAdapterFactories.toSortedMap().values.forEach(this::addCallAdapterFactory) }
.addConverterFactory(SearchConverter.Factory)
.build()
}
@@ -75,6 +76,11 @@ object RetrofitModule {
@SingleIn(AppScope::class)
fun provideApi(retrofit: Retrofit): LobstersApi = retrofit.create()
@Provides
@SingleIn(AppScope::class)
fun provideAuthenticatedApi(api: LobstersApi): AuthenticatedLobstersApi =
AuthenticatedLobstersApi(api)
@Provides
@SingleIn(AppScope::class)
fun provideSearchApi(@SearchApi retrofit: Retrofit): LobstersSearchApi = retrofit.create()
@@ -84,20 +90,29 @@ object RetrofitModule {
@IntoMap
fun provideApiResultConverter(): Converter.Factory = ApiResultConverterFactory
@Provides
@IntKey(1)
@IntoMap
fun provideZiplineHtmlConverter(parserClient: LobstersParserClient): Converter.Factory =
ZiplineHtmlConverterFactory(parserClient)
@Provides
@IntKey(2)
@IntoMap
fun provideUnitConverter(): Converter.Factory = UnitConverter.Factory
@Provides
@IntKey(0)
@IntoMap
fun provideApiResultCallAdapter(): CallAdapter.Factory = ApiResultCallAdapterFactory
@Provides
@IntKey(Int.MAX_VALUE)
@IntoMap
fun provideCSRFTokenConverter(): Converter.Factory = CSRFTokenConverter.Factory
@Provides
@SearchApi
fun provideConverters(): List<Converter.Factory> =
listOf(ApiResultConverterFactory, SearchConverter.Factory)
fun provideConverters(parserClient: LobstersParserClient): List<Converter.Factory> =
listOf(
ApiResultConverterFactory,
ZiplineHtmlConverterFactory(parserClient),
)
@Provides @Named("LobstersURL") fun provideLobstersUrl(): String = LobstersApi.BASE_URL
}
@@ -8,17 +8,19 @@ package dev.msfjarvis.claw.api
import com.google.common.truth.Truth.assertThat
import com.slack.eithernet.ApiResult.Success
import com.slack.eithernet.test.newEitherNetController
import dev.msfjarvis.claw.model.CSRFToken
import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.Tag
import dev.msfjarvis.claw.model.User
import dev.msfjarvis.claw.parser.LobstersParserServiceImpl
import dev.msfjarvis.claw.util.TestUtils.assertIs
import kotlin.time.Instant
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
class ApiTest {
private val wrapper = ApiWrapper(newEitherNetController())
private val wrapper = ApiWrapper()
private val api
get() = wrapper.api
@@ -37,11 +39,162 @@ class ApiTest {
assertThat(commentsOnlyPosts).hasSize(0)
}
@Test
fun `api parses hottest HTML fixture fields`() = runTest {
val posts = api.getHottestPosts(1)
assertIs<Success<List<LobstersPost>>>(posts)
val firstPost = posts.value[0]
assertThat(firstPost.shortId).isEqualTo("0mam5k")
assertThat(firstPost.title).isEqualTo("Lobsters Interview with mitchellh")
assertThat(firstPost.submitter).isEqualTo("veqq")
assertThat(firstPost.commentCount).isEqualTo(5)
assertThat(firstPost.commentsUrl)
.isEqualTo("https://lobste.rs/s/0mam5k/lobsters_interview_with_mitchellh")
assertThat(firstPost.tags).containsExactly("interview", "person")
assertThat(firstPost.userIsAuthor).isTrue()
assertThat(firstPost.createdAt).isEqualTo("2026-07-09T15:41:15Z")
Instant.parse(firstPost.createdAt)
val secondPost = posts.value[1]
assertThat(secondPost.shortId).isEqualTo("tedi5h")
assertThat(secondPost.title)
.isEqualTo(
"You paid me, a long-time Linux user, to use Windows 11 exclusively for a month: heres how it went"
)
assertThat(secondPost.submitter).isEqualTo("ninakali")
assertThat(secondPost.commentCount).isEqualTo(19)
assertThat(secondPost.commentsUrl)
.isEqualTo("https://lobste.rs/s/tedi5h/you_paid_me_long_time_linux_user_use")
assertThat(secondPost.tags).containsExactly("windows")
assertThat(secondPost.userIsAuthor).isFalse()
val thirdPost = posts.value[2]
assertThat(thirdPost.shortId).isEqualTo("3eo2nv")
assertThat(thirdPost.title)
.isEqualTo("I Did Not Kill Stanley Lieber: How to draw (with 9front)")
assertThat(thirdPost.submitter).isEqualTo("pmjv")
assertThat(thirdPost.commentCount).isEqualTo(6)
assertThat(thirdPost.commentsUrl)
.isEqualTo("https://lobste.rs/s/3eo2nv/i_did_not_kill_stanley_lieber_how_draw_with")
assertThat(thirdPost.tags).containsExactly("art")
assertThat(thirdPost.userIsAuthor).isTrue()
}
@Test
fun `api gets newest posts`() = runTest {
val posts = api.getNewestPosts(1)
assertIs<Success<List<LobstersPost>>>(posts)
assertThat(posts.value).hasSize(25)
assertThat(posts.value.first().tags).isNotEmpty()
}
@Test
fun `post details with comments`() = runTest {
val postDetails = api.getPostDetails("tdfoqh")
assertIs<Success<LobstersPostDetails>>(postDetails)
assertThat(postDetails.value.comments).hasSize(10)
assertThat(postDetails.value.tags).containsExactly("meta")
val comments = postDetails.value.comments
assertThat(comments).hasSize(10)
assertThat(comments.first().user).isEqualTo("dpercy")
assertThat(comments.first { it.shortId == "pcvbcd" }.score).isEqualTo(2)
assertThat(comments.first().comment).contains("Maybe take the max, instead of the sum?")
assertThat(comments.first { it.shortId == "pcvbcd" }.parentComment).isEqualTo("m3wyu5")
assertThat(comments.first { it.shortId == "lqqn3a" }.parentComment).isEqualTo("owddle")
}
@Test
fun `comments without visible upvoter count have one point from the author`() {
val parser = LobstersParserServiceImpl()
val postDetails =
parser.parsePostDetails(
"""
<ol class="stories">
<li class="story" data-shortid="story1">
<span class="link h-cite"><a href="/s/story1/test">Test story</a></span>
<div class="byline">
<a class="u-author" href="/~/submitter">submitter</a>
<time data-at-unix="1710000000"></time>
</div>
</li>
</ol>
<ol class="comments">
<li class="comments_subtree">
<div class="comment" data-shortid="abc123">
<div class="voters"></div>
<div class="details">
<div class="byline">
<a href="/~/author">author</a>
<a href="/c/abc123"><time data-at-unix="1710000000"></time></a>
</div>
<div class="comment_text"><p>Hello</p></div>
</div>
</div>
</li>
</ol>
"""
.trimIndent()
)
assertThat(postDetails.comments.single().score).isEqualTo(1)
}
@Test
fun `comments use upvoter title when the visible score is omitted`() {
val parser = LobstersParserServiceImpl()
val postDetails =
parser.parsePostDetails(
"""
<ol class="stories">
<li class="story" data-shortid="story1">
<span class="link h-cite"><a href="/s/story1/test">Test story</a></span>
<div class="byline">
<a class="u-author" href="/~/submitter">submitter</a>
<time data-at-unix="1710000000"></time>
</div>
</li>
</ol>
<ol class="comments">
<li class="comments_subtree">
<div class="comment" data-shortid="abc123">
<div class="voters">
<label for="comment_folder_abc123" class="comment_folder"></label>
<a class="upvoter" title="12" href="/login"></a>
</div>
<div class="details">
<div class="byline">
<a href="/~/author">author</a>
<a href="/c/abc123"><time data-at-unix="1710000000"></time></a>
</div>
<div class="comment_text"><p>Hello</p></div>
</div>
</div>
</li>
</ol>
"""
.trimIndent()
)
assertThat(postDetails.comments.single().score).isEqualTo(12)
}
@Test
fun `edited comments expose a single timestamp and edited state`() = runTest {
val postDetails = api.getPostDetails("tdfoqh")
assertIs<Success<LobstersPostDetails>>(postDetails)
val editedComment = postDetails.value.comments.first { it.shortId == "pcvbcd" }
assertThat(editedComment.edited).isTrue()
assertThat(editedComment.timestamp.epochSeconds).isEqualTo(1658588955)
}
@Test
fun `post details preserves upvoted comments`() = runTest {
val postDetails = wrapper.upvotedPostDetails
assertThat(postDetails.comments.filter { it.isUpvoted }.map { it.shortId })
.containsExactly("ncdsfc")
}
@Test
@@ -57,7 +210,7 @@ class ApiTest {
assertIs<Success<CSRFToken>>(token)
assertThat(token.value.value)
.isEqualTo(
"RPe1m3JaXAq0ti7t15hNixpLvmgJ793Ti6y4fOpxtfenyXQK3WU19m7MJb8RGoIf-tC3C9Ilwb8pv0WJUWLbNA"
"dvJ8r_CkOImcHQ5ZLUWlJeQVoPEPQ3rK85DNgiZJcehafqwYP8jESW8AhMf0uQGLqqLbsarYiISCghnDaUd6wA"
)
}
@@ -66,7 +219,18 @@ class ApiTest {
val tags = api.getTags()
assertIs<Success<List<Tag>>>(tags)
assertThat(tags.value).isNotEmpty()
assertThat(tags.value.first().tag).isEqualTo("ruby")
assertThat(tags.value.first().description).isEqualTo("Ruby programming")
val rubyTag = tags.value.first { it.tag == "ruby" }
assertThat(rubyTag.description).isEqualTo("Ruby programming")
assertThat(rubyTag.privileged).isFalse()
assertThat(rubyTag.active).isTrue()
assertThat(rubyTag.category).isEmpty()
assertThat(rubyTag.isMedia).isFalse()
assertThat(rubyTag.hotnessMod).isEqualTo(0.0)
val vibecodingTag = tags.value.first { it.tag == "vibecoding" }
assertThat(vibecodingTag.active).isTrue()
val videoTag = tags.value.first { it.tag == "video" }
assertThat(videoTag.isMedia).isTrue()
}
}
@@ -6,48 +6,68 @@
*/
package dev.msfjarvis.claw.api
import com.slack.eithernet.ApiResult.Companion.success
import com.slack.eithernet.test.EitherNetController
import com.slack.eithernet.test.enqueue
import dev.msfjarvis.claw.api.converters.CSRFTokenConverter
import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.Tag
import dev.msfjarvis.claw.model.User
import com.slack.eithernet.integration.retrofit.ApiResultCallAdapterFactory
import com.slack.eithernet.integration.retrofit.ApiResultConverterFactory
import dev.msfjarvis.claw.api.converters.UnitConverter
import dev.msfjarvis.claw.api.converters.ZiplineHtmlConverterFactory
import dev.msfjarvis.claw.parser.LobstersParserService
import dev.msfjarvis.claw.parser.LobstersParserServiceImpl
import dev.msfjarvis.claw.util.TestUtils.getResource
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import retrofit2.Retrofit
import retrofit2.create
@OptIn(ExperimentalSerializationApi::class)
class ApiWrapper(controller: EitherNetController<LobstersApi>) {
private val json = Json {
ignoreUnknownKeys = true
namingStrategy = JsonNamingStrategy.SnakeCase
}
private val hottest: List<LobstersPost> = json.decodeFromString(getResource("hottest.json"))
private val postDetails: LobstersPostDetails =
json.decodeFromString(getResource("post_details_tdfoqh.json"))
private val user: User = json.decodeFromString(getResource("msfjarvis.json"))
class ApiWrapper {
private val parser = LobstersParserServiceImpl()
val upvotedPostDetails = parser.parsePostDetails(getResource("post_details_upvoted.html"))
private val tags: List<Tag> = json.decodeFromString(getResource("tags.json"))
val api = controller.api
val api: LobstersApi
val authenticatedApi: AuthenticatedLobstersApi
init {
controller.enqueue(LobstersApi::getHottestPosts) { success(hottest) }
controller.enqueue(LobstersApi::getHottestPosts) { success(hottest) }
controller.enqueue(LobstersApi::getPostDetails) { success(postDetails) }
controller.enqueue(LobstersApi::getUser) { success(user) }
controller.enqueue(LobstersApi::getTags) { success(tags) }
controller.enqueue(LobstersApi::getCSRFToken) {
success(
CSRFTokenConverter.convert(
getResource("csrf_page.html").toResponseBody("text/html".toMediaType())
)
)
val parserClient =
object : LobstersParserClient {
override suspend fun service(): LobstersParserService = LobstersParserServiceImpl()
}
val retrofit =
Retrofit.Builder()
.baseUrl("https://lobste.rs/")
.client(OkHttpClient.Builder().addInterceptor(FixtureInterceptor()).build())
.addConverterFactory(ApiResultConverterFactory)
.addConverterFactory(ZiplineHtmlConverterFactory(parserClient))
.addConverterFactory(UnitConverter.Factory)
.addCallAdapterFactory(ApiResultCallAdapterFactory)
.build()
api = retrofit.create()
authenticatedApi = AuthenticatedLobstersApi(api)
}
private class FixtureInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val body =
when (request.url.encodedPath) {
"/page/1" -> getResource("hottest_page.html")
"/newest/page/1" -> getResource("hottest_page.html")
"/s/tdfoqh" -> getResource("post_details_tdfoqh.html")
"/~msfjarvis" -> getResource("msfjarvis.html")
"/" -> getResource("csrf_page.html")
"/tags" -> getResource("tags.html")
"/comments/edtrox/reply" -> getResource("reply_form.html")
else -> ""
}
return Response.Builder()
.request(request)
.protocol(Protocol.HTTP_1_1)
.code(200)
.message("OK")
.body(body.toByteArray().toResponseBody("text/html; charset=utf-8".toMediaType()))
.build()
}
}
}
@@ -0,0 +1,123 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.api
import com.google.common.truth.Truth.assertThat
import com.slack.eithernet.ApiResult
import dev.msfjarvis.claw.model.CSRFToken
import dev.msfjarvis.claw.model.FiltersPage
import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.ReplyForm
import dev.msfjarvis.claw.model.Tag
import dev.msfjarvis.claw.model.User
import kotlinx.coroutines.test.runTest
import okhttp3.MultipartBody
import org.junit.jupiter.api.Test
class AuthenticatedLobstersApiHeaderTest {
@Test
fun `reply submission matches browser xhr headers`() = runTest {
val api = RecordingLobstersApi()
val authenticatedApi = AuthenticatedLobstersApi(api)
authenticatedApi.reply("kkiqui", "2tskyy", "hello")
assertThat(api.replyFormRequestedWith).isEqualTo("XMLHttpRequest")
assertThat(api.replyFormReferer).isEqualTo("https://lobste.rs/s/2tskyy")
assertThat(api.postReplyRequestedWith).isEqualTo("XMLHttpRequest, XMLHttpRequest")
assertThat(api.postReplyReferer).isEqualTo("https://lobste.rs/s/2tskyy")
assertThat(api.postReplyOrigin).isEqualTo(LobstersApi.BASE_URL)
assertThat(api.postReplyAccept).isEqualTo("*/*")
}
}
private class RecordingLobstersApi : LobstersApi {
var replyFormRequestedWith: String? = null
var replyFormReferer: String? = null
var postReplyRequestedWith: String? = null
var postReplyReferer: String? = null
var postReplyOrigin: String? = null
var postReplyAccept: String? = null
override suspend fun getHottestPosts(page: Int): ApiResult<List<LobstersPost>, Unit> =
error("unused")
override suspend fun getNewestPosts(page: Int): ApiResult<List<LobstersPost>, Unit> =
error("unused")
override suspend fun getPostDetails(postId: String): ApiResult<LobstersPostDetails, Unit> =
error("unused")
override suspend fun getUser(username: String): ApiResult<User, Unit> = error("unused")
override suspend fun getCSRFToken(): ApiResult<CSRFToken, Unit> =
ApiResult.success(CSRFToken("csrf-token"))
override suspend fun getTags(): ApiResult<List<Tag>, Unit> = error("unused")
override suspend fun getFilters(): ApiResult<FiltersPage, Unit> = error("unused")
override suspend fun saveFilters(
authenticityToken: String,
tags: Map<String, String>,
commit: String,
): ApiResult<Unit, Unit> = error("unused")
override suspend fun upvoteComment(
commentId: String,
csrfToken: String,
requestedWith: String,
reason: MultipartBody.Part,
): ApiResult<Unit, Unit> = error("unused")
override suspend fun unvoteComment(
commentId: String,
csrfToken: String,
requestedWith: String,
reason: MultipartBody.Part,
): ApiResult<Unit, Unit> = error("unused")
override suspend fun getReplyForm(
commentId: String,
csrfToken: String,
requestedWith: String,
referer: String,
): ApiResult<ReplyForm, Unit> {
replyFormRequestedWith = requestedWith
replyFormReferer = referer
return ApiResult.success(
ReplyForm(
authenticityToken = "auth-token",
storyId = "2tskyy",
method = "post",
parentCommentShortId = commentId,
)
)
}
override suspend fun postReply(
csrfToken: String,
requestedWith: String,
referer: String,
origin: String,
accept: String,
authenticityToken: MultipartBody.Part,
storyId: MultipartBody.Part,
method: MultipartBody.Part,
parentCommentShortId: MultipartBody.Part,
comment: MultipartBody.Part,
commit: MultipartBody.Part,
): ApiResult<Unit, Unit> {
postReplyRequestedWith = requestedWith
postReplyReferer = referer
postReplyOrigin = origin
postReplyAccept = accept
return ApiResult.success(Unit)
}
}
@@ -0,0 +1,216 @@
/*
* Copyright © Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.api
import com.google.common.truth.Truth.assertThat
import com.slack.eithernet.ApiResult
import com.slack.eithernet.ApiResult.Failure
import com.slack.eithernet.ApiResult.Success
import dev.msfjarvis.claw.model.CSRFToken
import dev.msfjarvis.claw.model.FiltersPage
import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.ReplyForm
import dev.msfjarvis.claw.model.Tag
import dev.msfjarvis.claw.model.User
import dev.msfjarvis.claw.util.TestUtils.assertIs
import kotlinx.coroutines.test.runTest
import okhttp3.MultipartBody
import org.junit.jupiter.api.Test
class AuthenticatedLobstersApiTest {
private val wrapper = ApiWrapper()
private val authenticatedApi = wrapper.authenticatedApi
@Test
fun `upvote comment succeeds`() = runTest {
val result = authenticatedApi.upvoteComment("0unzyg")
assertIs<Success<Unit>>(result)
}
@Test
fun `unvote comment succeeds`() = runTest {
val result = authenticatedApi.unvoteComment("0unzyg")
assertIs<Success<Unit>>(result)
}
@Test
fun `reply succeeds`() = runTest {
val result = authenticatedApi.reply("edtrox", "znlkib", "Thanks for the details!")
assertIs<Success<Unit>>(result)
}
@Test
fun `save blocked tags posts full permanent set and returns canonical refreshed set`() = runTest {
val api =
RecordingFiltersLobstersApi(
filtersResponses =
ArrayDeque(
listOf(
FiltersPage(
authenticityToken = "filters-token",
tags = emptyList(),
blockedTags = setOf("existing"),
),
FiltersPage(
authenticityToken = "filters-token",
tags = emptyList(),
blockedTags = setOf("android", "kotlin"),
),
)
)
)
val authenticatedApi = AuthenticatedLobstersApi(api)
val result = authenticatedApi.saveBlockedTags(setOf("kotlin", "android"))
assertThat(api.savedAuthenticityToken).isEqualTo("filters-token")
assertThat(api.savedTags).containsExactly("tags[android]", "1", "tags[kotlin]", "1")
assertThat(api.savedCommit).isEqualTo("Save Filters")
assertIs<Success<Set<String>>>(result)
assertThat(result.value).containsExactly("android", "kotlin")
}
@Test
fun `save blocked tags fails fast when filters token is blank`() = runTest {
val api =
RecordingFiltersLobstersApi(
filtersResponses =
ArrayDeque(
listOf(
FiltersPage(authenticityToken = "", tags = emptyList(), blockedTags = emptySet())
)
)
)
val authenticatedApi = AuthenticatedLobstersApi(api)
val result = authenticatedApi.saveBlockedTags(setOf("kotlin"))
assertIs<Failure.UnknownFailure>(result)
assertThat(result.error).hasMessageThat().isEqualTo("Lobsters filters page token was empty")
assertThat(api.saveFiltersCallCount).isEqualTo(0)
}
@Test
fun `save blocked tags re-reads filters after save`() = runTest {
val api =
RecordingFiltersLobstersApi(
filtersResponses =
ArrayDeque(
listOf(
FiltersPage(
authenticityToken = "filters-token",
tags = emptyList(),
blockedTags = setOf("old"),
),
FiltersPage(
authenticityToken = "filters-token",
tags = emptyList(),
blockedTags = setOf("canonical"),
),
)
)
)
val authenticatedApi = AuthenticatedLobstersApi(api)
val result = authenticatedApi.saveBlockedTags(setOf("draft"))
assertThat(api.getFiltersCallCount).isEqualTo(2)
assertIs<Success<Set<String>>>(result)
assertThat(result.value).containsExactly("canonical")
}
}
private class RecordingFiltersLobstersApi(
private val filtersResponses: ArrayDeque<FiltersPage>,
private val saveFiltersResult: ApiResult<Unit, Unit> = ApiResult.success(Unit),
) : LobstersApi {
var getFiltersCallCount: Int = 0
private set
var saveFiltersCallCount: Int = 0
private set
var savedAuthenticityToken: String? = null
private set
var savedTags: Map<String, String>? = null
private set
var savedCommit: String? = null
private set
override suspend fun getHottestPosts(page: Int): ApiResult<List<LobstersPost>, Unit> =
error("unused")
override suspend fun getNewestPosts(page: Int): ApiResult<List<LobstersPost>, Unit> =
error("unused")
override suspend fun getPostDetails(postId: String): ApiResult<LobstersPostDetails, Unit> =
error("unused")
override suspend fun getUser(username: String): ApiResult<User, Unit> = error("unused")
override suspend fun getCSRFToken(): ApiResult<CSRFToken, Unit> = error("unused")
override suspend fun getTags(): ApiResult<List<Tag>, Unit> = error("unused")
override suspend fun getFilters(): ApiResult<FiltersPage, Unit> {
getFiltersCallCount += 1
return ApiResult.success(filtersResponses.removeFirst())
}
override suspend fun saveFilters(
authenticityToken: String,
tags: Map<String, String>,
commit: String,
): ApiResult<Unit, Unit> {
saveFiltersCallCount += 1
savedAuthenticityToken = authenticityToken
savedTags = tags
savedCommit = commit
return saveFiltersResult
}
override suspend fun upvoteComment(
commentId: String,
csrfToken: String,
requestedWith: String,
reason: MultipartBody.Part,
): ApiResult<Unit, Unit> = error("unused")
override suspend fun unvoteComment(
commentId: String,
csrfToken: String,
requestedWith: String,
reason: MultipartBody.Part,
): ApiResult<Unit, Unit> = error("unused")
override suspend fun getReplyForm(
commentId: String,
csrfToken: String,
requestedWith: String,
referer: String,
): ApiResult<ReplyForm, Unit> = error("unused")
override suspend fun postReply(
csrfToken: String,
requestedWith: String,
referer: String,
origin: String,
accept: String,
authenticityToken: MultipartBody.Part,
storyId: MultipartBody.Part,
method: MultipartBody.Part,
parentCommentShortId: MultipartBody.Part,
comment: MultipartBody.Part,
commit: MultipartBody.Part,
): ApiResult<Unit, Unit> = error("unused")
}
@@ -31,29 +31,28 @@ class SearchApiTest {
private val results =
listOf(
LobstersPost(
shortId = "vvky3g",
shortId = "2cjb1z",
createdAt = "",
title = "2024 State of Rust Survey Results",
url = "https://blog.rust-lang.org/2025/02/13/2024-State-Of-Rust-Survey-results.html",
title = "Why I am against GenAI and everything it stands for",
url = "https://lpcvoid.com/blog/0018_why_i_am_against_genai/index.html",
description = "",
commentCount = 10,
commentsUrl = "https://lobste.rs/s/vvky3g/2024_state_rust_survey_results",
submitter = "obsoleszenz",
userIsAuthor = false,
tags = listOf("rust"),
commentCount = 42,
commentsUrl = "https://lobste.rs/s/2cjb1z/why_i_am_against_genai_everything_it",
submitter = "Marc",
userIsAuthor = true,
tags = listOf("culture", "education", "vibecoding"),
),
LobstersPost(
shortId = "wjoric",
shortId = "kgem4b",
createdAt = "",
title = "New hack uses prompt injection to corrupt Geminis long-term memory",
url =
"https://arstechnica.com/security/2025/02/new-hack-uses-prompt-injection-to-corrupt-geminis-long-term-memory/",
title = "The social contract of writing",
url = "https://jola.dev/posts/the-social-contract-of-writing",
description = "",
commentCount = 0,
commentsUrl = "https://lobste.rs/s/wjoric/new_hack_uses_prompt_injection_corrupt",
submitter = "laktak",
userIsAuthor = false,
tags = listOf("ai"),
commentCount = 38,
commentsUrl = "https://lobste.rs/s/kgem4b/social_contract_writing",
submitter = "joladev",
userIsAuthor = true,
tags = listOf("vibecoding", "philosophy"),
),
)
}
@@ -6,24 +6,55 @@
*/
package dev.msfjarvis.claw.api
import com.slack.eithernet.ApiResult.Companion.success
import com.slack.eithernet.integration.retrofit.ApiResultCallAdapterFactory
import com.slack.eithernet.integration.retrofit.ApiResultConverterFactory
import com.slack.eithernet.test.EitherNetController
import com.slack.eithernet.test.enqueue
import dev.msfjarvis.claw.api.converters.SearchConverter
import dev.msfjarvis.claw.api.converters.ZiplineHtmlConverterFactory
import dev.msfjarvis.claw.parser.LobstersParserService
import dev.msfjarvis.claw.parser.LobstersParserServiceImpl
import dev.msfjarvis.claw.util.TestUtils.getResource
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import retrofit2.Retrofit
import retrofit2.create
class SearchApiWrapper(controller: EitherNetController<LobstersSearchApi>) {
val api = controller.api
val api: LobstersSearchApi
init {
controller.enqueue(LobstersSearchApi::searchPosts) {
success(
SearchConverter.convert(
getResource("search_chatgpt_page.html").toResponseBody("text/html".toMediaType())
val parserClient =
object : LobstersParserClient {
override suspend fun service(): LobstersParserService = LobstersParserServiceImpl()
}
val retrofit =
Retrofit.Builder()
.baseUrl("https://lobste.rs/")
.client(OkHttpClient.Builder().addInterceptor(FixtureInterceptor()).build())
.addConverterFactory(ApiResultConverterFactory)
.addConverterFactory(ZiplineHtmlConverterFactory(parserClient))
.addCallAdapterFactory(ApiResultCallAdapterFactory)
.build()
api = retrofit.create()
}
private class FixtureInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
return Response.Builder()
.request(request)
.protocol(Protocol.HTTP_1_1)
.code(200)
.message("OK")
.body(
getResource("search_chatgpt_page.html")
.toByteArray()
.toResponseBody("text/html; charset=utf-8".toMediaType())
)
)
.build()
}
}
}
+1430 -4
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+169
View File
@@ -0,0 +1,169 @@
<!doctype html>
<html lang="en" >
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<link rel="icon" sizes="144x144" type="image/png" href="/touch-icon-144.png">
<link rel="apple-touch-icon" sizes="57x57" href="/touch-icon.png">
<link rel="apple-touch-icon" sizes="114x114" href="/touch-icon.png">
<link rel="apple-touch-icon" sizes="72x72" href="/touch-icon-144.png">
<link rel="apple-touch-icon" sizes="144x144" href="/touch-icon-144.png">
<link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Lobsters">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="referrer" content="always">
<meta name="theme-color" content="#AC130D">
<meta name="story-flags" content="{&quot;O&quot;:&quot;Off-topic&quot;,&quot;A&quot;:&quot;Already Posted&quot;,&quot;B&quot;:&quot;Broken Link&quot;,&quot;S&quot;:&quot;Spam&quot;,&quot;&quot;:&quot;Cancel&quot;}">
<meta name="comment-flags" content="{&quot;O&quot;:&quot;Off-topic&quot;,&quot;M&quot;:&quot;Me-too&quot;,&quot;T&quot;:&quot;Troll&quot;,&quot;U&quot;:&quot;Unkind&quot;,&quot;S&quot;:&quot;Spam&quot;,&quot;&quot;:&quot;Cancel&quot;}">
<meta property="og:type" content="user">
<meta property="og:site_name" content="Lobsters">
<meta property="og:title" content="msfjarvis">
<meta property="og:description" content="Android and Kotlin developer, currently working for Cloudflare
openpgp4fpr:8F87050B0F9CB84115157399B7843F823355E9B9
">
<meta property="og:image" content="/avatars/msfjarvis-100.png">
<link rel="shortcut icon" href="/favicon.ico">
<title>msfjarvis | Lobsters</title>
<link rel="stylesheet" href="/assets/application-8d029e37.css">
<link rel="stylesheet" href="/assets/system-system-fd030c9d.css">
<link rel="stylesheet" href="/assets/tom-select-00e8031d.css" data-turbo-track="reload">
<link rel="stylesheet" href="/assets/TomSelect_remove_button-4d5c34b9.css" data-turbo-track="reload">
<meta name="csrf-param" content="authenticity_token">
<meta name="csrf-token" content="qzhV0a2izz-6n516jIrQyKzp06y_9Fb33KCrZtCTdx6_KPhm894c4F-8c5CTUkxlC_xqaR2nsqf416xcza-daw">
<meta name="robots" content="noai, noimageai">
<script type="speculationrules" nonce="/3z7A9BvciTcEaW9gzJQnQ==">
{
"prefetch": [
{
"where": {
"href_matches": "/*"
},
"eagerness": "moderate"
}
]
}
</script>
</head>
<body data-username='' data-now-unix='1780070762'>
<header id="nav">
<a id="logo" style="background-color: #6e0000;" href="/" title="Lobsters (Current traffic: 17%)"></a>
<div class="navholder">
<nav class="links">
<a class="" href="/active">Active</a>
<a class="" href="/recent">Recent</a>
<a class="" href="/comments">Comments</a>
<a class="" href="/search">Search</a>
<a class="corner" href="/login">Login</a>
</nav>
</div>
<nav class="corner">
<a class="corner" href="/login">Login</a>
</nav>
</header>
<div id="inside">
<h1 class="">msfjarvis</h1>
<section class="profile">
<div id="gravatar">
<img srcset="/avatars/msfjarvis-100.png 1x, /avatars/msfjarvis-200.png 2x" class="avatar" alt="msfjarvis avatar" loading="lazy" decoding="async" src="/avatars/msfjarvis-100.png" width="100" height="100">
</div>
<div class="labelled_grid">
<label>Status</label>
<span>
Active
user
</span>
<label>Joined</label>
<span>
<time title="2020-04-24 11:41:56" datetime="2020-04-24 11:41:56" data-at-unix="1587746516">6 years ago</time>
by <a href="/users#msfjarvis">invitation</a> from
<a href="/~Amolith">Amolith</a>
</span>
<label>Karma</label>
<span>
2817
</span>
<label>Stories Submitted</label>
<span>
<a href="/~msfjarvis/stories">38</a>, most commonly tagged <a class="tag tag_rust" title="Rust programming" href="/t/rust">rust</a>
</span>
<label>Comments Posted</label>
<span><a href="/~msfjarvis/threads">258</a></span>
<label>Homepage</label>
<span>
<a href="https://msfjarvis.dev"
rel="me ugc">https://msfjarvis.dev</a>
</span>
<label>GitHub</label>
<span>
<a href="https://github.com/msfjarvis"
rel="me ugc">https://github.com/msfjarvis</a>
</span>
<label>Mastodon</label>
<span>
<a href="https://androiddev.social/@msfjarvis"
rel="me ugc">@msfjarvis@androiddev.social
</a>
</span>
<label>About</label>
<div class="shorten_first_p">
<p>Android and Kotlin developer, currently working for <a href="https://cloudflare.com/" rel="ugc">Cloudflare</a></p>
<p>openpgp4fpr:8F87050B0F9CB84115157399B7843F823355E9B9</p>
</div>
</div>
</section>
</div>
<footer>
<span id="office-hours-live">
</span>
<nav>
<a href="/about">About</a>
<a href="/tags">Tags</a>
<a href="/filters">Filter</a>
<a href="/moderations">Moderation Log</a>
</nav>
</footer>
<span id="iab-pcm-sdk"></span><span id="iab-autofill-sdk"></span>
</body>
</html>
-1
View File
@@ -1 +0,0 @@
{"username":"msfjarvis","created_at":"2020-04-24T11:41:56.000-05:00","is_admin":false,"about":"Android and Kotlin developer, currently working for [Dyte](https://dyte.io/)","is_moderator":false,"karma":1343,"avatar_url":"/avatars/msfjarvis-100.png","invited_by_user":"Amolith","github_username":"msfjarvis"}
File diff suppressed because it is too large Load Diff
+813
View File
@@ -0,0 +1,813 @@
<!doctype html>
<html lang="en" >
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<link rel="icon" sizes="144x144" type="image/png" href="/touch-icon-144.png">
<link rel="apple-touch-icon" sizes="57x57" href="/touch-icon.png">
<link rel="apple-touch-icon" sizes="114x114" href="/touch-icon.png">
<link rel="apple-touch-icon" sizes="72x72" href="/touch-icon-144.png">
<link rel="apple-touch-icon" sizes="144x144" href="/touch-icon-144.png">
<link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Lobsters">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="referrer" content="always">
<meta name="theme-color" content="#AC130D">
<meta name="story-flags" content="{&quot;O&quot;:&quot;Off-topic&quot;,&quot;A&quot;:&quot;Already Posted&quot;,&quot;B&quot;:&quot;Broken Link&quot;,&quot;S&quot;:&quot;Spam&quot;,&quot;&quot;:&quot;Cancel&quot;}">
<meta name="comment-flags" content="{&quot;O&quot;:&quot;Off-topic&quot;,&quot;M&quot;:&quot;Me-too&quot;,&quot;T&quot;:&quot;Troll&quot;,&quot;U&quot;:&quot;Unkind&quot;,&quot;S&quot;:&quot;Spam&quot;,&quot;&quot;:&quot;Cancel&quot;}">
<meta property="og:type" content="article">
<meta property="og:site_name" content="Lobsters">
<meta property="og:title" content="Ranking comments by sum of replies&#39; scores">
<meta property="og:description" content="10 comments">
<meta property="og:image" content="https://lobste.rs/story_image/tdfoqh.png">
<meta property="article:author" content="https://lobste.rs/~gioele">
<link rel="shortcut icon" href="/favicon.ico">
<link href="https://lobste.rs/s/tdfoqh/ranking_comments_by_sum_replies_scores" rev="canonical" rel="self alternate shorter shorturl shortlink">
<title>Ranking comments by sum of replies&#39; scores | Lobsters</title>
<link rel="stylesheet" href="/assets/application-d417b8b4.css">
<link rel="stylesheet" href="/assets/system-system-fd030c9d.css">
<link rel="stylesheet" href="/assets/tom-select-00e8031d.css" data-turbo-track="reload">
<link rel="stylesheet" href="/assets/TomSelect_remove_button-4d5c34b9.css" data-turbo-track="reload">
<meta name="csrf-param" content="authenticity_token">
<meta name="csrf-token" content="x21lNnPzmG8xQppD1ULd26ofux_9rIbRINBAnkeKXk41Y0_S_aEHvYVgGU3yku7qxVwIpN5vOxWJoX07PHF4JA">
<meta name="robots" content="noai, noimageai">
<script type="speculationrules" nonce="KRObbTozM4i2tL2NUL7DWQ==">
{
"prefetch": [
{
"where": {
"href_matches": "/*"
},
"eagerness": "moderate"
}
]
}
</script>
</head>
<body data-username='' data-now-unix='1783665557'>
<header id="nav">
<a id="logo" style="background-color: #5b0000;" href="/" title="Lobsters (Current traffic: 6%)"></a>
<div class="navholder">
<nav class="links">
<a class="" href="/active">Active</a>
<a class="" href="/recent">Recent</a>
<a class="" href="/comments">Comments</a>
<a class="" href="/search">Search</a>
<a class="corner" href="/login">Login</a>
</nav>
</div>
<nav class="corner">
<a class="corner" href="/login">Login</a>
</nav>
</header>
<div id="inside">
<ol class="stories">
<li id="story_tdfoqh" data-shortid="tdfoqh" class="story">
<div class="story_liner h-entry">
<div class="voters">
<a class="upvoter" href="/login">18</a>
</div>
<div class="details" id="header_story_01g8ke0w20enk85qfby8bp0cgy">
<span role="heading" aria-level="1" class="link h-cite u-repost-of">
<a href="/s/tdfoqh/ranking_comments_by_sum_replies_scores" rel="ugc noreferrer" class="u-url">Ranking comments by sum of replies&#39; scores</a>
</span>
<a class="description_present" title="There are often cases where comments with a poor score (say, 1 point), invite high-quality replies that receive many upvotes.
These threads are not shown at the top of the story&#39;s comments page because their &quot;root comment&quot; has a low score.
An example, as of 2022-07-22 17:00 UTC, is &lt;https://lobste.rs/s/ekvqcf/random_wallpaper_with_just_bash_systemd#c_vop9bt&gt;:
* the root comment has 1 point,
* the first reply has 12 points,
* and, in total, 18 points have been accumulated by all the r..." href="/s/tdfoqh/ranking_comments_by_sum_replies_scores">&#x2636;</a>
<ul class="tags" aria-label="Tags">
<li><a aria-label="Tag meta" class="tag tag_meta" title="Lobsters-related bikeshedding - report bugs at https://github.com/lobsters/lobsters" href="/t/meta">meta</a></li>
</ul>
<div class="byline">
<span>authored by</span>
<a tabindex="-1" aria-hidden="true" href="/~gioele"><img srcset="/avatars/gioele-16.png 1x, /avatars/gioele-32.png 2x" class="avatar" alt="" loading="lazy" decoding="async" src="/avatars/gioele-16.png" width="16" height="16"></a>
<a class="user_is_author" href="/~gioele">gioele</a>
<time title="2022-07-22 12:06:59" datetime="2022-07-22 12:06:59" data-at-unix="1658509619">3 years ago</time>
<span class="comments_label">
<span aria-hidden="true"> | </span>
<a role="heading" aria-level="2" href="#comments-tdfoqh">
10
comments
</a>
</span>
</div>
</div>
</div>
</li>
</ol>
<div class="story_content">
<div class="story_text">
<p>There are often cases where comments with a poor score (say, 1 point), invite high-quality replies that receive many upvotes.</p>
<p>These threads are not shown at the top of the story's comments page because their "root comment" has a low score.</p>
<p>An example, as of 2022-07-22 17:00 UTC, is <a href="https://lobste.rs/s/ekvqcf/random_wallpaper_with_just_bash_systemd#c_vop9bt" rel="ugc">https://lobste.rs/s/ekvqcf/random_wallpaper_with_just_bash_systemd#c_vop9bt</a>:</p>
<ul>
<li>the root comment has 1 point,</li>
<li>the first reply has 12 points,</li>
<li>and, in total, 18 points have been accumulated by all the replies with score &gt; 1 point.</li>
</ul>
<p>Because the root comment has only 1 point, it remains at the bottom of the page, bringing down with itself also the good comment with 12 points and all the other upvoted comments.</p>
<p>Could an alternative ranking system be put in place, where the threads and subthreads are not sorted by the score of their root comment, but by the <em>sum of all the scores of their children</em>?</p>
<p>(Maybe taking into account only children with <code>&gt;1</code> point, to avoid giving importance to superficial back-and-forth discussions.)</p>
</div>
</div>
<ol class="comments comments1">
<li class="comments_subtree" id="comments-tdfoqh">
<ol class="comments" id="story_comments">
<li class="comments_subtree">
<input id="comment_folder_ncdsfc"
class="comment_folder_button" type="checkbox"
>
<div id="c_ncdsfc"
data-shortid="ncdsfc"
class="comment
">
<div class="voters">
<label for="comment_folder_ncdsfc" class="comment_folder"></label>
<a class="upvoter" title="18" href="/login">18</a>
</div>
<div class="details">
<div class="byline">
<a name="c_ncdsfc"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~dpercy"><img srcset="/avatars/dpercy-16.png 1x, /avatars/dpercy-32.png 2x" class="avatar" alt="" loading="lazy" decoding="async" src="/avatars/dpercy-16.png" width="16" height="16"></a>
<a href="/~dpercy">dpercy</a>
<a href="/c/ncdsfc"><time title="2022-07-22 14:04:36" datetime="2022-07-22 14:04:36" data-at-unix="1658516676">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<blockquote>
<p>to avoid giving importance to superficial back-and-forth discussions</p>
</blockquote>
<p>Maybe take the max, instead of the sum? That way it's still comparing the score of individual comments. A highly-upvoted child would rank higher than a less-upvoted toplevel comment, but long threads wouldn't get any bonus.</p>
</div>
</div>
</div>
<ol class="comments"></ol>
</li>
<li class="comments_subtree">
<input id="comment_folder_m3wyu5"
class="comment_folder_button" type="checkbox"
>
<div id="c_m3wyu5"
data-shortid="m3wyu5"
class="comment
">
<div class="voters">
<label for="comment_folder_m3wyu5" class="comment_folder"></label>
<a class="upvoter" title="8" href="/login">8</a>
</div>
<div class="details">
<div class="byline">
<a name="c_m3wyu5"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~friendlysock"><img srcset="/avatars/friendlysock-16.png 1x, /avatars/friendlysock-32.png 2x" class="avatar" alt="" loading="lazy" decoding="async" src="/avatars/friendlysock-16.png" width="16" height="16"></a>
<a class="inactive_user" aria-label="friendlysock - Inactive user" href="/~friendlysock">friendlysock</a>
<a href="/c/m3wyu5"><time title="2022-07-23 01:04:43" datetime="2022-07-23 01:04:43" data-at-unix="1658556283">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<p>For better or worse, the deeper you go into threads the less likely it is that the discussion is still about the original submission. Your proposal, or the max variant presented elsewhere, I fear would unduly reward flamebait and off-topic discussion.</p>
</div>
</div>
</div>
<ol class="comments">
<li class="comments_subtree">
<input id="comment_folder_pcvbcd"
class="comment_folder_button" type="checkbox"
>
<div id="c_pcvbcd"
data-shortid="pcvbcd"
class="comment
">
<div class="voters">
<label for="comment_folder_pcvbcd" class="comment_folder"></label>
<a class="upvoter" title="2" href="/login">2</a>
</div>
<div class="details">
<div class="byline">
<a name="c_pcvbcd"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~gioele"><img srcset="/avatars/gioele-16.png 1x, /avatars/gioele-32.png 2x" class="avatar" alt="" loading="lazy" decoding="async" src="/avatars/gioele-16.png" width="16" height="16"></a>
<a class="user_is_author user_is_submitter" aria-label="gioele - Submitter" href="/~gioele">gioele</a>
edited
<a href="/c/pcvbcd"><time title="2022-07-23 10:09:15" datetime="2022-07-23 10:09:15" data-at-unix="1658588955">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<blockquote>
<p>Your proposal, or the max variant presented elsewhere, I fear would unduly reward flamebait and off-topic discussion.</p>
</blockquote>
<p>Such off-topic discussions that stray away from the main topic do exist, are a nuisance, and, surely, they should not be rewarded with more attention. However, such sub-threads exists as a long string of short comments with no or very few upvotes. (I wonder if the data confirms this or it is just my impression.)</p>
<p>For this reason I believe that a) not counting in comments with score==1 or b) <a href="https://lobste.rs/~dpercy" rel="ugc">@dpercy</a>'s max variant would avoid rewarding such discussions.</p>
</div>
</div>
</div>
<ol class="comments"></ol>
</li>
</ol>
</li>
<li class="comments_subtree">
<input id="comment_folder_9vyyu5"
class="comment_folder_button" type="checkbox"
>
<div id="c_9vyyu5"
data-shortid="9vyyu5"
class="comment
">
<div class="voters">
<label for="comment_folder_9vyyu5" class="comment_folder"></label>
<a class="upvoter" title="5" href="/login">5</a>
</div>
<div class="details">
<div class="byline">
<a name="c_9vyyu5"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~kghose"><img srcset="/avatars/kghose-16.png 1x, /avatars/kghose-32.png 2x" class="avatar" alt="" loading="lazy" decoding="async" src="/avatars/kghose-16.png" width="16" height="16"></a>
<a href="/~kghose">kghose</a>
<a href="/c/9vyyu5"><time title="2022-07-22 13:39:20" datetime="2022-07-22 13:39:20" data-at-unix="1658515160">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<p>Just like to point out that the assumption here is that upvotes is a metric of quality. On lobste.rs as in any other social context, upvotes/support is a measure of popularity.</p>
<p>There are many instances where the two are correlated, and some important instances where they are not.</p>
</div>
</div>
</div>
<ol class="comments">
<li class="comments_subtree">
<input id="comment_folder_owddle"
class="comment_folder_button" type="checkbox"
>
<div id="c_owddle"
data-shortid="owddle"
class="comment
">
<div class="voters">
<label for="comment_folder_owddle" class="comment_folder"></label>
<a class="upvoter" title="7" href="/login">7</a>
</div>
<div class="details">
<div class="byline">
<a name="c_owddle"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~gioele"><img srcset="/avatars/gioele-16.png 1x, /avatars/gioele-32.png 2x" class="avatar" alt="" loading="lazy" decoding="async" src="/avatars/gioele-16.png" width="16" height="16"></a>
<a class="user_is_author user_is_submitter" aria-label="gioele - Submitter" href="/~gioele">gioele</a>
<a href="/c/owddle"><time title="2022-07-22 13:57:19" datetime="2022-07-22 13:57:19" data-at-unix="1658516239">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<blockquote>
<p>Just like to point out that the assumption here is that upvotes is a metric of quality. On lobste.rs as in any other social context, upvotes/support is a measure of popularity.</p>
</blockquote>
<p>I fully agree: upvotes are a metric of popularity and not of quality. But this proposal does not assume that.</p>
<p>Regardless of what upvotes represent, currently threads are sorted in descending order of (upvotes, time). All that this proposal suggests, is to change that to (sum(upvotes of root-and-children), time).</p>
<p>The only assumption here is: if ranking a thread by upvotes of its root comment is considered good (and at the moment it is), then ranking by the sum of the upvotes of the whole thread is better.</p>
</div>
</div>
</div>
<ol class="comments">
<li class="comments_subtree">
<input id="comment_folder_lqqn3a"
class="comment_folder_button" type="checkbox"
>
<div id="c_lqqn3a"
data-shortid="lqqn3a"
class="comment
">
<div class="voters">
<label for="comment_folder_lqqn3a" class="comment_folder"></label>
<a class="upvoter" title="2" href="/login">2</a>
</div>
<div class="details">
<div class="byline">
<a name="c_lqqn3a"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~kghose"><img srcset="/avatars/kghose-16.png 1x, /avatars/kghose-32.png 2x" class="avatar" alt="" loading="lazy" decoding="async" src="/avatars/kghose-16.png" width="16" height="16"></a>
<a href="/~kghose">kghose</a>
<a href="/c/lqqn3a"><time title="2022-07-22 14:28:27" datetime="2022-07-22 14:28:27" data-at-unix="1658518107">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<p>Tangential to your effort here, on a personal note, if I find a discussion interesting I read all the posts regardless of ranking. On the discussions I find worthwhile I find that post quality is unrelated to upvotes.</p>
<p>On some discussions I find the top voted comment has devolved into a long tail of niche discussion, often acrimonious, which is not as useful as later posts.</p>
<p>Worst to pick out of the noise are replies to replies which are great but buried in not so great comments.</p>
</div>
</div>
</div>
<ol class="comments"></ol>
</li>
</ol>
</li>
</ol>
</li>
<li class="comments_subtree">
<input id="comment_folder_7b0jgw"
class="comment_folder_button" type="checkbox"
>
<div id="c_7b0jgw"
data-shortid="7b0jgw"
class="comment
">
<div class="voters">
<label for="comment_folder_7b0jgw" class="comment_folder"></label>
<a class="upvoter" title="4" href="/login">4</a>
</div>
<div class="details">
<div class="byline">
<a name="c_7b0jgw"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~FeepingCreature"><img srcset="/avatars/FeepingCreature-16.png 1x, /avatars/FeepingCreature-32.png 2x" class="avatar" alt="" loading="lazy" decoding="async" src="/avatars/FeepingCreature-16.png" width="16" height="16"></a>
<a href="/~FeepingCreature">FeepingCreature</a>
<a href="/c/7b0jgw"><time title="2022-07-23 11:26:03" datetime="2022-07-23 11:26:03" data-at-unix="1658593563">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<p>Ranking by max or sum upvotes optimizes for photogenic smackdowns of bad opinions.</p>
<p>Maybe disaggregate "agreement upvotes" and "contribution upvotes"?</p>
</div>
</div>
</div>
<ol class="comments"></ol>
</li>
<li class="comments_subtree">
<input id="comment_folder_ei3nck"
class="comment_folder_button" type="checkbox"
>
<div id="c_ei3nck"
data-shortid="ei3nck"
class="comment
">
<div class="voters">
<label for="comment_folder_ei3nck" class="comment_folder"></label>
<a class="upvoter" title="2" href="/login">2</a>
</div>
<div class="details">
<div class="byline">
<a name="c_ei3nck"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~FRIGN"><img srcset="/avatars/FRIGN-16.png 1x, /avatars/FRIGN-32.png 2x" class="avatar" alt="" loading="lazy" decoding="async" src="/avatars/FRIGN-16.png" width="16" height="16"></a>
<a href="/~FRIGN">FRIGN</a>
edited
<a href="/c/ei3nck"><time title="2022-07-23 10:48:49" datetime="2022-07-23 10:48:49" data-at-unix="1658591329">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<p>I feel kind of honored to be taken as an example, so please let me share my personal thoughts (TL;DR below):</p>
<p>It is impossible to find <em>the</em> definite ranking system, but let me elaborate by using 3 extremes to see where the current and proposed systems fail:</p>
<ol>
<li>
<em>highly controversial root comment</em> with score 0 (100 upvotes, 100 downvotes), logically controversial responses with median 0,</li>
<li>
<em>root comment preaching to the choir</em> with logically high score and usually high-score responses,</li>
<li>
<em>low-score root comment with high-score responses</em> (like the presented case)</li>
<li>
<em>OC high-value root comment</em> with high score and high-value responses</li>
</ol>
<p>It all depends on personal preference though, but I personally would like more controversial topics to be on top and less controversial ones to be secondary. The reason for that is that you otherwise might end up with echo-chambers, given users are encouraged to preach to the choir for a high karma score. As a tangent, I would prefer two karmas for each user: A "paragon-karma" and "renegade-karma" whose sum would be an "influence" score (in the end up- or downvotes both reflect a certain influence) and whose difference would be a "non-controversiality-score" (a low difference reflects controversiality, a high score the opposite). All in all, maybe we should move away complete from the miriad of downvote-options on lobsters and simply have an "agree" and "disagree". If something is spam, you can report it, if something is incorrect, you can write a response rectifying it (which would then be subject to a vote of agreement and disagreement, depending on how well you state your case).</p>
<p>The proposal to add replies into the weight would not change the ranking of controversial topics (1), but even moreso weigh comments preaching to the choir (2) because everyone replying would aim to also get some karma. It would solve the presented case (3) though. Case (4) would also be favoured.</p>
<p>The proposal by <a href="https://lobste.rs/~dpercy" rel="ugc">@dpercy</a> to the take the max would not change (1)'s ranking, probably not affect (2) but help (3) as well. (4) is also positively affected.</p>
<p>TL;DR, here's my proposal so more highly-controversial topics (those that are interesting) are more favoured: Consider root comment and replies as equals and only take <em>influence</em> (sum of up- and downvotes) as a score.</p>
<p>This benefits (1), which in the current form end up at the bottom, which makes zero sense. It also benefits (2) and (4), which is a forced compromise, given the score does not really show how "original" a comment is (this is why Reddit probably introduced awards to allow users to weigh very good posts). By taking all replies into account, it also, of course, benefits (3), which makes sense to push up.</p>
<p>Replies with no votes increase the influence of the thread by 1, indeed, but isn't the purpose of the score to show what the hivemind thinks? By scoring them as zero (which was proposed here), you effectively value an elaborated response lower than a simple upvote of the original comment. Instead, why not simply "fold" long subthreads so they don't take up too much space?</p>
<p>As another tangent: By weighing root and children, this might encourage people to respond to "deep" threads instead of writing a new post.</p>
<p>Let's see what <a href="https://lobste.rs/~pushcx" rel="ugc">@pushcx</a> decides in the end. :)</p>
</div>
</div>
</div>
<ol class="comments"></ol>
</li>
<li class="comments_subtree">
<input id="comment_folder_4l8mzk"
class="comment_folder_button" type="checkbox"
>
<div id="c_4l8mzk"
data-shortid="4l8mzk"
class="comment
">
<div class="voters">
<label for="comment_folder_4l8mzk" class="comment_folder"></label>
<a class="upvoter" title="2" href="/login">2</a>
</div>
<div class="details">
<div class="byline">
<a name="c_4l8mzk"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~feoh"><img srcset="/avatars/feoh-16.png 1x, /avatars/feoh-32.png 2x" class="avatar" alt="" loading="lazy" decoding="async" src="/avatars/feoh-16.png" width="16" height="16"></a>
<a href="/~feoh">feoh</a>
<a href="/c/4l8mzk"><time title="2022-07-22 12:30:20" datetime="2022-07-22 12:30:20" data-at-unix="1658511020">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<p>I really like this idea but have no idea what its implementation would represent in terms of additional load to the server.</p>
</div>
</div>
</div>
<ol class="comments">
<li class="comments_subtree">
<input id="comment_folder_qy0l9k"
class="comment_folder_button" type="checkbox"
>
<div id="c_qy0l9k"
data-shortid="qy0l9k"
class="comment
">
<div class="voters">
<label for="comment_folder_qy0l9k" class="comment_folder"></label>
<a class="upvoter" title="5" href="/login">5</a>
</div>
<div class="details">
<div class="byline">
<a name="c_qy0l9k"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~pushcx"><img srcset="/avatars/pushcx-16.png 1x, /avatars/pushcx-32.png 2x" class="avatar" alt="" loading="lazy" decoding="async" src="/avatars/pushcx-16.png" width="16" height="16"></a>
<a href="/~pushcx">pushcx</a>
<span class="hat hat_sysop" title="Granted 2017-10-15"><span class="crown">Sysop</span></span>
<a href="/c/qy0l9k"><time title="2022-07-22 16:12:09" datetime="2022-07-22 16:12:09" data-at-unix="1658524329">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<p>I've moved most of the <a href="https://github.com/lobsters/lobsters/blob/cf52ab9f3f0b3ac805effc2e718f15922cb67332/app/models/comment.rb#L340" rel="ugc">vote</a> into the db, but <a href="https://github.com/lobsters/lobsters/blob/cf52ab9f3f0b3ac805effc2e718f15922cb67332/app/models/comment.rb#L219" rel="ugc">not all</a>. If <code>calculated_confidence</code> finished its move into the db almost any algorithm would be faster than the current implementation.</p>
</div>
</div>
</div>
<ol class="comments"></ol>
</li>
</ol>
</li>
</ol>
</li>
</ol>
<br>
</div>
<footer>
<span id="office-hours-live">
</span>
<nav>
<a href="/about">About</a>
<a href="/tags">Tags</a>
<a href="/filters">Filter</a>
<a href="/moderations">Moderation Log</a>
</nav>
</footer>
<span id="iab-pcm-sdk"></span><span id="iab-autofill-sdk"></span>
</body>
</html>
File diff suppressed because one or more lines are too long
+803
View File
@@ -0,0 +1,803 @@
<!doctype html>
<html lang="en" >
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<link rel="icon" sizes="144x144" type="image/png" href="/touch-icon-144.png">
<link rel="apple-touch-icon" sizes="57x57" href="/touch-icon.png">
<link rel="apple-touch-icon" sizes="114x114" href="/touch-icon.png">
<link rel="apple-touch-icon" sizes="72x72" href="/touch-icon-144.png">
<link rel="apple-touch-icon" sizes="144x144" href="/touch-icon-144.png">
<link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Lobsters">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="referrer" content="always">
<meta name="theme-color" content="#AC130D">
<meta name="story-flags" content="{&quot;O&quot;:&quot;Off-topic&quot;,&quot;A&quot;:&quot;Already Posted&quot;,&quot;B&quot;:&quot;Broken Link&quot;,&quot;S&quot;:&quot;Spam&quot;,&quot;&quot;:&quot;Cancel&quot;}">
<meta name="comment-flags" content="{&quot;O&quot;:&quot;Off-topic&quot;,&quot;M&quot;:&quot;Me-too&quot;,&quot;T&quot;:&quot;Troll&quot;,&quot;U&quot;:&quot;Unkind&quot;,&quot;S&quot;:&quot;Spam&quot;,&quot;&quot;:&quot;Cancel&quot;}">
<meta property="og:type" content="article">
<meta property="og:site_name" content="Lobsters">
<meta property="og:title" content="Ranking comments by sum of replies&#39; scores">
<meta property="og:description" content="10 comments">
<meta property="og:image" content="https://lobste.rs/story_image/tdfoqh.png">
<meta property="article:author" content="https://lobste.rs/~gioele">
<link rel="shortcut icon" href="/favicon.ico">
<link href="https://lobste.rs/s/tdfoqh/ranking_comments_by_sum_replies_scores" rev="canonical" rel="self alternate shorter shorturl shortlink">
<title>Ranking comments by sum of replies&#39; scores | Lobsters</title>
<link rel="stylesheet" href="/assets/application-50d965fb.css">
<link rel="stylesheet" href="/assets/system-system-fd030c9d.css">
<link rel="stylesheet" href="/assets/tom-select-00e8031d.css" data-turbo-track="reload">
<link rel="stylesheet" href="/assets/TomSelect_remove_button-4d5c34b9.css" data-turbo-track="reload">
<meta name="csrf-param" content="authenticity_token">
<meta name="csrf-token" content="I9QGMwqhWWuCkGBSdDF7vV0v16wATa6x6zmxpaCQKS4iehHByjKy34YLK6cPpB626ZL-FaO10jcm3edeXIFrBw">
<meta name="robots" content="noai, noimageai">
<script type="speculationrules" nonce="owvfCFqNS/eLJHNrSN2nYg==">
{
"prefetch": [
{
"where": {
"href_matches": "/*"
},
"eagerness": "moderate"
}
]
}
</script>
</head>
<body data-username='' data-now-unix='1778381500'>
<header id="nav">
<a id="logo" style="background-color: #550000;" href="/" title="Lobsters (Current traffic: 3%)"></a>
<div class="navholder">
<nav class="links">
<a class="" href="/active">Active</a>
<a class="" href="/recent">Recent</a>
<a class="" href="/comments">Comments</a>
<a class="" href="/search">Search</a>
<a class="corner" href="/login">Login</a>
</nav>
</div>
<nav class="corner">
<a class="corner" href="/login">Login</a>
</nav>
</header>
<div id="inside">
<ol class="stories">
<li id="story_tdfoqh" data-shortid="tdfoqh" class="story">
<div class="story_liner h-entry">
<div class="voters">
<a class="upvoter" href="/login">18</a>
</div>
<div class="details" id="header_story_01g8ke0w20enk85qfby8bp0cgy">
<span role="heading" aria-level="1" class="link h-cite u-repost-of">
<a href="/s/tdfoqh/ranking_comments_by_sum_replies_scores" rel="ugc noreferrer" class="u-url">Ranking comments by sum of replies&#39; scores</a>
</span>
<a class="description_present" title="There are often cases where comments with a poor score (say, 1 point), invite high-quality replies that receive many upvotes.
These threads are not shown at the top of the story&#39;s comments page because their &quot;root comment&quot; has a low score.
An example, as of 2022-07-22 17:00 UTC, is &lt;https://lobste.rs/s/ekvqcf/random_wallpaper_with_just_bash_systemd#c_vop9bt&gt;:
* the root comment has 1 point,
* the first reply has 12 points,
* and, in total, 18 points have been accumulated by all the r..." href="/s/tdfoqh/ranking_comments_by_sum_replies_scores">&#x2636;</a>
<span class="tags">
<a class="tag tag_meta" title="Lobsters-related bikeshedding - report bugs at https://github.com/lobsters/lobsters" href="/t/meta">meta</a>
</span>
<div class="byline">
<span>authored by</span>
<a tabindex="-1" aria-hidden="true" href="/~gioele"><img srcset="/avatars/gioele-16.png 1x, /avatars/gioele-32.png 2x" class="avatar" alt="gioele avatar" loading="lazy" decoding="async" src="/avatars/gioele-16.png" width="16" height="16"></a>
<a class="u-author h-card user_is_author" href="/~gioele">gioele</a>
<time title="2022-07-22 12:06:59" datetime="2022-07-22 12:06:59" data-at-unix="1658509619">3 years ago</time>
<span class="comments_label">
<span aria-hidden="true"> | </span>
<a role="heading" aria-level="2" href="/s/tdfoqh/ranking_comments_by_sum_replies_scores#comments_story_01g8ke0w20enk85qfby8bp0cgy">
10
comments
</a>
</span>
</div>
</div>
</div>
</li>
</ol>
<div class="story_content">
<div class="story_text">
<p>There are often cases where comments with a poor score (say, 1 point), invite high-quality replies that receive many upvotes.</p>
<p>These threads are not shown at the top of the story's comments page because their "root comment" has a low score.</p>
<p>An example, as of 2022-07-22 17:00 UTC, is <a href="https://lobste.rs/s/ekvqcf/random_wallpaper_with_just_bash_systemd#c_vop9bt" rel="ugc">https://lobste.rs/s/ekvqcf/random_wallpaper_with_just_bash_systemd#c_vop9bt</a>:</p>
<ul>
<li>the root comment has 1 point,</li>
<li>the first reply has 12 points,</li>
<li>and, in total, 18 points have been accumulated by all the replies with score &gt; 1 point.</li>
</ul>
<p>Because the root comment has only 1 point, it remains at the bottom of the page, bringing down with itself also the good comment with 12 points and all the other upvoted comments.</p>
<p>Could an alternative ranking system be put in place, where the threads and subthreads are not sorted by the score of their root comment, but by the <em>sum of all the scores of their children</em>?</p>
<p>(Maybe taking into account only children with <code>&gt;1</code> point, to avoid giving importance to superficial back-and-forth discussions.)</p>
</div>
</div>
<ol class="comments comments1">
<li class="comments_subtree" id="story_comments">
<ol class="comments">
<li class="comments_subtree">
<input id="comment_folder_ncdsfc"
class="comment_folder_button" type="checkbox"
>
<div id="c_ncdsfc"
data-shortid="ncdsfc"
class="comment upvoted
">
<div class="voters">
<label for="comment_folder_ncdsfc" class="comment_folder"></label>
<a class="upvoter" title="18" href="/login">18</a>
</div>
<div class="details">
<div class="byline">
<a name="c_ncdsfc"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~dpercy"><img srcset="/avatars/dpercy-16.png 1x, /avatars/dpercy-32.png 2x" class="avatar" alt="dpercy avatar" loading="lazy" decoding="async" src="/avatars/dpercy-16.png" width="16" height="16"></a>
<a href="/~dpercy">dpercy</a>
<a href="/c/ncdsfc"><time title="2022-07-22 14:04:36" datetime="2022-07-22 14:04:36" data-at-unix="1658516676">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<blockquote>
<p>to avoid giving importance to superficial back-and-forth discussions</p>
</blockquote>
<p>Maybe take the max, instead of the sum? That way it's still comparing the score of individual comments. A highly-upvoted child would rank higher than a less-upvoted toplevel comment, but long threads wouldn't get any bonus.</p>
</div>
</div>
</div>
<ol class="comments"></ol>
</li>
<li class="comments_subtree">
<input id="comment_folder_m3wyu5"
class="comment_folder_button" type="checkbox"
>
<div id="c_m3wyu5"
data-shortid="m3wyu5"
class="comment
">
<div class="voters">
<label for="comment_folder_m3wyu5" class="comment_folder"></label>
<a class="upvoter" title="8" href="/login">8</a>
</div>
<div class="details">
<div class="byline">
<a name="c_m3wyu5"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~friendlysock"><img srcset="/avatars/friendlysock-16.png 1x, /avatars/friendlysock-32.png 2x" class="avatar" alt="friendlysock avatar" loading="lazy" decoding="async" src="/avatars/friendlysock-16.png" width="16" height="16"></a>
<a class="inactive_user" aria-label="friendlysock - Inactive user" href="/~friendlysock">friendlysock</a>
<a href="/c/m3wyu5"><time title="2022-07-23 01:04:43" datetime="2022-07-23 01:04:43" data-at-unix="1658556283">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<p>For better or worse, the deeper you go into threads the less likely it is that the discussion is still about the original submission. Your proposal, or the max variant presented elsewhere, I fear would unduly reward flamebait and off-topic discussion.</p>
</div>
</div>
</div>
<ol class="comments">
<li class="comments_subtree">
<input id="comment_folder_pcvbcd"
class="comment_folder_button" type="checkbox"
>
<div id="c_pcvbcd"
data-shortid="pcvbcd"
class="comment
">
<div class="voters">
<label for="comment_folder_pcvbcd" class="comment_folder"></label>
<a class="upvoter" title="2" href="/login">2</a>
</div>
<div class="details">
<div class="byline">
<a name="c_pcvbcd"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~gioele"><img srcset="/avatars/gioele-16.png 1x, /avatars/gioele-32.png 2x" class="avatar" alt="gioele avatar" loading="lazy" decoding="async" src="/avatars/gioele-16.png" width="16" height="16"></a>
<a class="user_is_author" aria-label="gioele - Author" href="/~gioele">gioele</a>
edited
<a href="/c/pcvbcd"><time title="2022-07-23 10:09:15" datetime="2022-07-23 10:09:15" data-at-unix="1658588955">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<blockquote>
<p>Your proposal, or the max variant presented elsewhere, I fear would unduly reward flamebait and off-topic discussion.</p>
</blockquote>
<p>Such off-topic discussions that stray away from the main topic do exist, are a nuisance, and, surely, they should not be rewarded with more attention. However, such sub-threads exists as a long string of short comments with no or very few upvotes. (I wonder if the data confirms this or it is just my impression.)</p>
<p>For this reason I believe that a) not counting in comments with score==1 or b) <a href="https://lobste.rs/~dpercy" rel="ugc">@dpercy</a>'s max variant would avoid rewarding such discussions.</p>
</div>
</div>
</div>
<ol class="comments"></ol>
</li>
</ol>
</li>
<li class="comments_subtree">
<input id="comment_folder_9vyyu5"
class="comment_folder_button" type="checkbox"
>
<div id="c_9vyyu5"
data-shortid="9vyyu5"
class="comment
">
<div class="voters">
<label for="comment_folder_9vyyu5" class="comment_folder"></label>
<a class="upvoter" title="5" href="/login">5</a>
</div>
<div class="details">
<div class="byline">
<a name="c_9vyyu5"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~kghose"><img srcset="/avatars/kghose-16.png 1x, /avatars/kghose-32.png 2x" class="avatar" alt="kghose avatar" loading="lazy" decoding="async" src="/avatars/kghose-16.png" width="16" height="16"></a>
<a href="/~kghose">kghose</a>
<a href="/c/9vyyu5"><time title="2022-07-22 13:39:20" datetime="2022-07-22 13:39:20" data-at-unix="1658515160">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<p>Just like to point out that the assumption here is that upvotes is a metric of quality. On lobste.rs as in any other social context, upvotes/support is a measure of popularity.</p>
<p>There are many instances where the two are correlated, and some important instances where they are not.</p>
</div>
</div>
</div>
<ol class="comments">
<li class="comments_subtree">
<input id="comment_folder_owddle"
class="comment_folder_button" type="checkbox"
>
<div id="c_owddle"
data-shortid="owddle"
class="comment
">
<div class="voters">
<label for="comment_folder_owddle" class="comment_folder"></label>
<a class="upvoter" title="7" href="/login">7</a>
</div>
<div class="details">
<div class="byline">
<a name="c_owddle"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~gioele"><img srcset="/avatars/gioele-16.png 1x, /avatars/gioele-32.png 2x" class="avatar" alt="gioele avatar" loading="lazy" decoding="async" src="/avatars/gioele-16.png" width="16" height="16"></a>
<a class="user_is_author" aria-label="gioele - Author" href="/~gioele">gioele</a>
<a href="/c/owddle"><time title="2022-07-22 13:57:19" datetime="2022-07-22 13:57:19" data-at-unix="1658516239">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<blockquote>
<p>Just like to point out that the assumption here is that upvotes is a metric of quality. On lobste.rs as in any other social context, upvotes/support is a measure of popularity.</p>
</blockquote>
<p>I fully agree: upvotes are a metric of popularity and not of quality. But this proposal does not assume that.</p>
<p>Regardless of what upvotes represent, currently threads are sorted in descending order of (upvotes, time). All that this proposal suggests, is to change that to (sum(upvotes of root-and-children), time).</p>
<p>The only assumption here is: if ranking a thread by upvotes of its root comment is considered good (and at the moment it is), then ranking by the sum of the upvotes of the whole thread is better.</p>
</div>
</div>
</div>
<ol class="comments">
<li class="comments_subtree">
<input id="comment_folder_lqqn3a"
class="comment_folder_button" type="checkbox"
>
<div id="c_lqqn3a"
data-shortid="lqqn3a"
class="comment
">
<div class="voters">
<label for="comment_folder_lqqn3a" class="comment_folder"></label>
<a class="upvoter" title="2" href="/login">2</a>
</div>
<div class="details">
<div class="byline">
<a name="c_lqqn3a"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~kghose"><img srcset="/avatars/kghose-16.png 1x, /avatars/kghose-32.png 2x" class="avatar" alt="kghose avatar" loading="lazy" decoding="async" src="/avatars/kghose-16.png" width="16" height="16"></a>
<a href="/~kghose">kghose</a>
<a href="/c/lqqn3a"><time title="2022-07-22 14:28:27" datetime="2022-07-22 14:28:27" data-at-unix="1658518107">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<p>Tangential to your effort here, on a personal note, if I find a discussion interesting I read all the posts regardless of ranking. On the discussions I find worthwhile I find that post quality is unrelated to upvotes.</p>
<p>On some discussions I find the top voted comment has devolved into a long tail of niche discussion, often acrimonious, which is not as useful as later posts.</p>
<p>Worst to pick out of the noise are replies to replies which are great but buried in not so great comments.</p>
</div>
</div>
</div>
<ol class="comments"></ol>
</li>
</ol>
</li>
</ol>
</li>
<li class="comments_subtree">
<input id="comment_folder_7b0jgw"
class="comment_folder_button" type="checkbox"
>
<div id="c_7b0jgw"
data-shortid="7b0jgw"
class="comment
">
<div class="voters">
<label for="comment_folder_7b0jgw" class="comment_folder"></label>
<a class="upvoter" title="4" href="/login">4</a>
</div>
<div class="details">
<div class="byline">
<a name="c_7b0jgw"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~FeepingCreature"><img srcset="/avatars/FeepingCreature-16.png 1x, /avatars/FeepingCreature-32.png 2x" class="avatar" alt="FeepingCreature avatar" loading="lazy" decoding="async" src="/avatars/FeepingCreature-16.png" width="16" height="16"></a>
<a href="/~FeepingCreature">FeepingCreature</a>
<a href="/c/7b0jgw"><time title="2022-07-23 11:26:03" datetime="2022-07-23 11:26:03" data-at-unix="1658593563">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<p>Ranking by max or sum upvotes optimizes for photogenic smackdowns of bad opinions.</p>
<p>Maybe disaggregate "agreement upvotes" and "contribution upvotes"?</p>
</div>
</div>
</div>
<ol class="comments"></ol>
</li>
<li class="comments_subtree">
<input id="comment_folder_ei3nck"
class="comment_folder_button" type="checkbox"
>
<div id="c_ei3nck"
data-shortid="ei3nck"
class="comment
">
<div class="voters">
<label for="comment_folder_ei3nck" class="comment_folder"></label>
<a class="upvoter" title="2" href="/login">2</a>
</div>
<div class="details">
<div class="byline">
<a name="c_ei3nck"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~FRIGN"><img srcset="/avatars/FRIGN-16.png 1x, /avatars/FRIGN-32.png 2x" class="avatar" alt="FRIGN avatar" loading="lazy" decoding="async" src="/avatars/FRIGN-16.png" width="16" height="16"></a>
<a href="/~FRIGN">FRIGN</a>
edited
<a href="/c/ei3nck"><time title="2022-07-23 10:48:49" datetime="2022-07-23 10:48:49" data-at-unix="1658591329">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<p>I feel kind of honored to be taken as an example, so please let me share my personal thoughts (TL;DR below):</p>
<p>It is impossible to find <em>the</em> definite ranking system, but let me elaborate by using 3 extremes to see where the current and proposed systems fail:</p>
<ol>
<li>
<em>highly controversial root comment</em> with score 0 (100 upvotes, 100 downvotes), logically controversial responses with median 0,</li>
<li>
<em>root comment preaching to the choir</em> with logically high score and usually high-score responses,</li>
<li>
<em>low-score root comment with high-score responses</em> (like the presented case)</li>
<li>
<em>OC high-value root comment</em> with high score and high-value responses</li>
</ol>
<p>It all depends on personal preference though, but I personally would like more controversial topics to be on top and less controversial ones to be secondary. The reason for that is that you otherwise might end up with echo-chambers, given users are encouraged to preach to the choir for a high karma score. As a tangent, I would prefer two karmas for each user: A "paragon-karma" and "renegade-karma" whose sum would be an "influence" score (in the end up- or downvotes both reflect a certain influence) and whose difference would be a "non-controversiality-score" (a low difference reflects controversiality, a high score the opposite). All in all, maybe we should move away complete from the miriad of downvote-options on lobsters and simply have an "agree" and "disagree". If something is spam, you can report it, if something is incorrect, you can write a response rectifying it (which would then be subject to a vote of agreement and disagreement, depending on how well you state your case).</p>
<p>The proposal to add replies into the weight would not change the ranking of controversial topics (1), but even moreso weigh comments preaching to the choir (2) because everyone replying would aim to also get some karma. It would solve the presented case (3) though. Case (4) would also be favoured.</p>
<p>The proposal by <a href="https://lobste.rs/~dpercy" rel="ugc">@dpercy</a> to the take the max would not change (1)'s ranking, probably not affect (2) but help (3) as well. (4) is also positively affected.</p>
<p>TL;DR, here's my proposal so more highly-controversial topics (those that are interesting) are more favoured: Consider root comment and replies as equals and only take <em>influence</em> (sum of up- and downvotes) as a score.</p>
<p>This benefits (1), which in the current form end up at the bottom, which makes zero sense. It also benefits (2) and (4), which is a forced compromise, given the score does not really show how "original" a comment is (this is why Reddit probably introduced awards to allow users to weigh very good posts). By taking all replies into account, it also, of course, benefits (3), which makes sense to push up.</p>
<p>Replies with no votes increase the influence of the thread by 1, indeed, but isn't the purpose of the score to show what the hivemind thinks? By scoring them as zero (which was proposed here), you effectively value an elaborated response lower than a simple upvote of the original comment. Instead, why not simply "fold" long subthreads so they don't take up too much space?</p>
<p>As another tangent: By weighing root and children, this might encourage people to respond to "deep" threads instead of writing a new post.</p>
<p>Let's see what <a href="https://lobste.rs/~pushcx" rel="ugc">@pushcx</a> decides in the end. :)</p>
</div>
</div>
</div>
<ol class="comments"></ol>
</li>
<li class="comments_subtree">
<input id="comment_folder_4l8mzk"
class="comment_folder_button" type="checkbox"
>
<div id="c_4l8mzk"
data-shortid="4l8mzk"
class="comment
">
<div class="voters">
<label for="comment_folder_4l8mzk" class="comment_folder"></label>
<a class="upvoter" title="2" href="/login">2</a>
</div>
<div class="details">
<div class="byline">
<a name="c_4l8mzk"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~feoh"><img srcset="/avatars/feoh-16.png 1x, /avatars/feoh-32.png 2x" class="avatar" alt="feoh avatar" loading="lazy" decoding="async" src="/avatars/feoh-16.png" width="16" height="16"></a>
<a href="/~feoh">feoh</a>
<a href="/c/4l8mzk"><time title="2022-07-22 12:30:20" datetime="2022-07-22 12:30:20" data-at-unix="1658511020">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<p>I really like this idea but have no idea what its implementation would represent in terms of additional load to the server.</p>
</div>
</div>
</div>
<ol class="comments">
<li class="comments_subtree">
<input id="comment_folder_qy0l9k"
class="comment_folder_button" type="checkbox"
>
<div id="c_qy0l9k"
data-shortid="qy0l9k"
class="comment
">
<div class="voters">
<label for="comment_folder_qy0l9k" class="comment_folder"></label>
<a class="upvoter" title="5" href="/login">5</a>
</div>
<div class="details">
<div class="byline">
<a name="c_qy0l9k"></a>
<span class="">
<a tabindex="-1" aria-hidden="true" href="/~pushcx"><img srcset="/avatars/pushcx-16.png 1x, /avatars/pushcx-32.png 2x" class="avatar" alt="pushcx avatar" loading="lazy" decoding="async" src="/avatars/pushcx-16.png" width="16" height="16"></a>
<a href="/~pushcx">pushcx</a>
<span class="hat hat_sysop" title="Granted 2017-10-15"><span class="crown">Sysop</span></span>
<a href="/c/qy0l9k"><time title="2022-07-22 16:12:09" datetime="2022-07-22 16:12:09" data-at-unix="1658524329">3 years ago</time></a>
</span>
<span class="flagger flagger_stub"></span>
<span class="reason">
</span>
</div>
<div role="heading" aria-level="3" class="comment_text">
<p>I've moved most of the <a href="https://github.com/lobsters/lobsters/blob/cf52ab9f3f0b3ac805effc2e718f15922cb67332/app/models/comment.rb#L340" rel="ugc">vote</a> into the db, but <a href="https://github.com/lobsters/lobsters/blob/cf52ab9f3f0b3ac805effc2e718f15922cb67332/app/models/comment.rb#L219" rel="ugc">not all</a>. If <code>calculated_confidence</code> finished its move into the db almost any algorithm would be faster than the current implementation.</p>
</div>
</div>
</div>
<ol class="comments"></ol>
</li>
</ol>
</li>
</ol>
</li>
</ol>
<br>
</div>
<footer>
<span id="office-hours-live">
</span>
<nav>
<a href="/about">About</a>
<a href="/tags">Tags</a>
<a href="/filters">Filter</a>
<a href="/moderations">Moderation Log</a>
</nav>
</footer>
<span id="iab-pcm-sdk"></span><span id="iab-autofill-sdk"></span>
</body>
</html>
+22
View File
@@ -0,0 +1,22 @@
<div class="comment_form_container" data-shortid="">
<form action="/comments" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="AI0414bnzi152-mE0JTWEtwq5B0ZhALBW1W7rGiG5zR-sFaJjWzdARXFM7w_DbPQqWNjzh9bufWZbXG39v5T6g" autocomplete="off">
<input value="znlkib" autocomplete="off" type="hidden" name="story_id" id="story_id" />
<input value="post" autocomplete="off" type="hidden" name="_method" id="_method" />
<input value="edtrox" autocomplete="off" type="hidden" name="parent_comment_short_id" id="parent_comment_short_id" />
<div style="width: 100%;">
<textarea rows="5" placeholder="" required="required" name="comment" id="comment">
</textarea>
<div class="controls">
<div class="buttons">
<input type="submit" name="commit" value="Post" class="comment-post" data-disable-with="Post">
</div>
</div>
</div>
</form>
</div>
File diff suppressed because one or more lines are too long
+1548
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+14 -2
View File
@@ -8,6 +8,7 @@
plugins {
alias(libs.plugins.android.test)
id("dev.msfjarvis.claw.android-common")
id("dev.msfjarvis.claw.kotlin-android")
alias(libs.plugins.baselineprofile)
alias(libs.plugins.dependencyAnalysis)
@@ -16,7 +17,6 @@ plugins {
android {
namespace = "dev.msfjarvis.claw.benchmark"
compileSdk = 36
defaultConfig {
minSdk = 28
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -30,11 +30,23 @@ android {
}
}
targetProjectPath = ":android"
testOptions {
managedDevices {
localDevices {
create("pixel9api36") {
device = "Pixel 9"
apiLevel = 36
systemImageSource = "aosp"
}
}
}
}
}
baselineProfile {
useConnectedDevices = true
useConnectedDevices = false
enableEmulatorDisplay = false
managedDevices += "pixel9api36"
}
dependencies {
+4
View File
@@ -42,6 +42,10 @@ gradlePlugin {
id = "dev.msfjarvis.claw.kotlin-jvm"
implementationClass = "dev.msfjarvis.claw.gradle.KotlinJvmPlugin"
}
register("kotlin-multiplatform") {
id = "dev.msfjarvis.claw.kotlin-multiplatform"
implementationClass = "dev.msfjarvis.claw.gradle.KotlinMultiplatformPlugin"
}
register("rename-artifacts") {
id = "dev.msfjarvis.claw.rename-artifacts"
implementationClass = "dev.msfjarvis.claw.gradle.RenameArtifactsPlugin"
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 9.1.0" type="baseline" client="gradle" dependencies="false" name="AGP (9.1.0)" variant="all" version="9.1.0">
<issues format="6" by="lint 9.4.0-alpha03" type="baseline" client="gradle" dependencies="false" name="AGP (9.4.0-alpha03)" variant="all" version="9.4.0-alpha03">
</issues>

Some files were not shown because too many files have changed in this diff Show More