Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e29d61799 | ||
|
|
1ee9d92b40 | ||
|
|
9c07169b1b | ||
|
|
81f4d35f93 | ||
|
|
361082ded4 | ||
|
|
4fe43dbdc9 | ||
|
|
cef45bd3e5 | ||
|
|
c45dd76fc4 | ||
|
|
9a1f42764e | ||
|
|
e62a0eaa84 | ||
|
|
6296afe51a | ||
|
|
cf390de486 | ||
|
|
99a45ab5df | ||
|
|
5257d0876a | ||
|
|
bb3cef2f5b | ||
|
|
a791588423 | ||
|
|
535624bf22 | ||
|
|
b5a7225bd8 | ||
|
|
8c0734a19a | ||
|
|
71afc634b9 | ||
|
|
fbaede8215 | ||
|
|
948aea7ea8 | ||
|
|
6acab19b28 | ||
|
|
2555140ca1 | ||
|
|
4a5980fd62 | ||
|
|
215afd208e | ||
|
|
806ef65e08 | ||
|
|
35e143a458 | ||
|
|
5b4d1fc48e | ||
|
|
c9098bbd44 | ||
|
|
b7eecd10b4 | ||
|
|
c471fee4ee | ||
|
|
e3ed059fde | ||
|
|
44355de333 | ||
|
|
92dc6d9e4c | ||
|
|
68ca569b9d | ||
|
|
105a33e335 | ||
|
|
7862b4b5c8 | ||
|
|
b12b5c800f | ||
|
|
b56de40b12 | ||
|
|
8243b8998c | ||
|
|
195b50f919 | ||
|
|
2d7903efc1 | ||
|
|
e2ab469ecc | ||
|
|
4ccf8959d9 | ||
|
|
b73a758501 | ||
|
|
cfaa1c17a0 | ||
|
|
47332c22ec | ||
|
|
ce64454cec | ||
|
|
b3313efbbe | ||
|
|
bc14e58513 | ||
|
|
83eb3c13b2 | ||
|
|
59acbe2a37 | ||
|
|
7e31de67b4 | ||
|
|
37230e03c4 | ||
|
|
fbf937dce2 | ||
|
|
ee9db517f7 | ||
|
|
a4dcdc6f88 | ||
|
|
c9c908d1a5 | ||
|
|
2ce804ed90 | ||
|
|
3b044a8d23 | ||
|
|
bdca0799db | ||
|
|
ca37557297 | ||
|
|
9c0c270f5c | ||
|
|
8a9cc844b9 | ||
|
|
452ba7dbf9 | ||
|
|
9edfc23ee3 | ||
|
|
3057f5fede | ||
|
|
157748bc61 | ||
|
|
68a5de84f7 | ||
|
|
b69fa43d29 | ||
|
|
6f76a15ded | ||
|
|
9fe6e34015 | ||
|
|
847858d852 | ||
|
|
8735f7f8e8 | ||
|
|
5061843779 | ||
|
|
b8480f1994 | ||
|
|
60a7b815ea | ||
|
|
bff56aa52b | ||
|
|
3137a1bb42 | ||
|
|
127a69249e | ||
|
|
9361c7ef47 | ||
|
|
e8a9594e4c | ||
|
|
0a8ca5719d | ||
|
|
d87e2d1b9c | ||
|
|
774c6cea9b | ||
|
|
5753f65737 | ||
|
|
17965097bc | ||
|
|
f1d06116bc | ||
|
|
319e30c174 | ||
|
|
8e8b710603 | ||
|
|
a142563ad8 | ||
|
|
9bd0df2aa8 | ||
|
|
54ec5a4c8d | ||
|
|
c79b7114fb | ||
|
|
a6fb1fd788 | ||
|
|
5a268fa5bd | ||
|
|
431dabbc09 | ||
|
|
d7f71878f6 | ||
|
|
196dfd056b | ||
|
|
6d3e749ad6 | ||
|
|
e808e77ab8 | ||
|
|
d8b2ff6c8d | ||
|
|
55635f1101 | ||
|
|
a70801c635 | ||
|
|
3a60e8f2a1 | ||
|
|
80183fd248 | ||
|
|
7cbb8e6237 | ||
|
|
32cba40e91 | ||
|
|
a8cef93096 | ||
|
|
b3ea9c6faf | ||
|
|
215b259288 | ||
|
|
ab1f276a08 | ||
|
|
ca03561df0 | ||
|
|
687cb32f7c | ||
|
|
12e57298af | ||
|
|
5abb5f109b | ||
|
|
c8eaf8d769 | ||
|
|
24a4d20a03 | ||
|
|
57f95d790d | ||
|
|
09a6e3e32b | ||
|
|
491be53dc3 | ||
|
|
d1e6260136 | ||
|
|
c71b33a07f | ||
|
|
7d2094866b | ||
|
|
a3e9957e49 | ||
|
|
cec2d70fda | ||
|
|
ec4066c472 | ||
|
|
433727a386 | ||
|
|
1238012e0b | ||
|
|
96e53f104d | ||
|
|
e1fed9ccc8 | ||
|
|
ced43b00be | ||
|
|
0ca18d9216 | ||
|
|
1aa2dee779 | ||
|
|
7f4a382f40 | ||
|
|
ed5b6c21da | ||
|
|
d0bf2a4fb2 | ||
|
|
2b9680d3d8 | ||
|
|
d19c1e11be | ||
|
|
809cec61cd | ||
|
|
13feb83063 | ||
|
|
40b7884520 | ||
|
|
75b05b83c1 | ||
|
|
9afe913548 | ||
|
|
44d589e729 | ||
|
|
ace1a3c94c | ||
|
|
31c94dc8cc | ||
|
|
493ab8712c | ||
|
|
1fff9d9aa2 | ||
|
|
9357362ef6 | ||
|
|
7b07a4676b | ||
|
|
e272427031 | ||
|
|
7882b86b39 | ||
|
|
afb00c64fc | ||
|
|
3a7727e563 | ||
|
|
d28c9c2e61 | ||
|
|
99aff514f2 | ||
|
|
0e8f1a51bf | ||
|
|
52829fedb2 | ||
|
|
4e479cf0f1 | ||
|
|
2020f43dd6 | ||
|
|
659eaf9789 | ||
|
|
a410267f99 | ||
|
|
6f248c529e | ||
|
|
142fafba23 | ||
|
|
7119cda579 | ||
|
|
e3c1213be6 | ||
|
|
5487615436 | ||
|
|
619b771aff | ||
|
|
a12a1f92a5 | ||
|
|
dc70293bd4 | ||
|
|
f9ecdebc63 | ||
|
|
4a96af17f4 | ||
|
|
e9b69c5cad | ||
|
|
d3f9bb4c00 | ||
|
|
feae5d2630 | ||
|
|
df7868de2d | ||
|
|
8a66361a98 | ||
|
|
3099ffe201 | ||
|
|
0fc9c28354 | ||
|
|
6ba7dec98d | ||
|
|
332a84c6ec | ||
|
|
a19a5e926c | ||
|
|
bd952b74d8 | ||
|
|
3e374df3aa | ||
|
|
6b67819007 | ||
|
|
83af44b0e6 | ||
|
|
bec12d8e09 | ||
|
|
b34fc8c721 | ||
|
|
dc51fa7802 | ||
|
|
42a667462f | ||
|
|
c76661762f | ||
|
|
9dab223da8 | ||
|
|
1ef6b2bf91 | ||
|
|
f2f3319ac4 | ||
|
|
7fd9cef051 | ||
|
|
4d4cccbba9 | ||
|
|
aff0e366fe | ||
|
|
f00f185f6d | ||
|
|
a08ffb903c | ||
|
|
4eaf25e189 | ||
|
|
109176768c | ||
|
|
cf0ef021d8 | ||
|
|
59ba98174c | ||
|
|
afe78aa82f | ||
|
|
0a70924cab | ||
|
|
ae362c2e03 | ||
|
|
83ca0e6167 | ||
|
|
71977c5b2c | ||
|
|
047d6badb0 | ||
|
|
5c0b5bb4f7 | ||
|
|
7762733da2 | ||
|
|
9f705ca02a | ||
|
|
6c5b54587e | ||
|
|
e318ea3a35 | ||
|
|
5ba0c9a544 | ||
|
|
85fc5cac21 | ||
|
|
86a40d2e84 | ||
|
|
557407347b | ||
|
|
b0467aa36c | ||
|
|
d29b4e2ee2 | ||
|
|
64fb1235d5 | ||
|
|
d46993aaf6 | ||
|
|
310db93ff0 | ||
|
|
f07a5682a1 | ||
|
|
bcde4b9719 | ||
|
|
e2e2f4f574 | ||
|
|
a42139ae1e | ||
|
|
5d0e56f115 | ||
|
|
fa31e300e9 | ||
|
|
f54d790da4 | ||
|
|
96c8a0450e | ||
|
|
886cd65a32 | ||
|
|
8c8dd93136 | ||
|
|
9110244236 | ||
|
|
56f0ceddb6 | ||
|
|
c07752df4a | ||
|
|
3f18160dd0 | ||
|
|
0c3dc00137 | ||
|
|
4fb66975fb | ||
|
|
dc584a300d | ||
|
|
30121d6d88 | ||
|
|
2a97576e37 | ||
|
|
32a59d0104 | ||
|
|
a161769cbd | ||
|
|
9a6d7d267a | ||
|
|
9298fa9ea0 | ||
|
|
f0bd943607 | ||
|
|
30474377fc | ||
|
|
6d04230dc6 | ||
|
|
19f408b6cd | ||
|
|
ad528792fa | ||
|
|
78e34a0042 | ||
|
|
83ba17dea3 | ||
|
|
fcca85c51a | ||
|
|
17c7313f41 | ||
|
|
97126da808 | ||
|
|
fdf891c8eb | ||
|
|
af7c54203f | ||
|
|
334cf11b82 | ||
|
|
a9ba948d12 | ||
|
|
1e29a866a0 | ||
|
|
693713eb8d | ||
|
|
1b6c6af3c4 | ||
|
|
61b0ac83f0 | ||
|
|
bce435e3d2 | ||
|
|
833b3cd927 | ||
|
|
a6a3b2a0e4 | ||
|
|
73e0b4927f | ||
|
|
b68d58c19d | ||
|
|
3bccc8748d | ||
|
|
f115fd8dda | ||
|
|
954ef90d99 | ||
|
|
65f7013e78 | ||
|
|
af799e0165 | ||
|
|
c4fb53e09d | ||
|
|
982497a83f | ||
|
|
165c0eaaed | ||
|
|
2c62515746 | ||
|
|
376c2f138c | ||
|
|
d271dd3733 | ||
|
|
0c36875502 | ||
|
|
b31521affa | ||
|
|
488880ee35 | ||
|
|
f2065d897b | ||
|
|
e83b87be6c | ||
|
|
993b45034e | ||
|
|
9d6d497185 | ||
|
|
719d317263 | ||
|
|
8d78e1910a | ||
|
|
62f643135b | ||
|
|
595a8f7dfe | ||
|
|
1d7be62857 | ||
|
|
f86cdb0e62 | ||
|
|
ffc525a1ad | ||
|
|
de6b13fbfc | ||
|
|
e949ee0bb6 | ||
|
|
9f4caa7f62 | ||
|
|
a334eebe74 | ||
|
|
fd30320ed6 | ||
|
|
c293fced8a | ||
|
|
fa57bc9695 | ||
|
|
1d1c7a36b6 | ||
|
|
682377f6c5 | ||
|
|
a8d637e589 | ||
|
|
9e2564f352 | ||
|
|
eb17c20be8 | ||
|
|
e878699e2a | ||
|
|
9dc0b5cd13 | ||
|
|
875453cce5 | ||
|
|
82a1ba9ac3 | ||
|
|
ff4a09e41b | ||
|
|
6b670ad3e5 | ||
|
|
2bf7ccc952 | ||
|
|
d25476be11 | ||
|
|
6d7e2bc7aa | ||
|
|
b1148679e3 | ||
|
|
6a9cd40eaf | ||
|
|
bd16709a81 | ||
|
|
cbd7f2fca4 | ||
|
|
89b821ebf1 | ||
|
|
3fc0f891d4 | ||
|
|
74a7835a53 | ||
|
|
051b7ab2bb | ||
|
|
38f71460f0 | ||
|
|
1ab1520c60 | ||
|
|
0e5caf591a | ||
|
|
ad2582a28e | ||
|
|
c9d89f493d | ||
|
|
8669e3f780 | ||
|
|
e7d5676cb5 | ||
|
|
800bffbf24 | ||
|
|
17c02c2a25 | ||
|
|
89db5e3b3f | ||
|
|
1524eb23d4 | ||
|
|
d7dd2eb611 | ||
|
|
a3e150fdd9 | ||
|
|
fa2868edd6 | ||
|
|
e3e62b62ad | ||
|
|
a071a65ecc | ||
|
|
65e033cc80 |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 313 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 261 KiB After Width: | Height: | Size: 311 KiB |
+11
-2
@@ -7,6 +7,11 @@
|
||||
"github>msfjarvis/shared-workflows//renovate/automerge"
|
||||
],
|
||||
"branchConcurrentLimit": 15,
|
||||
"github-actions": {
|
||||
"fileMatch": [
|
||||
".github/reusable-workflows/.+\\.ya?ml$",
|
||||
],
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
matchDatasources: [
|
||||
@@ -33,8 +38,6 @@
|
||||
"matchPackagePatterns": [
|
||||
"^androidx.*",
|
||||
],
|
||||
"matchPackageNames": [
|
||||
],
|
||||
"groupName": "AndroidX"
|
||||
},
|
||||
{
|
||||
@@ -50,6 +53,12 @@
|
||||
],
|
||||
"allowedVersions": "!/.*-1-8$/",
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"^androidx.glance",
|
||||
],
|
||||
"allowedVersions": "<=1.0.0",
|
||||
},
|
||||
],
|
||||
"regexManagers": [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
name: 'Setup Gradle'
|
||||
description: 'Checks out the repository and sets up Java and Gradle'
|
||||
inputs:
|
||||
token:
|
||||
description: 'token input for actions/checkout'
|
||||
required: false
|
||||
default: ${{ github.token }}
|
||||
fetch-depth:
|
||||
description: 'fetch-depth input for actions/checkout'
|
||||
required: false
|
||||
default: 1
|
||||
cache-read-only:
|
||||
description: 'cache-read-only input for gradle/actions/setup-gradle'
|
||||
required: false
|
||||
default: ${{ github.event.repository != null && github.ref_name != github.event.repository.default_branch }}
|
||||
dependency-graph:
|
||||
description: 'dependency-graph input for gradle/actions/setup-gradle'
|
||||
required: false
|
||||
default: 'disabled'
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
fetch-depth: ${{ inputs.fetch-depth }}
|
||||
token: ${{ inputs.token }}
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 22
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@db19848a5fa7950289d3668fb053140cf3028d43 # v3
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: ${{ inputs.cache-read-only }}
|
||||
add-job-summary: always
|
||||
dependency-graph: ${{ inputs.dependency-graph }}
|
||||
validate-wrappers: true
|
||||
+17
-72
@@ -16,97 +16,42 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Setup build environment
|
||||
uses: msfjarvis/compose-lobsters/.github/reusable-workflows/setup-gradle@main
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Run unit tests
|
||||
uses: gradle/gradle-build-action@982da8e78c05368c70dac0351bb82647a9e9a5d2 # v2.11.1
|
||||
with:
|
||||
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: Check changelog format
|
||||
uses: mindsers/changelog-reader-action@32aa5b4c155d76c94e4ec883a223c947b2f02656 # v2.2.3
|
||||
with:
|
||||
path: ./CHANGELOG.md
|
||||
validation_level: error
|
||||
|
||||
- name: Run unit tests
|
||||
run: ./gradlew --no-configuration-cache --stacktrace check -PslimTests
|
||||
|
||||
- name: (Fail-only) Upload test report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: Test report
|
||||
path: '**/build/reports/tests/**'
|
||||
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: macos-14
|
||||
if: "github.event_name == 'push' && github.event.ref != 'refs/heads/main'"
|
||||
needs:
|
||||
- check
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@4cd9eb2d23752464a87e00499c30d256a59a01b4 # v4
|
||||
uses: actions/dependency-review-action@0c155c5e8556a497adf53f2c18edabf945ed8e70 # v4
|
||||
with:
|
||||
base-ref: refs/heads/main
|
||||
head-ref: ${{ github.ref }}
|
||||
|
||||
validate-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Check changelog format
|
||||
uses: mindsers/changelog-reader-action@b97ce03a10d9bdbb07beb491c76a5a01d78cd3ef # v2.2.2
|
||||
with:
|
||||
path: ./CHANGELOG.md
|
||||
validation_level: error
|
||||
|
||||
deploy-release-snapshot:
|
||||
runs-on: ubuntu-latest
|
||||
if: "github.event_name == 'push' && github.event.ref == 'refs/heads/main'"
|
||||
needs:
|
||||
- check
|
||||
- validate-changelog
|
||||
- dependency-review
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Decrypt secrets
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
./scripts/signing-setup.sh "$AGE_SECRET_KEY"
|
||||
env:
|
||||
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
||||
|
||||
- name: Build release app
|
||||
uses: gradle/gradle-build-action@982da8e78c05368c70dac0351bb82647a9e9a5d2 # v2.11.1
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
with:
|
||||
arguments: --no-configuration-cache --stacktrace collectReleaseApks
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
- name: Clean secrets
|
||||
run: scripts/signing-cleanup.sh
|
||||
|
||||
- name: Deploy snapshot
|
||||
run: scripts/deploy-snapshot.sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -20,25 +20,16 @@ permissions:
|
||||
jobs:
|
||||
codeql:
|
||||
name: CodeQL
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
- name: Setup build environment
|
||||
uses: msfjarvis/compose-lobsters/.github/reusable-workflows/setup-gradle@main
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@982da8e78c05368c70dac0351bb82647a9e9a5d2 # v2.11.1
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
fetch-depth: 0
|
||||
cache-read-only: true
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1
|
||||
uses: github/codeql-action/init@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
|
||||
with:
|
||||
languages: java
|
||||
tools: latest
|
||||
@@ -47,10 +38,10 @@ jobs:
|
||||
- name: Build project
|
||||
shell: bash
|
||||
run: |
|
||||
./gradlew assembleDebug assembleRelease
|
||||
./gradlew assembleDebug assembleInternal
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1
|
||||
uses: github/codeql-action/analyze@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
|
||||
with:
|
||||
category: "/language:java"
|
||||
mobsfscan:
|
||||
@@ -58,31 +49,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@982da8e78c05368c70dac0351bb82647a9e9a5d2 # v2.11.1
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup python
|
||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5
|
||||
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Run mobsfscan
|
||||
uses: MobSF/mobsfscan@e29e85c36fa1aa950d7f1b1064345d94a70b2a28
|
||||
uses: MobSF/mobsfscan@849b749e7f3244c7b4f418ff858a9fa4e1406115
|
||||
with:
|
||||
args: . --sarif --output results.sarif || true
|
||||
|
||||
- name: Upload mobsfscan report
|
||||
uses: github/codeql-action/upload-sarif@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1
|
||||
uses: github/codeql-action/upload-sarif@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
inAppUpdatePriority:
|
||||
description: 'In app update priority (0-5)'
|
||||
type: number
|
||||
default: "0"
|
||||
default: 0
|
||||
required: false
|
||||
|
||||
concurrency:
|
||||
@@ -13,19 +13,14 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
publish-google-play-release:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Setup build environment
|
||||
uses: msfjarvis/compose-lobsters/.github/reusable-workflows/setup-gradle@main
|
||||
with:
|
||||
token: ${{ secrets.POST_RELEASE_GH_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
- name: Set up Git author
|
||||
shell: bash
|
||||
@@ -33,12 +28,6 @@ jobs:
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email noreply@github.com
|
||||
|
||||
- name: Setup Gradle caching
|
||||
uses: gradle/gradle-build-action@982da8e78c05368c70dac0351bb82647a9e9a5d2 # v2.11.1
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
- name: Decrypt secrets
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
@@ -54,7 +43,7 @@ jobs:
|
||||
echo VERSION="${VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Update changelog
|
||||
uses: msfjarvis/keep-a-changelog-new-release@bf43ae42f4e42f137572950af426efcf6e777207 # 2.1.0
|
||||
uses: msfjarvis/keep-a-changelog-new-release@e60399f488b25149c0963a4e22fff928a9277ce7 # v2.2.0
|
||||
with:
|
||||
tag: v${{ env.VERSION }}
|
||||
version: ${{ env.VERSION }}
|
||||
@@ -70,14 +59,14 @@ jobs:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
run: |
|
||||
./gradlew --no-configuration-cache collectReleaseApks collectReleaseBundle -PsentryUploadMappings
|
||||
./gradlew --no-configuration-cache assembleRelease collectReleaseApks collectReleaseBundle -PenableSentry
|
||||
|
||||
- name: Clean secrets
|
||||
run: scripts/signing-cleanup.sh
|
||||
|
||||
- name: Get changelog entry
|
||||
id: changelog_reader
|
||||
uses: mindsers/changelog-reader-action@b97ce03a10d9bdbb07beb491c76a5a01d78cd3ef # v2.2.2
|
||||
uses: mindsers/changelog-reader-action@32aa5b4c155d76c94e4ec883a223c947b2f02656 # v2.2.3
|
||||
with:
|
||||
version: ${{ env.VERSION }}
|
||||
path: ./CHANGELOG.md
|
||||
@@ -91,7 +80,7 @@ jobs:
|
||||
printf '%s' "${CHANGELOG}" >> distribution/whatsnew/whatsnew-en-GB
|
||||
|
||||
- name: Publish bundle to Google Play
|
||||
uses: r0adkll/upload-google-play@9cf8fd30c9e616ec761db3b9abb2b0f15842ce58 # v1.1.2
|
||||
uses: r0adkll/upload-google-play@935ef9c68bb393a8e6116b1575626a7f5be3a7fb # v1.1.3
|
||||
with:
|
||||
mappingFile: android/bundle/mapping.txt
|
||||
packageName: dev.msfjarvis.claw.android
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Android release build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
versionName:
|
||||
description: 'User-facing release version name'
|
||||
required: true
|
||||
default: ""
|
||||
versionCode:
|
||||
description: 'versionCode or build number'
|
||||
required: true
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
signed-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup build environment
|
||||
uses: msfjarvis/compose-lobsters/.github/reusable-workflows/setup-gradle@main
|
||||
with:
|
||||
cache-read-only: true
|
||||
|
||||
- name: Build release app
|
||||
run: |
|
||||
./scripts/setup-age.sh
|
||||
./scripts/signing-setup.sh "$AGE_SECRET_KEY"
|
||||
./gradlew --no-configuration-cache --stacktrace assembleRelease collectReleaseBundle -PenableSentry
|
||||
./scripts/signing-cleanup.sh
|
||||
env:
|
||||
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
ORG_GRADLE_PROJECT_VERSION_NAME: ${{ github.event.inputs.versionName }}
|
||||
ORG_GRADLE_PROJECT_VERSION_CODE: ${{ github.event.inputs.versionCode }}
|
||||
|
||||
- name: Upload app bundle
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: release-aab
|
||||
path: android/build/outputs/bundle/release/android-release.aab
|
||||
@@ -247,3 +247,4 @@ keystore.properties
|
||||
distribution/
|
||||
screenshots/
|
||||
claw-export.json
|
||||
.kotlin/
|
||||
|
||||
+216
-153
@@ -7,421 +7,484 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.48.0] - 2024-06-05
|
||||
|
||||
### Added
|
||||
|
||||
- Swiping a post from left to right now offers a share action
|
||||
|
||||
### Fixed
|
||||
|
||||
- Try to workaround an infrequent crash when scrolling hottest/newest posts
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade to Kotlin 2.0.0
|
||||
- Upgrade to Compose May beta releases
|
||||
|
||||
## [1.47.0] - 2024-05-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Bring back dividers between posts (I regret my earlier choices)
|
||||
- Upgrade to Compose May stable releases
|
||||
|
||||
### Fixed
|
||||
|
||||
- Navigating to user profiles now works when invoked from the search
|
||||
results page
|
||||
- Fix occasional crashes due to the app incorrectly trying to open
|
||||
multiple database connections
|
||||
|
||||
## [1.46.0] - 2024-04-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove dividers between posts
|
||||
- Post titles in lists are now truncated to be a single line
|
||||
|
||||
### Fixed
|
||||
|
||||
- Disable logging of network errors to Sentry
|
||||
- Add potential workaround for navigation-related crashes
|
||||
|
||||
## [1.45.0] - 2024-04-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade to Compose April releases
|
||||
- Story items are now more compact so you can see more items
|
||||
on your screen.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a crash when clicking an item on the bottom navigation bar
|
||||
too quickly
|
||||
- Removed buggy deeplinks
|
||||
- Clicking a username now correctly navigates to the right page in-app
|
||||
|
||||
## [1.44.0] - 2024-03-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a bug in the database code that was triggering crashes for
|
||||
a subset of upgrading users
|
||||
|
||||
## [1.43.0] - 2024-03-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix crash when trying to open a comments-only post from
|
||||
the home screen widget
|
||||
|
||||
## [1.42.0] - 2024-03-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- Adapt to changes in lobste.rs API
|
||||
|
||||
## [1.41.0] - 2024-03-07
|
||||
|
||||
### Fixed
|
||||
|
||||
- Downgrade dependency that was pulling in extraneous storage
|
||||
permissions which the app does not require.
|
||||
|
||||
## [1.40.0] - 2024-03-07
|
||||
|
||||
### Changed
|
||||
|
||||
- Update to Compose March releases
|
||||
- Improve performance of frequently invoked database queries
|
||||
- Refactor UI data model to improve state handling
|
||||
- Make saved posts update in the background more often
|
||||
- Consolidate widget display logic
|
||||
- Rework some theming logic to align with new Material guidelines
|
||||
|
||||
## [1.39.0] - 2024-01-23
|
||||
|
||||
### Changed
|
||||
|
||||
* Add HTML bookmarks as an export format
|
||||
* Rework widget item layout for consistent touch targets
|
||||
* Add attribution for libraries used in the app
|
||||
* Redesign settings screen
|
||||
* Update to Compose January releases
|
||||
- Add HTML bookmarks as an export format
|
||||
- Rework widget item layout for consistent touch targets
|
||||
- Add attribution for libraries used in the app
|
||||
- Redesign settings screen
|
||||
- Update to Compose December releases
|
||||
|
||||
## [1.38.0] - 2023-11-20
|
||||
|
||||
### Changed
|
||||
|
||||
* Add brand new icon by dzuk
|
||||
* Fix a bug that caused the app to fetch the same 20 posts over and over
|
||||
* Significantly improve scrolling performance when there are a lot of saved posts
|
||||
* Move search feature to its own screen
|
||||
* Make top app and bottom system bars use the same color
|
||||
* Reduce unnecessary API calls in search screen
|
||||
* Upgrade to Compose November releases
|
||||
* Enable logging of SQLite queries
|
||||
- Add brand new icon by dzuk
|
||||
- Fix a bug that caused the app to fetch the same 20 posts over and over
|
||||
- Significantly improve scrolling performance when there are a lot of saved posts
|
||||
- Move search feature to its own screen
|
||||
- Make top app and bottom system bars use the same color
|
||||
- Reduce unnecessary API calls in search screen
|
||||
- Upgrade to Compose November releases
|
||||
- Enable logging of SQLite queries
|
||||
|
||||
## [1.37.0] - 2023-10-06
|
||||
|
||||
### Changed
|
||||
|
||||
* Fixed a crash that infrequently happened in the comments page
|
||||
* Fixed a crash when trying to list saved posts
|
||||
* Fixed a crash in home screen widget when user had less than 50 saved posts
|
||||
* Upgraded to Compose 1.6.0-alpha07
|
||||
- Fixed a crash that infrequently happened in the comments page
|
||||
- Fixed a crash when trying to list saved posts
|
||||
- Fixed a crash in home screen widget when user had less than 50 saved posts
|
||||
- Upgraded to Compose 1.6.0-alpha07
|
||||
|
||||
## [1.36.0] - 2023-10-04
|
||||
|
||||
### Added
|
||||
|
||||
* Introduce an initial attempt at a home screen widget
|
||||
- Introduce an initial attempt at a home screen widget
|
||||
|
||||
### Changed
|
||||
|
||||
* Fixed a crash triggered when swiping items
|
||||
* Adjusted pull to refresh component to match Material You theme
|
||||
- Fixed a crash triggered when swiping items
|
||||
- Adjusted pull to refresh component to match Material You theme
|
||||
|
||||
## [1.35.0] - 2023-09-19
|
||||
|
||||
### Changed
|
||||
|
||||
* Fixed missing vertical spacing between post tags
|
||||
* Fixed user profile links not displaying correctly in-app
|
||||
* Update to Compose October release
|
||||
- Fixed missing vertical spacing between post tags
|
||||
- Fixed user profile links not displaying correctly in-app
|
||||
- Update to Compose October release
|
||||
|
||||
## [1.34.0] - 2023-08-30
|
||||
|
||||
### Added
|
||||
|
||||
* Add a swipe action on each story to open the comments page on `lobste.rs`
|
||||
* Automatically mark posts as "read" and visually distinguish between them
|
||||
- Add a swipe action on each story to open the comments page on `lobste.rs`
|
||||
- Automatically mark posts as "read" and visually distinguish between them
|
||||
|
||||
### Changed
|
||||
|
||||
* Tweak list items to reduce vertical size and adjust colors
|
||||
* Drop bogus workarounds for native library crashes
|
||||
* Updated Jetpack Compose and SQLite
|
||||
- Tweak list items to reduce vertical size and adjust colors
|
||||
- Drop bogus workarounds for native library crashes
|
||||
- Updated Jetpack Compose and SQLite
|
||||
|
||||
### Fixed
|
||||
|
||||
* Rework how comments are displayed to adapt to lobste.rs API change
|
||||
- Rework how comments are displayed to adapt to lobste.rs API change
|
||||
|
||||
## [1.33.0] - 2023-07-31
|
||||
|
||||
### Changed
|
||||
|
||||
* Yet another attempt at fixing native library crashes
|
||||
* Rework how baseline profiles are generated
|
||||
- Yet another attempt at fixing native library crashes
|
||||
- Rework how baseline profiles are generated
|
||||
|
||||
## [1.32.0] - 2023-07-27
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade to AndroidX July 26 release
|
||||
* Revert selectable text feature due to sporadic crashes
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- Added another workaround for native library loading crash
|
||||
|
||||
## [1.29.0] - 2023-06-08
|
||||
|
||||
### Added
|
||||
|
||||
* Backup and restore options for saved posts
|
||||
- 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
|
||||
- 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
|
||||
- Navigation transitions have been slightly sped up
|
||||
|
||||
### Fixed
|
||||
|
||||
* Add workaround for a native library loading crash observed on some devices
|
||||
- Add workaround for a native library loading crash observed on some devices
|
||||
|
||||
## [1.27.0] - 2023-05-31
|
||||
|
||||
### Changed
|
||||
|
||||
* Small accessibility improvements
|
||||
* Slightly tweak the layout for story items to take less vertical space
|
||||
* Upgrade to Compose `1.5.0-beta01` release
|
||||
* Set accessibility web URI for profile screen
|
||||
* Directly use Material Icons from upstream artifacts
|
||||
* Upgrade dependencies
|
||||
* Add adaptive navigation support (thanks @Yash-Garg)
|
||||
* Use latest SQLite for backing databases
|
||||
- Small accessibility improvements
|
||||
- Slightly tweak the layout for story items to take less vertical space
|
||||
- Upgrade to Compose `1.5.0-beta01` release
|
||||
- Set accessibility web URI for profile screen
|
||||
- Directly use Material Icons from upstream artifacts
|
||||
- Upgrade dependencies
|
||||
- Add adaptive navigation support (thanks @Yash-Garg)
|
||||
- Use latest SQLite for backing databases
|
||||
|
||||
## [1.26.0] - 2023-05-03
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade to Compose May release
|
||||
* Disable Sentry performance reporting
|
||||
* Migrate to Compose Foundation `FlowRow` in `LobstersCard`
|
||||
- Upgrade to Compose May release
|
||||
- Disable Sentry performance reporting
|
||||
- Migrate to Compose Foundation `FlowRow` in `LobstersCard`
|
||||
|
||||
## [1.25.0] - 2023-04-18
|
||||
|
||||
### Changed
|
||||
|
||||
* Make the app draw edge to edge
|
||||
* Simplify Top App Bar color scheme
|
||||
* Rework how baseline profiles are generated
|
||||
* Upgrade to Compose April release
|
||||
- Make the app draw edge to edge
|
||||
- Simplify Top App Bar color scheme
|
||||
- Rework how baseline profiles are generated
|
||||
- Upgrade to Compose April release
|
||||
|
||||
## [1.24.0] - 2023-03-24
|
||||
|
||||
### Changed
|
||||
|
||||
* Rebuild app icon assets to align better with Material guidelines
|
||||
* Adopt Slack's Compose lints and address issues found by it
|
||||
* Upgrade to OkHttp 4.x
|
||||
* Upgrade to Compose March release
|
||||
* Start work on support for logging in with lobste.rs
|
||||
- Rebuild app icon assets to align better with Material guidelines
|
||||
- Adopt Slack's Compose lints and address issues found by it
|
||||
- Upgrade to OkHttp 4.x
|
||||
- Upgrade to Compose March release
|
||||
- Start work on support for logging in with lobste.rs
|
||||
|
||||
## [1.23.0] - 2023-03-02
|
||||
|
||||
### Added
|
||||
|
||||
* Introduce [Sentry](https://sentry.io) for error reporting and performance monitoring
|
||||
- Introduce [Sentry](https://sentry.io) for error reporting and performance monitoring
|
||||
|
||||
## [1.22.0] - 2023-03-02
|
||||
|
||||
### Changed
|
||||
|
||||
* In-app browser now respects the user's choice for dark mode
|
||||
* Upgrade dependencies
|
||||
- In-app browser now respects the user's choice for dark mode
|
||||
- Upgrade dependencies
|
||||
|
||||
## [1.21.0] - 2023-02-09
|
||||
|
||||
### Changed
|
||||
|
||||
* Fix bug that caused the app to crash on launch
|
||||
- Fix bug that caused the app to crash on launch
|
||||
|
||||
## [1.20.0] - 2023-02-09
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade dependencies
|
||||
- Upgrade dependencies
|
||||
|
||||
## [1.19.0] - 2023-02-01
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade dependencies
|
||||
* Optimize packaged baseline profiles
|
||||
- Upgrade dependencies
|
||||
- Optimize packaged baseline profiles
|
||||
|
||||
## [1.18.0] - 2023-01-21
|
||||
|
||||
### Changed
|
||||
|
||||
* Special-case deleted stories in comments API
|
||||
* Fix regression where save button had the incorrect visual state
|
||||
- Special-case deleted stories in comments API
|
||||
- Fix regression where save button had the incorrect visual state
|
||||
|
||||
## [1.17.0] - 2023-01-12
|
||||
|
||||
### Added
|
||||
|
||||
* The app will now mark new comments as unread when returning to posts
|
||||
- The app will now mark new comments as unread when returning to posts
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade to Kotlin 1.8.0
|
||||
* Revert questionable app bar scrolling behaviour change
|
||||
- Upgrade to Kotlin 1.8.0
|
||||
- Revert questionable app bar scrolling behaviour change
|
||||
|
||||
## [1.16.0] - 2022-12-24
|
||||
|
||||
### Changed
|
||||
|
||||
* Fix bug where newest posts tab actually showed hottest posts instead
|
||||
- Fix bug where newest posts tab actually showed hottest posts instead
|
||||
|
||||
## [1.15.0] - 2022-12-20
|
||||
|
||||
### Changed
|
||||
|
||||
* Fix a case where collapsing the parent of an already collapsed comment caused it to become expanded
|
||||
* Tweak progress bar UI to use a linear indicator
|
||||
* Allow app bar to hide when scrolling down
|
||||
* Fix bug where refreshing a list of posts caused it to jump around multiple times
|
||||
- Fix a case where collapsing the parent of an already collapsed comment caused it to become expanded
|
||||
- Tweak progress bar UI to use a linear indicator
|
||||
- Allow app bar to hide when scrolling down
|
||||
- Fix bug where refreshing a list of posts caused it to jump around multiple times
|
||||
|
||||
## [1.14.0] - 2022-12-12
|
||||
|
||||
### Changed
|
||||
|
||||
* Rework how often saved posts are updated
|
||||
* Collapsing a comment now collapses all comments under it similar to how it works on the website
|
||||
- Rework how often saved posts are updated
|
||||
- Collapsing a comment now collapses all comments under it similar to how it works on the website
|
||||
|
||||
## [1.13.0] - 2022-12-10
|
||||
|
||||
### Changed
|
||||
|
||||
* Fix crash when viewing [jcs](https://lobste.rs/u/jcs)' comments
|
||||
- Fix crash when viewing [jcs](https://lobste.rs/u/jcs)' comments
|
||||
|
||||
## [1.12.0] - 2022-12-09
|
||||
|
||||
### Changed
|
||||
|
||||
* Do not schedule post update job every time the app starts
|
||||
* Improve favicon loading to reduce unnecessary redraws
|
||||
* Fix post lists being reloaded unnecessarily
|
||||
- Do not schedule post update job every time the app starts
|
||||
- Improve favicon loading to reduce unnecessary redraws
|
||||
- Fix post lists being reloaded unnecessarily
|
||||
|
||||
## [1.11.0] - 2022-12-07
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade to Compose 2022.12.00 release
|
||||
* Refresh packaged baseline profile
|
||||
* Improve how saved state of posts is queried
|
||||
- Upgrade to Compose 2022.12.00 release
|
||||
- Refresh packaged baseline profile
|
||||
- Improve how saved state of posts is queried
|
||||
|
||||
## [1.10.0] - 2022-11-30
|
||||
|
||||
### Changed
|
||||
|
||||
* Configure Coil image loader with better caching settings
|
||||
* Refresh packaged baseline profile
|
||||
- Configure Coil image loader with better caching settings
|
||||
- Refresh packaged baseline profile
|
||||
|
||||
## [1.9.0] - 2022-11-28
|
||||
|
||||
### Changed
|
||||
|
||||
* A loading indicator is added to the bottom of the post list to identify when new posts are being fetched
|
||||
- A loading indicator is added to the bottom of the post list to identify when new posts are being fetched
|
||||
|
||||
## [1.8.0] - 2022-11-21
|
||||
|
||||
### Changed
|
||||
|
||||
* The release pipeline now automatically generates Play Store release notes from the changelog file
|
||||
- The release pipeline now automatically generates Play Store release notes from the changelog file
|
||||
|
||||
### Fixed
|
||||
|
||||
* Downgrade AGP to 8.0.0-alpha07 to fix Baseline Profiles not being packaged
|
||||
- Downgrade AGP to 8.0.0-alpha07 to fix Baseline Profiles not being packaged
|
||||
|
||||
## [1.7.0] - 2022-11-19
|
||||
|
||||
### Changed
|
||||
|
||||
* Switch to Compose Material's swipe refresh feature
|
||||
* Change metadata extractor logic to no longer prefer canonical URLs. The submitted URL will be retained as-is now.
|
||||
* Remove navigation transition animations
|
||||
- Switch to Compose Material's swipe refresh feature
|
||||
- Change metadata extractor logic to no longer prefer canonical URLs. The submitted URL will be retained as-is now.
|
||||
- Remove navigation transition animations
|
||||
|
||||
## [1.6.0] - 2022-11-14
|
||||
|
||||
### Changed
|
||||
|
||||
* Migrate dependency injection from Hilt to Anvil + Whetstone
|
||||
- Migrate dependency injection from Hilt to Anvil + Whetstone
|
||||
|
||||
## [1.5.0] - 2022-11-10
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade to Compose 2022.11.00 release
|
||||
- Upgrade to Compose 2022.11.00 release
|
||||
|
||||
## [1.4.0] - 2022-11-02
|
||||
|
||||
### Changed
|
||||
|
||||
* Misc fixes to link metadata extractor
|
||||
- Misc fixes to link metadata extractor
|
||||
|
||||
## [1.3.0] - 2022-10-24
|
||||
|
||||
### Changed
|
||||
|
||||
* Prevent errors during link metadata retrieval from crashing the app
|
||||
* Update Jetpack libraries to 2022-10-24 release
|
||||
- Prevent errors during link metadata retrieval from crashing the app
|
||||
- Update Jetpack libraries to 2022-10-24 release
|
||||
|
||||
## [1.2.0] - 2022-10-17
|
||||
|
||||
### Changed
|
||||
|
||||
* Lazily load link metadata to improve comment page loading speed
|
||||
* Upgrade to latest Compose release
|
||||
* Refactor code to align with Twitter's public Compose guidelines
|
||||
- Lazily load link metadata to improve comment page loading speed
|
||||
- Upgrade to latest Compose release
|
||||
- Refactor code to align with Twitter's public Compose guidelines
|
||||
|
||||
## [1.1.0] - 2022-09-30
|
||||
|
||||
### Added
|
||||
|
||||
* Add score and relative time to comments
|
||||
- Add score and relative time to comments
|
||||
|
||||
### Changed
|
||||
|
||||
* Update Jetpack Compose
|
||||
* Declare data backup and transfer rules
|
||||
* Improve legibility of links in comments and user profiles
|
||||
* Commonize and improve how network errors are displayed
|
||||
- Update Jetpack Compose
|
||||
- Declare data backup and transfer rules
|
||||
- Improve legibility of links in comments and user profiles
|
||||
- Commonize and improve how network errors are displayed
|
||||
|
||||
## [1.0.0] - 2022-09-20
|
||||
|
||||
* Initial Play Store release
|
||||
|
||||
[Unreleased]: https://github.com/msfjarvis/compose-lobsters/compare/v1.39.0...HEAD
|
||||
- Initial Play Store release
|
||||
|
||||
[Unreleased]: https://github.com/msfjarvis/compose-lobsters/compare/v1.48.0...HEAD
|
||||
[1.48.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.47.0...v1.48.0
|
||||
[1.47.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.46.0...v1.47.0
|
||||
[1.46.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.45.0...v1.46.0
|
||||
[1.45.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.44.0...v1.45.0
|
||||
[1.44.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.43.0...v1.44.0
|
||||
[1.43.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.42.0...v1.43.0
|
||||
[1.42.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.41.0...v1.42.0
|
||||
[1.41.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.40.0...v1.41.0
|
||||
[1.40.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.39.0...v1.40.0
|
||||
[1.39.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.38.0...v1.39.0
|
||||
|
||||
[1.38.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.37.0...v1.38.0
|
||||
|
||||
[1.37.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.36.0...v1.37.0
|
||||
|
||||
[1.36.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.35.0...v1.36.0
|
||||
|
||||
[1.35.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.34.0...v1.35.0
|
||||
|
||||
[1.34.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.33.0...v1.34.0
|
||||
|
||||
[1.33.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.32.0...v1.33.0
|
||||
|
||||
[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
|
||||
|
||||
[1.26.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.25.0...v1.26.0
|
||||
|
||||
[1.25.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.24.0...v1.25.0
|
||||
|
||||
[1.24.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.23.0...v1.24.0
|
||||
|
||||
[1.23.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.22.0...v1.23.0
|
||||
|
||||
[1.22.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.21.0...v1.22.0
|
||||
|
||||
[1.21.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.20.0...v1.21.0
|
||||
|
||||
[1.20.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.19.0...v1.20.0
|
||||
|
||||
[1.19.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.18.0...v1.19.0
|
||||
|
||||
[1.18.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.17.0...v1.18.0
|
||||
|
||||
[1.17.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.16.0...v1.17.0
|
||||
|
||||
[1.16.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.15.0...v1.16.0
|
||||
|
||||
[1.15.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.14.0...v1.15.0
|
||||
|
||||
[1.14.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.13.0...v1.14.0
|
||||
|
||||
[1.13.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.12.0...v1.13.0
|
||||
|
||||
[1.12.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.11.0...v1.12.0
|
||||
|
||||
[1.11.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.10.0...v1.11.0
|
||||
|
||||
[1.10.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.9.0...v1.10.0
|
||||
|
||||
[1.9.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.8.0...v1.9.0
|
||||
|
||||
[1.8.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.7.0...1.8.0
|
||||
|
||||
[1.7.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.6.0...v1.7.0
|
||||
|
||||
[1.6.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.5.0...v1.6.0
|
||||
|
||||
[1.5.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.4.0...v1.5.0
|
||||
|
||||
[1.4.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.3.0...v1.4.0
|
||||
|
||||
[1.3.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.2.0...v1.3.0
|
||||
|
||||
[1.2.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.1.0...v1.2.0
|
||||
|
||||
[1.1.0]: https://github.com/msfjarvis/compose-lobsters/compare/v1.0.0...v1.1.0
|
||||
|
||||
[1.0.0]: https://github.com/msfjarvis/compose-lobsters/compare/29c374859b17c5fcef03585b8a01c00070de9097...v1.0.0
|
||||
|
||||
@@ -8,7 +8,14 @@ Unofficial Android app for read-only access to [lobste.rs](https://lobste.rs), b
|
||||
height="80" />
|
||||
</a>
|
||||
|
||||
> Snapshots from the development branch can be obtained [here](https://github.com/msfjarvis/compose-lobsters/releases/tag/nightly).
|
||||
<a href="https://android.izzysoft.de/repo/apk/dev.msfjarvis.claw.android">
|
||||
<img src="./.github/izzy-badge.webp"
|
||||
alt="Get it on IzzyOnDroid"
|
||||
height="80" />
|
||||
</a>
|
||||
|
||||
> [!NOTE]
|
||||
> Daily builds are published to the Google Play 'Open Testing' channel (if there are any changes), which can be opted into via this link: [[Android]](https://play.google.com/store/apps/details?id=dev.msfjarvis.claw.android) [[Web]](https://play.google.com/apps/testing/dev.msfjarvis.claw.android)
|
||||
|
||||
<img src="https://github.com/msfjarvis/compose-lobsters/blob/main/.github/readme_feature_light.webp#gh-light-mode-only"
|
||||
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"
|
||||
|
||||
+30
-12
@@ -16,24 +16,28 @@ plugins {
|
||||
id("dev.msfjarvis.claw.sentry")
|
||||
id("dev.msfjarvis.claw.versioning-plugin")
|
||||
alias(libs.plugins.aboutlibraries)
|
||||
alias(libs.plugins.android.junit5)
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.modulegraphassert)
|
||||
alias(libs.plugins.whetstone)
|
||||
alias(libs.plugins.baselineprofile)
|
||||
alias(libs.plugins.licensee)
|
||||
alias(libs.plugins.tracelog)
|
||||
alias(libs.plugins.kotlin.composeCompiler)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "dev.msfjarvis.claw.android"
|
||||
defaultConfig.applicationId = "dev.msfjarvis.claw.android"
|
||||
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
buildFeatures.compose = true
|
||||
composeOptions {
|
||||
useLiveLiterals = false
|
||||
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
||||
composeOptions { useLiveLiterals = false }
|
||||
buildTypes.create("internal") {
|
||||
matchingFallbacks += "release"
|
||||
signingConfig = signingConfigs["debug"]
|
||||
applicationIdSuffix = ".internal"
|
||||
debuggable(true)
|
||||
}
|
||||
// kotlinx.serialization 1.6.0-RC regression
|
||||
packagingOptions.resources.pickFirsts += "**/*.bin"
|
||||
}
|
||||
|
||||
baselineProfile {
|
||||
@@ -42,14 +46,15 @@ baselineProfile {
|
||||
from(projects.benchmark.dependencyProject)
|
||||
}
|
||||
|
||||
composeCompiler { enableStrongSkippingMode = true }
|
||||
|
||||
licensee {
|
||||
allow("Apache-2.0")
|
||||
allow("MIT")
|
||||
ignoreDependencies("com.michael-bull.kotlin-result") { because("kotlin-result is ISC licensed") }
|
||||
ignoreDependencies("org.commonmark") { because("Commonmark is BSD licensed") }
|
||||
allowUrl("https://jsoup.org/license") { because("Jsoup is MIT licensed") }
|
||||
allowUrl("https://opensource.org/licenses/isc-license.txt") {
|
||||
because("kotlin-result is ISC licensed")
|
||||
}
|
||||
allowUrl("https://opensource.org/licenses/MIT") { because("That's the MIT license text") }
|
||||
}
|
||||
|
||||
moduleGraphAssert {
|
||||
@@ -70,38 +75,51 @@ dependencies {
|
||||
implementation(platform(libs.okhttp.bom))
|
||||
implementation(libs.aboutLibraries.m3)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.animation)
|
||||
implementation(libs.androidx.compose.foundation)
|
||||
implementation(libs.androidx.compose.glance)
|
||||
implementation(libs.androidx.compose.glance.m3)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material3.window.size)
|
||||
implementation(libs.androidx.compose.runtime)
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.text)
|
||||
implementation(libs.androidx.compose.ui.util)
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
implementation(libs.androidx.lifecycle.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.paging.compose)
|
||||
implementation(libs.androidx.profileinstaller)
|
||||
implementation(libs.androidx.work.runtime)
|
||||
implementation(libs.coil)
|
||||
implementation(libs.copydown)
|
||||
implementation(libs.dagger)
|
||||
implementation(libs.jsoup)
|
||||
implementation(libs.eithernet)
|
||||
implementation(libs.javax.inject)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.serialization.core)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.material3.pulltorefresh)
|
||||
implementation(libs.napier)
|
||||
implementation(libs.okhttp.core)
|
||||
implementation(libs.okhttp.loggingInterceptor)
|
||||
implementation(libs.retrofit)
|
||||
implementation(libs.sqldelight.extensions.coroutines)
|
||||
implementation(libs.swipe)
|
||||
implementation(libs.unfurl)
|
||||
implementation(projects.api)
|
||||
implementation(projects.common)
|
||||
implementation(projects.core)
|
||||
implementation(projects.database.core)
|
||||
implementation(projects.database.impl)
|
||||
implementation(projects.model)
|
||||
implementation(projects.web)
|
||||
|
||||
kapt(libs.dagger.compiler)
|
||||
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.okhttp.mockwebserver)
|
||||
addTestDependencies(project)
|
||||
androidTestImplementation(libs.androidx.test.espresso.core)
|
||||
androidTestImplementation(libs.androidx.test.uiautomator)
|
||||
androidTestImplementation(libs.leakcanary.android.test)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha16" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha16)" variant="all" version="8.3.0-alpha16">
|
||||
<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0)" variant="all" version="8.3.0">
|
||||
|
||||
<issue
|
||||
id="VectorPath"
|
||||
|
||||
Vendored
-20
@@ -1,22 +1,2 @@
|
||||
-dontobfuscate
|
||||
-keepattributes SourceFile, LineNumberTable
|
||||
|
||||
# From https://github.com/square/retrofit/blob/master/retrofit/src/main/resources/META-INF/proguard/retrofit2.pro
|
||||
|
||||
# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy
|
||||
# and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
|
||||
-if interface * { @retrofit2.http.* <methods>; }
|
||||
-keep,allowobfuscation interface <1>
|
||||
|
||||
# Keep inherited services.
|
||||
-if interface * { @retrofit2.http.* <methods>; }
|
||||
-keep,allowobfuscation interface * extends <1>
|
||||
|
||||
# With R8 full mode generic signatures are stripped for classes that are not
|
||||
# kept. Suspend functions are wrapped in continuations where the type argument
|
||||
# is used.
|
||||
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
|
||||
|
||||
# R8 full mode strips generic signatures from return types if not kept.
|
||||
-if interface * { @retrofit2.http.* public *** *(...); }
|
||||
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<!--
|
||||
~ Copyright © 2021-2024 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">
|
||||
|
||||
<!-- Performing the heap growth analysis in process requires more heap. -->
|
||||
<application
|
||||
android:largeHeap="true"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright © 2024 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
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
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
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import de.mannodermaus.junit5.ActivityScenarioExtension
|
||||
import leakcanary.repeatingAndroidInProcessScenario
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.RegisterExtension
|
||||
import shark.ObjectGrowthDetector
|
||||
import shark.forAndroidHeap
|
||||
|
||||
class HeapGrowthCheck {
|
||||
@JvmField
|
||||
@RegisterExtension
|
||||
val scenarioExtension = ActivityScenarioExtension.launch<MainActivity>()
|
||||
private val detector = ObjectGrowthDetector.forAndroidHeap().repeatingAndroidInProcessScenario()
|
||||
private lateinit var device: UiDevice
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||
device = UiDevice.getInstance(instrumentation)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verify_heap_growth() {
|
||||
val heapGrowth = detector.findRepeatedlyGrowingObjects { device.exploreScreens() }
|
||||
assertThat(heapGrowth.growingObjects).isEmpty()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
fun UiDevice.exploreScreens() {
|
||||
listOf("HOTTEST", "NEWEST", "SAVED").forEach { tag ->
|
||||
waitForObject(By.res(tag)).click()
|
||||
waitForIdle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun UiDevice.waitForObject(selector: BySelector, timeout: Long = 10_000L): UiObject2 {
|
||||
if (wait(Until.hasObject(selector), timeout)) {
|
||||
return findObject(selector)
|
||||
}
|
||||
|
||||
error("Object with selector [$selector] not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<!--
|
||||
~ Copyright © 2021-2024 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>
|
||||
@@ -11,7 +11,6 @@
|
||||
<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"
|
||||
@@ -32,108 +31,8 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/s/......" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/s/....../" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/s/....../...*" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/s/......" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/s/....../" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/s/....../...*" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/s/......" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/s/....../" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/s/....../...*" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="lobste.rs" />
|
||||
<data android:pathPattern="/u/......*" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".SearchActivity"
|
||||
android:configChanges="colorMode|density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
|
||||
@@ -161,6 +60,7 @@
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
|
||||
<receiver
|
||||
android:name=".glance.WidgetReceiver"
|
||||
android:exported="true">
|
||||
@@ -171,6 +71,7 @@
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/saved_posts_widget_info" />
|
||||
</receiver>
|
||||
|
||||
<!-- Required: set your sentry.io project identifier (DSN) -->
|
||||
<meta-data
|
||||
android:name="io.sentry.dsn"
|
||||
@@ -178,17 +79,19 @@
|
||||
<!-- enable automatic breadcrumbs for user interactions (clicks, swipes, scrolls) -->
|
||||
<meta-data
|
||||
android:name="io.sentry.traces.user-interaction.enable"
|
||||
android:value="${enableSentry}" />
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="io.sentry.breadcrumbs.user-interaction"
|
||||
android:value="${enableSentry}" />
|
||||
android:value="true" />
|
||||
<!-- enable screenshot for crashes -->
|
||||
<meta-data
|
||||
android:name="io.sentry.attach-screenshot"
|
||||
android:value="${enableSentry}" />
|
||||
android:value="true" />
|
||||
<!-- enable view hierarchy for crashes -->
|
||||
<meta-data
|
||||
android:name="io.sentry.attach-view-hierarchy"
|
||||
android:value="${enableSentry}" />
|
||||
android:value="true" />
|
||||
<!-- set the environment for this build -->
|
||||
<meta-data android:name="io.sentry.environment" android:value="${sentryEnvironment}" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
+5375
-9719
File diff suppressed because it is too large
Load Diff
+5375
-9719
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import com.deliveryhero.whetstone.Whetstone
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
@@ -24,6 +25,7 @@ import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import javax.inject.Inject
|
||||
|
||||
/** A base class that encapsulates all activities used by Claw. */
|
||||
@Stable
|
||||
abstract class BaseActivity : ComponentActivity() {
|
||||
|
||||
@Inject lateinit var urlLauncher: UrlLauncher
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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.
|
||||
@@ -23,8 +23,7 @@ import javax.inject.Inject
|
||||
@ContributesAppInjector(generateAppComponent = true)
|
||||
class ClawApplication : Application(), ApplicationComponentOwner {
|
||||
|
||||
override val applicationComponent by
|
||||
lazy(LazyThreadSafetyMode.NONE) { GeneratedApplicationComponent.create(this) }
|
||||
override val applicationComponent = GeneratedApplicationComponent.create(this)
|
||||
|
||||
@Inject lateinit var plugins: Set<@JvmSuppressWildcards AppPlugin>
|
||||
|
||||
@@ -34,19 +33,12 @@ class ClawApplication : Application(), ApplicationComponentOwner {
|
||||
plugins.forEach { plugin -> plugin.apply(this) }
|
||||
val postUpdateWorkRequest =
|
||||
PeriodicWorkRequestBuilder<SavedPostUpdaterWorker>(POST_REFRESH_PERIOD, TimeUnit.HOURS)
|
||||
.setConstraints(
|
||||
Constraints.Builder()
|
||||
.setRequiresCharging(false)
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiresDeviceIdle(true)
|
||||
.build()
|
||||
)
|
||||
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
||||
.build()
|
||||
WorkManager.getInstance(this)
|
||||
.enqueueUniquePeriodicWork(
|
||||
"updateSavedPosts",
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
postUpdateWorkRequest,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,44 +28,47 @@ import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import dev.msfjarvis.claw.common.theme.DarkThemeColors
|
||||
import dev.msfjarvis.claw.common.theme.LightThemeColors
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class SavedPostsWidget(private val posts: List<SavedPost>) : GlanceAppWidget() {
|
||||
class SavedPostsWidget(private val posts: Flow<List<UIPost>>) : GlanceAppWidget() {
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val postWindow = posts.first().take(50).toImmutableList()
|
||||
provideContent {
|
||||
GlanceTheme(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) GlanceTheme.colors
|
||||
else ColorProviders(light = LightThemeColors, dark = DarkThemeColors)
|
||||
) {
|
||||
WidgetHost()
|
||||
WidgetHost(postWindow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WidgetHost() {
|
||||
LazyColumn(
|
||||
modifier =
|
||||
GlanceModifier.fillMaxSize()
|
||||
.background(GlanceTheme.colors.background)
|
||||
.appWidgetBackground(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
item {
|
||||
val style = MaterialTheme.typography.titleLarge
|
||||
Text(
|
||||
text = "Saved posts",
|
||||
style =
|
||||
TextStyle(
|
||||
color = GlanceTheme.colors.onBackground,
|
||||
fontSize = style.fontSize,
|
||||
fontWeight = style.fontWeight.toGlance(),
|
||||
fontStyle = style.fontStyle.toGlance(),
|
||||
),
|
||||
modifier = GlanceModifier.padding(vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
items(posts) { post -> WidgetListEntry(post = post) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WidgetHost(posts: ImmutableList<UIPost>, modifier: GlanceModifier = GlanceModifier) {
|
||||
LazyColumn(
|
||||
modifier =
|
||||
modifier.fillMaxSize().background(GlanceTheme.colors.background).appWidgetBackground(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
item {
|
||||
val style = MaterialTheme.typography.titleLarge
|
||||
Text(
|
||||
text = "Saved posts",
|
||||
style =
|
||||
TextStyle(
|
||||
color = GlanceTheme.colors.onBackground,
|
||||
fontSize = style.fontSize,
|
||||
fontWeight = style.fontWeight.toGlance(),
|
||||
fontStyle = style.fontStyle.toGlance(),
|
||||
),
|
||||
modifier = GlanceModifier.padding(vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
items(posts) { post -> WidgetListEntry(post = post) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,19 +33,20 @@ import androidx.glance.text.TextStyle
|
||||
import dev.msfjarvis.claw.android.MainActivity
|
||||
import dev.msfjarvis.claw.android.MainActivity.Companion.NAVIGATION_KEY
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
|
||||
private val destinationKey = Key<String>(NAVIGATION_KEY)
|
||||
|
||||
@Composable
|
||||
@GlanceComposable
|
||||
fun WidgetListEntry(post: SavedPost, modifier: GlanceModifier = GlanceModifier) {
|
||||
fun WidgetListEntry(post: UIPost, modifier: GlanceModifier = GlanceModifier) {
|
||||
val titleStyle = MaterialTheme.typography.titleMedium
|
||||
val commentsAction =
|
||||
actionStartActivity<MainActivity>(actionParametersOf(destinationKey to post.shortId))
|
||||
// If the URL starts with a /, it's a relative URL and we should open the comments page directly.
|
||||
val postAction =
|
||||
if (post.url.isNotEmpty()) actionStartActivity(Intent(Intent.ACTION_VIEW, Uri.parse(post.url)))
|
||||
else commentsAction
|
||||
if (post.url.startsWith('/')) commentsAction
|
||||
else actionStartActivity(Intent(Intent.ACTION_VIEW, Uri.parse(post.url)))
|
||||
Box(modifier.padding(8.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -68,7 +69,7 @@ fun WidgetListEntry(post: SavedPost, modifier: GlanceModifier = GlanceModifier)
|
||||
)
|
||||
Image(
|
||||
provider = ImageProvider(R.drawable.ic_comment),
|
||||
contentDescription = "${post.commentCount ?: 0} comments",
|
||||
contentDescription = "${post.commentCount} comments",
|
||||
modifier = GlanceModifier.padding(end = 4.dp).clickable(commentsAction),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 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,8 +14,6 @@ import com.deliveryhero.whetstone.Whetstone
|
||||
import com.deliveryhero.whetstone.broadcastreceiver.ContributesBroadcastReceiverInjector
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@ContributesBroadcastReceiverInjector
|
||||
class WidgetReceiver : GlanceAppWidgetReceiver() {
|
||||
@@ -23,7 +21,7 @@ class WidgetReceiver : GlanceAppWidgetReceiver() {
|
||||
@Inject lateinit var viewModel: ClawViewModel
|
||||
|
||||
override val glanceAppWidget: GlanceAppWidget
|
||||
get() = SavedPostsWidget(runBlocking { viewModel.savedPosts.first().take(50) })
|
||||
get() = SavedPostsWidget(viewModel.savedPosts)
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Whetstone.inject(this, context)
|
||||
|
||||
+2
-2
@@ -22,11 +22,11 @@ object MetadataExtractorModule {
|
||||
fun provideUnfurlLogger(): UnfurlLogger {
|
||||
return object : UnfurlLogger {
|
||||
override fun log(message: String) {
|
||||
Napier.d { message }
|
||||
Napier.d(tag = "Unfurler") { message }
|
||||
}
|
||||
|
||||
override fun log(e: Throwable, message: String) {
|
||||
Napier.e(e) { message }
|
||||
Napier.e(tag = "Unfurler", throwable = e) { message }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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,10 +13,16 @@ import com.slack.eithernet.ApiResult.Success
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dev.msfjarvis.claw.android.viewmodel.ReadPostsRepository
|
||||
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.toUIPost
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class LobstersPagingSource
|
||||
@@ -24,15 +30,33 @@ class LobstersPagingSource
|
||||
constructor(
|
||||
@Assisted private val remoteFetcher: RemoteFetcher<LobstersPost>,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : PagingSource<Int, LobstersPost>() {
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
private val readPostsRepository: ReadPostsRepository,
|
||||
) : PagingSource<Int, UIPost>() {
|
||||
private lateinit var savedPosts: List<String>
|
||||
private lateinit var readPosts: List<String>
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, LobstersPost> {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UIPost> {
|
||||
if (!::savedPosts.isInitialized) {
|
||||
savedPosts = savedPostsRepository.savedPosts.first().map(SavedPost::shortId)
|
||||
}
|
||||
if (!::readPosts.isInitialized) {
|
||||
readPosts = readPostsRepository.readPosts.first()
|
||||
}
|
||||
val page = params.key ?: STARTING_PAGE_INDEX
|
||||
return when (val result = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }) {
|
||||
is Success ->
|
||||
LoadResult.Page(
|
||||
itemsBefore = (page - 1) * PAGE_SIZE,
|
||||
data = result.value,
|
||||
data =
|
||||
result.value.map {
|
||||
it
|
||||
.toUIPost()
|
||||
.copy(
|
||||
isSaved = savedPosts.contains(it.shortId),
|
||||
isRead = readPosts.contains(it.shortId),
|
||||
)
|
||||
},
|
||||
prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
|
||||
nextKey = page + 1,
|
||||
)
|
||||
@@ -43,7 +67,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, LobstersPost>): Int? {
|
||||
override fun getRefreshKey(state: PagingState<Int, UIPost>): Int? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
(anchorPosition / PAGE_SIZE).coerceAtLeast(STARTING_PAGE_INDEX)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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,11 +14,16 @@ 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.android.viewmodel.ReadPostsRepository
|
||||
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
|
||||
import dev.msfjarvis.claw.api.LobstersSearchApi
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.toUIPost
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
@@ -33,14 +38,19 @@ constructor(
|
||||
private val searchApi: LobstersSearchApi,
|
||||
@Assisted private val queryProvider: () -> String,
|
||||
@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)
|
||||
}
|
||||
}
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
private val readPostsRepository: ReadPostsRepository,
|
||||
) : PagingSource<Int, UIPost>() {
|
||||
private lateinit var savedPosts: List<String>
|
||||
private lateinit var readPosts: List<String>
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, LobstersPost> {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UIPost> {
|
||||
if (!::savedPosts.isInitialized) {
|
||||
savedPosts = savedPostsRepository.savedPosts.first().map(SavedPost::shortId)
|
||||
}
|
||||
if (!::readPosts.isInitialized) {
|
||||
readPosts = readPostsRepository.readPosts.first()
|
||||
}
|
||||
val searchQuery = queryProvider()
|
||||
// If there is no query, we don't need to call the API at all.
|
||||
if (searchQuery.isEmpty()) {
|
||||
@@ -55,7 +65,15 @@ constructor(
|
||||
val nextKey = if (result.value.isEmpty()) null else page + 1
|
||||
LoadResult.Page(
|
||||
itemsBefore = (page - 1) * PAGE_SIZE,
|
||||
data = result.value,
|
||||
data =
|
||||
result.value.map {
|
||||
it
|
||||
.toUIPost()
|
||||
.copy(
|
||||
isSaved = savedPosts.contains(it.shortId),
|
||||
isRead = readPosts.contains(it.shortId),
|
||||
)
|
||||
},
|
||||
prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1,
|
||||
nextKey = nextKey,
|
||||
)
|
||||
@@ -68,6 +86,12 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, UIPost>): Int? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
(anchorPosition / PAGE_SIZE).coerceAtLeast(STARTING_PAGE_INDEX)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(queryProvider: () -> String): SearchPagingSource
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.decorations
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.preview.DevicePreviews
|
||||
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
|
||||
import dev.msfjarvis.claw.common.ui.surfaceColorAtNavigationBarElevation
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun ClawAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
navigationIcon: @Composable () -> Unit = {},
|
||||
title: @Composable () -> Unit = {},
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
TopAppBar(
|
||||
title = title,
|
||||
modifier = modifier.shadow(8.dp),
|
||||
colors =
|
||||
TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtNavigationBarElevation()
|
||||
),
|
||||
navigationIcon = navigationIcon,
|
||||
actions = actions,
|
||||
)
|
||||
}
|
||||
|
||||
@DevicePreviews
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun ClawAppBarPreview() {
|
||||
LobstersTheme { ClawAppBar(title = { Text("Claw", fontWeight = FontWeight.Bold) }) }
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.android.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -16,9 +17,8 @@ import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
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 dev.msfjarvis.claw.model.UIPost
|
||||
|
||||
fun Context.getActivity(): ComponentActivity? {
|
||||
return when (this) {
|
||||
@@ -30,6 +30,7 @@ fun Context.getActivity(): ComponentActivity? {
|
||||
|
||||
@Composable
|
||||
fun rememberPostActions(
|
||||
context: Context,
|
||||
urlLauncher: UrlLauncher,
|
||||
navController: NavController,
|
||||
viewModel: ClawViewModel,
|
||||
@@ -49,21 +50,27 @@ fun rememberPostActions(
|
||||
if (currentRoute != Destinations.Comments.route) navController.navigate(newRoute)
|
||||
}
|
||||
|
||||
override fun viewCommentsPage(commentsUrl: String) {
|
||||
// Post links from lobste.rs are of the form $baseUrl/s/$postId/$postTitle
|
||||
// Interestingly, lobste.rs does not actually care for the value of $postTitle, and will
|
||||
// happily accept both a missing as well as a completely arbitrary $postTitle. We
|
||||
// leverage this to create a new URL format which looks like
|
||||
// $baseUrl/s/$postId/$postTitle/r, and does not trigger our deeplinks,
|
||||
// instead opening in the custom tab as we want it to.
|
||||
urlLauncher.openUri(commentsUrl.replaceAfterLast('/', "r"))
|
||||
override fun viewCommentsPage(post: UIPost) {
|
||||
urlLauncher.openUri(post.commentsUrl)
|
||||
}
|
||||
|
||||
override fun toggleSave(post: SavedPost) {
|
||||
override fun toggleSave(post: UIPost) {
|
||||
viewModel.toggleSave(post)
|
||||
}
|
||||
|
||||
override suspend fun getComments(postId: String): LobstersPostDetails {
|
||||
override fun share(post: UIPost) {
|
||||
val sendIntent: Intent =
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, post.url.ifEmpty { post.commentsUrl })
|
||||
putExtra(Intent.EXTRA_TITLE, post.title)
|
||||
type = "text/plain"
|
||||
}
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
context.startActivity(shareIntent)
|
||||
}
|
||||
|
||||
override suspend fun getComments(postId: String): UIPost {
|
||||
return viewModel.getPostComments(postId)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,13 +27,13 @@ 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
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun DatabasePosts(
|
||||
items: ImmutableMap<String, List<SavedPost>>,
|
||||
items: ImmutableMap<String, List<UIPost>>,
|
||||
listState: LazyListState,
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -54,12 +54,7 @@ fun DatabasePosts(
|
||||
items.forEach { (month, posts) ->
|
||||
stickyHeader(contentType = "month-header") { MonthHeader(label = month) }
|
||||
items(items = posts, key = { it.shortId }, contentType = { "LobstersItem" }) { item ->
|
||||
LobstersListItem(
|
||||
item = item,
|
||||
isSaved = { true },
|
||||
isRead = { false },
|
||||
postActions = postActions,
|
||||
)
|
||||
LobstersListItem(item = item, refresh = {}, postActions = postActions)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,40 +8,37 @@ package dev.msfjarvis.claw.android.ui.lists
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Reply
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import dev.msfjarvis.claw.common.posts.LobstersCard
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import me.saket.swipe.SwipeAction
|
||||
import me.saket.swipe.SwipeableActionsBox
|
||||
|
||||
@Composable
|
||||
fun LobstersListItem(
|
||||
item: SavedPost,
|
||||
isSaved: (SavedPost) -> Boolean,
|
||||
isRead: suspend (String) -> Boolean,
|
||||
item: UIPost,
|
||||
postActions: PostActions,
|
||||
refresh: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val read by produceState(false, item.shortId) { value = isRead(item.shortId) }
|
||||
val commentsAction =
|
||||
SwipeAction(
|
||||
icon = rememberVectorPainter(Icons.AutoMirrored.Filled.Reply),
|
||||
background = MaterialTheme.colorScheme.tertiary,
|
||||
onSwipe = { postActions.viewCommentsPage(item.commentsUrl) },
|
||||
onSwipe = { postActions.viewCommentsPage(item) },
|
||||
)
|
||||
SwipeableActionsBox(endActions = listOf(commentsAction)) {
|
||||
LobstersCard(
|
||||
post = item,
|
||||
isSaved = isSaved(item),
|
||||
isRead = read,
|
||||
postActions = postActions,
|
||||
modifier = modifier,
|
||||
val shareAction =
|
||||
SwipeAction(
|
||||
icon = rememberVectorPainter(Icons.Filled.Share),
|
||||
background = MaterialTheme.colorScheme.tertiary,
|
||||
onSwipe = { postActions.share(item) },
|
||||
)
|
||||
SwipeableActionsBox(startActions = listOf(shareAction), endActions = listOf(commentsAction)) {
|
||||
LobstersCard(post = item, postActions = postActions, refresh = refresh, modifier = modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -21,32 +22,35 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.itemContentType
|
||||
import androidx.paging.compose.itemKey
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.posts.TEST_POST
|
||||
import dev.msfjarvis.claw.common.posts.TEST_POST_ACTIONS
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.NetworkError
|
||||
import dev.msfjarvis.claw.common.ui.ProgressBar
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.LobstersPost
|
||||
import dev.msfjarvis.claw.model.toSavedPost
|
||||
import dev.msfjarvis.claw.common.ui.preview.DevicePreviews
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import eu.bambooapps.material3.pullrefresh.PullRefreshIndicator
|
||||
import eu.bambooapps.material3.pullrefresh.pullRefresh
|
||||
import eu.bambooapps.material3.pullrefresh.rememberPullRefreshState
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NetworkPosts(
|
||||
lazyPagingItems: LazyPagingItems<LobstersPost>,
|
||||
lazyPagingItems: LazyPagingItems<UIPost>,
|
||||
listState: LazyListState,
|
||||
isPostSaved: (SavedPost) -> Boolean,
|
||||
isPostRead: suspend (String) -> Boolean,
|
||||
postActions: PostActions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ReportDrawnWhen { lazyPagingItems.itemCount > 0 }
|
||||
val refreshLoadState = lazyPagingItems.loadState.refresh
|
||||
val isRefreshing = refreshLoadState == LoadState.Loading
|
||||
val isRefreshing = refreshLoadState == LoadState.Loading && lazyPagingItems.itemCount == 0
|
||||
val pullRefreshState = rememberPullRefreshState(isRefreshing, lazyPagingItems::refresh)
|
||||
Box(modifier = modifier.fillMaxSize().pullRefresh(pullRefreshState)) {
|
||||
if (lazyPagingItems.itemCount == 0 && refreshLoadState is LoadState.Error) {
|
||||
@@ -59,19 +63,16 @@ fun NetworkPosts(
|
||||
LazyColumn(state = listState) {
|
||||
items(
|
||||
count = lazyPagingItems.itemCount,
|
||||
key = lazyPagingItems.itemKey { it.shortId },
|
||||
key = lazyPagingItems.itemKey { item -> "${item.shortId}-${item.hashCode()}" },
|
||||
contentType = lazyPagingItems.itemContentType { "LobstersItem" },
|
||||
) { index ->
|
||||
val item = lazyPagingItems[index]
|
||||
if (item != null) {
|
||||
val dbModel = item.toSavedPost()
|
||||
LobstersListItem(
|
||||
item = dbModel,
|
||||
isSaved = isPostSaved,
|
||||
isRead = isPostRead,
|
||||
item = item,
|
||||
postActions = postActions,
|
||||
refresh = { lazyPagingItems.refresh() },
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
@@ -95,3 +96,17 @@ fun NetworkPosts(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DevicePreviews
|
||||
@Composable
|
||||
private fun ListPreview() {
|
||||
val items = List(20) { TEST_POST.copy(shortId = "${TEST_POST.shortId}${it}") }
|
||||
val flow = MutableStateFlow(PagingData.from(items))
|
||||
LobstersTheme {
|
||||
NetworkPosts(
|
||||
lazyPagingItems = flow.collectAsLazyPagingItems(),
|
||||
listState = rememberLazyListState(),
|
||||
postActions = TEST_POST_ACTIONS,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 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,15 +21,13 @@ 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 dev.msfjarvis.claw.model.UIPost
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Composable
|
||||
fun SearchList(
|
||||
items: Flow<PagingData<LobstersPost>>,
|
||||
items: Flow<PagingData<UIPost>>,
|
||||
listState: LazyListState,
|
||||
isPostSaved: (SavedPost) -> Boolean,
|
||||
postActions: PostActions,
|
||||
searchQuery: String,
|
||||
setSearchQuery: (String) -> Unit,
|
||||
@@ -50,8 +48,6 @@ fun SearchList(
|
||||
NetworkPosts(
|
||||
lazyPagingItems = lazyPagingItems,
|
||||
listState = listState,
|
||||
isPostSaved = isPostSaved,
|
||||
isPostRead = { false },
|
||||
postActions = postActions,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Copyright © 2024 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.
|
||||
|
||||
+37
-36
@@ -11,7 +11,6 @@ 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.Image
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -27,13 +26,14 @@ import androidx.compose.material.icons.filled.Whatshot
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material.icons.outlined.NewReleases
|
||||
import androidx.compose.material.icons.outlined.Whatshot
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -41,10 +41,9 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -58,14 +57,12 @@ import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import androidx.navigation.navDeepLink
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.deliveryhero.whetstone.compose.injectedViewModel
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import dev.msfjarvis.claw.android.MainActivity
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.android.SearchActivity
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawAppBar
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationBar
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationRail
|
||||
import dev.msfjarvis.claw.android.ui.decorations.NavigationItem
|
||||
@@ -76,7 +73,6 @@ import dev.msfjarvis.claw.android.ui.navigation.ClawNavigationType
|
||||
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||
import dev.msfjarvis.claw.android.ui.rememberPostActions
|
||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.common.comments.CommentsPage
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
@@ -85,7 +81,7 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LobstersPostsScreen(
|
||||
urlLauncher: UrlLauncher,
|
||||
@@ -95,16 +91,16 @@ fun LobstersPostsScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ClawViewModel = injectedViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val hottestListState = rememberLazyListState()
|
||||
val newestListState = rememberLazyListState()
|
||||
val savedListState = rememberLazyListState()
|
||||
val navController = rememberNavController()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val postActions = rememberPostActions(urlLauncher, navController, viewModel)
|
||||
val postActions = rememberPostActions(context, urlLauncher, navController, viewModel)
|
||||
val backStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentDestination = backStackEntry?.destination?.route
|
||||
val context = LocalContext.current
|
||||
|
||||
val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems()
|
||||
val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems()
|
||||
@@ -113,7 +109,7 @@ fun LobstersPostsScreen(
|
||||
val navigationType = ClawNavigationType.fromSize(windowSizeClass.widthSizeClass)
|
||||
|
||||
val postIdOverride = context.getActivity()?.intent?.extras?.getString(MainActivity.NAVIGATION_KEY)
|
||||
LaunchedEffect(false) {
|
||||
LaunchedEffect(Unit) {
|
||||
if (postIdOverride != null) {
|
||||
navController.navigate(
|
||||
Destinations.Comments.route.replace(Destinations.Comments.PLACEHOLDER, postIdOverride)
|
||||
@@ -129,7 +125,9 @@ fun LobstersPostsScreen(
|
||||
icon = Icons.Outlined.Whatshot,
|
||||
selectedIcon = Icons.Filled.Whatshot,
|
||||
) {
|
||||
coroutineScope.launch { hottestListState.animateScrollToItem(index = 0) }
|
||||
coroutineScope.launch {
|
||||
if (hottestPosts.itemCount > 0) hottestListState.animateScrollToItem(index = 0)
|
||||
}
|
||||
},
|
||||
NavigationItem(
|
||||
label = "Newest",
|
||||
@@ -137,7 +135,9 @@ fun LobstersPostsScreen(
|
||||
icon = Icons.Outlined.NewReleases,
|
||||
selectedIcon = Icons.Filled.NewReleases,
|
||||
) {
|
||||
coroutineScope.launch { newestListState.animateScrollToItem(index = 0) }
|
||||
coroutineScope.launch {
|
||||
if (newestPosts.itemCount > 0) newestListState.animateScrollToItem(index = 0)
|
||||
}
|
||||
},
|
||||
NavigationItem(
|
||||
label = "Saved",
|
||||
@@ -145,13 +145,14 @@ fun LobstersPostsScreen(
|
||||
icon = Icons.Outlined.FavoriteBorder,
|
||||
selectedIcon = Icons.Filled.Favorite,
|
||||
) {
|
||||
coroutineScope.launch { savedListState.animateScrollToItem(index = 0) }
|
||||
coroutineScope.launch { if (savedPosts.isNotEmpty()) savedListState.scrollToItem(0) }
|
||||
},
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
ClawAppBar(
|
||||
TopAppBar(
|
||||
modifier = Modifier.shadow(8.dp),
|
||||
navigationIcon = {
|
||||
if (
|
||||
navController.previousBackStackEntry != null &&
|
||||
@@ -165,19 +166,17 @@ fun LobstersPostsScreen(
|
||||
contentDescription = "Go back to previous screen",
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_launcher_foreground),
|
||||
contentDescription = "The app icon for Claw",
|
||||
modifier = Modifier.size(48.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
if (navItems.any { it.route == currentDestination }) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_launcher_foreground),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
@@ -222,14 +221,11 @@ fun LobstersPostsScreen(
|
||||
enterTransition = { fadeIn(animationSpec = tween(350)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(350)) },
|
||||
) {
|
||||
val uri = LobstersApi.BASE_URL
|
||||
composable(route = Destinations.Hottest.route) {
|
||||
setWebUri("https://lobste.rs/")
|
||||
NetworkPosts(
|
||||
lazyPagingItems = hottestPosts,
|
||||
listState = hottestListState,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
isPostRead = viewModel::isPostRead,
|
||||
postActions = postActions,
|
||||
)
|
||||
}
|
||||
@@ -238,8 +234,6 @@ fun LobstersPostsScreen(
|
||||
NetworkPosts(
|
||||
lazyPagingItems = newestPosts,
|
||||
listState = newestListState,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
isPostRead = viewModel::isPostRead,
|
||||
postActions = postActions,
|
||||
)
|
||||
}
|
||||
@@ -250,11 +244,6 @@ fun LobstersPostsScreen(
|
||||
composable(
|
||||
route = Destinations.Comments.route,
|
||||
arguments = listOf(navArgument("postId") { type = NavType.StringType }),
|
||||
deepLinks =
|
||||
listOf(
|
||||
navDeepLink { uriPattern = "$uri/s/${Destinations.Comments.PLACEHOLDER}/.*" },
|
||||
navDeepLink { uriPattern = "$uri/s/${Destinations.Comments.PLACEHOLDER}" },
|
||||
),
|
||||
) { backStackEntry ->
|
||||
val postId =
|
||||
requireNotNull(backStackEntry.arguments?.getString("postId")) {
|
||||
@@ -267,19 +256,31 @@ fun LobstersPostsScreen(
|
||||
htmlConverter = htmlConverter,
|
||||
getSeenComments = viewModel::getSeenComments,
|
||||
markSeenComments = viewModel::markSeenComments,
|
||||
openUserProfile = {
|
||||
navController.navigate(
|
||||
Destinations.User.route.replace(Destinations.User.PLACEHOLDER, it)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Destinations.User.route,
|
||||
arguments = listOf(navArgument("username") { type = NavType.StringType }),
|
||||
deepLinks = listOf(navDeepLink { uriPattern = "$uri/u/${Destinations.User.PLACEHOLDER}" }),
|
||||
) { backStackEntry ->
|
||||
val username =
|
||||
requireNotNull(backStackEntry.arguments?.getString("username")) {
|
||||
"Navigating to ${Destinations.User.route} without necessary 'username' argument"
|
||||
}
|
||||
setWebUri("https://lobste.rs/u/$username")
|
||||
UserProfile(username = username, getProfile = viewModel::getUserProfile)
|
||||
UserProfile(
|
||||
username = username,
|
||||
getProfile = viewModel::getUserProfile,
|
||||
openUserProfile = {
|
||||
navController.navigate(
|
||||
Destinations.User.route.replace(Destinations.User.PLACEHOLDER, it)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
composable(route = Destinations.Settings.route) {
|
||||
SettingsScreen(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 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.
|
||||
@@ -11,6 +11,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
@@ -24,6 +25,7 @@ import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||
import dev.msfjarvis.claw.common.comments.CommentsPage
|
||||
import dev.msfjarvis.claw.common.comments.HTMLConverter
|
||||
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
|
||||
import dev.msfjarvis.claw.common.user.UserProfile
|
||||
|
||||
@Composable
|
||||
fun SearchScreen(
|
||||
@@ -34,7 +36,7 @@ fun SearchScreen(
|
||||
viewModel: ClawViewModel = injectedViewModel(),
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val postActions = rememberPostActions(urlLauncher, navController, viewModel)
|
||||
val postActions = rememberPostActions(LocalContext.current, urlLauncher, navController, viewModel)
|
||||
val listState = rememberLazyListState()
|
||||
Scaffold(modifier = modifier) { paddingValues ->
|
||||
NavHost(
|
||||
@@ -47,7 +49,6 @@ fun SearchScreen(
|
||||
SearchList(
|
||||
items = viewModel.searchResults,
|
||||
listState = listState,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
postActions = postActions,
|
||||
searchQuery = viewModel.searchQuery,
|
||||
setSearchQuery = { query -> viewModel.searchQuery = query },
|
||||
@@ -68,6 +69,30 @@ fun SearchScreen(
|
||||
htmlConverter = htmlConverter,
|
||||
getSeenComments = viewModel::getSeenComments,
|
||||
markSeenComments = viewModel::markSeenComments,
|
||||
openUserProfile = { username: String ->
|
||||
navController.navigate(
|
||||
Destinations.User.route.replace(Destinations.User.PLACEHOLDER, username)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Destinations.User.route,
|
||||
arguments = listOf(navArgument("username") { type = NavType.StringType }),
|
||||
) { backStackEntry ->
|
||||
val username =
|
||||
requireNotNull(backStackEntry.arguments?.getString("username")) {
|
||||
"Navigating to ${Destinations.User.route} without necessary 'username' argument"
|
||||
}
|
||||
setWebUri("https://lobste.rs/u/$username")
|
||||
UserProfile(
|
||||
username = username,
|
||||
getProfile = viewModel::getUserProfile,
|
||||
openUserProfile = {
|
||||
navController.navigate(
|
||||
Destinations.User.route.replace(Destinations.User.PLACEHOLDER, it)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.cachedIn
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.deliveryhero.whetstone.viewmodel.ContributesViewModel
|
||||
import com.slack.eithernet.ApiResult.Failure
|
||||
@@ -31,8 +32,10 @@ import dev.msfjarvis.claw.android.paging.SearchPagingSource
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||
import dev.msfjarvis.claw.core.injection.MainDispatcher
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.fromSavedPost
|
||||
import dev.msfjarvis.claw.model.toUIPost
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
@@ -45,11 +48,8 @@ import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@ContributesViewModel
|
||||
@@ -68,48 +68,44 @@ constructor(
|
||||
@MainDispatcher private val mainDispatcher: CoroutineDispatcher,
|
||||
@ForScope(ApplicationScope::class) context: Context,
|
||||
) : AndroidViewModel(context as Application) {
|
||||
private val hottestPostsPager =
|
||||
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
|
||||
pagingSourceFactory.create(api::getHottestPosts)
|
||||
}
|
||||
private val newestPostsPager =
|
||||
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 { searchQuery }
|
||||
}
|
||||
|
||||
val hottestPosts
|
||||
get() = hottestPostsPager.flow
|
||||
|
||||
val newestPosts
|
||||
get() = newestPostsPager.flow
|
||||
|
||||
val savedPosts
|
||||
get() = savedPostsRepository.savedPosts
|
||||
|
||||
val hottestPosts =
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = PAGE_SIZE),
|
||||
initialKey = STARTING_PAGE_INDEX,
|
||||
pagingSourceFactory = { pagingSourceFactory.create(api::getHottestPosts) },
|
||||
)
|
||||
.flow
|
||||
.cachedIn(viewModelScope)
|
||||
val newestPosts =
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = PAGE_SIZE),
|
||||
initialKey = STARTING_PAGE_INDEX,
|
||||
pagingSourceFactory = { pagingSourceFactory.create(api::getNewestPosts) },
|
||||
)
|
||||
.flow
|
||||
.cachedIn(viewModelScope)
|
||||
val searchResults =
|
||||
Pager(
|
||||
PagingConfig(pageSize = PAGE_SIZE),
|
||||
initialKey = STARTING_PAGE_INDEX,
|
||||
pagingSourceFactory = { searchPagingSourceFactory.create { searchQuery } },
|
||||
)
|
||||
.flow
|
||||
val savedPosts = savedPostsRepository.savedPosts.map { it.map(UIPost.Companion::fromSavedPost) }
|
||||
val savedPostsByMonth
|
||||
get() = savedPosts.map(::mapSavedPosts)
|
||||
|
||||
val searchResults
|
||||
get() = searchResultsPager.flow
|
||||
get() = savedPosts.map(::groupSavedPosts)
|
||||
|
||||
var searchQuery by mutableStateOf("")
|
||||
|
||||
private val _savedPostsMutex = Mutex()
|
||||
private var _readPosts = emptyList<String>()
|
||||
private var _savedPosts = emptyList<String>()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
savedPosts.collectLatest {
|
||||
_savedPostsMutex.withLock { _savedPosts = it.map(SavedPost::shortId) }
|
||||
}
|
||||
}
|
||||
viewModelScope.launch { savedPosts.collectLatest { _savedPosts = it.map(UIPost::shortId) } }
|
||||
viewModelScope.launch { readPostsRepository.readPosts.collectLatest { _readPosts = it } }
|
||||
}
|
||||
|
||||
private fun mapSavedPosts(items: List<SavedPost>): ImmutableMap<String, List<SavedPost>> {
|
||||
private fun groupSavedPosts(items: List<UIPost>): ImmutableMap<String, List<UIPost>> {
|
||||
val sorted =
|
||||
items.sortedWith { post1, post2 ->
|
||||
val post1Date = post1.createdAt.toLocalDateTime()
|
||||
@@ -130,29 +126,17 @@ constructor(
|
||||
.toImmutableMap()
|
||||
}
|
||||
|
||||
fun isPostSaved(post: SavedPost): Boolean {
|
||||
return _savedPosts.contains(post.shortId)
|
||||
}
|
||||
|
||||
fun toggleSave(post: SavedPost) {
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
val saved = isPostSaved(post)
|
||||
if (saved) {
|
||||
savedPostsRepository.removePost(post)
|
||||
} else {
|
||||
savedPostsRepository.savePost(post)
|
||||
}
|
||||
val newPosts = savedPosts.first()
|
||||
withContext(mainDispatcher) {
|
||||
SavedPostsWidget(newPosts.take(50)).updateAll(getApplication())
|
||||
}
|
||||
fun toggleSave(post: UIPost) {
|
||||
viewModelScope.launch {
|
||||
savedPostsRepository.toggleSave(post)
|
||||
withContext(mainDispatcher) { SavedPostsWidget(savedPosts).updateAll(getApplication()) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getPostComments(postId: String) =
|
||||
withContext(ioDispatcher) {
|
||||
when (val result = api.getPostDetails(postId)) {
|
||||
is Success -> result.value
|
||||
is Success -> result.value.toUIPost()
|
||||
is Failure.NetworkFailure -> throw result.error
|
||||
is Failure.UnknownFailure -> throw result.error
|
||||
is Failure.HttpFailure -> {
|
||||
@@ -198,8 +182,6 @@ constructor(
|
||||
viewModelScope.launch { readPostsRepository.markRead(postId) }
|
||||
}
|
||||
|
||||
suspend fun isPostRead(postId: String) = readPostsRepository.isRead(postId)
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
+5
-9
@@ -6,17 +6,16 @@
|
||||
*/
|
||||
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.SavedPostSerializer
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.database.local.SavedPostQueries
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
@@ -29,8 +28,7 @@ class DataTransferRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val json: Json,
|
||||
private val savedPostQueries: SavedPostQueries,
|
||||
@DatabaseDispatcher private val dbDispatcher: CoroutineDispatcher,
|
||||
private val savedPostsRepository: SavedPostsRepository,
|
||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
private val serializer = ListSerializer(SavedPostSerializer)
|
||||
@@ -38,13 +36,11 @@ constructor(
|
||||
suspend fun importPosts(input: InputStream) {
|
||||
val posts: List<SavedPost> =
|
||||
withContext(ioDispatcher) { json.decodeFromStream(serializer, input) }
|
||||
withContext(dbDispatcher) {
|
||||
savedPostQueries.transaction { posts.forEach(savedPostQueries::insertOrReplacePost) }
|
||||
}
|
||||
savedPostsRepository.savePosts(posts)
|
||||
}
|
||||
|
||||
suspend fun exportPostsAsJson(output: OutputStream) {
|
||||
val posts = withContext(dbDispatcher) { savedPostQueries.selectAllPosts().executeAsList() }
|
||||
val posts = savedPostsRepository.savedPosts.first()
|
||||
withContext(ioDispatcher) { json.encodeToStream(serializer, posts, output) }
|
||||
}
|
||||
|
||||
@@ -55,7 +51,7 @@ constructor(
|
||||
return instant.toEpochMilli()
|
||||
}
|
||||
|
||||
val posts = withContext(dbDispatcher) { savedPostQueries.selectAllPosts().executeAsList() }
|
||||
val posts = savedPostsRepository.savedPosts.first()
|
||||
val header =
|
||||
"""
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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 app.cash.sqldelight.coroutines.asFlow
|
||||
import app.cash.sqldelight.coroutines.mapToList
|
||||
import dev.msfjarvis.claw.core.injection.DatabaseDispatcher
|
||||
import dev.msfjarvis.claw.database.local.ReadPostsQueries
|
||||
import javax.inject.Inject
|
||||
@@ -18,11 +20,9 @@ constructor(
|
||||
private val readPostsQueries: ReadPostsQueries,
|
||||
@DatabaseDispatcher private val dbDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
val readPosts = readPostsQueries.selectAllPosts().asFlow().mapToList(dbDispatcher)
|
||||
|
||||
suspend fun markRead(postId: String) {
|
||||
withContext(dbDispatcher) { readPostsQueries.markRead(postId) }
|
||||
}
|
||||
|
||||
suspend fun isRead(postId: String): Boolean =
|
||||
withContext(dbDispatcher) { readPostsQueries.isRead(postId).executeAsOneOrNull() != null }
|
||||
}
|
||||
|
||||
+11
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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.
|
||||
@@ -11,6 +11,8 @@ import app.cash.sqldelight.coroutines.mapToList
|
||||
import dev.msfjarvis.claw.core.injection.DatabaseDispatcher
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.database.local.SavedPostQueries
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.toSavedPost
|
||||
import io.github.aakira.napier.Napier
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
@@ -24,9 +26,14 @@ constructor(
|
||||
) {
|
||||
val savedPosts = savedPostQueries.selectAllPosts().asFlow().mapToList(dbDispatcher)
|
||||
|
||||
suspend fun savePost(post: SavedPost) {
|
||||
Napier.d(tag = TAG) { "Saving post: ${post.shortId}" }
|
||||
withContext(dbDispatcher) { savedPostQueries.insertOrReplacePost(post) }
|
||||
suspend fun toggleSave(post: UIPost) {
|
||||
if (post.isSaved) {
|
||||
Napier.d(tag = TAG) { "Removing post: ${post.shortId}" }
|
||||
withContext(dbDispatcher) { savedPostQueries.deletePost(post.shortId) }
|
||||
} else {
|
||||
Napier.d(tag = TAG) { "Saving post: ${post.shortId}" }
|
||||
withContext(dbDispatcher) { savedPostQueries.insertOrReplacePost(post.toSavedPost()) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun savePosts(posts: List<SavedPost>) {
|
||||
@@ -38,11 +45,6 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removePost(post: SavedPost) {
|
||||
Napier.d(tag = TAG) { "Removing post: ${post.shortId}" }
|
||||
withContext(dbDispatcher) { savedPostQueries.deletePost(post.shortId) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "SavedPostsRepository"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.
|
||||
@@ -18,9 +18,12 @@ import dev.msfjarvis.claw.android.glance.SavedPostsWidget
|
||||
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
|
||||
import dev.msfjarvis.claw.api.LobstersApi
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import dev.msfjarvis.claw.model.fromSavedPost
|
||||
import dev.msfjarvis.claw.model.toSavedPost
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* WorkManager-backed [CoroutineWorker] that gets all the posts from [SavedPostsRepository], fetches
|
||||
@@ -44,7 +47,10 @@ constructor(
|
||||
.filterIsInstance<Success<LobstersPostDetails>>()
|
||||
.map { result -> result.value.toSavedPost() }
|
||||
.let { savedPostsRepository.savePosts(it) }
|
||||
SavedPostsWidget(savedPostsRepository.savedPosts.first().take(50)).updateAll(applicationContext)
|
||||
SavedPostsWidget(
|
||||
savedPostsRepository.savedPosts.map { it.map(UIPost.Companion::fromSavedPost) }
|
||||
)
|
||||
.updateAll(applicationContext)
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# This file was automatically generated by 'versioning-plugin'. DO NOT EDIT MANUALLY.
|
||||
#
|
||||
versioning-plugin.versionCode=13900
|
||||
versioning-plugin.versionName=1.39.0
|
||||
versioning-plugin.versionCode=14800
|
||||
versioning-plugin.versionName=1.48.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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.
|
||||
@@ -18,17 +18,18 @@ android { namespace = "dev.msfjarvis.claw.api" }
|
||||
anvil { generateDaggerFactories.set(true) }
|
||||
|
||||
dependencies {
|
||||
api(libs.eithernet)
|
||||
api(libs.dagger)
|
||||
api(libs.javax.inject)
|
||||
api(libs.okhttp.core)
|
||||
api(libs.retrofit)
|
||||
api(projects.model)
|
||||
|
||||
implementation(libs.dagger)
|
||||
implementation(libs.javax.inject)
|
||||
implementation(libs.eithernet)
|
||||
implementation(libs.jsoup)
|
||||
|
||||
testImplementation(testFixtures(libs.eithernet))
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.kotlinx.serialization.core)
|
||||
testImplementation(libs.kotlinx.serialization.json)
|
||||
testImplementation(libs.retrofit.kotlinxSerializationConverter)
|
||||
addTestDependencies(project)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha16" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha16)" variant="all" version="8.3.0-alpha16">
|
||||
<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0)" variant="all" version="8.3.0">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -8,7 +8,6 @@ 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
|
||||
@@ -32,8 +31,7 @@ object SearchConverter : Converter<ResponseBody, List<LobstersPost>> {
|
||||
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"))
|
||||
val submitter = elem.select("div.byline > a.u-author").text()
|
||||
return LobstersPost(
|
||||
shortId = shortId,
|
||||
title = title,
|
||||
@@ -55,24 +53,6 @@ object SearchConverter : Converter<ResponseBody, List<LobstersPost>> {
|
||||
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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.
|
||||
@@ -33,7 +33,7 @@ class ApiTest {
|
||||
val posts = api.getHottestPosts(1)
|
||||
assertIs<Success<List<LobstersPost>>>(posts)
|
||||
val commentsOnlyPosts = posts.value.asSequence().filter { it.url.isEmpty() }.toSet()
|
||||
assertThat(commentsOnlyPosts).hasSize(2)
|
||||
assertThat(commentsOnlyPosts).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 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.
|
||||
@@ -10,7 +10,6 @@ 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
|
||||
@@ -38,7 +37,7 @@ class SearchApiTest {
|
||||
createdAt = "",
|
||||
commentCount = 3,
|
||||
commentsUrl = "https://lobste.rs/s/gjlsdg/chatgpt_visits_emacs_doctor",
|
||||
submitter = User("xenodium", "", null, "/avatars/xenodium-16.png", ""),
|
||||
submitter = "xenodium",
|
||||
tags = listOf("ai", "emacs"),
|
||||
description = "",
|
||||
),
|
||||
@@ -50,7 +49,7 @@ class SearchApiTest {
|
||||
createdAt = "",
|
||||
commentCount = 0,
|
||||
commentsUrl = "https://lobste.rs/s/astcqf/implementing_question_answering_system",
|
||||
submitter = User("asteroid", "", null, "/avatars/asteroid-16.png", ""),
|
||||
submitter = "asteroid",
|
||||
tags = listOf("ai"),
|
||||
description = "",
|
||||
),
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"username":"msfjarvis","created_at":"2020-04-24T11:41:56.000-05:00","is_admin":false,"about":"Android and Kotlin developer\r\n","is_moderator":false,"karma":574,"avatar_url":"/avatars/msfjarvis-100.png","invited_by_user":"Amolith","github_username":"msfjarvis","twitter_username":"msfjarvis"}
|
||||
{"username":"msfjarvis","created_at":"2020-04-24T11:41:56.000-05:00","is_admin":false,"about":"Android and Kotlin developer, currently working for [Dyte](https://dyte.io/)","is_moderator":false,"karma":1343,"avatar_url":"/avatars/msfjarvis-100.png","invited_by_user":"Amolith","github_username":"msfjarvis"}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,13 +1,11 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.
|
||||
*/
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||
|
||||
plugins {
|
||||
id(libs.plugins.android.test.get().pluginId)
|
||||
id("dev.msfjarvis.claw.kotlin-android")
|
||||
@@ -30,21 +28,11 @@ android {
|
||||
matchingFallbacks += "release"
|
||||
}
|
||||
}
|
||||
|
||||
testOptions.managedDevices.devices {
|
||||
create<ManagedVirtualDevice>("api31") {
|
||||
device = "Pixel 6"
|
||||
apiLevel = 31
|
||||
systemImageSource = "aosp"
|
||||
}
|
||||
}
|
||||
|
||||
targetProjectPath = ":android"
|
||||
}
|
||||
|
||||
baselineProfile {
|
||||
managedDevices += "api31"
|
||||
useConnectedDevices = false
|
||||
useConnectedDevices = true
|
||||
enableEmulatorDisplay = false
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
plugins { `kotlin-dsl` }
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
alias(libs.plugins.android.lint)
|
||||
}
|
||||
|
||||
kotlin.jvmToolchain(17)
|
||||
|
||||
@@ -54,9 +57,15 @@ gradlePlugin {
|
||||
id = "dev.msfjarvis.claw.versioning-plugin"
|
||||
implementationClass = "dev.msfjarvis.claw.gradle.versioning.VersioningPlugin"
|
||||
}
|
||||
register("versions") {
|
||||
id = "dev.msfjarvis.claw.versions"
|
||||
implementationClass = "dev.msfjarvis.claw.gradle.DependencyUpdatesPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lint.baseline = project.file("lint-baseline.xml")
|
||||
|
||||
dependencies {
|
||||
implementation(libs.build.agp)
|
||||
implementation(libs.build.cachefix)
|
||||
@@ -64,7 +73,10 @@ dependencies {
|
||||
implementation(libs.build.semver)
|
||||
implementation(libs.build.sentry)
|
||||
implementation(libs.build.spotless)
|
||||
implementation(libs.build.vcu)
|
||||
|
||||
// Expose the generated version catalog API to the plugin.
|
||||
implementation(files(libs::class.java.superclass.protectionDomain.codeSource.location))
|
||||
|
||||
lintChecks(libs.androidx.lint)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0)" variant="all" version="8.3.0">
|
||||
|
||||
</issues>
|
||||
@@ -23,6 +23,14 @@ pluginManagement {
|
||||
includeModule("org.gradle.kotlin", "gradle-kotlin-dsl-plugins")
|
||||
}
|
||||
}
|
||||
exclusiveContent {
|
||||
forRepository { google() }
|
||||
filter {
|
||||
includeGroup("androidx.databinding")
|
||||
includeGroup("com.google.testing.platform")
|
||||
includeGroupAndSubgroups("com.android")
|
||||
}
|
||||
}
|
||||
mavenCentral { mavenContent { releasesOnly() } }
|
||||
}
|
||||
}
|
||||
@@ -33,6 +41,7 @@ dependencyResolutionManagement {
|
||||
forRepository(::google)
|
||||
filter {
|
||||
includeGroup("androidx.databinding")
|
||||
includeGroup("androidx.lint")
|
||||
includeGroupByRegex("com.android.*")
|
||||
includeGroup("com.google.testing.platform")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.
|
||||
@@ -33,26 +33,22 @@ class ApplicationPlugin : Plugin<Project> {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
all {
|
||||
setProguardFiles(
|
||||
listOf(
|
||||
"proguard-android-optimize.pro",
|
||||
"proguard-rules.pro",
|
||||
"proguard-rules-missing-classes.pro",
|
||||
)
|
||||
buildTypes.configureEach {
|
||||
setProguardFiles(
|
||||
listOf(
|
||||
"proguard-android-optimize.pro",
|
||||
"proguard-rules.pro",
|
||||
"proguard-rules-missing-classes.pro",
|
||||
)
|
||||
}
|
||||
named("release") {
|
||||
)
|
||||
if (name == "release") {
|
||||
isMinifyEnabled = !project.providers.environmentVariable("DISABLE_MINIFY").isPresent
|
||||
}
|
||||
named("debug") {
|
||||
} else if (name == "debug") {
|
||||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-debug"
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
project.configureBuildSigning()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright © 2024 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 com.github.zafarkhaja.semver.Version
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import nl.littlerobots.vcu.plugin.VersionCatalogUpdateExtension
|
||||
import nl.littlerobots.vcu.plugin.VersionCatalogUpdatePlugin
|
||||
import nl.littlerobots.vcu.plugin.versionSelector
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
|
||||
@Suppress("Unused")
|
||||
class DependencyUpdatesPlugin : Plugin<Project> {
|
||||
override fun apply(project: Project) {
|
||||
project.pluginManager.apply(VersionCatalogUpdatePlugin::class)
|
||||
project.extensions.configure<VersionCatalogUpdateExtension> {
|
||||
keep.keepUnusedLibraries.set(true)
|
||||
versionSelector {
|
||||
val currentVersion = Version.tryParse(it.currentVersion).getOrNull()
|
||||
val newVersion = Version.tryParse(it.candidate.version).getOrNull()
|
||||
if (currentVersion == null || newVersion == null) {
|
||||
false
|
||||
} else {
|
||||
newVersion.isHigherThan(currentVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.
|
||||
@@ -7,15 +7,12 @@
|
||||
package dev.msfjarvis.claw.gradle
|
||||
|
||||
import dev.msfjarvis.claw.gradle.KotlinCommonPlugin.Companion.JVM_TOOLCHAIN_ACTION
|
||||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
@Suppress("Unused")
|
||||
class KotlinAndroidPlugin : Plugin<Project> {
|
||||
@@ -26,25 +23,5 @@ class KotlinAndroidPlugin : Plugin<Project> {
|
||||
apply(KotlinCommonPlugin::class)
|
||||
}
|
||||
project.extensions.getByType<KotlinProjectExtension>().jvmToolchain(JVM_TOOLCHAIN_ACTION)
|
||||
val libs = project.extensions.getByName("libs") as LibrariesForLibs
|
||||
val composeCompilerVersion = libs.versions.composeCompiler.get()
|
||||
val kotlinVersion = libs.versions.kotlin.get()
|
||||
val matches = COMPOSE_COMPILER_VERSION_REGEX.find(composeCompilerVersion)
|
||||
if (matches != null) {
|
||||
val (compilerKotlinVersion) = matches.destructured
|
||||
if (compilerKotlinVersion != kotlinVersion) {
|
||||
project.tasks.withType<KotlinCompile>().configureEach {
|
||||
compilerOptions.freeCompilerArgs.addAll(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=$kotlinVersion",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// Matches against 1.5.0-dev-k1.9.0-6a60475e07f
|
||||
val COMPOSE_COMPILER_VERSION_REGEX = "\\d.\\d.\\d-dev-k(\\d.\\d.\\d+)-[a-z0-9]+".toRegex()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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,7 @@ class KotlinKaptPlugin : Plugin<Project> {
|
||||
}
|
||||
}
|
||||
project.tasks
|
||||
.matching { it.name.startsWith("kapt") && it.name.endsWith("UnitTestKotlin") }
|
||||
.named { it.startsWith("kapt") && it.endsWith("UnitTestKotlin") }
|
||||
.configureEach { enabled = false }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 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.
|
||||
@@ -31,15 +31,13 @@ object LintConfig {
|
||||
if (!isJVM) {
|
||||
enable += "ComposeM2Api"
|
||||
error += "ComposeM2Api"
|
||||
// False-positives in the TestContainers library
|
||||
disable += "DeprecatedCall"
|
||||
}
|
||||
baseline = project.file("lint-baseline.xml")
|
||||
// This is extremely annoying
|
||||
disable += "AndroidGradlePluginVersion"
|
||||
disable += "GradleDependency"
|
||||
disable += "NewerVersionAvailable"
|
||||
// Jetpack Compose lints are incompatible with AGP 8.3.0-alpha06
|
||||
// Can't do anything about this
|
||||
disable += "ObsoleteLintCustomCheck"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 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.
|
||||
@@ -25,25 +25,33 @@ class SentryPlugin : Plugin<Project> {
|
||||
project.extensions.configure<ApplicationAndroidComponentsExtension> {
|
||||
onVariants(selector().all()) { variant ->
|
||||
val sentryDsn = project.providers.environmentVariable(SENTRY_DSN_PROPERTY)
|
||||
val enableSentry =
|
||||
project.providers.gradleProperty(SENTRY_ENABLE_GRADLE_PROPERTY).isPresent
|
||||
variant.manifestPlaceholders.put("sentryDsn", sentryDsn.getOrElse(""))
|
||||
variant.manifestPlaceholders.put("enableSentry", "${variant.name == "release"}")
|
||||
variant.manifestPlaceholders.put(
|
||||
"sentryEnvironment",
|
||||
when {
|
||||
variant.name.contains("release", true) && enableSentry -> "production"
|
||||
variant.name.contains("release", true) && !enableSentry -> "nightly"
|
||||
else -> "dev"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
project.plugins.apply(io.sentry.android.gradle.SentryPlugin::class)
|
||||
project.extensions.configure<SentryPluginExtension> {
|
||||
val enableMappings =
|
||||
project.providers.gradleProperty(SENTRY_UPLOAD_MAPPINGS_PROPERTY).isPresent
|
||||
includeProguardMapping.set(enableMappings)
|
||||
autoUploadProguardMapping.set(enableMappings)
|
||||
includeProguardMapping.set(true)
|
||||
autoUploadProguardMapping.set(true)
|
||||
uploadNativeSymbols.set(false)
|
||||
autoUploadNativeSymbols.set(false)
|
||||
includeNativeSources.set(false)
|
||||
ignoredVariants.set(emptySet())
|
||||
ignoredBuildTypes.set(setOf("benchmark", "debug"))
|
||||
ignoredBuildTypes.set(setOf("benchmark"))
|
||||
ignoredFlavors.set(emptySet())
|
||||
tracingInstrumentation {
|
||||
enabled.set(true)
|
||||
debug.set(false)
|
||||
logcat.enabled.set(true)
|
||||
forceInstrumentDependencies.set(false)
|
||||
features.set(EnumSet.allOf(InstrumentationFeature::class.java))
|
||||
}
|
||||
@@ -53,14 +61,27 @@ class SentryPlugin : Plugin<Project> {
|
||||
sentryVersion.set(libs.versions.sentry.sdk)
|
||||
}
|
||||
includeDependenciesReport.set(true)
|
||||
includeSourceContext.set(true)
|
||||
autoUploadSourceContext.set(true)
|
||||
additionalSourceDirsForSourceContext.set(emptySet())
|
||||
debug.set(false)
|
||||
org.set("claw")
|
||||
projectName.set("compose-lobsters")
|
||||
authToken.set(project.providers.environmentVariable("SENTRY_AUTH_TOKEN"))
|
||||
url.set(null)
|
||||
telemetry.set(false)
|
||||
telemetryDsn.set(null)
|
||||
}
|
||||
with(project.dependencies) {
|
||||
addProvider("implementation", platform(libs.sentry.bom))
|
||||
addProvider("implementation", libs.sentry.android)
|
||||
}
|
||||
with(project.dependencies) { addProvider("implementation", platform(libs.sentry.bom)) }
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
private const val SENTRY_DSN_PROPERTY = "SENTRY_DSN"
|
||||
private const val SENTRY_UPLOAD_MAPPINGS_PROPERTY = "sentryUploadMappings"
|
||||
private const val SENTRY_ENABLE_GRADLE_PROPERTY = "enableSentry"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,6 @@ class SpotlessPlugin : Plugin<Project> {
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val KTFMT_VERSION = "0.47"
|
||||
private const val KTFMT_VERSION = "0.49"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 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.
|
||||
@@ -16,19 +16,23 @@ import org.gradle.kotlin.dsl.exclude
|
||||
/** Extension function to configure JUnit5 dependencies with the Truth assertion library. */
|
||||
fun DependencyHandlerScope.addTestDependencies(project: Project) {
|
||||
val libs = project.extensions.getByName("libs") as LibrariesForLibs
|
||||
addProvider("testImplementation", libs.junit.jupiter.api)
|
||||
addProvider<MinimalExternalModuleDependency, ExternalModuleDependency>(
|
||||
"testImplementation",
|
||||
libs.truth,
|
||||
) {
|
||||
exclude(group = "junit", module = "junit")
|
||||
}
|
||||
addProvider("testRuntimeOnly", libs.junit.jupiter.engine)
|
||||
addProvider<MinimalExternalModuleDependency, ExternalModuleDependency>(
|
||||
"testRuntimeOnly",
|
||||
libs.junit.legacy,
|
||||
) {
|
||||
// See https://github.com/google/truth/issues/333
|
||||
because("Truth needs it")
|
||||
}
|
||||
arrayOf("test", "androidTest")
|
||||
.filter { sourceSet -> project.configurations.findByName("${sourceSet}Implementation") != null }
|
||||
.forEach { sourceSet ->
|
||||
addProvider("${sourceSet}Implementation", libs.junit.jupiter.api)
|
||||
addProvider<MinimalExternalModuleDependency, ExternalModuleDependency>(
|
||||
"${sourceSet}Implementation",
|
||||
libs.truth,
|
||||
) {
|
||||
exclude(group = "junit", module = "junit")
|
||||
}
|
||||
addProvider("${sourceSet}RuntimeOnly", libs.junit.jupiter.engine)
|
||||
addProvider<MinimalExternalModuleDependency, ExternalModuleDependency>(
|
||||
"${sourceSet}RuntimeOnly",
|
||||
libs.junit.legacy,
|
||||
) {
|
||||
// See https://github.com/google/truth/issues/333
|
||||
because("Truth needs it")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,34 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.signing
|
||||
|
||||
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
|
||||
import com.android.build.api.dsl.ApplicationBuildType
|
||||
import com.android.build.api.dsl.CommonExtension
|
||||
import java.util.Properties
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
|
||||
private const val KEYSTORE_CONFIG_PATH = "keystore.properties"
|
||||
|
||||
/** Configure signing for all build types. */
|
||||
@Suppress("UnstableApiUsage")
|
||||
internal fun Project.configureBuildSigning() {
|
||||
val keystoreConfigFile = rootProject.layout.projectDirectory.file(KEYSTORE_CONFIG_PATH)
|
||||
if (keystoreConfigFile.asFile.exists()) {
|
||||
extensions.configure<BaseAppModuleExtension> {
|
||||
extensions.configure<CommonExtension<*, ApplicationBuildType, *, *, *, *>>("android") {
|
||||
val contents = providers.fileContents(keystoreConfigFile).asText
|
||||
val keystoreProperties = Properties()
|
||||
keystoreProperties.load(contents.get().byteInputStream())
|
||||
signingConfigs {
|
||||
register("release") {
|
||||
val releaseSigningConfig =
|
||||
signingConfigs.register("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["keyPassword"] as String
|
||||
storeFile = rootProject.file(keystoreProperties["storeFile"] as String)
|
||||
storePassword = keystoreProperties["storePassword"] as String
|
||||
}
|
||||
}
|
||||
val signingConfig = signingConfigs.getByName("release")
|
||||
buildTypes.all { setSigningConfig(signingConfig) }
|
||||
buildTypes.configureEach { signingConfig = releaseSigningConfig.get() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-21
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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,19 @@ package dev.msfjarvis.claw.gradle.versioning
|
||||
|
||||
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
|
||||
import com.android.build.api.variant.VariantOutputConfiguration
|
||||
import com.android.build.gradle.internal.plugins.AppPlugin
|
||||
import com.vdurmont.semver4j.Semver
|
||||
import com.github.zafarkhaja.semver.Version
|
||||
import java.util.Properties
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.register
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
|
||||
/**
|
||||
* A Gradle [Plugin] that takes a [Project] with the [AppPlugin] applied and dynamically sets the
|
||||
* versionCode and versionName properties based on values read from a [VERSIONING_PROP_FILE] file in
|
||||
* the [Project.getProjectDir] directory. It also adds Gradle tasks to bump the major, minor, and
|
||||
* patch versions along with one to prepare the next snapshot.
|
||||
* A Gradle [Plugin] that takes a [Project] with the `com.android.application` applied and
|
||||
* dynamically sets the versionCode and versionName properties based on values read from a
|
||||
* [VERSIONING_PROP_FILE] file in the [Project.getProjectDir] directory. It also adds Gradle tasks
|
||||
* to bump the major, minor, and patch versions along with one to prepare the next snapshot.
|
||||
*/
|
||||
@Suppress("Unused")
|
||||
class VersioningPlugin : Plugin<Project> {
|
||||
@@ -37,14 +35,24 @@ class VersioningPlugin : Plugin<Project> {
|
||||
val contents = providers.fileContents(propFile).asText
|
||||
val versionProps = Properties().also { it.load(contents.get().byteInputStream()) }
|
||||
val versionName =
|
||||
requireNotNull(versionProps.getProperty(VERSIONING_PROP_VERSION_NAME)) {
|
||||
"version.properties must contain a '$VERSIONING_PROP_VERSION_NAME' property"
|
||||
}
|
||||
providers
|
||||
.gradleProperty("VERSION_NAME")
|
||||
.orElse(
|
||||
requireNotNull(versionProps.getProperty(VERSIONING_PROP_VERSION_NAME)) {
|
||||
"version.properties must contain a '$VERSIONING_PROP_VERSION_NAME' property"
|
||||
}
|
||||
)
|
||||
val versionCode =
|
||||
requireNotNull(versionProps.getProperty(VERSIONING_PROP_VERSION_CODE).toInt()) {
|
||||
"version.properties must contain a '$VERSIONING_PROP_VERSION_CODE' property"
|
||||
}
|
||||
project.plugins.withType<AppPlugin> {
|
||||
providers
|
||||
.gradleProperty("VERSION_CODE")
|
||||
.orElse(
|
||||
requireNotNull(versionProps.getProperty(VERSIONING_PROP_VERSION_CODE)) {
|
||||
"version.properties must contain a '$VERSIONING_PROP_VERSION_CODE' property"
|
||||
}
|
||||
)
|
||||
.map(String::toInt)
|
||||
|
||||
project.pluginManager.withPlugin("com.android.application") {
|
||||
androidAppPluginApplied.set(true)
|
||||
extensions.configure<ApplicationAndroidComponentsExtension> {
|
||||
onVariants { variant ->
|
||||
@@ -57,30 +65,30 @@ class VersioningPlugin : Plugin<Project> {
|
||||
}
|
||||
}
|
||||
}
|
||||
val version = Semver(versionName)
|
||||
val version = versionName.map(Version::parse)
|
||||
tasks.register<VersioningTask>("clearPreRelease") {
|
||||
description = "Remove the pre-release suffix from the version"
|
||||
semverString.set(version.withClearedSuffix().toString())
|
||||
semverString.set(version.map(Version::toStableVersion).map(Version::toString))
|
||||
propertyFile.set(propFile)
|
||||
}
|
||||
tasks.register<VersioningTask>("bumpMajor") {
|
||||
description = "Increment the major version"
|
||||
semverString.set(version.withIncMajor().withClearedSuffix().toString())
|
||||
semverString.set(version.map(Version::nextMajorVersion).map(Version::toString))
|
||||
propertyFile.set(propFile)
|
||||
}
|
||||
tasks.register<VersioningTask>("bumpMinor") {
|
||||
description = "Increment the minor version"
|
||||
semverString.set(version.withIncMinor().withClearedSuffix().toString())
|
||||
semverString.set(version.map(Version::nextMinorVersion).map(Version::toString))
|
||||
propertyFile.set(propFile)
|
||||
}
|
||||
tasks.register<VersioningTask>("bumpPatch") {
|
||||
description = "Increment the patch version"
|
||||
semverString.set(version.withIncPatch().withClearedSuffix().toString())
|
||||
semverString.set(version.map(Version::nextPatchVersion).map(Version::toString))
|
||||
propertyFile.set(propFile)
|
||||
}
|
||||
tasks.register<VersioningTask>("bumpSnapshot") {
|
||||
description = "Increment the minor version and add the `SNAPSHOT` suffix"
|
||||
semverString.set(version.withIncMinor().withSuffix("SNAPSHOT").toString())
|
||||
semverString.set(version.map { it.nextMinorVersion("SNAPSHOT") }.map(Version::toString))
|
||||
propertyFile.set(propFile)
|
||||
}
|
||||
afterEvaluate {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* Copyright © 2022 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.versioning
|
||||
|
||||
import com.vdurmont.semver4j.Semver
|
||||
import com.github.zafarkhaja.semver.Version
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.provider.Property
|
||||
@@ -22,11 +22,11 @@ abstract class VersioningTask : DefaultTask() {
|
||||
@get:OutputFile abstract val propertyFile: RegularFileProperty
|
||||
|
||||
/** Generate the Android 'versionCode' property */
|
||||
private fun Semver.androidCode(): Int {
|
||||
return major * 1_00_00 + minor * 1_00 + patch
|
||||
private fun Version.androidCode(): Long {
|
||||
return majorVersion() * 1_00_00 + minorVersion() * 1_00 + patchVersion()
|
||||
}
|
||||
|
||||
private fun Semver.toPropFileText(): String {
|
||||
private fun Version.toPropFileText(): String {
|
||||
val newVersionCode = androidCode()
|
||||
val newVersionName = toString()
|
||||
return buildString {
|
||||
@@ -46,6 +46,6 @@ abstract class VersioningTask : DefaultTask() {
|
||||
|
||||
@TaskAction
|
||||
fun execute() {
|
||||
propertyFile.get().asFile.writeText(Semver(semverString.get()).toPropFileText())
|
||||
propertyFile.get().asFile.writeText(Version.parse(semverString.get()).toPropFileText())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,7 @@
|
||||
*/
|
||||
plugins {
|
||||
id("dev.msfjarvis.claw.spotless")
|
||||
id("dev.msfjarvis.claw.versions")
|
||||
id("dev.msfjarvis.claw.kotlin-common")
|
||||
alias(libs.plugins.dependencyAnalysis)
|
||||
}
|
||||
|
||||
+11
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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,6 +13,7 @@ plugins {
|
||||
id("dev.msfjarvis.claw.kotlin-android")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.whetstone)
|
||||
alias(libs.plugins.kotlin.composeCompiler)
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -20,10 +21,7 @@ android {
|
||||
androidResources = true
|
||||
compose = true
|
||||
}
|
||||
composeOptions {
|
||||
useLiveLiterals = false
|
||||
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
|
||||
}
|
||||
composeOptions { useLiveLiterals = false }
|
||||
namespace = "dev.msfjarvis.claw.common"
|
||||
}
|
||||
|
||||
@@ -31,7 +29,12 @@ androidComponents { beforeVariants { (it as HasUnitTestBuilder).enableUnitTest =
|
||||
|
||||
anvil { generateDaggerFactories.set(true) }
|
||||
|
||||
composeCompiler { enableStrongSkippingMode = true }
|
||||
|
||||
dependencies {
|
||||
api(libs.androidx.compose.ui)
|
||||
api(libs.dagger)
|
||||
api(libs.javax.inject)
|
||||
api(projects.core)
|
||||
api(projects.database.core)
|
||||
api(projects.model)
|
||||
@@ -44,12 +47,15 @@ dependencies {
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.runtime)
|
||||
implementation(libs.androidx.compose.ui.text)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.coil)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.compose.richtext.markdown)
|
||||
implementation(libs.compose.richtext.material3)
|
||||
implementation(libs.compose.richtext.ui)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinResult)
|
||||
implementation(libs.kotlinResult.coroutines)
|
||||
implementation(libs.napier)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha16" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha16)" variant="all" version="8.3.0-alpha16">
|
||||
<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0)" variant="all" version="8.3.0">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -29,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.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
@@ -46,22 +45,22 @@ import dev.msfjarvis.claw.common.posts.TagRow
|
||||
import dev.msfjarvis.claw.common.ui.NetworkImage
|
||||
import dev.msfjarvis.claw.common.ui.ThemedRichText
|
||||
import dev.msfjarvis.claw.model.LinkMetadata
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import java.time.Instant
|
||||
import java.time.temporal.TemporalAccessor
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun CommentsHeader(
|
||||
postDetails: LobstersPostDetails,
|
||||
post: UIPost,
|
||||
postActions: PostActions,
|
||||
htmlConverter: HTMLConverter,
|
||||
openUserProfile: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val linkMetadata by
|
||||
produceState(initialValue = LinkMetadata(postDetails.url, null)) {
|
||||
runSuspendCatching { postActions.getLinkMetadata(postDetails.url) }
|
||||
produceState(initialValue = LinkMetadata(post.url, null)) {
|
||||
runSuspendCatching { postActions.getLinkMetadata(post.url) }
|
||||
.onSuccess { metadata -> value = metadata }
|
||||
}
|
||||
|
||||
@@ -70,8 +69,8 @@ internal fun CommentsHeader(
|
||||
modifier = Modifier.padding(16.dp).fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
PostTitle(title = postDetails.title, isRead = false)
|
||||
TagRow(tags = postDetails.tags.toImmutableList())
|
||||
PostTitle(title = post.title, isRead = false, singleLineTitle = false)
|
||||
TagRow(tags = post.tags.toImmutableList())
|
||||
Spacer(Modifier.height(4.dp))
|
||||
|
||||
if (linkMetadata.url.isNotBlank()) {
|
||||
@@ -79,24 +78,21 @@ internal fun CommentsHeader(
|
||||
linkMetadata = linkMetadata,
|
||||
modifier =
|
||||
Modifier.clickable {
|
||||
postActions.viewPost(postDetails.shortId, linkMetadata.url, postDetails.commentsUrl)
|
||||
postActions.viewPost(post.shortId, linkMetadata.url, post.commentsUrl)
|
||||
},
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
|
||||
if (postDetails.description.isNotBlank()) {
|
||||
ThemedRichText(htmlConverter.convertHTMLToMarkdown(postDetails.description))
|
||||
if (post.description.isNotBlank()) {
|
||||
ThemedRichText(htmlConverter.convertHTMLToMarkdown(post.description))
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
Submitter(
|
||||
text = AnnotatedString("Submitted by ${postDetails.submitter.username}"),
|
||||
avatarUrl = "https://lobste.rs/${postDetails.submitter.avatarUrl}",
|
||||
contentDescription = "User avatar for ${postDetails.submitter.username}",
|
||||
modifier =
|
||||
Modifier.clickable {
|
||||
uriHandler.openUri("https://lobste.rs/u/${postDetails.submitter.username}")
|
||||
},
|
||||
text = AnnotatedString("Submitted by ${post.submitter}"),
|
||||
avatarUrl = "https://lobste.rs/avatars/${post.submitter}-100.png",
|
||||
contentDescription = "User avatar for ${post.submitter}",
|
||||
modifier = Modifier.clickable { openUserProfile(post.submitter) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -135,9 +131,9 @@ internal fun CommentEntry(
|
||||
commentNode: CommentNode,
|
||||
htmlConverter: HTMLConverter,
|
||||
toggleExpanded: (CommentNode) -> Unit,
|
||||
openUserProfile: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val comment = commentNode.comment
|
||||
Box(
|
||||
modifier =
|
||||
@@ -156,16 +152,15 @@ internal fun CommentEntry(
|
||||
Submitter(
|
||||
text =
|
||||
buildCommenterString(
|
||||
commenterName = comment.user.username,
|
||||
commenterName = comment.user,
|
||||
score = comment.score,
|
||||
isUnread = commentNode.isUnread,
|
||||
createdAt = comment.createdAt,
|
||||
updatedAt = comment.updatedAt,
|
||||
),
|
||||
avatarUrl = "https://lobste.rs/${comment.user.avatarUrl}",
|
||||
contentDescription = "User avatar for ${comment.user.username}",
|
||||
modifier =
|
||||
Modifier.clickable { uriHandler.openUri("https://lobste.rs/u/${comment.user.username}") },
|
||||
avatarUrl = "https://lobste.rs/avatars/${comment.user}-100.png",
|
||||
contentDescription = "User avatar for ${comment.user}",
|
||||
modifier = Modifier.clickable { openUserProfile(comment.user) },
|
||||
)
|
||||
if (commentNode.isExpanded) {
|
||||
ThemedRichText(
|
||||
|
||||
@@ -71,7 +71,7 @@ internal fun setExpanded(commentNode: CommentNode, expanded: Boolean): CommentNo
|
||||
return commentNode
|
||||
}
|
||||
|
||||
internal fun findTopMostParent(node: CommentNode): CommentNode {
|
||||
internal tailrec fun findTopMostParent(node: CommentNode): CommentNode {
|
||||
val parent = node.parent
|
||||
return if (parent != null) {
|
||||
findTopMostParent(parent)
|
||||
@@ -84,9 +84,15 @@ internal fun LazyListScope.nodes(
|
||||
nodes: List<CommentNode>,
|
||||
htmlConverter: HTMLConverter,
|
||||
toggleExpanded: (CommentNode) -> Unit,
|
||||
openUserProfile: (String) -> Unit,
|
||||
) {
|
||||
nodes.forEach { node ->
|
||||
node(node = node, htmlConverter = htmlConverter, toggleExpanded = toggleExpanded)
|
||||
node(
|
||||
node = node,
|
||||
htmlConverter = htmlConverter,
|
||||
toggleExpanded = toggleExpanded,
|
||||
openUserProfile = openUserProfile,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,16 +100,27 @@ private fun LazyListScope.node(
|
||||
node: CommentNode,
|
||||
htmlConverter: HTMLConverter,
|
||||
toggleExpanded: (CommentNode) -> Unit,
|
||||
openUserProfile: (String) -> Unit,
|
||||
) {
|
||||
// Skip the node if neither the node nor its parent is expanded
|
||||
if (!node.isExpanded && node.parent?.isExpanded == false) {
|
||||
return
|
||||
}
|
||||
item {
|
||||
CommentEntry(commentNode = node, htmlConverter = htmlConverter, toggleExpanded = toggleExpanded)
|
||||
item(key = node.comment.shortId) {
|
||||
CommentEntry(
|
||||
commentNode = node,
|
||||
htmlConverter = htmlConverter,
|
||||
toggleExpanded = toggleExpanded,
|
||||
openUserProfile = openUserProfile,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
if (node.children.isNotEmpty()) {
|
||||
nodes(node.children, htmlConverter = htmlConverter, toggleExpanded = toggleExpanded)
|
||||
nodes(
|
||||
node.children,
|
||||
htmlConverter = htmlConverter,
|
||||
toggleExpanded = toggleExpanded,
|
||||
openUserProfile = openUserProfile,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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.
|
||||
@@ -36,16 +36,17 @@ import dev.msfjarvis.claw.common.ui.NetworkError
|
||||
import dev.msfjarvis.claw.common.ui.ProgressBar
|
||||
import dev.msfjarvis.claw.database.local.PostComments
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun CommentsPageInternal(
|
||||
details: LobstersPostDetails,
|
||||
details: UIPost,
|
||||
postActions: PostActions,
|
||||
htmlConverter: HTMLConverter,
|
||||
commentState: PostComments?,
|
||||
markSeenComments: (String, List<Comment>) -> Unit,
|
||||
openUserProfile: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val commentNodes = createListNode(details.comments, commentState).toMutableStateList()
|
||||
@@ -55,9 +56,10 @@ private fun CommentsPageInternal(
|
||||
LazyColumn(modifier = modifier, contentPadding = PaddingValues(bottom = 24.dp)) {
|
||||
item {
|
||||
CommentsHeader(
|
||||
postDetails = details,
|
||||
post = details,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
openUserProfile = openUserProfile,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -83,6 +85,7 @@ private fun CommentsPageInternal(
|
||||
commentNodes.add(index, parent)
|
||||
}
|
||||
},
|
||||
openUserProfile = openUserProfile,
|
||||
)
|
||||
} else {
|
||||
item {
|
||||
@@ -108,6 +111,7 @@ fun CommentsPage(
|
||||
getSeenComments: suspend (String) -> PostComments?,
|
||||
markSeenComments: (String, List<Comment>) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
openUserProfile: (String) -> Unit,
|
||||
) {
|
||||
val postDetails by
|
||||
produceState<NetworkState>(Loading) {
|
||||
@@ -123,11 +127,12 @@ fun CommentsPage(
|
||||
when (postDetails) {
|
||||
is Success<*> -> {
|
||||
CommentsPageInternal(
|
||||
details = (postDetails as Success<LobstersPostDetails>).data,
|
||||
details = (postDetails as Success<UIPost>).data,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
commentState = commentState,
|
||||
markSeenComments = markSeenComments,
|
||||
openUserProfile = openUserProfile,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.common.posts
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -47,39 +48,47 @@ 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
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.NetworkImage
|
||||
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.LinkMetadata
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.User
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun LobstersCard(
|
||||
post: SavedPost,
|
||||
isSaved: Boolean,
|
||||
isRead: Boolean,
|
||||
post: UIPost,
|
||||
postActions: PostActions,
|
||||
refresh: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var localSavedState by remember(post, isSaved) { mutableStateOf(isSaved) }
|
||||
var localReadState by remember(post) { mutableStateOf(post.isRead) }
|
||||
var localSavedState by remember(post) { mutableStateOf(post.isSaved) }
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { postActions.viewPost(post.shortId, post.url, post.commentsUrl) }
|
||||
.clickable {
|
||||
postActions.viewPost(post.shortId, post.url, post.commentsUrl)
|
||||
localReadState = true
|
||||
refresh()
|
||||
}
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(start = 16.dp, top = 16.dp, end = 4.dp, bottom = 16.dp)
|
||||
.padding(start = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
PostDetails(modifier = Modifier.weight(1f), post = post, isRead = isRead)
|
||||
PostDetails(
|
||||
post = post,
|
||||
isRead = localReadState,
|
||||
singleLineTitle = true,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.wrapContentHeight(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
@@ -91,6 +100,7 @@ fun LobstersCard(
|
||||
Modifier.clickable(role = Role.Button) {
|
||||
localSavedState = !localSavedState
|
||||
postActions.toggleSave(post)
|
||||
refresh()
|
||||
},
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.width(48.dp))
|
||||
@@ -99,7 +109,11 @@ fun LobstersCard(
|
||||
modifier =
|
||||
Modifier.clickable(
|
||||
role = Role.Button,
|
||||
onClick = { postActions.viewComments(post.shortId) },
|
||||
onClick = {
|
||||
postActions.viewComments(post.shortId)
|
||||
localReadState = true
|
||||
refresh()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -108,27 +122,39 @@ fun LobstersCard(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PostDetails(post: SavedPost, isRead: Boolean, modifier: Modifier = Modifier) {
|
||||
fun PostDetails(
|
||||
post: UIPost,
|
||||
isRead: Boolean,
|
||||
singleLineTitle: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
PostTitle(title = post.title, isRead = isRead)
|
||||
PostTitle(title = post.title, isRead = isRead, singleLineTitle = singleLineTitle)
|
||||
TagRow(tags = post.tags.toImmutableList())
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Submitter(
|
||||
text = AnnotatedString("Submitted by ${post.submitterName}"),
|
||||
avatarUrl = "https://lobste.rs/${post.submitterAvatarUrl}",
|
||||
contentDescription = "User avatar for ${post.submitterName}",
|
||||
text = AnnotatedString("Submitted by ${post.submitter}"),
|
||||
avatarUrl = "https://lobste.rs/avatars/${post.submitter}-100.png",
|
||||
contentDescription = "User avatar for ${post.submitter}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun PostTitle(title: String, isRead: Boolean, modifier: Modifier = Modifier) {
|
||||
internal fun PostTitle(
|
||||
title: String,
|
||||
isRead: Boolean,
|
||||
singleLineTitle: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = modifier,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = if (isRead) FontWeight.Normal else FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = if (singleLineTitle) 1 else Int.MAX_VALUE,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -163,7 +189,7 @@ internal fun Submitter(
|
||||
@Composable
|
||||
private fun SaveButton(isSaved: Boolean, modifier: Modifier = Modifier) {
|
||||
Crossfade(targetState = isSaved, label = "save-button") { saved ->
|
||||
Box(modifier = modifier.padding(12.dp)) {
|
||||
Box(modifier = modifier.padding(vertical = 12.dp)) {
|
||||
Icon(
|
||||
imageVector = if (saved) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
@@ -177,7 +203,7 @@ private fun SaveButton(isSaved: Boolean, modifier: Modifier = Modifier) {
|
||||
@Composable
|
||||
private fun CommentsButton(commentCount: Int?, modifier: Modifier = Modifier) {
|
||||
BadgedBox(
|
||||
modifier = modifier.padding(12.dp),
|
||||
modifier = modifier.padding(vertical = 12.dp),
|
||||
badge = {
|
||||
if (commentCount != null) {
|
||||
Badge(
|
||||
@@ -228,55 +254,57 @@ private fun TagText(tag: String, modifier: Modifier = Modifier) {
|
||||
)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
val TEST_POST_ACTIONS =
|
||||
object : PostActions {
|
||||
override fun viewPost(postId: String, postUrl: String, commentsUrl: String) {}
|
||||
|
||||
override fun viewComments(postId: String) {}
|
||||
|
||||
override fun viewCommentsPage(post: UIPost) {}
|
||||
|
||||
override fun toggleSave(post: UIPost) {}
|
||||
|
||||
override fun share(post: UIPost) {}
|
||||
|
||||
override suspend fun getComments(postId: String): UIPost {
|
||||
return UIPost(
|
||||
shortId = "ooga",
|
||||
title = "Simple Anomaly Detection Using Plain SQL",
|
||||
url = "https://hakibenita.com/sql-anomaly-detection",
|
||||
createdAt = "2020-09-21T08:04:24.000-05:00",
|
||||
commentCount = 1,
|
||||
commentsUrl = "https://lobste.rs/s/q1hh1g/simple_anomaly_detection_using_plain_sql",
|
||||
tags = listOf("databases", "apis"),
|
||||
description = "",
|
||||
submitter = "Haki",
|
||||
comments = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getLinkMetadata(url: String): LinkMetadata {
|
||||
return LinkMetadata("", "")
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
val TEST_POST =
|
||||
UIPost(
|
||||
shortId = "ooga",
|
||||
title = "Simple Anomaly Detection Using Plain SQL",
|
||||
url = "https://hakibenita.com/sql-anomaly-detection",
|
||||
createdAt = "2020-09-21T08:04:24.000-05:00",
|
||||
commentCount = 1,
|
||||
commentsUrl = "https://lobste.rs/s/q1hh1g/simple_anomaly_detection_using_plain_sql",
|
||||
submitter = "Haki",
|
||||
tags = listOf("databases", "apis"),
|
||||
description = "",
|
||||
isSaved = true,
|
||||
isRead = true,
|
||||
)
|
||||
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun LobstersCardPreview() {
|
||||
LobstersTheme {
|
||||
LobstersCard(
|
||||
post =
|
||||
SavedPost(
|
||||
shortId = "ooga",
|
||||
title = "Simple Anomaly Detection Using Plain SQL",
|
||||
url = "https://hakibenita.com/sql-anomaly-detection",
|
||||
createdAt = "2020-09-21T08:04:24.000-05:00",
|
||||
commentCount = 1,
|
||||
commentsUrl = "https://lobste.rs/s/q1hh1g/simple_anomaly_detection_using_plain_sql",
|
||||
submitterName = "Haki",
|
||||
submitterAvatarUrl = "/avatars/Haki-100.png",
|
||||
tags = listOf("databases", "apis"),
|
||||
description = "",
|
||||
),
|
||||
isRead = true,
|
||||
isSaved = true,
|
||||
postActions =
|
||||
object : PostActions {
|
||||
override fun viewPost(postId: String, postUrl: String, commentsUrl: String) {}
|
||||
|
||||
override fun viewComments(postId: String) {}
|
||||
|
||||
override fun viewCommentsPage(commentsUrl: String) {}
|
||||
|
||||
override fun toggleSave(post: SavedPost) {}
|
||||
|
||||
override suspend fun getComments(postId: String): LobstersPostDetails {
|
||||
return LobstersPostDetails(
|
||||
shortId = "ooga",
|
||||
title = "Simple Anomaly Detection Using Plain SQL",
|
||||
url = "https://hakibenita.com/sql-anomaly-detection",
|
||||
createdAt = "2020-09-21T08:04:24.000-05:00",
|
||||
commentCount = 1,
|
||||
commentsUrl = "https://lobste.rs/s/q1hh1g/simple_anomaly_detection_using_plain_sql",
|
||||
tags = listOf("databases", "apis"),
|
||||
description = "",
|
||||
submitter = User("Haki", "", "", "", ""),
|
||||
comments = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getLinkMetadata(url: String): LinkMetadata {
|
||||
return LinkMetadata("", "")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
private fun LobstersCardPreview() {
|
||||
LobstersTheme { LobstersCard(post = TEST_POST, postActions = TEST_POST_ACTIONS, refresh = {}) }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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.
|
||||
@@ -7,9 +7,8 @@
|
||||
package dev.msfjarvis.claw.common.posts
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import dev.msfjarvis.claw.database.local.SavedPost
|
||||
import dev.msfjarvis.claw.model.LinkMetadata
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
|
||||
@Stable
|
||||
interface PostActions {
|
||||
@@ -17,11 +16,13 @@ interface PostActions {
|
||||
|
||||
fun viewComments(postId: String)
|
||||
|
||||
fun viewCommentsPage(commentsUrl: String)
|
||||
fun viewCommentsPage(post: UIPost)
|
||||
|
||||
fun toggleSave(post: SavedPost)
|
||||
fun toggleSave(post: UIPost)
|
||||
|
||||
suspend fun getComments(postId: String): LobstersPostDetails
|
||||
fun share(post: UIPost)
|
||||
|
||||
suspend fun getComments(postId: String): UIPost
|
||||
|
||||
suspend fun getLinkMetadata(url: String): LinkMetadata
|
||||
}
|
||||
|
||||
@@ -1,20 +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.
|
||||
*/
|
||||
package dev.msfjarvis.claw.common.ui
|
||||
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.LocalAbsoluteTonalElevation
|
||||
import androidx.compose.material3.NavigationBarDefaults
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Composable
|
||||
fun ColorScheme.surfaceColorAtNavigationBarElevation(): Color {
|
||||
val elevation = LocalAbsoluteTonalElevation.current + NavigationBarDefaults.Elevation
|
||||
return surfaceColorAtElevation(elevation)
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.commonmark.Markdown
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.material3.RichText
|
||||
import com.halilibo.richtext.ui.string.RichTextStringStyle
|
||||
|
||||
@@ -30,7 +30,7 @@ import io.github.aakira.napier.Napier
|
||||
|
||||
@Composable
|
||||
fun NetworkError(label: String, error: Throwable, modifier: Modifier = Modifier) {
|
||||
LaunchedEffect(true) { Napier.e(error, "NetworkError") { "Failed to load posts" } }
|
||||
LaunchedEffect(Unit) { Napier.e(error, "NetworkError") { "Failed to load posts" } }
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp), modifier = modifier) {
|
||||
Text(
|
||||
|
||||
@@ -70,7 +70,7 @@ fun SearchBar(
|
||||
@DevicePreviews
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun SearchBarPreview() {
|
||||
private fun SearchBarPreview() {
|
||||
LobstersTheme {
|
||||
Box(Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.background).padding(8.dp)) {
|
||||
var value by remember { mutableStateOf("") }
|
||||
|
||||
@@ -19,7 +19,6 @@ import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.theme.LobstersTheme
|
||||
import dev.msfjarvis.claw.common.ui.preview.DevicePreviews
|
||||
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
|
||||
import dev.msfjarvis.claw.common.ui.surfaceColorAtNavigationBarElevation
|
||||
|
||||
@Composable
|
||||
fun MonthHeader(label: String, modifier: Modifier = Modifier) {
|
||||
@@ -27,7 +26,7 @@ fun MonthHeader(label: String, modifier: Modifier = Modifier) {
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.background(MaterialTheme.colorScheme.surfaceColorAtNavigationBarElevation())
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
@@ -40,6 +39,6 @@ fun MonthHeader(label: String, modifier: Modifier = Modifier) {
|
||||
@DevicePreviews
|
||||
@ThemePreviews
|
||||
@Composable
|
||||
fun MonthHeaderPreview() {
|
||||
private fun MonthHeaderPreview() {
|
||||
LobstersTheme { MonthHeader("April 2023") }
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ 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.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.michaelbull.result.coroutines.runSuspendCatching
|
||||
import com.github.michaelbull.result.fold
|
||||
@@ -42,6 +44,7 @@ import dev.msfjarvis.claw.model.User
|
||||
fun UserProfile(
|
||||
username: String,
|
||||
getProfile: suspend (username: String) -> User,
|
||||
openUserProfile: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val user by
|
||||
@@ -56,7 +59,11 @@ fun UserProfile(
|
||||
}
|
||||
when (user) {
|
||||
is Success<*> -> {
|
||||
UserProfileInternal(user = (user as Success<User>).data, modifier = modifier)
|
||||
UserProfileInternal(
|
||||
user = (user as Success<User>).data,
|
||||
openUserProfile = openUserProfile,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
is Error -> {
|
||||
val error = user as Error
|
||||
@@ -77,7 +84,11 @@ fun UserProfile(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserProfileInternal(user: User, modifier: Modifier = Modifier) {
|
||||
private fun UserProfileInternal(
|
||||
user: User,
|
||||
openUserProfile: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(modifier = modifier) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
@@ -93,7 +104,19 @@ private fun UserProfileInternal(user: User, modifier: Modifier = Modifier) {
|
||||
Text(text = user.username, style = MaterialTheme.typography.displaySmall)
|
||||
ThemedRichText(text = user.about)
|
||||
user.invitedBy?.let { invitedBy ->
|
||||
ThemedRichText(text = "Invited by [${invitedBy}](https://lobste.rs/u/${user.invitedBy})")
|
||||
Text(
|
||||
text =
|
||||
buildAnnotatedString {
|
||||
append("Invited by ")
|
||||
pushLink(
|
||||
LinkAnnotation.Clickable(
|
||||
tag = "username",
|
||||
linkInteractionListener = { openUserProfile(invitedBy) },
|
||||
)
|
||||
)
|
||||
append(invitedBy)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.
|
||||
@@ -16,16 +16,17 @@ android { namespace = "dev.msfjarvis.claw.core" }
|
||||
anvil { generateDaggerFactories.set(true) }
|
||||
|
||||
dependencies {
|
||||
api(libs.dagger)
|
||||
api(libs.javax.inject)
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
api(libs.kotlinx.serialization.json)
|
||||
api(libs.okhttp.loggingInterceptor)
|
||||
api(libs.napier)
|
||||
api(libs.okhttp.core)
|
||||
api(libs.retrofit)
|
||||
|
||||
implementation(platform(libs.okhttp.bom))
|
||||
implementation(platform(libs.sentry.bom))
|
||||
implementation(libs.dagger)
|
||||
implementation(libs.napier)
|
||||
implementation(libs.okhttp.core)
|
||||
implementation(libs.kotlinx.serialization.core)
|
||||
implementation(libs.retrofit.kotlinxSerializationConverter)
|
||||
implementation(libs.sentry.okhttp)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha16" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha16)" variant="all" version="8.3.0-alpha16">
|
||||
<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0)" variant="all" version="8.3.0">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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.
|
||||
@@ -7,7 +7,6 @@
|
||||
package dev.msfjarvis.claw.core.injection
|
||||
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -18,6 +17,7 @@ import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNamingStrategy
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import retrofit2.Converter
|
||||
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
|
||||
@Module
|
||||
@ContributesTo(ApplicationScope::class)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2022-2023 Harsh Shandilya.
|
||||
* Copyright © 2022-2024 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.
|
||||
@@ -18,7 +18,6 @@ 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.okhttp.SentryOkHttpInterceptor
|
||||
import java.net.Socket
|
||||
import javax.net.SocketFactory
|
||||
import okhttp3.Cache
|
||||
@@ -88,11 +87,5 @@ interface OkHttpModule {
|
||||
fun provideHttpLoggingInterceptor(logger: HttpLoggingInterceptor.Logger): Interceptor {
|
||||
return HttpLoggingInterceptor(logger).setLevel(HttpLoggingInterceptor.Level.BASIC)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideSentryInterceptor(): Interceptor {
|
||||
return SentryOkHttpInterceptor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha16" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha16)" variant="all" version="8.3.0-alpha16">
|
||||
<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0)" variant="all" version="8.3.0">
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 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.
|
||||
@@ -32,7 +32,6 @@ object SavedPostSerializer : KSerializer<SavedPost> {
|
||||
element<Int?>("commentCount", isOptional = true)
|
||||
element<String>("commentsUrl")
|
||||
element<String>("submitterName")
|
||||
element<String>("submitterAvatarUrl")
|
||||
element<List<String>>("tags")
|
||||
element<String>("description")
|
||||
}
|
||||
@@ -46,7 +45,6 @@ object SavedPostSerializer : KSerializer<SavedPost> {
|
||||
var commentCount: Int? = null
|
||||
var commentsUrl = ""
|
||||
var submitterName = ""
|
||||
var submitterAvatarUrl = ""
|
||||
var tags = emptyList<String>()
|
||||
var description = ""
|
||||
while (true) {
|
||||
@@ -58,9 +56,8 @@ object SavedPostSerializer : KSerializer<SavedPost> {
|
||||
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)
|
||||
7 -> tags = decodeSerializableElement(descriptor, 7, delegateSerializer)
|
||||
8 -> description = decodeStringElement(descriptor, 8)
|
||||
CompositeDecoder.DECODE_DONE -> break
|
||||
else -> error("Unexpected index: $index")
|
||||
}
|
||||
@@ -73,7 +70,6 @@ object SavedPostSerializer : KSerializer<SavedPost> {
|
||||
commentCount = commentCount,
|
||||
commentsUrl = commentsUrl,
|
||||
submitterName = submitterName,
|
||||
submitterAvatarUrl = submitterAvatarUrl,
|
||||
tags = tags,
|
||||
description = description,
|
||||
)
|
||||
@@ -89,9 +85,8 @@ object SavedPostSerializer : KSerializer<SavedPost> {
|
||||
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)
|
||||
encodeSerializableElement(descriptor, 7, delegateSerializer, value.tags)
|
||||
encodeStringElement(descriptor, 8, value.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
CREATE TABLE ReadPosts(
|
||||
CREATE TABLE IF NOT EXISTS ReadPosts(
|
||||
id TEXT NOT NULL PRIMARY KEY
|
||||
);
|
||||
|
||||
selectAllPosts:
|
||||
SELECT *
|
||||
FROM ReadPosts;
|
||||
|
||||
markRead:
|
||||
INSERT OR REPLACE
|
||||
INSERT OR IGNORE
|
||||
INTO ReadPosts(id)
|
||||
VALUES (?);
|
||||
|
||||
markUnread:
|
||||
DELETE FROM ReadPosts
|
||||
WHERE id = ?;
|
||||
|
||||
isRead:
|
||||
SELECT *
|
||||
FROM ReadPosts
|
||||
WHERE id = ?;
|
||||
|
||||
@@ -10,7 +10,6 @@ CREATE TABLE IF NOT EXISTS SavedPost(
|
||||
commentCount INTEGER AS Int,
|
||||
commentsUrl TEXT NOT NULL,
|
||||
submitterName TEXT NOT NULL,
|
||||
submitterAvatarUrl TEXT NOT NULL,
|
||||
tags TEXT AS List<String> NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT ""
|
||||
);
|
||||
@@ -36,8 +35,3 @@ deletePost:
|
||||
DELETE
|
||||
FROM SavedPost
|
||||
WHERE shortId = ?;
|
||||
|
||||
selectPost:
|
||||
SELECT *
|
||||
FROM SavedPost
|
||||
WHERE shortId = ?;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
CREATE TABLE ReadPosts(
|
||||
CREATE TABLE IF NOT EXISTS ReadPosts(
|
||||
id TEXT NOT NULL PRIMARY KEY
|
||||
);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import kotlin.Int;
|
||||
import kotlin.String;
|
||||
import kotlin.collections.List;
|
||||
|
||||
ALTER TABLE SavedPost RENAME TO SavedPost_Old;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS SavedPost(
|
||||
shortId TEXT NOT NULL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
createdAt TEXT NOT NULL,
|
||||
commentCount INTEGER AS Int,
|
||||
commentsUrl TEXT NOT NULL,
|
||||
submitterName TEXT NOT NULL,
|
||||
tags TEXT AS List<String> NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT ""
|
||||
);
|
||||
|
||||
INSERT INTO SavedPost(shortId, title, url, createdAt, commentCount, commentsUrl, submitterName, tags, description)
|
||||
SELECT shortId, title, url, createdAt, commentCount, commentsUrl, submitterName, tags, description
|
||||
FROM SavedPost_Old;
|
||||
|
||||
DROP TABLE SavedPost_Old;
|
||||
+1
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 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.
|
||||
@@ -52,7 +52,6 @@ class SavedPostSerializerTest {
|
||||
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",
|
||||
|
||||
@@ -1 +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"}
|
||||
{"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","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"}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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.
|
||||
@@ -18,6 +18,8 @@ android { namespace = "dev.msfjarvis.claw.database" }
|
||||
anvil { generateDaggerFactories.set(true) }
|
||||
|
||||
dependencies {
|
||||
api(libs.dagger)
|
||||
api(libs.javax.inject)
|
||||
api(projects.database.core)
|
||||
implementation(libs.napier)
|
||||
implementation(libs.dagger)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 8.3.0-alpha16" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0-alpha16)" variant="all" version="8.3.0-alpha16">
|
||||
<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="true" name="AGP (8.3.0)" variant="all" version="8.3.0">
|
||||
|
||||
</issues>
|
||||
|
||||
+5
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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,6 +13,7 @@ import app.cash.sqldelight.logs.LogSqliteDriver
|
||||
import com.deliveryhero.whetstone.app.ApplicationScope
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import com.squareup.anvil.annotations.optional.ForScope
|
||||
import com.squareup.anvil.annotations.optional.SingleIn
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dev.msfjarvis.claw.database.LobstersDatabase
|
||||
@@ -28,7 +29,9 @@ object DatabaseModule {
|
||||
|
||||
private const val LOBSTERS_DATABASE_NAME = "SavedPosts.db"
|
||||
|
||||
@[Provides InternalDatabaseApi]
|
||||
@Provides
|
||||
@InternalDatabaseApi
|
||||
@SingleIn(ApplicationScope::class)
|
||||
fun provideDatabase(@ForScope(ApplicationScope::class) context: Context): LobstersDatabase {
|
||||
val driver =
|
||||
LogSqliteDriver(
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2023 Harsh Shandilya.
|
||||
* Copyright © 2023-2024 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.
|
||||
@@ -23,8 +23,8 @@ class ReadPostsQueriesTest {
|
||||
fun `mark post as read`() {
|
||||
val id = UUID.randomUUID().toString()
|
||||
postQueries.markRead(id)
|
||||
assertThat(postQueries.isRead(id).executeAsOne()).isNotNull()
|
||||
assertThat(postQueries.selectAllPosts().executeAsList()).contains(id)
|
||||
postQueries.markUnread(id)
|
||||
assertThat(postQueries.isRead(id).executeAsOneOrNull()).isNull()
|
||||
assertThat(postQueries.selectAllPosts().executeAsList()).doesNotContain(id)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2021-2023 Harsh Shandilya.
|
||||
* Copyright © 2021-2024 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.
|
||||
@@ -32,7 +32,7 @@ class SavedPostQueriesTest {
|
||||
@Test
|
||||
fun `update post in database`() {
|
||||
// Get 1 post
|
||||
val post = createTestData(1)[0]
|
||||
val post = createTestData(1).first()
|
||||
|
||||
// Insert post into DB
|
||||
postQueries.insertOrReplacePost(post)
|
||||
@@ -46,15 +46,14 @@ class SavedPostQueriesTest {
|
||||
assertThat(postsCount).isEqualTo(1)
|
||||
|
||||
// Check if post is updated
|
||||
val postFromDb = postQueries.selectPost(post.shortId).executeAsOne()
|
||||
|
||||
val postFromDb = postQueries.selectAllPosts().executeAsOne()
|
||||
assertThat(postFromDb.submitterName).isEqualTo("Fake name")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get post from db`() {
|
||||
// Get 1 post
|
||||
val post = createTestData(1)[0]
|
||||
val post = createTestData(1).first()
|
||||
|
||||
// Insert post into DB
|
||||
postQueries.insertOrReplacePost(post)
|
||||
@@ -123,7 +122,6 @@ class SavedPostQueriesTest {
|
||||
commentCount = 0,
|
||||
commentsUrl = "test_comments_url",
|
||||
submitterName = "test_user_$i",
|
||||
submitterAvatarUrl = "test_avatar_url",
|
||||
tags = listOf(),
|
||||
description = "",
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user