Compare commits

...
244 Commits
Author SHA1 Message Date
GitHub Actions a41b465648 feat(release): bump version 2023-07-27 08:06:08 +00:00
Harsh Shandilya a7038600b3 fix(release): update signing key secret in publish workflow
Fixes: ca6b941cbb ("fix(release): update secrets handling scripts")
2023-07-27 13:34:22 +05:30
Harsh Shandilya 3143556c63 fix: re-roll Lint baseline 2023-07-27 12:04:50 +05:30
GitHub Actions 5e2d9a2355 chore(android): refresh baseline profile 2023-07-27 01:38:58 +00:00
Harsh Shandilya b4adfede90 fix(android): avoid crash from empty creation time
Fixes COMPOSE-LOBSTERS-17
2023-07-27 02:57:35 +05:30
Harsh Shandilya ed85ba905d fix(deps): update dev.chrisbanes.compose:compose-bom to 2023.07.00-alpha02 2023-07-27 02:36:28 +05:30
Harsh Shandilya c29e4065d6 chore: update changelog 2023-07-27 02:03:48 +05:30
Harsh Shandilya bcf4f02a01 Revert "Revert "Revert "fix(common): make text in comments screen selectable"""
Still causes crashes :(

This reverts commit c5ec6cd680.

Fixes COMPOSE-LOBSTERS-16
2023-07-27 01:28:21 +05:30
renovate[bot]andGitHub 72b9409383 chore(deps): update github/codeql-action action to v2.21.1 2023-07-26 19:31:22 +00:00
renovate[bot]andGitHub 2c762428af chore(deps): update androidx 2023-07-26 18:27:51 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
58e427c13a chore(deps): update sqldelight to v2.0.0 (#391)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-26 23:57:37 +05:30
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
974f03ee89 fix(deps): update dependency androidx.paging:paging-compose to v3 (#392)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-26 23:57:28 +05:30
renovate[bot]andGitHub f5e6b25ec6 chore(deps): update dependency gradle to v8.3-rc-2 2023-07-26 16:46:13 +00:00
renovate[bot]andGitHub be5f073acf fix(deps): update sentry to v6.27.0 2023-07-26 08:41:17 +00:00
Harsh Shandilya ca6b941cbb fix(release): update secrets handling scripts 2023-07-26 14:10:49 +05:30
GitHub Actions 1e5f830652 chore(android): refresh baseline profile 2023-07-26 06:39:23 +00:00
renovate[bot]andGitHub e72a5fda5c chore(deps): update agp to v8.2.0-alpha14 2023-07-25 21:59:32 +00:00
renovate[bot]andGitHub 5c2fe6e630 fix(deps): update coroutines to v1.7.3 2023-07-25 19:04:26 +00:00
Harsh ShandilyaandGitHub 5907c2a09a fix(scripts): make boot_emulator naming consistent 2023-07-25 21:52:32 +05:30
Harsh Shandilya e3b381c665 chore: refresh feature graphic 2023-07-25 20:25:21 +05:30
Harsh Shandilya f1d30e59b0 feat: add Search flow to Maestro 2023-07-25 20:17:27 +05:30
Harsh Shandilya 7669a85f43 chore: refresh README graphic 2023-07-25 19:25:35 +05:30
GitHub Actions c05db765c8 feat(release): start next development iteration 2023-07-25 10:42:52 +00:00
GitHub Actions 4c8e30bcc9 feat(release): bump version 2023-07-25 10:38:59 +00:00
GitHub Actions d32819b1ff chore(android): refresh baseline profile 2023-07-25 06:39:00 +00:00
Harsh Shandilya a2d046bd0b Revert "fix(benchmark): upgrade to API level 33"
This reverts commit 1defffa504.
2023-07-25 11:46:47 +05:30
Harsh Shandilya 3ad4ff1c63 fix(build): use Whetstone from Maven Central 2023-07-25 11:40:02 +05:30
Harsh Shandilya 1defffa504 fix(benchmark): upgrade to API level 33 2023-07-25 01:31:01 +05:30
Harsh Shandilya 0d78cb4112 fix(benchmark): rework baseline profile benchmark 2023-07-25 01:31:01 +05:30
Harsh Shandilya 7c59130af9 feat(benchmark): restore full UI exploration 2023-07-25 01:31:01 +05:30
Harsh Shandilya 4137d73539 chore: update changelog 2023-07-25 00:56:07 +05:30
renovate[bot]andGitHub 0f8a649e3b chore(deps): update gradle/gradle-build-action action to v2.7.0 2023-07-24 16:23:53 +00:00
Harsh ShandilyaandGitHub 7e7a4bec83 chore(renovate): re-enable Kotlin upgrades 2023-07-24 21:49:52 +05:30
Harsh Shandilya b442b14cc1 fix(deps): upgrade to Kotlin 1.9.0 2023-07-24 21:29:24 +05:30
renovate[bot]andGitHub 97886f6ece chore(deps): update actions/setup-java action to v3.12.0 2023-07-24 14:03:55 +00:00
GitHub Actions 7b9245e62c chore(android): refresh baseline profile 2023-07-24 01:25:48 +00:00
renovate[bot]andGitHub 99aa62d0e4 fix(deps): update junit5 monorepo to v5.10.0 2023-07-23 13:39:47 +00:00
GitHub Actions c3cf8a8a39 chore(android): refresh baseline profile 2023-07-23 01:27:03 +00:00
GitHub Actions 340655f18a chore(android): refresh baseline profile 2023-07-22 01:27:26 +00:00
renovate[bot]andGitHub 10fa84b09e fix(deps): update richtext to v0.17.0 2023-07-21 05:01:52 +00:00
GitHub Actions f21a03b20b chore(android): refresh baseline profile 2023-07-21 01:29:17 +00:00
GitHub Actions 56394fc275 chore(android): refresh baseline profile 2023-07-20 01:27:18 +00:00
Harsh Shandilya 9fd2d9f17b fix(android): avoid triggering another useless search API call 2023-07-20 01:11:45 +05:30
Harsh Shandilya 0c0506b6d7 fix(android): fix content description of search page icon 2023-07-20 01:06:13 +05:30
Harsh Shandilya f94741144c refactor(android): make search page manage localized state 2023-07-19 22:24:43 +05:30
Harsh Shandilya 29c0369d43 fix(build): set proguard files for all build types
Some cursed day you might be enabling R8 on debug builds
2023-07-19 22:24:23 +05:30
Harsh Shandilya e700987d14 chore(deps): update dev.chrisbanes.compose:compose-bom to 2023.07.00-alpha01 2023-07-19 22:15:33 +05:30
Harsh Shandilya c5ec6cd680 Revert "Revert "fix(common): make text in comments screen selectable""
This reverts commit 401214794d.
2023-07-19 21:58:08 +05:30
renovate[bot]andGitHub e48a6d41d5 chore(deps): update github/codeql-action action to v2.21.0 2023-07-19 16:02:19 +00:00
Harsh Shandilya 5870af4017 fix(core): remove unused DispatcherProvider#unconfined 2023-07-19 21:31:43 +05:30
Harsh Shandilya cb4c85b41c chore(build): upgrade to Gradle 8.3 RC1 2023-07-19 21:31:43 +05:30
renovate[bot]andGitHub cd6a04c5fd fix(deps): update sentry to v6.26.0 2023-07-19 13:37:30 +00:00
Harsh Shandilya 401214794d Revert "fix(common): make text in comments screen selectable"
Causes frequent crashes

This reverts commit 778be597dd.
2023-07-19 10:03:04 +00:00
Harsh Shandilya fdd2f7008c Revert "fix(common): use SystemClock for getting current millis"
This broke relative time accounting

This reverts commit 42cc852a20.
2023-07-19 10:02:20 +00:00
renovate[bot]andGitHub 18ef9c63b9 chore(deps): update dependency com.android.test to v8.2.0-alpha13 2023-07-18 21:20:15 +00:00
Harsh Shandilya d343f3db2b feat(api): remove tag search endpoints 2023-07-19 00:50:10 +05:30
Harsh Shandilya 42cc852a20 fix(common): use SystemClock for getting current millis 2023-07-19 00:41:33 +05:30
Harsh Shandilya 44be6eacbb chore: update changelog 2023-07-19 00:04:25 +05:30
Harsh Shandilya 778be597dd fix(common): make text in comments screen selectable
With this change you can now select
- The post title
- The post description if it exists, and
- Each individual comment

Fixes #385
2023-07-18 23:23:24 +05:30
Harsh Shandilya 255439e2b4 feat(common): add a clickable icon to SearchBar 2023-07-18 23:23:24 +05:30
renovate[bot]andGitHub 63c3e26f5c fix(deps): update dependency nl.littlerobots.version-catalog-update:nl.littlerobots.version-catalog-update.gradle.plugin to v0.8.1 2023-07-18 17:38:40 +00:00
Harsh Shandilya d3c3e3ca08 feat(android): wire up search UI into navigation graph 2023-07-18 23:08:07 +05:30
Harsh Shandilya 8d6aba9865 feat(android): add search UI 2023-07-18 23:08:07 +05:30
Harsh Shandilya 5e91e20c51 feat(android): add specialized PagingSource for search endpoint 2023-07-18 23:08:07 +05:30
Harsh Shandilya 84c9dd3762 feat(common): add a simple SearchBar component
Material 3's SearchBar has way too much "interaction" for my liking
2023-07-18 23:08:07 +05:30
renovate[bot]andGitHub 24ef11b315 fix(deps): update dependency androidx.benchmark:benchmark-macro-junit4 to v1.2.0-beta01 2023-07-18 17:34:11 +00:00
Harsh Shandilya bdf218cd85 refactor(api): rework Retrofit DI and expose LobstersSearchApi 2023-07-18 17:53:36 +05:30
Harsh Shandilya b3ff40b12b refactor(core): RetrofitModule -> JsonModule 2023-07-18 17:11:52 +05:30
Harsh Shandilya bad5bf5457 refactor(api): make SearchConverter a singleton 2023-07-18 17:11:06 +05:30
Harsh Shandilya cf3ca27966 fix(api): correct usage of @Query feature in search API 2023-07-18 17:04:50 +05:30
renovate[bot]andHarsh Shandilya 127a0674a2 chore(deps): update gradle/gradle-build-action action to v2.6.1 2023-07-18 17:01:52 +05:30
renovate[bot]andHarsh Shandilya b1a18c5dec fix(deps): update dependency com.diffplug.spotless:spotless-plugin-gradle to v6.20.0 2023-07-18 17:01:52 +05:30
GitHub ActionsandHarsh Shandilya 0a39f56775 chore(android): refresh baseline profile 2023-07-18 17:01:52 +05:30
Harsh Shandilya 5daeb8d580 feat(common): log errors from NetworkError composable
Having the errors in the logcat stream makes debugging quite a lot faster.
2023-07-18 17:01:52 +05:30
Harsh Shandilya 415ddae9f7 refactor(android): enforce DI encapsulation 2023-07-18 17:01:52 +05:30
Harsh Shandilya 4c95680f3b fix(renovate): remove now-unnecessary sentry-sdk regex manager 2023-07-17 16:03:15 +05:30
Harsh Shandilya b6cb9a2077 fix(renovate): consolidate and cleanup disabled packages 2023-07-17 16:01:36 +05:30
Harsh Shandilya 13eefbba2a chore(gradle): ensure all custom repositories are named 2023-07-17 15:43:58 +05:30
Harsh Shandilya 6548f7d654 fix(renovate): drop Poko from Renovate list
This is tied to the Kotlin version
2023-07-17 15:12:09 +05:30
Harsh Shandilya 1e4217598f fix(build): make slimTests skip debug rather than select release 2023-07-17 15:08:52 +05:30
Harsh Shandilya 0359860b82 refactor(android): move link metadata extraction to Unfurl
Crux is doing major changes for no good reason
2023-07-17 15:03:55 +05:30
Harsh Shandilya 37f42dc107 feat(api): add a Retrofit service for search 2023-07-17 08:26:56 +00:00
Harsh Shandilya 7eb4b45ab1 fix(model): ensure all classes have a readable toString 2023-07-17 13:36:42 +05:30
GitHub Actions 3706fdd284 chore(android): refresh baseline profile 2023-07-17 01:28:06 +00:00
renovate[bot]andGitHub f1fa96f8ca fix(deps): update dependency com.chimbori.crux:crux to v5 2023-07-16 10:22:42 +00:00
renovate[bot]andGitHub 36cadcdd78 fix(deps): update dependency com.chimbori.crux:crux to v4 2023-07-16 03:55:22 +00:00
renovate[bot]andGitHub cf3ac329f6 fix(deps): update dependency com.chimbori.crux:crux to v3.13.0 2023-07-16 00:26:09 +00:00
Harsh Shandilya 0581cedfb0 fix: remove useless get 2023-07-16 00:49:16 +05:30
GitHub Actions 0fc5adc197 chore(android): refresh baseline profile 2023-07-15 01:25:21 +00:00
renovate[bot]andGitHub 0680585b77 chore(deps): update github/codeql-action action to v2.20.4 2023-07-14 15:04:14 +00:00
Harsh Shandilya 2a3514e3b5 Revert "fix(android): remove explicit item keys"
This causes subtle UI artifacts that make me more annoyed than the occasional crash.

This reverts commit 2a97383e7d.
2023-07-14 19:08:32 +05:30
renovate[bot]andGitHub fd8b65be72 chore(deps): update sentry to v6.25.2 2023-07-14 10:57:40 +00:00
renovate[bot]andGitHub 50870b5fb5 fix(deps): update dagger to v2.47 2023-07-14 05:07:17 +00:00
GitHub Actions 7c5d935801 chore(android): refresh baseline profile 2023-07-14 01:31:32 +00:00
GitHub Actions 2862ada772 chore(android): refresh baseline profile 2023-07-13 01:25:54 +00:00
renovate[bot]andGitHub 65d1457386 chore(deps): update sentry to v6.25.1 2023-07-12 10:33:55 +00:00
GitHub Actions 0f63611c2d chore(android): refresh baseline profile 2023-07-12 01:25:10 +00:00
renovate[bot]andGitHub 7767f1c6d7 chore(deps): update agp to v8.2.0-alpha12 2023-07-11 19:08:20 +00:00
GitHub Actions f351b60073 chore(android): refresh baseline profile 2023-07-11 01:27:20 +00:00
Harsh Shandilya 752dee8ed6 fix(ci): submit generated dependency graph from running tests 2023-07-11 06:28:33 +05:30
Harsh Shandilya 0573e8cb2d fix(ci): remove build-tools workaround 2023-07-11 06:14:04 +05:30
Harsh Shandilya 75fd41a0d8 fix(ci): make CodeQL only run on schedule 2023-07-11 06:14:04 +05:30
renovate[bot]andGitHub 59f8577d1f chore(deps): update plugin org.gradle.toolchains.foojay-resolver-convention to v0.6.0 2023-07-10 18:46:20 +00:00
renovate[bot]andGitHub 85d4b50d28 chore(deps): update gradle/gradle-build-action action to v2.6.0 2023-07-10 18:32:28 +00:00
renovate[bot]andGitHub c3499d434b chore(deps): update dependency gradle to v8.2.1 2023-07-10 15:18:37 +00:00
GitHub Actions 701e0d7fcc chore(android): refresh baseline profile 2023-07-10 01:28:01 +00:00
Harsh Shandilya 9a5a417418 feat(build): add copyright license headers to XML files 2023-07-09 22:59:58 +05:30
renovate[bot]andHarsh Shandilya 1221992d26 chore(deps): update agp to v8.2.0-alpha11 2023-07-09 22:56:37 +05:30
renovate[bot]andHarsh Shandilya 144cdb5e50 chore(deps): update github/codeql-action action to v2.20.3 2023-07-09 22:56:37 +05:30
GitHub Actions 8133b2c091 chore(android): refresh baseline profile 2023-07-05 01:24:08 +00:00
renovate[bot]andGitHub 869cc650e3 chore(deps): update sentry to v6.25.0 2023-07-04 17:23:45 +00:00
GitHub Actions 7a3262338d chore(android): refresh baseline profile 2023-07-04 01:26:35 +00:00
renovate[bot]andGitHub 85ff15e8ca chore(deps): update github/codeql-action action to v2.20.2 2023-07-03 11:17:13 +00:00
GitHub Actions 3d34c4f466 chore(android): refresh baseline profile 2023-07-03 06:43:06 +00:00
GitHub Actions 6da6364a55 feat(release): start next development iteration 2023-07-02 18:43:07 +00:00
GitHub Actions 26b04cce16 feat(release): bump version 2023-07-02 18:39:14 +00:00
GitHub Actions 9ab2400e1f chore(android): refresh baseline profile 2023-07-02 17:01:00 +00:00
GitHub Actions c3b30cc86c chore(android): refresh baseline profile 2023-07-02 01:25:14 +00:00
Harsh Shandilya 2a97383e7d fix(android): remove explicit item keys
Hail mary fix for Sentry issue COMPOSE-LOBSTERS-2
2023-07-01 18:59:17 +05:30
Harsh Shandilya dd20885270 fix(deps): bump coroutines to 1.7.2 2023-07-01 18:57:26 +05:30
Harsh Shandilya fe752f839a feat(build): migrate to Gradle JVM Toolchains 2023-07-01 18:21:10 +05:30
GitHub ActionsandHarsh Shandilya be9a888816 chore(android): refresh baseline profile 2023-07-01 18:21:10 +05:30
renovate[bot]andGitHub 648e7b31cf chore(deps): update dependency gradle to v8.2 2023-06-30 21:58:31 +00:00
GitHub Actions 21b520f786 chore(android): refresh baseline profile 2023-06-30 01:27:40 +00:00
renovate[bot]andGitHub 8bbcabc8f6 chore(deps): update gradle/gradle-build-action action to v2.5.1 2023-06-29 19:44:16 +00:00
Harsh Shandilya 0e4e344a2f fix(deps): upgrade workmanager to 2.9.0-alpha01 2023-06-29 18:12:26 +05:30
GitHub Actions 7b25b6aa85 chore(android): refresh baseline profile 2023-06-29 01:25:13 +00:00
renovate[bot]andGitHub 5343168b8a chore(deps): update gradle/gradle-build-action action to v2.5.0 2023-06-29 00:05:15 +00:00
renovate[bot]andGitHub 9b39fcf1fc fix(deps): update dependency androidx.navigation:navigation-compose to v2.7.0-beta02 2023-06-28 22:59:50 +00:00
renovate[bot]andGitHub 8ce7ac96d0 chore(deps): update dependency gradle to v8.2-rc-3 2023-06-28 10:44:19 +00:00
Harsh Shandilya b5c57500b1 feat: revert back to old text pipeline
Things have been entirely too slow on the lobste.rs front and I don't care much to continue pushing for it.

Fixes #382
Fixes #383
2023-06-28 10:58:39 +05:30
Harsh Shandilya c089e5ae5a fix(build): make slim tests optional 2023-06-28 10:46:59 +05:30
Harsh Shandilya d8a76cf1e7 Revert "fix(build): temporarily revert to AGP 8.0.x"
This reverts commit 1e888494ee.
2023-06-28 10:46:59 +05:30
renovate[bot]andGitHub 92fce55feb fix(deps): update dependency org.gradle.android.cache-fix:org.gradle.android.cache-fix.gradle.plugin to v2.7.2 2023-06-28 03:22:11 +00:00
GitHub Actions 60a430baa4 chore(android): refresh baseline profile 2023-06-28 01:35:58 +00:00
Harsh Shandilya b0c8de888c chore(deps): bump sqldelight to 2.0.0-rc02 2023-06-28 00:18:54 +05:30
renovate[bot]andGitHub 73ab2dd1d3 fix(deps): update dependency androidx.test:runner to v1.6.0-alpha03 2023-06-27 16:48:14 +00:00
GitHub Actions 7c4f843f42 chore(android): refresh baseline profile 2023-06-27 01:33:28 +00:00
GitHub Actions 123ec904e3 chore(android): refresh baseline profile 2023-06-26 01:32:43 +00:00
GitHub Actions 80c9211d37 chore(android): refresh baseline profile 2023-06-25 01:32:29 +00:00
GitHub Actions b030694f63 chore(android): refresh baseline profile 2023-06-24 01:35:33 +00:00
GitHub Actions e40e196354 chore(android): refresh baseline profile 2023-06-23 01:32:30 +00:00
renovate[bot]andGitHub b7abef3f8f chore(deps): update sentry to v6.24.0 2023-06-22 16:00:38 +00:00
Harsh ShandilyaandGitHub 5bde6cd9cb chore(deps): bump Compose to June alpha release 2023-06-22 21:29:15 +05:30
Harsh Shandilya cb8c90ace9 fix(renovate): include JitPack repository 2023-06-22 12:09:21 +05:30
Harsh Shandilya 9c2c238e1e fix(build): raise target SDK to 34 2023-06-22 11:27:42 +05:30
renovate[bot]andHarsh Shandilya 21a7baa309 fix(deps): update dependency androidx.benchmark:benchmark-macro-junit4 to v1.2.0-alpha16 2023-06-22 11:23:58 +05:30
GitHub Actions a9d75148fd chore(android): refresh baseline profile 2023-06-22 01:35:13 +00:00
renovate[bot]andGitHub 3209b5b1fe chore(deps): update github/codeql-action action to v2.20.1 2023-06-21 13:16:25 +00:00
renovate[bot]andGitHub c749a97ded fix(deps): update dependency io.sentry.android.gradle:io.sentry.android.gradle.gradle.plugin to v3.11.1 2023-06-21 09:28:39 +00:00
GitHub Actions f1ab468ceb chore(android): refresh baseline profile 2023-06-21 01:30:06 +00:00
renovate[bot]andGitHub ebd0e5cb5d chore(deps): update plugin modulegraphassert to v2.4.1 2023-06-19 22:42:11 +00:00
GitHub Actions 19bf81ac0e chore(android): refresh baseline profile 2023-06-19 01:35:03 +00:00
renovate[bot]andGitHub 569d5af8fb fix(deps): update dependency com.google.truth:truth to v1.1.5 2023-06-18 16:26:46 +00:00
GitHub Actions dc754dabb3 chore(android): refresh baseline profile 2023-06-18 01:37:26 +00:00
GitHub Actions ed42532416 chore(android): refresh baseline profile 2023-06-17 01:38:56 +00:00
GitHub Actions 211f39da52 chore(android): refresh baseline profile 2023-06-16 01:34:00 +00:00
renovate[bot]andHarsh Shandilya fefac25781 chore(deps): update sentry 2023-06-15 23:27:40 +05:30
Harsh Shandilya e0c9c88ca9 chore(common): prettify HTMLText preview 2023-06-15 23:27:40 +05:30
Harsh Shandilya ea7f7b6306 fix(common): add some visual distinction for blockquotes 2023-06-15 23:27:40 +05:30
GitHub Actions 8ceca6c5ed chore(android): refresh baseline profile 2023-06-15 01:36:06 +00:00
GitHub Actions e9ff85ad44 chore(android): refresh baseline profile 2023-06-14 01:35:35 +00:00
renovate[bot]andGitHub b8e6d4d3fe chore(deps): update github/codeql-action action to v2.20.0 2023-06-13 22:15:55 +00:00
GitHub Actions e7fdfcab69 chore(android): refresh baseline profile 2023-06-13 01:38:12 +00:00
Harsh Shandilya ad8c830b17 fix(common): make HTMLText use M3 Text component 2023-06-13 02:47:17 +05:30
renovate[bot]andHarsh Shandilya 25d2d4ddb7 chore(deps): update actions/checkout action to v3.5.3 2023-06-13 01:44:31 +05:30
Harsh Shandilya 22d1755fd2 fix(android): use SoLoader to load libraries 2023-06-13 01:44:31 +05:30
Harsh Shandilya d397680d33 chore: add changelog entry 2023-06-09 20:09:44 +05:30
Harsh Shandilya 40cd7758dc chore: remove unused Markdown dependencies 2023-06-09 20:09:44 +05:30
Harsh Shandilya 2dc8eae861 refactor: migrate to HTMLText 2023-06-09 20:09:44 +05:30
Harsh Shandilya 2d23ceee83 feat(common): add HTMLText composable
This allows rendering raw HTML text directly without roundtripping it as Markdown
2023-06-09 20:09:44 +05:30
renovate[bot]andGitHub cccc6fd64b fix(deps): update dependency com.github.ben-manes:gradle-versions-plugin to v0.47.0 2023-06-09 11:04:49 +00:00
GitHub Actions aced95c542 chore(android): refresh baseline profile 2023-06-09 01:30:17 +00:00
Harsh Shandilya 90edeefd6e fix: remove useless toList 2023-06-09 02:04:58 +05:30
GitHub Actions a5b90851c8 feat(release): start next development iteration 2023-06-08 15:03:41 +00:00
GitHub Actions 7d0c6972ef feat(release): bump version 2023-06-08 15:02:17 +00:00
Harsh Shandilya 18cb5dc07c chore: refresh feature graphic 2023-06-08 20:18:09 +05:30
Harsh Shandilya 5343e13215 fix(scripts): ensure demo mode is correctly enabled 2023-06-08 19:55:55 +05:30
renovate[bot]andGitHub 4d06a2722c fix(deps): update dependency dev.chrisbanes.compose:compose-bom to v2023.04.00-beta02 2023-06-08 12:14:43 +00:00
Harsh Shandilya b9f5bee35e fix(scripts): make setup-age architecture agnostic 2023-06-08 11:53:55 +00:00
Harsh Shandilya 72078b2768 fix(scripts): bump age to v1.1.1 2023-06-08 11:53:55 +00:00
GitHub Actions 465191609b chore(android): refresh baseline profile 2023-06-08 11:48:51 +00:00
Harsh ShandilyaandGitHub 6e1858a55e fix(benchmark): adjust for API changes 2023-06-08 07:03:28 +00:00
renovate[bot]andGitHub 3eeb947914 fix(deps): update dependency io.sentry.android.gradle:io.sentry.android.gradle.gradle.plugin to v3.10.0 2023-06-08 00:02:53 +00:00
renovate[bot]andGitHub d2c99fa8b7 fix(deps): update androidx 2023-06-07 19:06:35 +00:00
renovate[bot]andGitHub dbba96426d chore(deps): update sentry 2023-06-07 15:43:33 +00:00
Harsh Shandilya a5be35a667 feat(core): manually wire in Sentry OkHttp integration 2023-06-07 21:04:24 +05:30
Harsh Shandilya 6e291a8df1 refactor(scripts): rework how Maestro flows are run 2023-06-07 18:07:18 +05:30
Harsh Shandilya 24898cf9fb chore: refresh feature graphic 2023-06-07 17:19:43 +05:30
Harsh Shandilya 66e64ac305 chore: update changelog 2023-06-07 16:24:08 +05:30
Harsh Shandilya ee6bcc864f feat: update iconography for consistency 2023-06-07 16:24:02 +05:30
Harsh Shandilya 8de49d670b fix(android): add visual indication when there are no saved posts 2023-06-07 16:11:18 +05:30
GitHub Actions 13b1c699f6 chore(android): refresh baseline profile 2023-06-07 01:34:08 +00:00
Harsh Shandilya 86a70ffb48 feat(common): remove animation for comment text 2023-06-07 02:42:15 +05:30
Harsh Shandilya 2bb71238bc fix(android): dismiss existing snackbars when showing a new one 2023-06-07 02:37:43 +05:30
Harsh Shandilya 7d491de86c fix(android): prevent comments page from stacking when spammed 2023-06-07 02:19:54 +05:30
Harsh Shandilya 02575a917b fix(android): check for previous back stack entry 2023-06-07 02:05:14 +05:30
renovate[bot]andGitHub 5206851b22 chore(deps): update dependency gradle to v8.2-rc-2 2023-06-06 19:03:10 +00:00
GitHub Actions 74c36f27ae chore(android): refresh baseline profile 2023-06-06 01:36:55 +00:00
Harsh Shandilya 2048a2bb9b chore: add test resources to .rgignore 2023-06-06 00:45:13 +05:30
renovate[bot]andGitHub ea48cdb4f3 chore(deps): update plugin modulegraphassert to v2.4.0 2023-06-05 16:10:44 +00:00
Harsh Shandilya 942b9f3fb4 fix(android): report fully drawn status 2023-06-05 16:50:29 +05:30
Harsh Shandilya 5f6d635c7a fix(android): inline useless function 2023-06-05 14:58:28 +05:30
Harsh Shandilya 47a398f518 refactor: move metadata-extractor to android 2023-06-05 13:24:22 +05:30
Harsh Shandilya 9fdd72220f feat(scripts): add a quick way to refresh the README graphic 2023-06-05 13:24:22 +05:30
Harsh Shandilya 0a1913e056 feat(build): add module-graph-assert plugin 2023-06-05 10:04:41 +05:30
Harsh Shandilya 68db3dcda6 refactor: move coroutine-utils into core 2023-06-05 09:52:12 +05:30
Harsh Shandilya c50c9c8528 chore: refresh module graph 2023-06-05 09:46:09 +05:30
Harsh Shandilya 8a418f6649 refactor: make SavedPostSerializer an object 2023-06-05 09:41:52 +05:30
Harsh Shandilya acdc924047 chore: cleanup redundant @OptIn annotations 2023-06-05 09:34:48 +05:30
GitHub Actions 2fa1d3f06f chore(android): refresh baseline profile 2023-06-05 01:37:40 +00:00
Harsh Shandilya 7ffa225487 feat(database): add test for SavedPostSerializer 2023-06-05 02:51:53 +05:30
Harsh Shandilya c1659eb2dd chore(gitignore): add claw-export.json 2023-06-05 02:18:09 +05:30
Harsh Shandilya 1e888494ee fix(build): temporarily revert to AGP 8.0.x
Android Studio's canary version is going through some trouble recognizing test sources
2023-06-05 02:16:57 +05:30
Harsh Shandilya 054ba13dc3 fix(ci): enforce concurrency for all workflows 2023-06-05 02:00:15 +05:30
Harsh Shandilya 301760c39f refactor(android): make String.toLocalDateTime private 2023-06-05 01:59:17 +05:30
Harsh Shandilya 79fd814c14 fix(android): add Snackbars to data transfer flow 2023-06-05 01:50:23 +05:30
Harsh Shandilya a82d0e7063 fix(android): only show data transfer action on home screen 2023-06-05 00:56:20 +05:30
Harsh Shandilya c6ea1182e0 refactor: remove explicit calls to rememberVectorPainter 2023-06-04 17:14:09 +00:00
Harsh Shandilya 5e7b79eb89 fix(android): hide data transfer action on data transfer screen 2023-06-04 21:19:22 +05:30
Harsh Shandilya 3738d94789 feat(android): wire up import/export settings to UI 2023-06-04 18:45:14 +05:30
Harsh Shandilya fb0e372917 feat(android): add DataTransferRepository 2023-06-04 18:45:14 +05:30
Harsh Shandilya 8945f9e33f feat(database): build a custom serializer for SavedPost 2023-06-04 18:45:14 +05:30
Harsh Shandilya 2895e7ffe7 refactor(android): split out comments functionality 2023-06-04 18:45:14 +05:30
GitHub Actions f102166fae chore(android): refresh baseline profile 2023-06-04 01:28:30 +00:00
Harsh Shandilya c51e28b8e1 fix: touch up version catalog 2023-06-04 01:33:25 +05:30
GitHub Actions a9923eb8c0 feat(release): start next development iteration 2023-06-03 18:50:53 +00:00
GitHub Actions de2f3964e0 feat(release): bump version 2023-06-03 18:46:20 +00:00
Harsh Shandilya 7c5ccbf63b fix: make nav transitions run at 2x of defaults 2023-06-04 00:03:38 +05:30
Harsh Shandilya b4b96222ed chore: reorder manifest attributes 2023-06-04 00:03:38 +05:30
Harsh Shandilya a920f899fb fix(android): automatically extract native libraries 2023-06-04 00:03:38 +05:30
GitHub Actions 31b39a49d6 chore(android): refresh baseline profile 2023-06-03 01:33:42 +00:00
renovate[bot]andGitHub bfb4e9741a chore(deps): update msfjarvis/keep-a-changelog-new-release action to v2.1.0 2023-06-02 22:59:13 +00:00
GitHub Actions bc5c5a13b4 chore(android): refresh baseline profile 2023-06-02 01:22:03 +00:00
renovate[bot]andGitHub 19c50f40cd chore(deps): update github/codeql-action action to v2.3.6 2023-06-01 19:54:14 +00:00
renovate[bot]andGitHub bb0a987d80 fix(deps): update dependency io.sentry.android.gradle:io.sentry.android.gradle.gradle.plugin to v3.8.0 2023-06-01 10:22:25 +00:00
Harsh Shandilya dba380e4c4 fix(gradle): remove default-enabled feature prop 2023-06-01 12:46:32 +05:30
Harsh Shandilya 4a52524212 fix(gradle): correct property name for CC 2023-06-01 12:46:32 +05:30
Harsh Shandilya 1a94ba5a40 fix(gradle): upgrade to 8.2-rc-1 2023-06-01 12:38:05 +05:30
GitHub Actions 2022255200 chore(android): refresh baseline profile 2023-06-01 01:33:44 +00:00
renovate[bot]andGitHub ab1951f9c5 fix(deps): update dependency com.slack.eithernet:eithernet to v1.4.1 2023-05-31 19:31:40 +00:00
renovate[bot]andGitHub a9176b8508 chore(deps): update dependency io.sentry:sentry-android to v6.21.0 2023-05-31 08:54:31 +00:00
GitHub Actions 02ad998b06 chore(android): refresh baseline profile 2023-05-31 01:23:45 +00:00
GitHub Actions 31059abf23 feat(release): start next development iteration 2023-05-31 01:11:15 +00:00
130 changed files with 25779 additions and 17461 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 294 KiB

+15 -30
View File
@@ -1,4 +1,5 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base",
":dependencyDashboard",
@@ -15,24 +16,25 @@
"https://maven.google.com/",
"https://repo1.maven.org/maven2",
"https://plugins.gradle.org/m2/",
"https://jitpack.io",
],
},
{
"matchPackageNames": [
"androidx.compose.compiler:compiler",
"com.squareup.anvil",
"dev.drewhamilton.poko",
"org.jetbrains.kotlin:kotlin-gradle-plugin",
"org.jetbrains.kotlin.plugin.serialization",
],
"groupName": "Kotlin and Compose",
},
{
"matchPackagePatterns": [
"^androidx.*",
],
"groupName": "androidx"
},
{
"matchPackagePatterns": [
"^kotlin",
"^org.jetbrains.kotlin",
"^androidx.compose.compiler",
"^anvil",
"^coil",
],
"enabled": false
},
{
"matchPackagePatterns": [
"^io.sentry",
@@ -41,16 +43,10 @@
"groupName": "sentry"
},
{
"managers": [
"gradle"
],
"packagePatterns": [
// Renovate refuses to stop resolving this from the snapshots repo
"^app.cash.sqldelight",
// Currently on a pre-release version for bugfixes
"matchPackageNames": [
"com.android.tools:r8",
],
"enabled": false
"enabled": false,
},
],
"regexManagers": [
@@ -76,16 +72,5 @@
"depNameTemplate": "androidx.compose.compiler:compiler",
"registryUrlTemplate": "https://maven.google.com",
},
{
"fileMatch": [
"gradle/libs.versions.toml"
],
"matchStrings": [
"sentry-sdk = \"(?<currentValue>.*)\""
],
"datasourceTemplate": "maven",
"depNameTemplate": "io.sentry:sentry-android",
"registryUrlTemplate": "https://repo1.maven.org/maven2/",
}
]
],
}
+6 -18
View File
@@ -6,6 +6,9 @@ on:
- cron: '43 0 * * *'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
baseline-profile:
runs-on: macos-latest
@@ -15,28 +18,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
token: ${{ secrets.POST_RELEASE_GH_TOKEN }}
fetch-depth: 0
- name: Accept all SDK licenses
shell: bash
run: printf 'y\ny\ny\ny\ny\ny\n' | $ANDROID_HOME/tools/bin/sdkmanager --licenses
- name: Get build-tools directory
id: build-tools-path
shell: bash
run: echo "dir=${ANDROID_HOME}/build-tools/34.0.0-rc3" >> "${GITHUB_OUTPUT}"
- name: Cache build-tools
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3
with:
path: ${{ steps.build-tools-path.outputs.dir }}
key: ${{ runner.os }}-34.0.0-rc3
- name: Set up JDK
uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
with:
distribution: temurin
java-version: 18
@@ -48,7 +36,7 @@ jobs:
git config user.email noreply@github.com
- name: Setup Gradle caching
uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629 # v2.4.2
uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # v2.7.0
with:
gradle-home-cache-cleanup: true
cache-read-only: true
+10 -39
View File
@@ -18,37 +18,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
fetch-depth: 0
- name: Accept all SDK licenses
shell: bash
run: printf 'y\ny\ny\ny\ny\ny\n' | $ANDROID_HOME/tools/bin/sdkmanager --licenses
- name: Get build-tools directory
id: build-tools-path
shell: bash
run: echo "dir=${ANDROID_HOME}/build-tools/34.0.0-rc3" >> "${GITHUB_OUTPUT}"
- name: Cache build-tools
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3
with:
path: ${{ steps.build-tools-path.outputs.dir }}
key: ${{ runner.os }}-34.0.0-rc3
- name: Set up JDK
uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
with:
distribution: temurin
java-version: 18
- name: Run unit tests
uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629 # v2.4.2
uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # v2.7.0
with:
arguments: --no-configuration-cache --stacktrace check
arguments: --no-configuration-cache --stacktrace check -PslimTests
gradle-home-cache-cleanup: true
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
dependency-graph: generate-and-submit
- name: (Fail-only) Upload test report
if: failure()
@@ -63,25 +49,10 @@ jobs:
needs: [ "check" ]
steps:
- name: Checkout repository
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- name: Accept all SDK licenses
shell: bash
run: printf 'y\ny\ny\ny\ny\ny\n' | $ANDROID_HOME/tools/bin/sdkmanager --licenses
- name: Get build-tools directory
id: build-tools-path
shell: bash
run: echo "dir=${ANDROID_HOME}/build-tools/34.0.0-rc3" >> "${GITHUB_OUTPUT}"
- name: Cache build-tools
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3
with:
path: ${{ steps.build-tools-path.outputs.dir }}
key: ${{ runner.os }}-34.0.0-rc3
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Set up JDK
uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
with:
distribution: temurin
java-version: 18
@@ -89,12 +60,12 @@ jobs:
- name: Decrypt secrets
run: |
./scripts/setup-age.sh
./scripts/signing-setup.sh "$ENCRYPT_KEY"
./scripts/signing-setup.sh "$AGE_SECRET_KEY"
env:
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
- name: Build release app
uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629 # v2.4.2
uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # v2.7.0
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
with:
+11 -24
View File
@@ -2,9 +2,11 @@ name: CodeQL
on:
workflow_dispatch:
push:
branches:
- main
schedule:
- cron: '31 7 * * 3'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
analyze:
@@ -16,37 +18,22 @@ jobs:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- name: Accept all SDK licenses
shell: bash
run: printf 'y\ny\ny\ny\ny\ny\n' | $ANDROID_HOME/tools/bin/sdkmanager --licenses
- name: Get build-tools directory
id: build-tools-path
shell: bash
run: echo "dir=${ANDROID_HOME}/build-tools/34.0.0-rc3" >> "${GITHUB_OUTPUT}"
- name: Cache build-tools
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3
with:
path: ${{ steps.build-tools-path.outputs.dir }}
key: ${{ runner.os }}-34.0.0-rc3
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Set up JDK
uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
with:
distribution: temurin
java-version: 18
- name: Setup Gradle caching
uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629 # v2.4.2
uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # v2.7.0
with:
gradle-home-cache-cleanup: true
cache-read-only: true
- name: Initialize CodeQL
uses: github/codeql-action/init@0225834cc549ee0ca93cb085b92954821a145866 # v2.3.5
uses: github/codeql-action/init@6ca1aa8c195c3ca3e77c174fe0356db1bce3b319 # v2.21.1
with:
languages: java
tools: latest
@@ -55,9 +42,9 @@ jobs:
- name: Build project
shell: bash
run: |
./gradlew assemble
./gradlew assemble -PslimTests
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@0225834cc549ee0ca93cb085b92954821a145866 # v2.3.5
uses: github/codeql-action/analyze@6ca1aa8c195c3ca3e77c174fe0356db1bce3b319 # v2.21.1
with:
category: "/language:java"
+9 -21
View File
@@ -2,33 +2,21 @@ name: Release to Google Play
on:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
publish-google-play-release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
token: ${{ secrets.POST_RELEASE_GH_TOKEN }}
fetch-depth: 0
- name: Accept all SDK licenses
shell: bash
run: printf 'y\ny\ny\ny\ny\ny\n' | $ANDROID_HOME/tools/bin/sdkmanager --licenses
- name: Get build-tools directory
id: build-tools-path
shell: bash
run: echo "dir=${ANDROID_HOME}/build-tools/34.0.0-rc3" >> "${GITHUB_OUTPUT}"
- name: Cache build-tools
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3
with:
path: ${{ steps.build-tools-path.outputs.dir }}
key: ${{ runner.os }}-34.0.0-rc3
- name: Set up JDK
uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
with:
distribution: temurin
java-version: 18
@@ -40,7 +28,7 @@ jobs:
git config user.email noreply@github.com
- name: Setup Gradle caching
uses: gradle/gradle-build-action@749f47bda3e44aa060e82d7b3ef7e40d953bd629 # v2.4.2
uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # v2.7.0
with:
gradle-home-cache-cleanup: true
cache-read-only: true
@@ -48,9 +36,9 @@ jobs:
- name: Decrypt secrets
run: |
./scripts/setup-age.sh
./scripts/signing-setup.sh "$ENCRYPT_KEY"
./scripts/signing-setup.sh "$AGE_SECRET_KEY"
env:
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
- name: Get current version
shell: bash
@@ -60,7 +48,7 @@ jobs:
echo VERSION="${VERSION}" >> $GITHUB_ENV
- name: Update changelog
uses: msfjarvis/keep-a-changelog-new-release@ce8769801992253adb0209055c236a0cd192500b # 2.0.0
uses: msfjarvis/keep-a-changelog-new-release@bf43ae42f4e42f137572950af426efcf6e777207 # 2.1.0
with:
tag: v${{ env.VERSION }}
version: ${{ env.VERSION }}
+1
View File
@@ -246,3 +246,4 @@ hs_err_pid*
keystore.properties
distribution/
screenshots/
claw-export.json
+1
View File
@@ -1,2 +1,3 @@
*.json
baseline-prof.txt
**/test/resources/**
+58 -1
View File
@@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.32.0] - 2023-07-27
### Changed
* Upgrade to AndroidX July 26 release
* Revert selectable text feature due to sporadic crashes
## [1.31.0] - 2023-07-25
### Added
* Added the ability to search for posts
* Text in the comments page is now selectable
### Changed
* Upgrade to Compose July release
* Upgrade to Kotlin 1.9.0
## [1.30.0] - 2023-07-02
### Changed
* Added another workaround for native library loading crash
## [1.29.0] - 2023-06-08
### Added
* Backup and restore options for saved posts
### Fixed
* Spamming the comments button no longer causes it to be opened multiple times
* Saved posts screen now has a visual indication when you have nothing saved
* Iconography has been updated across the board to be more consistent
## [1.28.0] - 2023-06-03
### Changed
* Navigation transitions have been slightly sped up
### Fixed
* Add workaround for a native library loading crash observed on some devices
## [1.27.0] - 2023-05-31
### Changed
@@ -222,7 +269,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Initial Play Store release
[Unreleased]: https://github.com/msfjarvis/compose-lobsters/compare/v1.27.0...HEAD
[Unreleased]: https://github.com/msfjarvis/compose-lobsters/compare/v1.32.0...HEAD
[1.32.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.31.0...v1.32.0
[1.31.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.30.0...v1.31.0
[1.30.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.29.0...v1.30.0
[1.29.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.28.0...v1.29.0
[1.28.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.27.0...v1.28.0
[1.27.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.26.0...v1.27.0
+3 -6
View File
@@ -11,10 +11,11 @@ Unofficial Android app for read-only access to [lobste.rs](https://lobste.rs), b
> Snapshots from the development branch can be obtained [here](https://github.com/msfjarvis/compose-lobsters/releases/tag/nightly).
<img src="https://github.com/msfjarvis/compose-lobsters/blob/main/.github/readme_feature.webp"
alt="Side by side screenshots of the app's main page in dark and light UI modes"
height="550" />
alt="A grid of screenshots from the app, in clockwise order: the main screen, the comments page, the search page and the saved posts page"
width="550" />
## Dependency Diagram
```mermaid
%%{
init: {
@@ -28,16 +29,12 @@ graph LR
android --> api
android --> common
android --> core
android --> coroutine-utils
android --> database
android --> metadata-extractor
android --> model
api --> model
common --> core
common --> database
common --> model
metadata-extractor --> model
database --> core
```
## License
+11 -3
View File
@@ -16,6 +16,7 @@ plugins {
id("dev.msfjarvis.claw.sentry")
id("dev.msfjarvis.claw.versioning-plugin")
alias(libs.plugins.anvil)
alias(libs.plugins.modulegraphassert)
alias(libs.plugins.whetstone)
}
@@ -36,6 +37,13 @@ android {
applicationIdSuffix = ".benchmark"
}
}
packaging { jniLibs { useLegacyPackaging = true } }
}
moduleGraphAssert {
assertOnAnyBuild = true
maxHeight = 4
restricted = arrayOf(":core -X> :.*")
}
whetstone {
@@ -60,7 +68,7 @@ dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.profileinstaller)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.androidx.work.runtime)
implementation(libs.coil)
implementation(libs.copydown)
implementation(libs.dagger)
@@ -68,13 +76,13 @@ dependencies {
implementation(libs.kotlinx.collections.immutable)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.napier)
implementation(libs.soloader)
implementation(libs.sqldelight.extensions.coroutines)
implementation(libs.unfurl)
implementation(projects.api)
implementation(projects.common)
implementation(projects.core)
implementation(projects.coroutineUtils)
implementation(projects.database)
implementation(projects.metadataExtractor)
implementation(projects.model)
kapt(libs.dagger.compiler)
+12 -1
View File
@@ -1,4 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 8.0.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.1)" variant="all" version="8.0.1">
<issues format="6" by="lint 8.2.0-alpha14" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha14)" variant="all" version="8.2.0-alpha14">
<issue
id="UsingMaterialAndMaterial3Libraries"
message="Using a material import while also using the material3 library"
errorLine1="import androidx.compose.material.ExperimentalMaterialApi"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/lists/NetworkPosts.kt"
line="17"
column="8"/>
</issue>
</issues>
+6 -1
View File
@@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
~ Copyright © 2021-2023 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.
-->
<resources>
<string name="app_name">Claw β</string>
</resources>
+7 -2
View File
@@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
~ Copyright © 2021-2023 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
@@ -6,13 +11,13 @@
<application
android:name=".ClawApplication"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/full_backup_content"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/Theme.Claw.SplashScreen"
android:enableOnBackInvokedCallback="true"
tools:targetApi="tiramisu">
<profileable
android:shell="true"
+24074 -16726
View File
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
/*
* Copyright © 2021-2022 Harsh Shandilya.
* Copyright © 2021-2023 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.
@@ -15,6 +15,7 @@ import androidx.work.WorkManager
import com.deliveryhero.whetstone.Whetstone
import com.deliveryhero.whetstone.app.ApplicationComponentOwner
import com.deliveryhero.whetstone.app.ContributesAppInjector
import com.facebook.soloader.SoLoader
import dev.msfjarvis.claw.android.work.SavedPostUpdaterWorker
import dev.msfjarvis.claw.core.injection.AppPlugin
import java.util.concurrent.TimeUnit
@@ -31,6 +32,7 @@ class ClawApplication : Application(), ApplicationComponentOwner {
override fun onCreate() {
Whetstone.inject(this)
super.onCreate()
SoLoader.init(this, false)
plugins.forEach { plugin -> plugin.apply(this) }
val postUpdateWorkRequest =
PeriodicWorkRequestBuilder<SavedPostUpdaterWorker>(POST_REFRESH_PERIOD, TimeUnit.HOURS)
@@ -16,7 +16,7 @@ import com.deliveryhero.whetstone.app.ApplicationScope
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import dev.msfjarvis.claw.util.coroutines.DispatcherProvider
import dev.msfjarvis.claw.core.coroutines.DispatcherProvider
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
@@ -1,5 +1,5 @@
/*
* Copyright © 2021-2022 Harsh Shandilya.
* Copyright © 2021-2023 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.
@@ -1,25 +1,44 @@
/*
* Copyright © 2021-2022 Harsh Shandilya.
* Copyright © 2021-2023 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.injection
import com.chimbori.crux.Crux
import com.deliveryhero.whetstone.app.ApplicationScope
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import io.github.aakira.napier.Napier
import me.saket.unfurl.UnfurlLogger
import me.saket.unfurl.Unfurler
import okhttp3.OkHttpClient
@Module
@ContributesTo(ApplicationScope::class)
object MetadataExtractorModule {
@Provides
fun provideCrux(
fun provideUnfurlLogger(): UnfurlLogger {
return object : UnfurlLogger {
override fun log(message: String) {
Napier.d { message }
}
override fun log(e: Throwable, message: String) {
Napier.e(e) { message }
}
}
}
@Provides
fun provideUnfurler(
okHttpClient: OkHttpClient,
): Crux {
return Crux(okHttpClient = okHttpClient)
logger: UnfurlLogger,
): Unfurler {
return Unfurler(
httpClient = okHttpClient,
logger = logger,
)
}
}
@@ -1,15 +0,0 @@
/*
* Copyright © 2022 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.injection
import javax.inject.Qualifier
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class DatabaseDispatcher
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class MainDispatcher
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class IODispatcher
@@ -1,5 +1,5 @@
/*
* Copyright © 2021-2022 Harsh Shandilya.
* Copyright © 2021-2023 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.
@@ -13,7 +13,7 @@ import com.slack.eithernet.ApiResult.Success
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dev.msfjarvis.claw.android.injection.IODispatcher
import dev.msfjarvis.claw.core.injection.IODispatcher
import dev.msfjarvis.claw.model.LobstersPost
import java.io.IOException
import kotlinx.coroutines.CoroutineDispatcher
@@ -0,0 +1,67 @@
/*
* Copyright © 2021-2023 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.paging
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.slack.eithernet.ApiResult
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.PAGE_SIZE
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.STARTING_PAGE_INDEX
import dev.msfjarvis.claw.core.injection.IODispatcher
import dev.msfjarvis.claw.model.LobstersPost
import java.io.IOException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
/**
* Duplicate of [LobstersPagingSource] with an additional optimization to not continue loading
* further pages if nothing is found in the current one. This is required to prevent the app from
* hammering the search endpoint with up to 70 consecutive requests for the search results of a
* blank query.
*/
class SearchPagingSource
@AssistedInject
constructor(
@Assisted private val remoteFetcher: RemoteFetcher<LobstersPost>,
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
) : PagingSource<Int, LobstersPost>() {
override fun getRefreshKey(state: PagingState<Int, LobstersPost>): Int? {
return state.anchorPosition?.let { anchorPosition ->
(anchorPosition / PAGE_SIZE).coerceAtLeast(STARTING_PAGE_INDEX)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, LobstersPost> {
val page = params.key ?: STARTING_PAGE_INDEX
return when (val result = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }) {
is ApiResult.Success -> {
// Optimization: prevent fetching more pages if we found no items, this means
// there is no active search query.
val nextKey = if (result.value.isEmpty()) null else page + 1
LoadResult.Page(
itemsBefore = (page - 1) * PAGE_SIZE,
data = result.value,
prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
nextKey = nextKey,
)
}
is ApiResult.Failure.NetworkFailure -> LoadResult.Error(result.error)
is ApiResult.Failure.UnknownFailure -> LoadResult.Error(result.error)
is ApiResult.Failure.HttpFailure,
is ApiResult.Failure.ApiFailure ->
LoadResult.Error(IOException("API returned an invalid response"))
}
}
@AssistedFactory
interface Factory {
fun create(remoteFetcher: RemoteFetcher<LobstersPost>): SearchPagingSource
}
}
@@ -7,29 +7,37 @@
package dev.msfjarvis.claw.android.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.ImportExport
import androidx.compose.material.icons.filled.NewReleases
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Whatshot
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material.icons.outlined.NavigateBefore
import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Whatshot
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
@@ -46,12 +54,14 @@ import androidx.navigation.navDeepLink
import androidx.paging.compose.collectAsLazyPagingItems
import com.deliveryhero.whetstone.compose.injectedViewModel
import dev.msfjarvis.claw.android.R
import dev.msfjarvis.claw.android.ui.datatransfer.DataTransferScreen
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationBar
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationRail
import dev.msfjarvis.claw.android.ui.decorations.NavigationItem
import dev.msfjarvis.claw.android.ui.decorations.TransparentSystemBars
import dev.msfjarvis.claw.android.ui.lists.DatabasePosts
import dev.msfjarvis.claw.android.ui.lists.NetworkPosts
import dev.msfjarvis.claw.android.ui.lists.SearchList
import dev.msfjarvis.claw.android.ui.navigation.ClawNavigationType
import dev.msfjarvis.claw.android.ui.navigation.Destinations
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
@@ -79,8 +89,10 @@ fun LobstersApp(
val hottestListState = rememberLazyListState()
val newestListState = rememberLazyListState()
val savedListState = rememberLazyListState()
val searchListState = rememberLazyListState()
val navController = rememberNavController()
val coroutineScope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val postActions = rememberPostActions(urlLauncher, navController, viewModel)
val backStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = backStackEntry?.destination?.route
@@ -101,52 +113,75 @@ fun LobstersApp(
NavigationItem(
label = "Hottest",
route = Destinations.Hottest.route,
icon = rememberVectorPainter(Icons.Outlined.Whatshot),
selectedIcon = rememberVectorPainter(Icons.Filled.Whatshot),
icon = Icons.Outlined.Whatshot,
selectedIcon = Icons.Filled.Whatshot,
) {
coroutineScope.launch { hottestListState.animateScrollToItem(index = 0) }
},
NavigationItem(
label = "Newest",
route = Destinations.Newest.route,
icon = rememberVectorPainter(Icons.Outlined.NewReleases),
selectedIcon = rememberVectorPainter(Icons.Filled.NewReleases),
icon = Icons.Outlined.NewReleases,
selectedIcon = Icons.Filled.NewReleases,
) {
coroutineScope.launch { newestListState.animateScrollToItem(index = 0) }
},
NavigationItem(
label = "Saved",
route = Destinations.Saved.route,
icon = rememberVectorPainter(Icons.Outlined.FavoriteBorder),
selectedIcon = rememberVectorPainter(Icons.Filled.Favorite),
icon = Icons.Outlined.FavoriteBorder,
selectedIcon = Icons.Filled.Favorite,
) {
coroutineScope.launch { savedListState.animateScrollToItem(index = 0) }
},
NavigationItem(
label = "Search",
route = Destinations.Search.route,
icon = Icons.Outlined.Search,
selectedIcon = Icons.Filled.Search,
) {
coroutineScope.launch { searchListState.animateScrollToItem(index = 0) }
},
)
TransparentSystemBars()
Scaffold(
topBar = {
ClawAppBar(
navigationIcon = {
if (navItems.none { it.route == currentDestination }) {
IconButton(
onClick = { if (!navController.popBackStack()) context.getActivity()?.finish() }
if (currentDestination != Destinations.Search.route) {
ClawAppBar(
navigationIcon = {
if (
navController.previousBackStackEntry != null &&
navItems.none { it.route == currentDestination }
) {
Icon(
painter = rememberVectorPainter(Icons.Outlined.NavigateBefore),
contentDescription = "Go back to previous screen",
)
IconButton(
onClick = { if (!navController.popBackStack()) context.getActivity()?.finish() },
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Go back to previous screen",
)
}
}
}
},
title = {
if (navItems.any { it.route == currentDestination }) {
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
}
},
)
},
title = {
if (navItems.any { it.route == currentDestination }) {
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
}
},
actions = {
if (navItems.any { it.route == currentDestination }) {
IconButton(onClick = { navController.navigate(Destinations.DataTransfer.route) }) {
Icon(
imageVector = Icons.Filled.ImportExport,
contentDescription = "Data transfer options",
)
}
}
},
)
}
},
bottomBar = {
AnimatedVisibility(visible = navigationType == ClawNavigationType.BOTTOM_NAVIGATION) {
@@ -157,6 +192,7 @@ fun LobstersApp(
)
}
},
snackbarHost = { SnackbarHost(snackbarHostState) },
modifier = modifier.semantics { testTagsAsResourceId = true },
) { paddingValues ->
Row(modifier = Modifier.padding(paddingValues)) {
@@ -171,6 +207,9 @@ fun LobstersApp(
NavHost(
navController = navController,
startDestination = Destinations.startDestination.route,
// Make animations 2x faster than default specs
enterTransition = { fadeIn(animationSpec = tween(350)) },
exitTransition = { fadeOut(animationSpec = tween(350)) },
) {
val uri = LobstersApi.BASE_URL
composable(route = Destinations.Hottest.route) {
@@ -199,6 +238,17 @@ fun LobstersApp(
postActions = postActions,
)
}
composable(Destinations.Search.route) {
setWebUri("https://lobste.rs/search")
SearchList(
items = viewModel.searchResults,
listState = searchListState,
isPostSaved = viewModel::isPostSaved,
postActions = postActions,
searchQuery = viewModel.searchQuery,
setSearchQuery = { query -> viewModel.searchQuery = query },
)
}
composable(
route = Destinations.Comments.route,
arguments = listOf(navArgument("postId") { type = NavType.StringType }),
@@ -231,6 +281,14 @@ fun LobstersApp(
getProfile = viewModel::getUserProfile,
)
}
composable(route = Destinations.DataTransfer.route) {
DataTransferScreen(
context = context,
importPosts = viewModel::importPosts,
exportPosts = viewModel::exportPosts,
snackbarHostState = snackbarHostState,
)
}
}
}
}
@@ -0,0 +1,138 @@
/*
* Copyright © 2023 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.datatransfer
import android.content.Context
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.activity.result.contract.ActivityResultContracts.GetContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Upload
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import java.io.InputStream
import java.io.OutputStream
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
private const val MIME_TYPE = "application/json"
@Composable
fun DataTransferScreen(
context: Context,
importPosts: suspend (InputStream) -> Unit,
exportPosts: suspend (OutputStream) -> Unit,
snackbarHostState: SnackbarHostState,
modifier: Modifier = Modifier,
) {
val coroutineScope = rememberCoroutineScope()
Column(modifier = modifier) {
ImportOption(context, coroutineScope, importPosts, snackbarHostState)
ExportOption(context, coroutineScope, exportPosts, snackbarHostState)
}
}
@Composable
private fun ImportOption(
context: Context,
coroutineScope: CoroutineScope,
importPosts: suspend (InputStream) -> Unit,
snackbarHostState: SnackbarHostState,
) {
val importAction =
rememberLauncherForActivityResult(GetContent()) { uri ->
if (uri == null) {
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
return@rememberLauncherForActivityResult
}
coroutineScope.launch {
context.contentResolver.openInputStream(uri)?.use { stream ->
importPosts(stream)
snackbarHostState.showSnackbarDismissing("Successfully imported posts")
}
}
}
SettingsActionItem(
title = "Import saved posts",
description = "Import saved posts from a previously generated export",
icon = Icons.Filled.Download,
) {
importAction.launch(MIME_TYPE)
}
}
@Composable
private fun ExportOption(
context: Context,
coroutineScope: CoroutineScope,
exportPosts: suspend (OutputStream) -> Unit,
snackbarHostState: SnackbarHostState,
) {
val exportAction =
rememberLauncherForActivityResult(CreateDocument(MIME_TYPE)) { uri ->
if (uri == null) {
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
return@rememberLauncherForActivityResult
}
coroutineScope.launch {
context.contentResolver.openOutputStream(uri)?.use { stream ->
exportPosts(stream)
snackbarHostState.showSnackbarDismissing("Successfully exported posts")
}
}
}
SettingsActionItem(
title = "Export posts to file",
description = "Write all saved posts into a JSON file that can be imported at a later date",
icon = Icons.Filled.Upload,
) {
exportAction.launch("claw-export.json")
}
}
@Composable
private fun SettingsActionItem(
title: String,
modifier: Modifier = Modifier,
description: String? = null,
icon: ImageVector? = null,
onClick: (() -> Unit)? = null,
) {
ListItem(
headlineContent = { Text(title) },
supportingContent = { description?.let { Text(it) } },
leadingContent = {
icon?.let {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.height(32.dp),
)
}
},
modifier = modifier.clickable { onClick?.invoke() },
)
}
/** Shows a Snackbar but dismisses any existing ones first. */
private suspend fun SnackbarHostState.showSnackbarDismissing(
text: String,
) {
currentSnackbarData?.dismiss()
showSnackbar(text)
}
@@ -19,7 +19,7 @@ import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.navigation.NavController
import dev.msfjarvis.claw.android.ui.navigation.Destinations
@@ -57,7 +57,7 @@ fun ClawNavigationBar(
icon = {
Crossfade(isCurrentDestination, label = "nav-label") {
Icon(
painter = if (it) navItem.selectedIcon else navItem.icon,
imageVector = if (it) navItem.selectedIcon else navItem.icon,
contentDescription = navItem.label.replaceFirstChar(Char::uppercase),
)
}
@@ -86,7 +86,7 @@ fun ClawNavigationBar(
class NavigationItem(
val label: String,
val route: String,
val icon: Painter,
val selectedIcon: Painter,
val icon: ImageVector,
val selectedIcon: ImageVector,
val listStateResetCallback: () -> Unit,
)
@@ -56,7 +56,7 @@ fun ClawNavigationRail(
icon = {
Crossfade(isCurrentDestination, label = "nav-label") {
Icon(
painter = if (it) navItem.selectedIcon else navItem.icon,
imageVector = if (it) navItem.selectedIcon else navItem.icon,
contentDescription = navItem.label.replaceFirstChar(Char::uppercase),
)
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Copyright © 2022-2023 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.
@@ -19,8 +19,6 @@ import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.LinkMetadata
import dev.msfjarvis.claw.model.LobstersPostDetails
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
fun Context.getActivity(): ComponentActivity? {
return when (this) {
@@ -30,14 +28,6 @@ fun Context.getActivity(): ComponentActivity? {
}
}
/**
* Parses a given [String] into a [LocalDateTime]. This method is only intended to be used for dates
* in the format returned by the Lobsters API, and is not a general purpose parsing solution.
*/
fun String.toLocalDateTime(): LocalDateTime {
return LocalDateTime.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(this))
}
@Composable
fun rememberPostActions(
urlLauncher: UrlLauncher,
@@ -51,9 +41,10 @@ fun rememberPostActions(
}
override fun viewComments(postId: String) {
navController.navigate(
val currentRoute = navController.currentDestination?.route
val newRoute =
Destinations.Comments.route.replace(Destinations.Comments.placeholder, postId)
)
if (currentRoute != Destinations.Comments.route) navController.navigate(newRoute)
}
override fun viewCommentsPage(commentsUrl: String) {
@@ -6,15 +6,25 @@
*/
package dev.msfjarvis.claw.android.ui.lists
import androidx.activity.compose.ReportDrawn
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Divider
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Inbox
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.msfjarvis.claw.common.posts.PostActions
import dev.msfjarvis.claw.common.ui.decorations.MonthHeader
import dev.msfjarvis.claw.database.local.SavedPost
@@ -29,23 +39,33 @@ fun DatabasePosts(
postActions: PostActions,
modifier: Modifier = Modifier,
) {
ReportDrawn()
Box(modifier = modifier.fillMaxSize()) {
LazyColumn(
state = listState,
) {
items.forEach { (month, posts) ->
stickyHeader(contentType = "month-header") { MonthHeader(month = month) }
items(
items = posts,
key = { it.shortId },
contentType = { "LobstersItem" },
) { item ->
ListItem(
item = item,
isSaved = { true },
postActions = postActions,
)
Divider()
if (items.isEmpty()) {
Column(modifier = Modifier.align(Alignment.Center)) {
Icon(
imageVector = Icons.Filled.Inbox,
contentDescription = "Empty inbox icon",
modifier = Modifier.align(Alignment.CenterHorizontally).size(36.dp),
)
Text(text = "No saved posts", style = MaterialTheme.typography.headlineSmall)
}
} else {
LazyColumn(state = listState) {
items.forEach { (month, posts) ->
stickyHeader(contentType = "month-header") { MonthHeader(month = month) }
items(
items = posts,
key = { it.shortId },
contentType = { "LobstersItem" },
) { item ->
ListItem(
item = item,
isSaved = { true },
postActions = postActions,
)
HorizontalDivider()
}
}
}
}
@@ -6,6 +6,7 @@
*/
package dev.msfjarvis.claw.android.ui.lists
import androidx.activity.compose.ReportDrawnWhen
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -17,7 +18,7 @@ import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Divider
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
@@ -44,6 +45,7 @@ fun NetworkPosts(
postActions: PostActions,
modifier: Modifier = Modifier,
) {
ReportDrawnWhen { lazyPagingItems.itemCount > 0 }
val refreshLoadState = lazyPagingItems.loadState.refresh
val isRefreshing = refreshLoadState == LoadState.Loading
val pullRefreshState = rememberPullRefreshState(isRefreshing, lazyPagingItems::refresh)
@@ -72,7 +74,7 @@ fun NetworkPosts(
postActions = postActions,
)
Divider()
HorizontalDivider()
}
}
if (lazyPagingItems.loadState.append == LoadState.Loading) {
@@ -0,0 +1,94 @@
/*
* Copyright © 2023 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.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SearchOff
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
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.platform.testTag
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import dev.msfjarvis.claw.common.posts.PostActions
import dev.msfjarvis.claw.common.ui.SearchBar
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.LobstersPost
import kotlinx.coroutines.flow.Flow
@Composable
fun SearchList(
items: Flow<PagingData<LobstersPost>>,
listState: LazyListState,
isPostSaved: suspend (SavedPost) -> Boolean,
postActions: PostActions,
searchQuery: String,
setSearchQuery: (String) -> Unit,
modifier: Modifier = Modifier,
) {
val lazyPagingItems = items.collectAsLazyPagingItems()
val triggerSearch = { query: String ->
setSearchQuery(query)
lazyPagingItems.refresh()
}
DisposableEffect(true) {
// Clear search field when navigating away
onDispose { setSearchQuery("") }
}
var searchActive by remember { mutableStateOf(false) }
Column(modifier = Modifier.semantics { isTraversalGroup = true }.zIndex(1f).fillMaxWidth()) {
SearchBar(
value = searchQuery,
onValueChange = setSearchQuery,
onSearch = {
triggerSearch(it)
searchActive = true
},
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp).testTag("search_bar"),
)
if (searchActive) {
NetworkPosts(
lazyPagingItems = lazyPagingItems,
listState = listState,
isPostSaved = isPostSaved,
postActions = postActions,
modifier = modifier,
)
} else {
Box(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.align(Alignment.Center)) {
Icon(
imageVector = Icons.Filled.SearchOff,
contentDescription = "No search results",
modifier = Modifier.align(Alignment.CenterHorizontally).size(36.dp),
)
Text(text = "Nothing to see here", style = MaterialTheme.typography.headlineSmall)
}
}
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2021-2022 Harsh Shandilya.
* Copyright © 2021-2023 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.
@@ -21,6 +21,10 @@ sealed class Destinations {
override val route = "saved"
}
object Search : Destinations() {
override val route = "search"
}
object Comments : Destinations() {
const val placeholder = "{postId}"
override val route = "comments/$placeholder"
@@ -31,6 +35,10 @@ sealed class Destinations {
override val route = "user/$placeholder"
}
object DataTransfer : Destinations() {
override val route: String = "datatransfer"
}
companion object {
val startDestination
get() = Hottest
@@ -1,5 +1,5 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Copyright © 2023 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.
@@ -6,8 +6,8 @@
*/
package dev.msfjarvis.claw.android.viewmodel
import dev.msfjarvis.claw.android.injection.IODispatcher
import dev.msfjarvis.claw.api.injection.BaseUrl
import dev.msfjarvis.claw.core.injection.IODispatcher
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
@@ -24,7 +24,7 @@ constructor(
@BaseUrl private val url: String,
) {
suspend fun extractToken(): String? {
val request = Request.Builder().url(url).get().build()
val request = Request.Builder().url(url).build()
return withContext(dispatcher) {
okHttpClient.newCall(request).execute().use { response ->
val doc = Jsoup.parse(response.body?.string() ?: return@use null)
@@ -6,6 +6,9 @@
*/
package dev.msfjarvis.claw.android.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
@@ -13,17 +16,23 @@ import androidx.paging.PagingConfig
import com.deliveryhero.whetstone.viewmodel.ContributesViewModel
import com.slack.eithernet.ApiResult.Failure
import com.slack.eithernet.ApiResult.Success
import dev.msfjarvis.claw.android.injection.IODispatcher
import dev.msfjarvis.claw.android.paging.LobstersPagingSource
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.PAGE_SIZE
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.STARTING_PAGE_INDEX
import dev.msfjarvis.claw.android.ui.toLocalDateTime
import dev.msfjarvis.claw.android.paging.SearchPagingSource
import dev.msfjarvis.claw.api.LobstersApi
import dev.msfjarvis.claw.api.LobstersSearchApi
import dev.msfjarvis.claw.core.injection.IODispatcher
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.Comment
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.net.HttpURLConnection
import java.time.LocalDateTime
import java.time.Month
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import javax.inject.Inject
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableMap
@@ -38,9 +47,13 @@ class ClawViewModel
@Inject
constructor(
private val api: LobstersApi,
private val searchApi: LobstersSearchApi,
private val savedPostsRepository: SavedPostsRepository,
private val commentsRepository: CommentsRepository,
private val linkMetadataRepository: LinkMetadataRepository,
private val dataTransferRepository: DataTransferRepository,
private val pagingSourceFactory: LobstersPagingSource.Factory,
private val searchPagingSourceFactory: SearchPagingSource.Factory,
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() {
private val hottestPostsPager =
@@ -51,6 +64,10 @@ constructor(
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
pagingSourceFactory.create(api::getNewestPosts)
}
private val searchResultsPager =
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
searchPagingSourceFactory.create { searchApi.searchPosts(searchQuery, it) }
}
val hottestPosts
get() = hottestPostsPager.flow
@@ -64,6 +81,11 @@ constructor(
val savedPosts
get() = savedPostsFlow.map(::mapSavedPosts)
val searchResults
get() = searchResultsPager.flow
var searchQuery by mutableStateOf("")
private fun mapSavedPosts(items: List<SavedPost>): ImmutableMap<Month, List<SavedPost>> {
val sorted = items.sortedByDescending { post -> post.createdAt.toLocalDateTime() }
return sorted.groupBy { post -> post.createdAt.toLocalDateTime().month }.toImmutableMap()
@@ -101,10 +123,10 @@ constructor(
}
}
suspend fun getSeenComments(postId: String) = savedPostsRepository.getSeenComments(postId)
suspend fun getSeenComments(postId: String) = commentsRepository.getSeenComments(postId)
fun markSeenComments(postId: String, comments: List<Comment>) {
viewModelScope.launch { savedPostsRepository.markSeenComments(postId, comments) }
viewModelScope.launch { commentsRepository.markSeenComments(postId, comments) }
}
suspend fun getLinkMetadata(url: String) =
@@ -120,4 +142,18 @@ constructor(
is Failure.ApiFailure -> throw IOException("API returned an invalid response")
}
}
suspend fun importPosts(input: InputStream) = dataTransferRepository.importPosts(input)
suspend fun exportPosts(output: OutputStream) = dataTransferRepository.exportPosts(output)
/**
* Parses a given [String] into a [LocalDateTime]. This method is only intended to be used for
* dates in the format returned by the Lobsters API, and is not a general purpose parsing
* solution.
*/
private fun String.toLocalDateTime(): LocalDateTime {
if (isEmpty()) return LocalDateTime.now(ZoneId.systemDefault())
return LocalDateTime.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(this))
}
}
@@ -0,0 +1,33 @@
/*
* Copyright © 2021-2023 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 dev.msfjarvis.claw.core.injection.DatabaseDispatcher
import dev.msfjarvis.claw.database.LobstersDatabase
import dev.msfjarvis.claw.database.local.PostComments
import dev.msfjarvis.claw.model.Comment
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
class CommentsRepository
@Inject
constructor(
database: LobstersDatabase,
@DatabaseDispatcher private val dbDispatcher: CoroutineDispatcher,
) {
private val postCommentsQueries = database.postCommentsQueries
suspend fun getSeenComments(postId: String) =
withContext(dbDispatcher) { postCommentsQueries.getCommentIds(postId).executeAsOneOrNull() }
suspend fun markSeenComments(postId: String, comments: List<Comment>) {
withContext(dbDispatcher) {
postCommentsQueries.rememberComments(PostComments(postId, comments.map { it.shortId }))
}
}
}
@@ -0,0 +1,54 @@
/*
* Copyright © 2023 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 dev.msfjarvis.claw.core.injection.DatabaseDispatcher
import dev.msfjarvis.claw.core.injection.IODispatcher
import dev.msfjarvis.claw.database.LobstersDatabase
import dev.msfjarvis.claw.database.SavedPostSerializer
import dev.msfjarvis.claw.database.local.SavedPost
import java.io.InputStream
import java.io.OutputStream
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream
@OptIn(ExperimentalSerializationApi::class)
class DataTransferRepository
@Inject
constructor(
private val database: LobstersDatabase,
private val json: Json,
@DatabaseDispatcher private val dbDispatcher: CoroutineDispatcher,
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
) {
private val serializer = ListSerializer(SavedPostSerializer)
private val savedPostQueries = database.savedPostQueries
suspend fun importPosts(input: InputStream) {
val posts: List<SavedPost> =
withContext(ioDispatcher) {
json.decodeFromStream(
serializer,
input,
)
}
withContext(dbDispatcher) {
database.transaction { posts.forEach(savedPostQueries::insertOrReplacePost) }
}
}
suspend fun exportPosts(output: OutputStream) {
val posts = withContext(dbDispatcher) { savedPostQueries.selectAllPosts().executeAsList() }
withContext(ioDispatcher) { json.encodeToStream(serializer, posts, output) }
}
}
@@ -1,21 +1,26 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Copyright © 2022-2023 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 dev.msfjarvis.claw.metadata.MetadataExtractor
import dev.msfjarvis.claw.core.injection.IODispatcher
import dev.msfjarvis.claw.model.LinkMetadata
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import me.saket.unfurl.Unfurler
class LinkMetadataRepository
@Inject
constructor(
private val metadataExtractor: MetadataExtractor,
private val unfurler: Unfurler,
@IODispatcher private val dispatcher: CoroutineDispatcher,
) {
suspend fun getLinkMetadata(url: String): LinkMetadata {
return metadataExtractor.getExtractedMetadata(url)
val result = withContext(dispatcher) { unfurler.unfurl(url) }
return LinkMetadata(url, result?.favicon?.toString())
}
}
@@ -8,11 +8,9 @@ package dev.msfjarvis.claw.android.viewmodel
import app.cash.sqldelight.coroutines.asFlow
import app.cash.sqldelight.coroutines.mapToList
import dev.msfjarvis.claw.android.injection.DatabaseDispatcher
import dev.msfjarvis.claw.core.injection.DatabaseDispatcher
import dev.msfjarvis.claw.database.LobstersDatabase
import dev.msfjarvis.claw.database.local.PostComments
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.Comment
import io.github.aakira.napier.Napier
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -25,7 +23,6 @@ constructor(
@DatabaseDispatcher private val dbDispatcher: CoroutineDispatcher,
) {
private val savedPostQueries = database.savedPostQueries
private val postCommentsQueries = database.postCommentsQueries
val savedPosts = savedPostQueries.selectAllPosts().asFlow().mapToList(dbDispatcher)
suspend fun savePost(post: SavedPost) {
@@ -47,15 +44,6 @@ constructor(
withContext(dbDispatcher) { savedPostQueries.deletePost(post.shortId) }
}
suspend fun getSeenComments(postId: String) =
withContext(dbDispatcher) { postCommentsQueries.getCommentIds(postId).executeAsOneOrNull() }
suspend fun markSeenComments(postId: String, comments: List<Comment>) {
withContext(dbDispatcher) {
postCommentsQueries.rememberComments(PostComments(postId, comments.map { it.shortId }))
}
}
private companion object {
private const val TAG = "SavedPostsRepository"
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Copyright © 2022-2023 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.
@@ -41,7 +41,6 @@ constructor(
.map { post -> lobstersApi.getPostDetails(post.shortId) }
.filterIsInstance<Success<LobstersPostDetails>>()
.map { result -> result.value.toDbModel() }
.toList()
.let { savedPostsRepository.savePosts(it) }
return Result.success()
}
@@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © 2023 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.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+6 -1
View File
@@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
~ Copyright © 2021-2023 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.
-->
<resources>
<bool name="use_light_status_bar">false</bool>
</resources>
+6 -1
View File
@@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
~ Copyright © 2021-2023 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.
-->
<resources>
<bool name="use_light_status_bar">true</bool>
</resources>
+6 -1
View File
@@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
~ Copyright © 2021-2023 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.
-->
<resources>
<string name="app_name">Claw</string>
</resources>
+6 -1
View File
@@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
~ Copyright © 2021-2023 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.
-->
<resources>
<style name="Theme.Claw.SplashScreen" parent="Theme.SplashScreen">
@@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © 2022-2023 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.
-->
<data-extraction-rules>
<cloud-backup>
<include
@@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © 2022-2023 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.
-->
<full-backup-content>
<include
domain="database"
@@ -8,7 +8,6 @@ package dev.msfjarvis.claw.android.viewmodel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.Dispatcher
@@ -18,7 +17,6 @@ import okhttp3.mockwebserver.RecordedRequest
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
@OptIn(ExperimentalCoroutinesApi::class)
class CSRFRepositoryTest {
@Test
fun `correctly extracts CSRF token`() = runTest {
+2 -2
View File
@@ -1,5 +1,5 @@
#
# This file was automatically generated by 'versioning-plugin'. DO NOT EDIT MANUALLY.
#
versioning-plugin.versionCode=12700
versioning-plugin.versionName=1.27.0
versioning-plugin.versionCode=13200
versioning-plugin.versionName=1.32.0
+1
View File
@@ -24,6 +24,7 @@ dependencies {
implementation(libs.dagger)
implementation(libs.javax.inject)
implementation(libs.jsoup)
testImplementation(testFixtures(libs.eithernet))
testImplementation(libs.kotlinx.coroutines.test)
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 8.0.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.1)" variant="all" version="8.0.1">
<issues format="6" by="lint 8.2.0-alpha14" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha14)" variant="all" version="8.2.0-alpha14">
</issues>
@@ -9,7 +9,6 @@ package dev.msfjarvis.claw.api
import com.slack.eithernet.ApiResult
import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.Tags
import dev.msfjarvis.claw.model.User
import retrofit2.http.GET
import retrofit2.http.Path
@@ -30,12 +29,6 @@ interface LobstersApi {
@GET("u/{username}.json")
suspend fun getUser(@Path("username") username: String): ApiResult<User, Unit>
@GET("t/{tags}.json")
suspend fun getPostsByTags(
@Path("tags") tag: Tags,
@Query("page") page: Int,
): ApiResult<List<LobstersPost>, Unit>
companion object {
const val BASE_URL = "https://lobste.rs"
}
@@ -0,0 +1,20 @@
/*
* Copyright © 2023 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 dev.msfjarvis.claw.model.LobstersPost
import retrofit2.http.GET
import retrofit2.http.Query
interface LobstersSearchApi {
@GET("/search?what=stories&order=newest")
suspend fun searchPosts(
@Query("q") searchQuery: String,
@Query("page") page: Int,
): ApiResult<List<LobstersPost>, Unit>
}
@@ -0,0 +1,85 @@
/*
* Copyright © 2023 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 dev.msfjarvis.claw.model.User
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> {
val elements =
Jsoup.parse(value.string(), LobstersApi.BASE_URL).select("div.story_liner.h-entry")
return elements.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 =
getSubmitter(elem.select("div.byline").first() ?: error("No byline element found"))
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 = "",
)
}
private fun getCommentsData(elem: Elements): Pair<Int, String> {
val linkElement = elem.select("a")
val countString = linkElement.text().trimStart().substringBefore(" ")
val commentsUrl = linkElement.attr("href")
return (countString.toIntOrNull() ?: 0) to commentsUrl
}
/**
* Make a bare-bones [User] object given a byline [elem]. We only need this to be usable for
* displaying in a list.
*/
private fun getSubmitter(elem: Element): User {
val userElement = elem.select("a.u-author")
val avatarElement = elem.select("img.avatar")
val username = userElement.text()
val avatarUrl = avatarElement.attr("src")
return User(
username = username,
about = "",
invitedBy = null,
avatarUrl = avatarUrl,
createdAt = "",
)
}
object Factory : Converter.Factory() {
override fun responseBodyConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, List<LobstersPost>> {
return SearchConverter
}
}
}
@@ -1,48 +0,0 @@
/*
* Copyright © 2021-2023 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.injection
import com.deliveryhero.whetstone.app.ApplicationScope
import com.slack.eithernet.ApiResultCallAdapterFactory
import com.slack.eithernet.ApiResultConverterFactory
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import dev.msfjarvis.claw.api.LobstersApi
import javax.inject.Qualifier
import okhttp3.OkHttpClient
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.create
@Module
@ContributesTo(ApplicationScope::class)
object ApiModule {
@Provides
fun provideRetrofit(
client: OkHttpClient,
converterFactories: Set<@JvmSuppressWildcards Converter.Factory>,
@BaseUrl baseUrl: String,
): Retrofit {
return Retrofit.Builder()
.client(client)
.baseUrl(baseUrl)
.addConverterFactory(ApiResultConverterFactory)
.addCallAdapterFactory(ApiResultCallAdapterFactory)
.apply { converterFactories.forEach(this::addConverterFactory) }
.build()
}
@Provides
fun provideApi(retrofit: Retrofit): LobstersApi {
return retrofit.create()
}
@Provides @BaseUrl fun provideBaseUrl(): String = LobstersApi.BASE_URL
}
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class BaseUrl
@@ -0,0 +1,98 @@
/*
* Copyright © 2023 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.injection
import com.deliveryhero.whetstone.app.ApplicationScope
import com.slack.eithernet.ApiResultCallAdapterFactory
import com.slack.eithernet.ApiResultConverterFactory
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntKey
import dagger.multibindings.IntoMap
import dev.msfjarvis.claw.api.LobstersApi
import dev.msfjarvis.claw.api.LobstersSearchApi
import dev.msfjarvis.claw.api.converters.SearchConverter
import javax.inject.Qualifier
import okhttp3.OkHttpClient
import retrofit2.CallAdapter
import retrofit2.Converter
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
* [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].
*/
@Module
@ContributesTo(ApplicationScope::class)
object RetrofitModule {
@Provides
fun provideRetrofit(
client: OkHttpClient,
converterFactories: Map<Int, @JvmSuppressWildcards Converter.Factory>,
callAdapterFactories: Map<Int, @JvmSuppressWildcards CallAdapter.Factory>,
@BaseUrl baseUrl: String,
): Retrofit {
return Retrofit.Builder()
.client(client)
.baseUrl(baseUrl)
.apply { converterFactories.toSortedMap().values.forEach(this::addConverterFactory) }
.apply { callAdapterFactories.toSortedMap().values.forEach(this::addCallAdapterFactory) }
.build()
}
@Provides
@SearchApi
fun provideSearchApiRetrofit(
client: OkHttpClient,
@SearchApi converterFactories: List<@JvmSuppressWildcards Converter.Factory>,
callAdapterFactories: Map<Int, @JvmSuppressWildcards CallAdapter.Factory>,
@BaseUrl baseUrl: String,
): Retrofit {
return Retrofit.Builder()
.client(client)
.baseUrl(baseUrl)
.apply { converterFactories.forEach(this::addConverterFactory) }
.apply { callAdapterFactories.toSortedMap().values.forEach(this::addCallAdapterFactory) }
.addConverterFactory(SearchConverter.Factory)
.build()
}
@Provides fun provideApi(retrofit: Retrofit): LobstersApi = retrofit.create()
@Provides
fun provideSearchApi(@SearchApi retrofit: Retrofit): LobstersSearchApi = retrofit.create()
@Provides
@IntKey(0)
@IntoMap
fun provideApiResultConverter(): Converter.Factory = ApiResultConverterFactory
@Provides
@IntKey(0)
@IntoMap
fun provideApiResultCallAdapter(): CallAdapter.Factory = ApiResultCallAdapterFactory
@Provides
@SearchApi
fun provideConverters(): List<@JvmSuppressWildcards Converter.Factory> =
listOf(
ApiResultConverterFactory,
SearchConverter.Factory,
)
@Provides @BaseUrl fun provideBaseUrl(): String = LobstersApi.BASE_URL
}
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class BaseUrl
/** Internal Dagger [Qualifier] to identify dependencies exclusive to [LobstersSearchApi]. */
@Qualifier @Retention(AnnotationRetention.RUNTIME) private annotation class SearchApi
@@ -11,14 +11,11 @@ import com.slack.eithernet.ApiResult.Success
import com.slack.eithernet.test.newEitherNetController
import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.Tags
import dev.msfjarvis.claw.model.User
import dev.msfjarvis.claw.util.TestUtils.assertIs
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
@OptIn(ExperimentalCoroutinesApi::class)
class ApiTest {
private val wrapper = ApiWrapper(newEitherNetController())
private val api
@@ -52,25 +49,4 @@ class ApiTest {
assertIs<Success<User>>(user)
assertThat(user.value.username).isEqualTo("msfjarvis")
}
@Test
fun `get posts by single tag`() = runTest {
val tags = Tags()
tags.addTag("meta")
val posts = api.getPostsByTags(tags, 1)
assertIs<Success<List<LobstersPost>>>(posts)
assertThat(posts.value).hasSize(25)
assertThat(posts.value[0].tags).contains("meta")
}
@Test
fun `get posts by multiple tags`() = runTest {
val tags = Tags()
tags.addTag("programming")
tags.addTag("rust")
val posts = api.getPostsByTags(tags, 1)
assertIs<Success<List<LobstersPost>>>(posts)
assertThat(posts.value).hasSize(25)
assertThat(posts.value[0].tags).containsAnyOf("programming", "rust")
}
}
@@ -6,17 +6,14 @@
*/
package dev.msfjarvis.claw.api
import com.slack.eithernet.ApiResult.Companion.httpFailure
import com.slack.eithernet.ApiResult.Companion.success
import com.slack.eithernet.test.EitherNetController
import com.slack.eithernet.test.enqueue
import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.Tags
import dev.msfjarvis.claw.model.User
import dev.msfjarvis.claw.util.TestUtils.getJson
import dev.msfjarvis.claw.util.TestUtils.getResource
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy
@@ -26,23 +23,10 @@ class ApiWrapper(controller: EitherNetController<LobstersApi>) {
ignoreUnknownKeys = true
namingStrategy = JsonNamingStrategy.SnakeCase
}
private val hottest: List<LobstersPost> = json.decodeFromString(getJson("hottest.json"))
private val hottest: List<LobstersPost> = json.decodeFromString(getResource("hottest.json"))
private val postDetails: LobstersPostDetails =
json.decodeFromString(getJson("post_details_tdfoqh.json"))
private val user: User = json.decodeFromString(getJson("msfjarvis.json"))
private val metaPosts: List<LobstersPost> = json.decodeFromString(getJson("meta.json"))
private val programmingRustPosts: List<LobstersPost> =
json.decodeFromString(getJson("programming_rust.json"))
private val getPostsBody = { args: Array<Any> ->
val tags = args[0] as Tags
if ("meta" in tags) {
success(metaPosts)
} else if ("programming" in tags && "rust" in tags) {
success(programmingRustPosts)
} else {
httpFailure(400)
}
}
json.decodeFromString(getResource("post_details_tdfoqh.json"))
private val user: User = json.decodeFromString(getResource("msfjarvis.json"))
val api = controller.api
@@ -51,7 +35,5 @@ class ApiWrapper(controller: EitherNetController<LobstersApi>) {
controller.enqueue(LobstersApi::getHottestPosts) { success(hottest) }
controller.enqueue(LobstersApi::getPostDetails) { success(postDetails) }
controller.enqueue(LobstersApi::getUser) { success(user) }
controller.enqueue(LobstersApi::getPostsByTags, getPostsBody)
controller.enqueue(LobstersApi::getPostsByTags, getPostsBody)
}
}
@@ -0,0 +1,59 @@
/*
* Copyright © 2023 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.test.newEitherNetController
import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.model.User
import dev.msfjarvis.claw.util.TestUtils.assertIs
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
class SearchApiTest {
private val wrapper = SearchApiWrapper(newEitherNetController())
private val api
get() = wrapper.api
@Test
fun `search is able to parse HTML`() = runTest {
val posts = api.searchPosts("ChatGPT", 1)
assertIs<ApiResult.Success<List<LobstersPost>>>(posts)
assertThat(posts.value).containsAtLeastElementsIn(results).inOrder()
assertThat(posts.value).hasSize(20)
}
private companion object {
private val results =
listOf(
LobstersPost(
shortId = "gjlsdg",
title = "ChatGPT visits the Emacs doctor",
url = "https://xenodium.com/chatgpt-visits-the-emacs-doctor/",
createdAt = "",
commentCount = 3,
commentsUrl = "/s/gjlsdg/chatgpt_visits_emacs_doctor",
submitter = User("xenodium", "", null, "/avatars/xenodium-16.png", ""),
tags = listOf("ai", "emacs"),
description = "",
),
LobstersPost(
shortId = "astcqf",
title =
"Implementing a question-answering system for PDF documents using ChatGPT and Redis",
url = "https://mstack.nl/blog/20230623-chatgpt-question-pdf-document/",
createdAt = "",
commentCount = 0,
commentsUrl = "/s/astcqf/implementing_question_answering_system",
submitter = User("asteroid", "", null, "/avatars/asteroid-16.png", ""),
tags = listOf("ai"),
description = "",
),
)
}
}
@@ -0,0 +1,29 @@
/*
* Copyright © 2023 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.Companion.success
import com.slack.eithernet.test.EitherNetController
import com.slack.eithernet.test.enqueue
import dev.msfjarvis.claw.api.converters.SearchConverter
import dev.msfjarvis.claw.util.TestUtils.getResource
import okhttp3.MediaType
import okhttp3.ResponseBody
class SearchApiWrapper(controller: EitherNetController<LobstersSearchApi>) {
val api = controller.api
init {
controller.enqueue(LobstersSearchApi::searchPosts) {
success(
SearchConverter.convert(
ResponseBody.create(MediaType.get("text/html"), getResource("search_chatgpt_page.html"))
)
)
}
}
}
@@ -13,7 +13,7 @@ import kotlin.contracts.contract
@OptIn(ExperimentalContracts::class)
object TestUtils {
fun getJson(path: String): String {
fun getResource(path: String): String {
// Load the JSON response
val uri = javaClass.classLoader!!.getResource(path)
val file = File(uri.path)
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 one or more lines are too long
+6 -1
View File
@@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © 2022-2023 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<package android:name="dev.msfjarvis.claw.android" />
@@ -1,5 +1,5 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Copyright © 2022-2023 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.
@@ -8,6 +8,7 @@ package dev.msfjarvis.claw.benchmark
import androidx.benchmark.macro.BaselineProfileMode
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.FrameTimingMetric
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
@@ -28,29 +29,39 @@ class BaselineProfileBenchmark {
}
@Test
fun startupNoCompilation() {
startup(CompilationMode.None())
fun noCompilation() {
exploreUI(CompilationMode.None())
}
@Test
fun startupBaselineFullyCompiled() {
startup(CompilationMode.Full())
fun fullyCompiled() {
exploreUI(CompilationMode.Full())
}
@Test
fun startupBaselineProfile() {
startup(CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require))
fun baselineProfile() {
exploreUI(CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require))
}
private fun startup(compilationMode: CompilationMode) {
private fun exploreUI(compilationMode: CompilationMode) {
benchmarkRule.measureRepeated(
packageName = PACKAGE_NAME,
metrics = listOf(StartupTimingMetric()),
iterations = 10,
metrics = listOf(FrameTimingMetric(), StartupTimingMetric()),
compilationMode = compilationMode,
startupMode = StartupMode.COLD,
compilationMode = compilationMode
iterations = 10,
) {
device.executeShellCommand("pm clear $PACKAGE_NAME")
startActivityAndWait()
device.waitForIdle()
device.savePosts()
device.exploreScreens()
device.returnToHottestScreen()
}
}
}
@@ -6,7 +6,6 @@
*/
package dev.msfjarvis.claw.benchmark
import androidx.benchmark.macro.ExperimentalStableBaselineProfilesApi
import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
@@ -14,7 +13,6 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
@OptIn(ExperimentalStableBaselineProfilesApi::class)
class BaselineProfileGenerator {
@get:Rule val baselineProfileRule = BaselineProfileRule()
private lateinit var device: UiDevice
@@ -27,11 +25,20 @@ class BaselineProfileGenerator {
@Test
fun generateBaselineProfile() =
baselineProfileRule.collectStableBaselineProfile(
baselineProfileRule.collect(
packageName = PACKAGE_NAME,
stableIterations = 2,
maxIterations = 8,
) {
device.executeShellCommand("pm clear $PACKAGE_NAME")
startActivityAndWait()
device.waitForIdle()
device.savePosts()
device.exploreScreens()
device.returnToHottestScreen()
}
}
@@ -6,56 +6,38 @@
*/
package dev.msfjarvis.claw.benchmark
import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.test.uiautomator.By
import androidx.test.uiautomator.BySelector
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until
const val PACKAGE_NAME = "dev.msfjarvis.claw.android.benchmark"
private const val AWAIT_TIMEOUT = 10_000L
private const val SAVE_BUTTON_DESC = "Add to saved posts"
private const val SAVE_BUTTON_ID = "save_button"
private const val NAV_ID_HOTTEST = "HOTTEST"
private const val NAV_ID_NEWEST = "NEWEST"
private const val NAV_ID_SAVED = "SAVED"
private const val COMMENT_BUTTON_DESC = "Open comments"
@Suppress("Unused")
fun MacrobenchmarkScope.exploreUI(device: UiDevice) {
startActivityAndWait()
device.run {
savePosts()
exploreScreens()
returnToHottestScreen()
openCommentsScreen()
}
fun UiDevice.savePosts() {
waitForObject(By.res(SAVE_BUTTON_ID)).click()
}
private fun UiDevice.waitForSubmitterName() {
wait(Until.hasObject(By.textContains("Submitted by")), AWAIT_TIMEOUT)
}
private fun UiDevice.savePosts() {
waitForSubmitterName()
findObjects(By.desc(SAVE_BUTTON_DESC)).forEach(UiObject2::click)
}
private fun UiDevice.exploreScreens() {
fun UiDevice.exploreScreens() {
listOf(NAV_ID_HOTTEST, NAV_ID_NEWEST, NAV_ID_SAVED).forEach { tag ->
findObject(By.res(tag)).click()
waitForSubmitterName()
waitForObject(By.res(tag)).click()
waitForIdle()
}
}
private fun UiDevice.returnToHottestScreen() {
findObject(By.res(NAV_ID_HOTTEST)).click()
waitForSubmitterName()
fun UiDevice.returnToHottestScreen() {
waitForObject(By.res(NAV_ID_HOTTEST)).click()
}
private fun UiDevice.openCommentsScreen() {
waitForSubmitterName()
findObjects(By.desc(COMMENT_BUTTON_DESC)).first().click()
private fun UiDevice.waitForObject(selector: BySelector, timeout: Long = AWAIT_TIMEOUT): UiObject2 {
if (wait(Until.hasObject(selector), timeout)) {
return findObject(selector)
}
error("Object with selector [$selector] not found")
}
+1 -14
View File
@@ -4,22 +4,9 @@
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
import org.gradle.api.JavaVersion
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { `kotlin-dsl` }
tasks.withType<JavaCompile>().configureEach {
sourceCompatibility = JavaVersion.VERSION_17.toString()
targetCompatibility = JavaVersion.VERSION_17.toString()
}
tasks.withType<KotlinCompile>().configureEach {
compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
}
kotlin.jvmToolchain { languageVersion.set(JavaLanguageVersion.of(17)) }
gradlePlugin {
plugins {
@@ -16,7 +16,6 @@ import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.gradle.BaseExtension
import dev.msfjarvis.claw.gradle.LintConfig.configureLint
import org.gradle.android.AndroidCacheFixPlugin
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
@@ -28,9 +27,10 @@ import org.gradle.kotlin.dsl.getByType
class AndroidCommonPlugin : Plugin<Project> {
private companion object {
const val COMPILE_SDK = 33
const val COMPILE_SDK = 34
const val MIN_SDK = 26
const val TARGET_SDK = 33
const val TARGET_SDK = 34
const val SLIM_TESTS_PROPERTY = "slimTests"
}
override fun apply(project: Project) {
@@ -52,11 +52,6 @@ class AndroidCommonPlugin : Plugin<Project> {
resources.excludes.add("**/META-INF/LGPL2.1")
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
testOptions {
animationsDisabled = true
unitTests.isReturnDefaultValues = true
@@ -68,22 +63,27 @@ class AndroidCommonPlugin : Plugin<Project> {
val libs = catalog.named("libs")
project.dependencies.addProvider("lintChecks", libs.findLibrary("slack-compose-lints").get())
}
}
private fun Project.configureSlimTests() {
// Disable unit test tasks on the release build type for Android Library projects
extensions.findByType<LibraryAndroidComponentsExtension>()?.run {
beforeVariants(selector().withBuildType("release")) {
(it as HasUnitTestBuilder).enableUnitTest = false
it.enableAndroidTest = false
private fun Project.configureSlimTests() {
if (!providers.gradleProperty(SLIM_TESTS_PROPERTY).isPresent) {
return
}
// Disable unit test tasks on the release build type for Android Library projects
extensions.findByType<LibraryAndroidComponentsExtension>()?.run {
beforeVariants(selector().all()) {
if (it.name == "debug") return@beforeVariants
(it as HasUnitTestBuilder).enableUnitTest = false
it.enableAndroidTest = false
}
}
}
// Disable unit test tasks on the release build type for Android Application projects.
extensions.findByType<ApplicationAndroidComponentsExtension>()?.run {
beforeVariants(selector().withBuildType("release")) {
(it as HasUnitTestBuilder).enableUnitTest = false
it.enableAndroidTest = false
// Disable unit test tasks on the release build type for Android Application projects.
extensions.findByType<ApplicationAndroidComponentsExtension>()?.run {
beforeVariants(selector().all()) {
if (it.name == "debug") return@beforeVariants
(it as HasUnitTestBuilder).enableUnitTest = false
it.enableAndroidTest = false
}
}
}
}
@@ -34,8 +34,7 @@ class ApplicationPlugin : Plugin<Project> {
}
buildTypes {
named("release") {
isMinifyEnabled = true
all {
setProguardFiles(
listOf(
"proguard-android-optimize.pro",
@@ -44,6 +43,7 @@ class ApplicationPlugin : Plugin<Project> {
)
)
}
named("release") { isMinifyEnabled = true }
named("debug") {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
@@ -1,14 +1,17 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Copyright © 2022-2023 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.gradle
import dev.msfjarvis.claw.gradle.KotlinCommonPlugin.Companion.JVM_TOOLCHAIN_ACTION
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.getByType
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper
@Suppress("Unused")
@@ -19,5 +22,6 @@ class KotlinAndroidPlugin : Plugin<Project> {
apply(KotlinAndroidPluginWrapper::class)
apply(KotlinCommonPlugin::class)
}
project.extensions.getByType<KotlinProjectExtension>().jvmToolchain(JVM_TOOLCHAIN_ACTION)
}
}
@@ -6,16 +6,16 @@
*/
package dev.msfjarvis.claw.gradle
import org.gradle.api.JavaVersion
import org.gradle.api.Action
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.testing.Test
import org.gradle.api.tasks.testing.logging.TestLogEvent
import org.gradle.jvm.toolchain.JavaLanguageVersion
import org.gradle.jvm.toolchain.JavaToolchainSpec
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@Suppress("Unused")
class KotlinCommonPlugin : Plugin<Project> {
@@ -27,18 +27,11 @@ class KotlinCommonPlugin : Plugin<Project> {
LintConfig.configureSubProject(project)
}
project.tasks.run {
withType<JavaCompile>().configureEach {
sourceCompatibility = JavaVersion.VERSION_17.toString()
targetCompatibility = JavaVersion.VERSION_17.toString()
}
withType<KotlinJvmCompile>().configureEach {
withType<KotlinCompile>().configureEach {
compilerOptions {
allWarningsAsErrors.set(
project.providers.environmentVariable("GITHUB_WORKFLOW").isPresent
)
jvmTarget.set(JvmTarget.JVM_17)
allWarningsAsErrors.set(true)
freeCompilerArgs.addAll(ADDITIONAL_COMPILER_ARGS)
languageVersion.set(KotlinVersion.KOTLIN_1_7)
languageVersion.set(KotlinVersion.KOTLIN_1_8)
}
}
withType<Test>().configureEach {
@@ -49,10 +42,13 @@ class KotlinCommonPlugin : Plugin<Project> {
}
}
private companion object {
companion object {
private val ADDITIONAL_COMPILER_ARGS =
listOf(
"-opt-in=kotlin.RequiresOptIn",
)
val JVM_TOOLCHAIN_ACTION =
Action<JavaToolchainSpec> { languageVersion.set(JavaLanguageVersion.of(17)) }
}
}
@@ -8,19 +8,25 @@ package dev.msfjarvis.claw.gradle
import com.android.build.api.dsl.Lint
import com.android.build.gradle.LintPlugin
import dev.msfjarvis.claw.gradle.KotlinCommonPlugin.Companion.JVM_TOOLCHAIN_ACTION
import dev.msfjarvis.claw.gradle.LintConfig.configureLint
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.findByType
import org.gradle.kotlin.dsl.getByType
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper
@Suppress("Unused")
class KotlinJvmPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.pluginManager.apply(KotlinPluginWrapper::class)
project.pluginManager.apply(LintPlugin::class)
project.pluginManager.run {
apply(KotlinPluginWrapper::class)
apply(LintPlugin::class)
apply(KotlinCommonPlugin::class)
}
project.extensions.findByType<Lint>()?.configureLint(project, isJVM = true)
project.pluginManager.apply(KotlinCommonPlugin::class)
project.extensions.getByType<KotlinProjectExtension>().jvmToolchain(JVM_TOOLCHAIN_ACTION)
}
}
@@ -37,10 +37,14 @@ class SpotlessPlugin : Plugin<Project> {
}
format("xml") {
target("**/*.xml")
targetExclude("**/build/", ".idea/")
targetExclude("**/build/", ".idea/", "/spotless/", "**/lint-baseline.xml")
trimTrailingWhitespace()
indentWithSpaces()
endWithNewline()
licenseHeaderFile(
project.file("spotless/license.xml"),
"<manifest|<resources|<full-backup-content|<data-extraction-rules|<adaptive-icon"
)
}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 8.0.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.1)" variant="all" version="8.0.1">
<issues format="6" by="lint 8.2.0-alpha14" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha14)" variant="all" version="8.2.0-alpha14">
</issues>
+6 -1
View File
@@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
~ Copyright © 2021-2023 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
@@ -7,7 +7,6 @@
package dev.msfjarvis.claw.common.comments
import android.text.format.DateUtils
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -30,7 +29,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
@@ -61,7 +59,7 @@ internal fun CommentsHeader(
val uriHandler = LocalUriHandler.current
val linkMetadata by
produceState(
initialValue = LinkMetadata(postDetails.url, null, null),
initialValue = LinkMetadata(postDetails.url, null),
) {
runCatching { postActions.getLinkMetadata(postDetails.url) }
.onSuccess { metadata -> value = metadata }
@@ -116,7 +114,7 @@ private fun PostLink(
Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
NetworkImage(
url = linkMetadata.faviconUrl,
placeholder = rememberVectorPainter(Icons.Filled.Public),
placeholder = Icons.Filled.Public,
contentDescription = "",
modifier = Modifier.size(24.dp),
)
@@ -170,16 +168,11 @@ internal fun CommentEntry(
modifier =
Modifier.clickable { uriHandler.openUri("https://lobste.rs/u/${comment.user.username}") },
)
AnimatedContent(
label = "comment_body",
targetState = commentNode.isExpanded,
) { expandedState ->
if (expandedState) {
ThemedRichText(
text = htmlConverter.convertHTMLToMarkdown(comment.comment),
modifier = Modifier.padding(top = 8.dp)
)
}
if (commentNode.isExpanded) {
ThemedRichText(
text = htmlConverter.convertHTMLToMarkdown(comment.comment),
modifier = Modifier.padding(top = 8.dp)
)
}
}
}
@@ -7,7 +7,7 @@
package dev.msfjarvis.claw.common.comments
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Divider
import androidx.compose.material3.HorizontalDivider
import dev.msfjarvis.claw.database.local.PostComments
import dev.msfjarvis.claw.model.Comment
@@ -95,7 +95,7 @@ private fun LazyListScope.node(
htmlConverter = htmlConverter,
toggleExpanded = toggleExpanded,
)
Divider()
HorizontalDivider()
}
if (node.children.isNotEmpty()) {
nodes(
@@ -1,5 +1,5 @@
/*
* Copyright © 2021-2022 Harsh Shandilya.
* Copyright © 2023 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.
@@ -28,13 +28,13 @@ import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Comment
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.outlined.Comment
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -46,7 +46,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
@@ -98,7 +98,7 @@ fun LobstersCard(
postActions.toggleSave(post)
},
)
Divider(modifier = Modifier.width(48.dp))
HorizontalDivider(modifier = Modifier.width(48.dp))
CommentsButton(
commentCount = post.commentCount,
modifier =
@@ -154,7 +154,7 @@ internal fun Submitter(
) {
NetworkImage(
url = avatarUrl,
placeholder = rememberVectorPainter(Icons.Filled.AccountCircle),
placeholder = Icons.Filled.AccountCircle,
contentDescription = contentDescription,
modifier = Modifier.requiredSize(24.dp).clip(CircleShape),
)
@@ -171,13 +171,10 @@ private fun SaveButton(
Crossfade(targetState = isSaved, label = "save-button") { saved ->
Box(modifier = modifier.padding(12.dp)) {
Icon(
painter =
rememberVectorPainter(
if (saved) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder
),
imageVector = if (saved) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
tint = MaterialTheme.colorScheme.secondary,
contentDescription = if (saved) "Remove from saved posts" else "Add to saved posts",
modifier = Modifier.align(Alignment.Center),
modifier = Modifier.align(Alignment.Center).testTag("save_button"),
)
}
}
@@ -207,10 +204,10 @@ private fun CommentsButton(
},
) {
Icon(
painter = rememberVectorPainter(Icons.Outlined.Comment),
imageVector = Icons.Filled.Comment,
tint = MaterialTheme.colorScheme.secondary,
contentDescription = "Open comments",
modifier = Modifier.align(Alignment.Center),
modifier = Modifier.align(Alignment.Center).testTag("comments_button"),
)
}
}
@@ -292,7 +289,7 @@ fun LobstersCardPreview() {
}
override suspend fun getLinkMetadata(url: String): LinkMetadata {
return LinkMetadata("", "", "")
return LinkMetadata("", "")
}
},
)
@@ -1,5 +1,5 @@
/*
* Copyright © 2021-2022 Harsh Shandilya.
* Copyright © 2021-2023 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.
@@ -1,5 +1,5 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Copyright © 2023 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.
@@ -1,5 +1,5 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Copyright © 2022-2023 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.
@@ -14,6 +14,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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
@@ -25,6 +26,7 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import dev.msfjarvis.claw.common.theme.LobstersTheme
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
import io.github.aakira.napier.Napier
@Composable
fun NetworkError(
@@ -32,6 +34,7 @@ fun NetworkError(
error: Throwable,
modifier: Modifier = Modifier,
) {
LaunchedEffect(true) { Napier.e(error, "NetworkError") { "Failed to load posts" } }
var showDialog by remember { mutableStateOf(false) }
Column(verticalArrangement = Arrangement.spacedBy(4.dp), modifier = modifier) {
Text(
@@ -1,5 +1,5 @@
/*
* Copyright © 2021-2022 Harsh Shandilya.
* Copyright © 2021-2023 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.
@@ -8,21 +8,23 @@ package dev.msfjarvis.claw.common.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import coil.compose.AsyncImage
@Composable
internal fun NetworkImage(
url: String?,
placeholder: Painter,
placeholder: ImageVector,
contentDescription: String,
modifier: Modifier = Modifier,
) {
val painter = rememberVectorPainter(placeholder)
AsyncImage(
model = url,
placeholder = placeholder,
error = placeholder,
fallback = placeholder,
placeholder = painter,
error = painter,
fallback = painter,
contentDescription = contentDescription,
modifier = modifier,
)
@@ -0,0 +1,80 @@
/*
* Copyright © 2023 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.common.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
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.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import dev.msfjarvis.claw.common.theme.LobstersTheme
@Composable
fun SearchBar(
value: String,
onValueChange: (String) -> Unit,
onSearch: (String) -> Unit,
modifier: Modifier = Modifier,
) {
TextField(
value = value,
onValueChange = onValueChange,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
textStyle = MaterialTheme.typography.bodyLarge,
placeholder = { Text(text = "Search") },
trailingIcon = {
IconButton(onClick = { onSearch(value) }) {
Icon(imageVector = Icons.Outlined.Search, contentDescription = "Search")
}
},
keyboardActions = KeyboardActions(onSearch = { onSearch(value) }),
keyboardOptions =
KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Search,
),
singleLine = true,
modifier = modifier.focusable(),
)
}
@Preview
@Composable
fun SearchBarPreview() {
LobstersTheme {
Box(Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.background).padding(8.dp)) {
var value by remember { mutableStateOf("") }
SearchBar(
value = value,
onValueChange = { value = it },
onSearch = {},
modifier = Modifier.align(Alignment.TopCenter)
)
}
}
}
@@ -24,7 +24,6 @@ import androidx.compose.runtime.produceState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.unit.dp
import dev.msfjarvis.claw.common.NetworkState
import dev.msfjarvis.claw.common.NetworkState.Error
@@ -92,7 +91,7 @@ private fun UserProfileInternal(
) {
NetworkImage(
url = "https://lobste.rs/${user.avatarUrl}",
placeholder = rememberVectorPainter(Icons.Filled.AccountCircle),
placeholder = Icons.Filled.AccountCircle,
contentDescription = "Avatar of ${user.username}",
modifier = Modifier.requiredSize(120.dp).clip(CircleShape),
)
+4 -1
View File
@@ -16,13 +16,16 @@ android { namespace = "dev.msfjarvis.claw.core" }
anvil { generateDaggerFactories.set(true) }
dependencies {
api(libs.javax.inject)
api(libs.kotlinx.coroutines.core)
api(libs.kotlinx.serialization.json)
api(libs.okhttp.loggingInterceptor)
implementation(platform(libs.okhttp.bom))
implementation(platform(libs.sentry.bom))
implementation(libs.dagger)
implementation(libs.javax.inject)
implementation(libs.napier)
implementation(libs.okhttp.core)
implementation(libs.retrofit.kotlinxSerializationConverter)
implementation(libs.sentry.okhttp)
}
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 8.0.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.1)" variant="all" version="8.0.1">
<issues format="6" by="lint 8.2.0-alpha14" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha14)" variant="all" version="8.2.0-alpha14">
</issues>
@@ -6,7 +6,7 @@
*/
@file:Suppress("InjectDispatcher") // False-positive
package dev.msfjarvis.claw.util.coroutines
package dev.msfjarvis.claw.core.coroutines
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -23,8 +23,6 @@ interface DispatcherProvider {
fun io(): CoroutineDispatcher = Dispatchers.IO
fun unconfined(): CoroutineDispatcher = Dispatchers.Unconfined
fun database(): CoroutineDispatcher = Dispatchers.IO.limitedParallelism(1)
}
@@ -1,20 +1,29 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Copyright © 2022-2023 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.injection
package dev.msfjarvis.claw.core.injection
import com.deliveryhero.whetstone.app.ApplicationScope
import com.squareup.anvil.annotations.ContributesTo
import dagger.Binds
import dagger.Module
import dagger.Provides
import dev.msfjarvis.claw.util.coroutines.DefaultDispatcherProvider
import dev.msfjarvis.claw.util.coroutines.DispatcherProvider
import dev.msfjarvis.claw.core.coroutines.DefaultDispatcherProvider
import dev.msfjarvis.claw.core.coroutines.DispatcherProvider
import javax.inject.Qualifier
import kotlinx.coroutines.CoroutineDispatcher
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class DatabaseDispatcher
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class MainDispatcher
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class IODispatcher
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class DefaultDispatcher
@Module
@ContributesTo(ApplicationScope::class)
interface CoroutineDispatcherModule {
@@ -36,5 +45,10 @@ interface CoroutineDispatcherModule {
fun provideMainDispatcher(dispatcherProvider: DispatcherProvider): CoroutineDispatcher {
return dispatcherProvider.main()
}
@[Provides DefaultDispatcher]
fun provideDefaultDispatcher(dispatcherProvider: DispatcherProvider): CoroutineDispatcher {
return dispatcherProvider.default()
}
}
}
@@ -11,7 +11,8 @@ import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFact
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet
import dagger.multibindings.IntKey
import dagger.multibindings.IntoMap
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy
@@ -21,10 +22,11 @@ import retrofit2.Converter
@Module
@ContributesTo(ApplicationScope::class)
@OptIn(ExperimentalSerializationApi::class)
object RetrofitModule {
object JsonModule {
@Provides
@IntoSet
@IntKey(1)
@IntoMap
fun provideJsonConverterFactory(json: Json): Converter.Factory {
val contentType = "application/json".toMediaType()
return json.asConverterFactory(contentType)
@@ -18,6 +18,7 @@ import dagger.multibindings.IntoSet
import dev.msfjarvis.claw.core.network.DelegatingSocketFactory
import dev.msfjarvis.claw.core.network.NapierLogger
import dev.msfjarvis.claw.core.network.UserAgentInterceptor
import io.sentry.android.okhttp.SentryOkHttpInterceptor
import java.net.Socket
import javax.net.SocketFactory
import okhttp3.Cache
@@ -87,5 +88,11 @@ interface OkHttpModule {
fun provideHttpLoggingInterceptor(logger: HttpLoggingInterceptor.Logger): Interceptor {
return HttpLoggingInterceptor(logger).setLevel(HttpLoggingInterceptor.Level.BASIC)
}
@Provides
@IntoSet
fun provideSentryInterceptor(): Interceptor {
return SentryOkHttpInterceptor()
}
}
}
-12
View File
@@ -1,12 +0,0 @@
/*
* Copyright © 2022-2023 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.
*/
plugins { id("dev.msfjarvis.claw.kotlin-jvm") }
dependencies {
api(libs.javax.inject)
api(libs.kotlinx.coroutines.core)
}
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 8.0.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.1)" variant="all" version="8.0.1">
</issues>
+3
View File
@@ -9,6 +9,7 @@ import dev.msfjarvis.claw.gradle.addTestDependencies
plugins {
id("dev.msfjarvis.claw.android-library")
id("dev.msfjarvis.claw.kotlin-android")
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.anvil)
alias(libs.plugins.sqldelight)
alias(libs.plugins.whetstone)
@@ -33,7 +34,9 @@ dependencies {
implementation(libs.sqldelight.androidDriver)
implementation(libs.sqldelight.primitiveAdapters)
implementation(libs.sqlite.android)
implementation(libs.kotlinx.serialization.core)
testImplementation(libs.sqldelight.jvmDriver)
testImplementation(libs.kotlinx.serialization.json)
addTestDependencies(project)
}
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 8.0.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.1)" variant="all" version="8.0.1">
<issues format="6" by="lint 8.2.0-alpha14" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha14)" variant="all" version="8.2.0-alpha14">
</issues>
+6
View File
@@ -1 +1,7 @@
<!--
~ Copyright © 2021-2023 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.
-->
<manifest />
@@ -0,0 +1,97 @@
/*
* Copyright © 2023 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.database
import dev.msfjarvis.claw.database.local.SavedPost
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.descriptors.element
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.encoding.encodeStructure
@OptIn(ExperimentalSerializationApi::class)
object SavedPostSerializer : KSerializer<SavedPost> {
private val delegateSerializer = ListSerializer(String.serializer())
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("SavedPost") {
element<String>("shortId")
element<String>("title")
element<String>("url")
element<String>("createdAt")
element<Int?>("commentCount", isOptional = true)
element<String>("commentsUrl")
element<String>("submitterName")
element<String>("submitterAvatarUrl")
element<List<String>>("tags")
element<String>("description")
}
override fun deserialize(decoder: Decoder): SavedPost {
return decoder.decodeStructure(descriptor) {
var shortId = ""
var title = ""
var url = ""
var createdAt = ""
var commentCount: Int? = null
var commentsUrl = ""
var submitterName = ""
var submitterAvatarUrl = ""
var tags = emptyList<String>()
var description = ""
while (true) {
when (val index = decodeElementIndex(descriptor)) {
0 -> shortId = decodeStringElement(descriptor, 0)
1 -> title = decodeStringElement(descriptor, 1)
2 -> url = decodeStringElement(descriptor, 2)
3 -> createdAt = decodeStringElement(descriptor, 3)
4 -> commentCount = decodeNullableSerializableElement(descriptor, 4, Int.serializer())
5 -> commentsUrl = decodeStringElement(descriptor, 5)
6 -> submitterName = decodeStringElement(descriptor, 6)
7 -> submitterAvatarUrl = decodeStringElement(descriptor, 7)
8 -> tags = decodeSerializableElement(descriptor, 8, delegateSerializer)
9 -> description = decodeStringElement(descriptor, 9)
CompositeDecoder.DECODE_DONE -> break
else -> error("Unexpected index: $index")
}
}
SavedPost(
shortId = shortId,
title = title,
url = url,
createdAt = createdAt,
commentCount = commentCount,
commentsUrl = commentsUrl,
submitterName = submitterName,
submitterAvatarUrl = submitterAvatarUrl,
tags = tags,
description = description,
)
}
}
override fun serialize(encoder: Encoder, value: SavedPost) {
encoder.encodeStructure(descriptor) {
encodeStringElement(descriptor, 0, value.shortId)
encodeStringElement(descriptor, 1, value.title)
encodeStringElement(descriptor, 2, value.url)
encodeStringElement(descriptor, 3, value.createdAt)
encodeNullableSerializableElement(descriptor, 4, Int.serializer(), value.commentCount)
encodeStringElement(descriptor, 5, value.commentsUrl)
encodeStringElement(descriptor, 6, value.submitterName)
encodeStringElement(descriptor, 7, value.submitterAvatarUrl)
encodeSerializableElement(descriptor, 8, delegateSerializer, value.tags)
encodeStringElement(descriptor, 9, value.description)
}
}
}
@@ -0,0 +1,61 @@
/*
* Copyright © 2023 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.database
import com.google.common.truth.Truth.assertThat
import dev.msfjarvis.claw.database.local.SavedPost
import java.io.File
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy
import org.junit.jupiter.api.Test
@OptIn(ExperimentalSerializationApi::class)
class SavedPostSerializerTest {
private val json = Json {
ignoreUnknownKeys = true
namingStrategy = JsonNamingStrategy.SnakeCase
}
private val text = getJson()
@Test
fun serialize() {
val encoded = json.encodeToString(SavedPostSerializer, SAVED_POST)
assertThat(encoded).isNotEmpty()
assertThat(encoded).isEqualTo(text)
}
@Test
fun deserialize() {
val decoded = json.decodeFromString(SavedPostSerializer, text)
assertThat(decoded).isEqualTo(SAVED_POST)
}
private fun getJson(): String {
// Load the JSON response
val uri = javaClass.classLoader!!.getResource("saved_post.json")
val file = File(uri.path)
return String(file.readBytes())
}
private companion object {
private val SAVED_POST =
SavedPost(
title = "Fun Format Friday: You now have a super computer. What next?",
shortId = "nbigsf",
url = "",
createdAt = "2023-05-04T23:43:50.000-05:00",
commentCount = 13,
commentsUrl = "https://lobste.rs/s/nbigsf/fun_format_friday_you_now_have_super",
submitterName = "LenFalken",
submitterAvatarUrl = "/avatars/LenFalken-100.png",
tags = listOf("ask", "programming"),
description =
"<p>You suddenly have in your possession a super computer. What comes next? What projects are suddenly possible for you? What performance tests can you now explore?</p>\n",
)
}
}
@@ -0,0 +1 @@
{"short_id":"nbigsf","title":"Fun Format Friday: You now have a super computer. What next?","url":"","created_at":"2023-05-04T23:43:50.000-05:00","comment_count":13,"comments_url":"https://lobste.rs/s/nbigsf/fun_format_friday_you_now_have_super","submitter_name":"LenFalken","submitter_avatar_url":"/avatars/LenFalken-100.png","tags":["ask","programming"],"description":"<p>You suddenly have in your possession a super computer. What comes next? What projects are suddenly possible for you? What performance tests can you now explore?</p>\n"}

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