Compare commits
346 Commits
Author | SHA1 | Date |
---|---|---|
Eliot Lash | 9883bfa7c2 | |
Weblate | 8584e72f48 | |
renovate[bot] | 434b79fb52 | |
Levi Bard | c77a755ea0 | |
Konrad Pozniak | 6a37cd20b5 | |
Konrad Pozniak | b205e53213 | |
Konrad Pozniak | 34b53a3c59 | |
Konrad Pozniak | adbe694471 | |
Konrad Pozniak | bb329ae8b8 | |
Konrad Pozniak | 831646f93c | |
Ihor Hordiichuk | 571d2289fd | |
Ihor Hordiichuk | 33e3327366 | |
Ümit Solmaz | 2702150867 | |
fin-w | b39f1c3807 | |
Hồ Nhất Duy | 091831dd41 | |
Danial Behzadi | 35a869028b | |
Manuel | bf2788d80b | |
Konrad Pozniak | 8aaca3bb2c | |
Konrad Pozniak | e05ded2e32 | |
renovate[bot] | 6b32e72e19 | |
Konrad Pozniak | b262b6cb9b | |
XoseM | 1a01c9debd | |
Ümit Solmaz | 2db0bbcc39 | |
Manuel | 6a4e2d52d0 | |
Konrad Pozniak | 16fbbc6bf6 | |
renovate[bot] | f10ad7482c | |
renovate[bot] | 494f5d9a8b | |
renovate[bot] | c9a6e50ace | |
Levi Bard | f14a82325d | |
Levi Bard | b2d4092124 | |
Christophe Beyls | d200d1e15e | |
Konrad Pozniak | 0483440381 | |
Christophe Beyls | 83403ebb58 | |
Christophe Beyls | 59184f1717 | |
Christophe Beyls | c668cdc633 | |
Christophe Beyls | 2cbd629150 | |
Konrad Pozniak | f9b5eeaf76 | |
Konrad Pozniak | 32dea86502 | |
Konrad Pozniak | 9805bc2cce | |
Manuel | e8a2c8ba47 | |
Ümit Solmaz | 85559456cd | |
fin-w | 62e33d56f3 | |
Konrad Pozniak | d554d71958 | |
Konrad Pozniak | 78b1fcb9c6 | |
Konrad Pozniak | f0a9c8be53 | |
Konrad Pozniak | 9052e91dc8 | |
renovate[bot] | 4d14d0dbcf | |
renovate[bot] | 050afecdde | |
renovate[bot] | 0824be7a45 | |
Salif Mehmed | 83fe00b2e4 | |
Stavros | 20c53d13bd | |
Quentí | da0ba0018c | |
Hồ Nhất Duy | a517bfdcc6 | |
Manuel | 9c75c09ac3 | |
Danial Behzadi | 92962347b5 | |
fin-w | 53912f43a7 | |
renovate[bot] | ba471b5ccb | |
renovate[bot] | e85d987705 | |
renovate[bot] | 203e896892 | |
renovate[bot] | 6c9b67da03 | |
renovate[bot] | 2d16de8527 | |
Christophe Beyls | 0053d676db | |
Christophe Beyls | 952f9a8614 | |
Christophe Beyls | 973ebd236c | |
fries1234 | a690b537c2 | |
Christophe Beyls | 45d36a6a87 | |
Christophe Beyls | f9221b3d75 | |
Christophe Beyls | 11d18e1e70 | |
renovate[bot] | 2ce03c2e89 | |
renovate[bot] | 224a3b89de | |
Konrad Pozniak | 1ad3c74959 | |
renovate[bot] | df372ce30d | |
Konrad Pozniak | 5cb1e278dc | |
Christophe Beyls | dc4ca06551 | |
Mārtiņš Bruņenieks | 1c48cc43e2 | |
Connyduck | bb481cbe95 | |
Ihor Hordiichuk | 2bb234b7db | |
fin-w | 0cc63d931e | |
Stavros | aad1431075 | |
Ümit Solmaz | f3f26091be | |
Quentí | 81ae9e612b | |
Danial Behzadi | 6568fd6748 | |
Hồ Nhất Duy | 86115375a3 | |
Weblate | c1fe58c48c | |
UlrichKu | b60e111144 | |
Christophe Beyls | 84fda7bade | |
Christophe Beyls | d1518956e1 | |
Conny Duck | 0028ab79c3 | |
Conny Duck | bf9be47f0f | |
Konrad Pozniak | f2d7de0144 | |
Konrad Pozniak | 5137bbfade | |
Konrad Pozniak | f483cf7f29 | |
Konrad Pozniak | ba575dbfc6 | |
Konrad Pozniak | dc4fd8f1d0 | |
Konrad Pozniak | 401ab6d453 | |
Konrad Pozniak | 836c71b899 | |
Konrad Pozniak | 4dec228926 | |
Konrad Pozniak | 3736034952 | |
Konrad Pozniak | 82817a089b | |
Christophe Beyls | 05c7e7b806 | |
Konrad Pozniak | 7dc71a5888 | |
Conny Duck | 1eca92a349 | |
Conny Duck | 7e9e729331 | |
Konrad Pozniak | c10f82ffa6 | |
Konrad Pozniak | 056aaa7e0e | |
Christophe Beyls | ad1afdd241 | |
Hồ Nhất Duy | 65995e4492 | |
Manuel | a1367037ba | |
Anonymous | 450fe72983 | |
Konrad Pozniak | b2c0b18c8e | |
renovate[bot] | 3bbf96b057 | |
renovate[bot] | 5b312c22ff | |
renovate[bot] | 1aa2159754 | |
renovate[bot] | 50b17a1d74 | |
Konrad Pozniak | 71424401a1 | |
Konrad Pozniak | d5a01f671c | |
Konrad Pozniak | f8a25f896b | |
Konrad Pozniak | 36d982a359 | |
Christophe Beyls | 76c6ec5510 | |
Conny Duck | e96ca01dec | |
Konrad Pozniak | 88fbf33832 | |
Konrad Pozniak | b2547c5eef | |
Konrad Pozniak | 05b78a2a00 | |
Konrad Pozniak | 320c42fc45 | |
Ümit Solmaz | 389059da43 | |
Connyduck | 6bdbbfd4bb | |
Konrad Pozniak | 9087b4186f | |
Konrad Pozniak | 338ec54b17 | |
Balázs Meskó | cc6925d866 | |
Konrad Pozniak | 197a1f4eda | |
Konrad Pozniak | e1904080b0 | |
XoseM | 85a27dea3e | |
Ümit Solmaz | c4c391434f | |
Hồ Nhất Duy | d090222412 | |
fin-w | 42df5f7c8a | |
Luna Jernberg | b8adc818c1 | |
Konrad Pozniak | c55d79562c | |
Konrad Pozniak | f2ffba1679 | |
Ümit Solmaz | 7b2b4612c5 | |
Weblate | fe7103f2b9 | |
Weblate | 7960db6c78 | |
Konrad Pozniak | 333beec17d | |
charlag | 8e4aab8bae | |
Conny Duck | 0362a32163 | |
Conny Duck | 3fe900c2dd | |
Conny Duck | 51b7745cee | |
Conny Duck | 0a9485f8e9 | |
Conny Duck | a9fe9ba079 | |
Christophe Beyls | 72ee0b4292 | |
Konrad Pozniak | 4e822c9a0a | |
renovate[bot] | 703641bf06 | |
Konrad Pozniak | e15516af68 | |
Christophe Beyls | f69cae2315 | |
Konrad Pozniak | 2504f42f7b | |
renovate[bot] | 546145da88 | |
renovate[bot] | d87754fabe | |
renovate[bot] | b5c9356851 | |
renovate[bot] | 0a5d277623 | |
Christophe Beyls | 65af26993b | |
Konrad Pozniak | ee9a9fc51e | |
Christophe Beyls | ec599c8f8a | |
Konrad Pozniak | 2a4d60bed8 | |
Konrad Pozniak | f1b0e0fbc2 | |
Ümit Solmaz | 9e0796ae6c | |
Konrad Pozniak | 0d3b1b1c5a | |
renovate[bot] | b524d57d64 | |
Zongle Wang | d01d4b2157 | |
Konrad Pozniak | a312492b1d | |
renovate[bot] | 721a2a6d03 | |
renovate[bot] | ec065b7c2e | |
XoseM | 98864e8097 | |
Konrad Pozniak | fa3c7db919 | |
Christophe Beyls | df7b11afc3 | |
Konrad Pozniak | 5343766886 | |
renovate[bot] | 9491ebb031 | |
Konrad Pozniak | b022767ae6 | |
Zongle Wang | ba495f41a5 | |
renovate[bot] | af016c1766 | |
Zongle Wang | 1acae50845 | |
Zongle Wang | e865ffafde | |
renovate[bot] | 06f283575d | |
Konrad Pozniak | b85ada930b | |
Zongle Wang | 211983c7ee | |
Konrad Pozniak | 50b84b6ee0 | |
Zongle Wang | f029b7f84d | |
Zongle Wang | a3d87de8ac | |
Weblate | efa29d37b2 | |
fin-w | 3fdb2c14a9 | |
Ümit Solmaz | adf4dcb3c8 | |
xzFantom | eb75cf0818 | |
Konrad Pozniak | 3274bd2660 | |
Konrad Pozniak | c7a1ddd589 | |
Deleted User | 4849f6772e | |
renovate[bot] | 00d7cc72b1 | |
Zongle Wang | 83cbbe9ada | |
renovate[bot] | be8140d628 | |
Konrad Pozniak | d4c8d6213c | |
Deleted User | 010a3ed5fb | |
fin-w | 08f1525b6e | |
Ihor Hordiichuk | fa43d7738f | |
Sveinn í Felli | d516c6e3a1 | |
fin-w | fd78cc7355 | |
Konrad Pozniak | d0e6e4ba39 | |
fin-w | 594b89408f | |
Gera, Zoltan | 55e763a34b | |
cuithon | 6d7a66a441 | |
Konrad Pozniak | 5396452e1c | |
Konrad Pozniak | 0b87ba2031 | |
Konrad Pozniak | d5eb37595c | |
Hồ Nhất Duy | 23dd0d447c | |
fin-w | b05965923d | |
Willow | fbb22799dc | |
renovate[bot] | 80982d061e | |
renovate[bot] | bcde8ea8be | |
renovate[bot] | 06a896e755 | |
renovate[bot] | 8e435aaea2 | |
Konrad Pozniak | f4782489ff | |
Konrad Pozniak | ce196738bf | |
Ümit Solmaz | e47ae6e71f | |
Konrad Pozniak | be8b7c3a31 | |
Christophe Beyls | 9901376d38 | |
Ümit Solmaz | 071ae0bed2 | |
Konrad Pozniak | f09a5b00e0 | |
Konrad Pozniak | 109abc5ac3 | |
renovate[bot] | 69938c7de9 | |
renovate[bot] | 3d5dfeb1c3 | |
XoseM | 0132b758af | |
Hồ Nhất Duy | c4bc15c04f | |
Ümit Solmaz | df75dd61c9 | |
Konrad Pozniak | 7a05530359 | |
Konrad Pozniak | fd8d7db343 | |
renovate[bot] | 7c15f5e8b0 | |
Sveinn í Felli | 9084116305 | |
Luna Jernberg | f083c463a0 | |
Ihor Hordiichuk | b1ac7e587d | |
XoseM | 4aa1980033 | |
fin-w | 21e97faf78 | |
Christophe Beyls | 722b75e5c2 | |
Christophe Beyls | 40fde54e0b | |
renovate[bot] | 91fe7a51cc | |
Konrad Pozniak | 7448fd2416 | |
renovate[bot] | 1fab0b8460 | |
Konrad Pozniak | 6249b53718 | |
Willow | c666a6b534 | |
Konrad Pozniak | 9987a78044 | |
renovate[bot] | 847ea9975b | |
renovate[bot] | 17379e662d | |
renovate[bot] | 3660a10cac | |
renovate[bot] | f3f8017c43 | |
Christophe Beyls | a19540f0e4 | |
renovate[bot] | 7e5eef4060 | |
renovate[bot] | 3fbe6e9786 | |
renovate[bot] | 5de9c5ce15 | |
renovate[bot] | 713da85340 | |
Konrad Pozniak | 7dca2fed58 | |
Konrad Pozniak | 166e5ac20e | |
renovate[bot] | 498f682bfb | |
Conny Duck | fef0535197 | |
Zongle Wang | efeb218003 | |
renovate[bot] | 5c3a029dd9 | |
renovate[bot] | 36a9b7a570 | |
renovate[bot] | d8b1a5a1dc | |
Weblate | 0dec496ccb | |
renovate[bot] | c04ed01b36 | |
renovate[bot] | fce8343a7f | |
renovate[bot] | 5dcb7f6e8e | |
Zongle Wang | e3c68e0992 | |
Zongle Wang | 88c75c8d9b | |
renovate[bot] | 5d5bc15f42 | |
renovate[bot] | 6071ad9ab0 | |
renovate[bot] | 1618ebdeff | |
renovate[bot] | e8369c1d2a | |
renovate[bot] | 5d52f0985e | |
renovate[bot] | 5153e138fc | |
renovate[bot] | 51133720c5 | |
renovate[bot] | 7c391b8bba | |
Konrad Pozniak | b976fe5296 | |
Konrad Pozniak | fa8bede7d6 | |
Konrad Pozniak | 7d3aafdd65 | |
Willow | 22ec78c75a | |
Konrad Pozniak | 48afcacd98 | |
Salif Mehmed | 4e43feb9dc | |
Rhoslyn Prys | 2c13805d84 | |
Levi Bard | 6994693496 | |
Konrad Pozniak | 7173d5e1e7 | |
Konrad Pozniak | 1cd8b497f7 | |
Konrad Pozniak | 17e99bbc2e | |
Konrad Pozniak | 7f7751624e | |
Ihor Hordiichuk | e2c2db1d31 | |
Hồ Nhất Duy | dbe82fe1b0 | |
Ümit Solmaz | b826ca15a9 | |
fin-w | 61de55efa7 | |
XoseM | c9d29fa92f | |
Konrad Pozniak | 7fef19efc6 | |
Konrad Pozniak | 8a39fc643c | |
Konrad Pozniak | d66866648e | |
Konrad Pozniak | 0c2b8b114b | |
Konrad Pozniak | 0b9f61c100 | |
Konrad Pozniak | 750e255029 | |
ButterflyOfFire | 5ae6611072 | |
Konrad Pozniak | db9e6562cf | |
Konrad Pozniak | a4f931da83 | |
Hồ Nhất Duy | 002852dc27 | |
Bruno Miguel | 92879700a1 | |
Marco Baptista | 3a551b48f6 | |
XoseM | 88dbc8de12 | |
Sveinn í Felli | 5b04a0e8e1 | |
Hồ Nhất Duy | 9005e6525c | |
Bruno Miguel | 6f22072d0e | |
João Alves | 8df7a58679 | |
Bruno Miguel | a25f9f5823 | |
Newidyn | a489be35b1 | |
fin-w | c536b11072 | |
Ihor Hordiichuk | 111b301246 | |
Konrad Pozniak | 5174c00558 | |
Maximilian Ertl | 27a610bd48 | |
Zongle Wang | c9f8b043c5 | |
Konrad Pozniak | 36ff1b55d1 | |
Ümit Solmaz | a08ef5d68f | |
Hồ Nhất Duy | 4efc54246f | |
Konrad Pozniak | afbc183c02 | |
Konrad Pozniak | aca3a638e9 | |
renovate[bot] | fb063dfb0f | |
renovate[bot] | 520c202e55 | |
renovate[bot] | 1a3277bb85 | |
Konrad Pozniak | 5192fb08a5 | |
Konrad Pozniak | 33cd6fdb98 | |
Konrad Pozniak | d3c91904c8 | |
Sveinn í Felli | 99e78eab78 | |
Rhoslyn Prys | 305953358b | |
Eric | 260f18ffce | |
Ricardo | 3193453775 | |
Danial Behzadi | d822eb72c4 | |
Quentí | 6c80db6e03 | |
fin-w | f9817633bd | |
mcclure | 70f8e8ba93 | |
Danial Behzadi | b6d0c19636 | |
fin-w | 347552d200 | |
renovate[bot] | d87995ccd6 | |
Konrad Pozniak | 1f698e0732 | |
UlrichKu | 0698333665 | |
Zongle Wang | 6494247301 | |
sanao | e8e7bad110 | |
Konrad Pozniak | 966ba38dbe | |
Konrad Pozniak | 7c5238a6bc | |
charlag | af6b3a3a33 |
|
@ -8,12 +8,20 @@ insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
[*.{java,kt}]
|
[*.{java,kt}]
|
||||||
|
ij_kotlin_imports_layout = *
|
||||||
|
|
||||||
# Disable wildcard imports
|
# Disable wildcard imports
|
||||||
ij_kotlin_name_count_to_use_star_import = 999
|
ij_kotlin_name_count_to_use_star_import = 999
|
||||||
ij_kotlin_name_count_to_use_star_import_for_members = 999
|
ij_kotlin_name_count_to_use_star_import_for_members = 999
|
||||||
ij_java_class_count_to_use_import_on_demand = 999
|
ij_java_class_count_to_use_import_on_demand = 999
|
||||||
# Enable trailing comma
|
|
||||||
ktlint_disabled_rules=trailing-comma-on-call-site,trailing-comma-on-declaration-site
|
ktlint_code_style = android_studio
|
||||||
|
|
||||||
|
# Disable trailing comma
|
||||||
|
ktlint_standard_trailing-comma-on-call-site = disabled
|
||||||
|
ktlint_standard_trailing-comma-on-declaration-site = disabled
|
||||||
|
|
||||||
|
max_line_length = off
|
||||||
|
|
||||||
[*.{yml,yaml}]
|
[*.{yml,yaml}]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
name: Bug Report
|
||||||
|
description: If something isn't working as expected
|
||||||
|
labels: [bug]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Make sure that you are submitting a new bug that was not previously reported or already fixed.
|
||||||
|
|
||||||
|
Please use a concise and distinct title for the issue.
|
||||||
|
|
||||||
|
If possible, attach screenshots, videos or links to posts to illustrate the problem.
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Detailed description
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce the problem
|
||||||
|
description: What were you trying to do?
|
||||||
|
value: |
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Debug information
|
||||||
|
description: |
|
||||||
|
This info can be copied from the 'About' screen in Tusky 24+.
|
||||||
|
If you are on a lower version or can't access the screen, please provide us with the Tusky Version, Android Version, Device and the Mastodon instance this problem occurred on.
|
||||||
|
placeholder: |
|
||||||
|
Tusky Test 22.0-b814c2c0
|
||||||
|
Android 12
|
||||||
|
Fairphone 4
|
||||||
|
mastodon.social
|
||||||
|
validations:
|
||||||
|
required: true
|
|
@ -0,0 +1,19 @@
|
||||||
|
name: Feature Request
|
||||||
|
description: I have a suggestion
|
||||||
|
labels: [enhancement]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: Please use a concise and distinct title for the issue.
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Pitch
|
||||||
|
description: Describe your idea for a feature. Make sure it has not already been suggested/implemented/turned down before.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Motivation
|
||||||
|
description: Why do you think this feature is needed? Who would benefit from it?
|
||||||
|
validations:
|
||||||
|
required: true
|
|
@ -0,0 +1 @@
|
||||||
|
blank_issues_enabled: true
|
|
@ -13,21 +13,21 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-java@v3
|
- uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '21'
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
|
|
||||||
- name: Gradle Wrapper Validation
|
- name: Gradle Wrapper Validation
|
||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/actions/wrapper-validation@v3
|
||||||
|
|
||||||
- name: Copy CI gradle.properties
|
- name: Copy CI gradle.properties
|
||||||
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
|
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
- name: Gradle Build Action
|
- name: Gradle Build Action
|
||||||
uses: gradle/gradle-build-action@v2
|
uses: gradle/gradle-build-action@v3
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }}
|
||||||
|
|
||||||
|
|
|
@ -13,20 +13,20 @@ jobs:
|
||||||
name: app:buildGreenDebug
|
name: app:buildGreenDebug
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-java@v3
|
- uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '21'
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
|
|
||||||
- name: Gradle Wrapper Validation
|
- name: Gradle Wrapper Validation
|
||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/actions/wrapper-validation@v3
|
||||||
|
|
||||||
- name: Copy CI gradle.properties
|
- name: Copy CI gradle.properties
|
||||||
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
|
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
- uses: gradle/gradle-build-action@v2
|
- uses: gradle/gradle-build-action@v3
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }}
|
||||||
|
|
||||||
|
|
|
@ -7,4 +7,5 @@ build
|
||||||
/captures
|
/captures
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
app/release
|
app/release
|
||||||
app-release.apk
|
app-release.apk
|
||||||
|
.kotlin
|
||||||
|
|
60
CHANGELOG.md
60
CHANGELOG.md
|
@ -6,13 +6,69 @@
|
||||||
|
|
||||||
### Significant bug fixes
|
### Significant bug fixes
|
||||||
|
|
||||||
|
## v25.2
|
||||||
|
|
||||||
|
### Significant bug fixes
|
||||||
|
|
||||||
|
- Fixes a bug that could sometimes crash Tusky when rotating the screen while viewing an account list [PR#4430](https://github.com/tuskyapp/Tusky/pull/4430)
|
||||||
|
- Fixes a bug that could crash Tusky at startup under certain conditions [PR#4431](https://github.com/tuskyapp/Tusky/pull/4431)
|
||||||
|
- Fixes a bug that caused Tusky to crash when custom emojis with too large dimensions were loaded [PR#4429](https://github.com/tuskyapp/Tusky/pull/4429)
|
||||||
|
- Makes Tusky work again with Iceshrimp by working around a quirk in their API implementation [PR#4426](https://github.com/tuskyapp/Tusky/pull/4426)
|
||||||
|
- Fixes a bug that made translations not work on some servers [PR#4422](https://github.com/tuskyapp/Tusky/pull/4422)
|
||||||
|
|
||||||
|
## v25.1
|
||||||
|
|
||||||
|
### Significant bug fixes
|
||||||
|
|
||||||
|
- Fixed two crashes at startup introduced in 25.0 [PR#4415](https://github.com/tuskyapp/Tusky/pull/4415) [PR#4417](https://github.com/tuskyapp/Tusky/pull/4417)
|
||||||
|
|
||||||
|
## v25.0
|
||||||
|
|
||||||
|
### New features and other improvements
|
||||||
|
|
||||||
|
- Added support for the [Mastodon translation api](https://docs.joinmastodon.org/methods/statuses/#translate).
|
||||||
|
You can now find a new option "translate" in the three-dot-menu on posts that are not in your display language when your server supports the translation api.
|
||||||
|
Support is determined by checking the `configuration.translation.enabled` attribute of the `/api/v2/instance` endpoint.
|
||||||
|
[PR#4307](https://github.com/tuskyapp/Tusky/pull/4307)
|
||||||
|
- The language of a post is now shown in the metadata section of the detail post view, if it is available. [PR#4127](https://github.com/tuskyapp/Tusky/pull/4127)
|
||||||
|
- The transitions between screens have been changed to feel faster and align more with default Android transitions. [PR#4285](https://github.com/tuskyapp/Tusky/pull/4285)
|
||||||
|
- The post statistic section below the detail post view is now always shown to prevent layout shifts on the first like or boost.
|
||||||
|
[PR#4205](https://github.com/tuskyapp/Tusky/pull/4205) [PR#4260](https://github.com/tuskyapp/Tusky/pull/4260)
|
||||||
|
- The filters for boosts/replies/self-boosts in the home timeline have moved from general preferences to account specific preferences. [PR#4115](https://github.com/tuskyapp/Tusky/pull/4115)
|
||||||
|
- The json parsing library has been migrated from Gson to Moshi. This change will make Tusky no longer crash on unexpected server responses. [PR#4309](https://github.com/tuskyapp/Tusky/pull/4309)
|
||||||
|
- Small layout improvements to the header of the profile view [PR#4375](https://github.com/tuskyapp/Tusky/pull/4375) [PR#4371](https://github.com/tuskyapp/Tusky/pull/4371)
|
||||||
|
- support for Android 14 Upside Down Cake [PR#4224](https://github.com/tuskyapp/Tusky/pull/4224)
|
||||||
|
- Various internal refactorings to improve performance and maintainability.
|
||||||
|
[PR#4269](https://github.com/tuskyapp/Tusky/pull/4269)
|
||||||
|
[PR#4290](https://github.com/tuskyapp/Tusky/pull/4290)
|
||||||
|
[PR#4291](https://github.com/tuskyapp/Tusky/pull/4291)
|
||||||
|
[PR#4296](https://github.com/tuskyapp/Tusky/pull/4296)
|
||||||
|
[PR#4364](https://github.com/tuskyapp/Tusky/pull/4364)
|
||||||
|
[PR#4366](https://github.com/tuskyapp/Tusky/pull/4366)
|
||||||
|
[PR#4372](https://github.com/tuskyapp/Tusky/pull/4372)
|
||||||
|
[PR#4356](https://github.com/tuskyapp/Tusky/pull/4356)
|
||||||
|
[PR#4348](https://github.com/tuskyapp/Tusky/pull/4348)
|
||||||
|
[PR#4339](https://github.com/tuskyapp/Tusky/pull/4339)
|
||||||
|
[PR#4337](https://github.com/tuskyapp/Tusky/pull/4337)
|
||||||
|
[PR#4336](https://github.com/tuskyapp/Tusky/pull/4336)
|
||||||
|
[PR#4330](https://github.com/tuskyapp/Tusky/pull/4330)
|
||||||
|
[PR#4235](https://github.com/tuskyapp/Tusky/pull/4235)
|
||||||
|
[PR#4081](https://github.com/tuskyapp/Tusky/pull/4081)
|
||||||
|
|
||||||
|
### Significant bug fixes
|
||||||
|
|
||||||
|
- The setting to hide the notification filter bar that was accidentally removed is back. [PR#4225](https://github.com/tuskyapp/Tusky/pull/4225)
|
||||||
|
- The profile picture in the bottom navigation bar now has the correct content description. [PR#4400](https://github.com/tuskyapp/Tusky/pull/4400)
|
||||||
|
|
||||||
## v24.1
|
## v24.1
|
||||||
|
|
||||||
- The screen will stay on again while a video is playing. [PR#4168](https://github.com/tuskyapp/Tusky/pull/4168)
|
- The screen will stay on again while a video is playing. [PR#4168](https://github.com/tuskyapp/Tusky/pull/4168)
|
||||||
- A memory leak has been fixed. This should improve stability and performance. [PR#4150](https://github.com/tuskyapp/Tusky/pull/4150) [PR#4153](https://github.com/tuskyapp/Tusky/pull/4153)
|
- A memory leak has been fixed. This should improve stability and performance. [PR#4150](https://github.com/tuskyapp/Tusky/pull/4150) [PR#4153](https://github.com/tuskyapp/Tusky/pull/4153)
|
||||||
- Emojis are now correctly counted as 1 character when composing a post. [PR#4152](hhttps://github.com/tuskyapp/Tusky/pull/4152)
|
- Emojis are now correctly counted as 1 character when composing a post. [PR#4152](https://github.com/tuskyapp/Tusky/pull/4152)
|
||||||
- Fixed a crash when text was selected on some devices. [PR#4166](https://github.com/tuskyapp/Tusky/pull/4166)
|
- Fixed a crash when text was selected on some devices. [PR#4166](https://github.com/tuskyapp/Tusky/pull/4166)
|
||||||
- The icons in the help texts of empty timelines will now always be correctly aligned. [PR#4179](https://github.com/tuskyapp/Tusky/pull/4179)
|
- The icons in the help texts of empty timelines will now always be correctly
|
||||||
|
aligned. [PR#4179](https://github.com/tuskyapp/Tusky/pull/4179)
|
||||||
|
- Fixed ANR caused by direct message badge [PR#4182](https://github.com/tuskyapp/Tusky/pull/4182)
|
||||||
|
|
||||||
## v24.0
|
## v24.0
|
||||||
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
[Issue text goes here].
|
|
||||||
|
|
||||||
* * * *
|
|
||||||
- Tusky Version:
|
|
||||||
- Android Version:
|
|
||||||
- Android Device:
|
|
||||||
- Mastodon instance (if applicable):
|
|
||||||
|
|
||||||
- [ ] I searched or browsed the repo’s other issues to ensure this is not a duplicate.
|
|
|
@ -1,8 +1,8 @@
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.google.ksp)
|
alias(libs.plugins.google.ksp)
|
||||||
|
alias(libs.plugins.hilt.android)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.kotlin.kapt)
|
|
||||||
alias(libs.plugins.kotlin.parcelize)
|
alias(libs.plugins.kotlin.parcelize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,15 +22,15 @@ final def CUSTOM_INSTANCE = ""
|
||||||
final def SUPPORT_ACCOUNT_URL = "https://mastodon.social/@Tusky"
|
final def SUPPORT_ACCOUNT_URL = "https://mastodon.social/@Tusky"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk 33
|
compileSdk 34
|
||||||
namespace "com.keylesspalace.tusky"
|
namespace "com.keylesspalace.tusky"
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId APP_ID
|
applicationId APP_ID
|
||||||
namespace "com.keylesspalace.tusky"
|
namespace "com.keylesspalace.tusky"
|
||||||
minSdk 24
|
minSdk 24
|
||||||
targetSdk 33
|
targetSdk 34
|
||||||
versionCode 117
|
versionCode 121
|
||||||
versionName "24.1"
|
versionName "25.2"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
|
@ -41,10 +41,21 @@ android {
|
||||||
buildConfigField("String", "SUPPORT_ACCOUNT_URL", "\"$SUPPORT_ACCOUNT_URL\"")
|
buildConfigField("String", "SUPPORT_ACCOUNT_URL", "\"$SUPPORT_ACCOUNT_URL\"")
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
isDefault true
|
||||||
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources true
|
shrinkResources true
|
||||||
proguardFiles 'proguard-rules.pro'
|
proguardFiles 'proguard-rules.pro'
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
freeCompilerArgs = [
|
||||||
|
"-Xno-param-assertions",
|
||||||
|
"-Xno-call-assertions",
|
||||||
|
"-Xno-receiver-assertions"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,12 +66,13 @@ android {
|
||||||
resValue "string", "app_name", APP_NAME + " Test"
|
resValue "string", "app_name", APP_NAME + " Test"
|
||||||
applicationIdSuffix ".test"
|
applicationIdSuffix ".test"
|
||||||
versionNameSuffix "-" + gitSha
|
versionNameSuffix "-" + gitSha
|
||||||
|
isDefault true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
lintConfig file("lint.xml")
|
lintConfig file("lint.xml")
|
||||||
// Regenerate by running `./gradlew app:newLintBaseline`
|
// Regenerate by deleting the file and running `./gradlew app:lintGreenDebug`
|
||||||
baseline = file("lint-baseline.xml")
|
baseline = file("lint-baseline.xml")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,6 +81,7 @@ android {
|
||||||
resValues true
|
resValues true
|
||||||
viewBinding true
|
viewBinding true
|
||||||
}
|
}
|
||||||
|
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests {
|
unitTests {
|
||||||
returnDefaultValues = true
|
returnDefaultValues = true
|
||||||
|
@ -109,6 +122,7 @@ android {
|
||||||
|
|
||||||
ksp {
|
ksp {
|
||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
arg("room.generateKotlin", "true")
|
||||||
arg("room.incremental", "true")
|
arg("room.incremental", "true")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +136,6 @@ configurations {
|
||||||
// library versions are in PROJECT_ROOT/gradle/libs.versions.toml
|
// library versions are in PROJECT_ROOT/gradle/libs.versions.toml
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation libs.kotlinx.coroutines.android
|
implementation libs.kotlinx.coroutines.android
|
||||||
implementation libs.kotlinx.coroutines.rx3
|
|
||||||
|
|
||||||
implementation libs.bundles.androidx
|
implementation libs.bundles.androidx
|
||||||
implementation libs.bundles.room
|
implementation libs.bundles.room
|
||||||
|
@ -130,24 +143,24 @@ dependencies {
|
||||||
|
|
||||||
implementation libs.android.material
|
implementation libs.android.material
|
||||||
|
|
||||||
implementation libs.gson
|
implementation libs.bundles.moshi
|
||||||
|
ksp libs.moshi.kotlin.codegen
|
||||||
|
|
||||||
implementation libs.bundles.retrofit
|
implementation libs.bundles.retrofit
|
||||||
implementation libs.networkresult.calladapter
|
implementation libs.networkresult.calladapter
|
||||||
|
|
||||||
implementation libs.bundles.okhttp
|
implementation libs.bundles.okhttp
|
||||||
|
implementation libs.okio
|
||||||
|
|
||||||
implementation libs.conscrypt.android
|
implementation libs.conscrypt.android
|
||||||
|
|
||||||
implementation libs.bundles.glide
|
implementation libs.bundles.glide
|
||||||
ksp libs.glide.compiler
|
ksp libs.glide.compiler
|
||||||
|
|
||||||
implementation libs.bundles.rxjava3
|
implementation libs.hilt.android
|
||||||
|
ksp libs.hilt.compiler
|
||||||
implementation libs.bundles.autodispose
|
implementation libs.androidx.hilt.work
|
||||||
|
ksp libs.androidx.hilt.compiler
|
||||||
implementation libs.bundles.dagger
|
|
||||||
kapt libs.bundles.dagger.processors
|
|
||||||
|
|
||||||
implementation libs.sparkbutton
|
implementation libs.sparkbutton
|
||||||
|
|
||||||
|
@ -179,20 +192,3 @@ dependencies {
|
||||||
androidTestImplementation libs.androidx.room.testing
|
androidTestImplementation libs.androidx.room.testing
|
||||||
androidTestImplementation libs.androidx.test.junit
|
androidTestImplementation libs.androidx.test.junit
|
||||||
}
|
}
|
||||||
|
|
||||||
// Work around warnings of:
|
|
||||||
// WARNING: Illegal reflective access by org.jetbrains.kotlin.kapt3.util.ModuleManipulationUtilsKt (file:/C:/Users/Andi/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-annotation-processing-gradle/1.8.22/28dab7e0ee9ce62c03bf97de3543c911dc653700/kotlin-annotation-processing-gradle-1.8.22.jar) to constructor com.sun.tools.javac.util.Context()
|
|
||||||
// See https://youtrack.jetbrains.com/issue/KT-30589/Kapt-An-illegal-reflective-access-operation-has-occurred
|
|
||||||
tasks.withType(org.jetbrains.kotlin.gradle.internal.KaptWithoutKotlincTask).configureEach {
|
|
||||||
kaptProcessJvmArgs.addAll([
|
|
||||||
"--add-opens", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
|
|
||||||
"--add-opens", "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
|
|
||||||
"--add-opens", "jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
|
|
||||||
"--add-opens", "jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED",
|
|
||||||
"--add-opens", "jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
|
|
||||||
"--add-opens", "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
|
|
||||||
"--add-opens", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
|
|
||||||
"--add-opens", "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
|
|
||||||
"--add-opens", "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
|
|
||||||
"--add-opens", "jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED"])
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,27 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<issues format="6" by="lint 8.1.2" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.2)" variant="all" version="8.1.2">
|
<issues format="6" by="lint 8.3.2" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.2)" variant="all" version="8.3.2">
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="GestureBackNavigation"
|
||||||
|
message="If intercepting back events, this should be handled through the registration of callbacks on the window level; Please see https://developer.android.com/about/versions/13/features/predictive-back-gesture"
|
||||||
|
errorLine1=" if (keyCode == KeyEvent.KEYCODE_BACK) {"
|
||||||
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
|
||||||
|
<location
|
||||||
|
file="src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt"
|
||||||
|
line="1314"
|
||||||
|
column="28"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="DefaultLocale"
|
||||||
|
message="Implicitly using the default locale is a common source of bugs: Use `toUpperCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
||||||
|
errorLine1=" sb.append(language.toUpperCase());"
|
||||||
|
errorLine2=" ~~~~~~~~~~~">
|
||||||
|
<location
|
||||||
|
file="src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java"
|
||||||
|
line="104"
|
||||||
|
column="32"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="InvalidPackage"
|
id="InvalidPackage"
|
||||||
|
@ -31,14 +53,14 @@
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="PrivateResource"
|
id="PrivateResource"
|
||||||
message="Overriding `@layout/exo_player_control_view` which is marked as private in androidx.media3:media3-ui:1.1.1. If deliberate, use tools:override="true", otherwise pick a different name.">
|
message="Overriding `@layout/exo_player_control_view` which is marked as private in androidx.media3:media3-ui:1.3.1. If deliberate, use tools:override="true", otherwise pick a different name.">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/layout/exo_player_control_view.xml"/>
|
file="src/main/res/layout/exo_player_control_view.xml"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="PrivateResource"
|
id="PrivateResource"
|
||||||
message="The resource `@color/exo_bottom_bar_background` is marked as private in androidx.media3:media3-ui:1.1.1"
|
message="The resource `@color/exo_bottom_bar_background` is marked as private in androidx.media3:media3-ui:1.3.1"
|
||||||
errorLine1=" android:background="@color/exo_bottom_bar_background""
|
errorLine1=" android:background="@color/exo_bottom_bar_background""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
|
@ -49,7 +71,7 @@
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="PrivateResource"
|
id="PrivateResource"
|
||||||
message="The resource `@dimen/exo_styled_controls_padding` is marked as private in androidx.media3:media3-ui:1.1.1"
|
message="The resource `@dimen/exo_styled_controls_padding` is marked as private in androidx.media3:media3-ui:1.3.1"
|
||||||
errorLine1=" android:padding="@dimen/exo_styled_controls_padding""
|
errorLine1=" android:padding="@dimen/exo_styled_controls_padding""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
|
@ -60,7 +82,7 @@
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="PrivateResource"
|
id="PrivateResource"
|
||||||
message="The resource `@layout/exo_player_control_rewind_button` is marked as private in androidx.media3:media3-ui:1.1.1"
|
message="The resource `@layout/exo_player_control_rewind_button` is marked as private in androidx.media3:media3-ui:1.3.1"
|
||||||
errorLine1=" <include layout="@layout/exo_player_control_rewind_button" />"
|
errorLine1=" <include layout="@layout/exo_player_control_rewind_button" />"
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
|
@ -71,7 +93,7 @@
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="PrivateResource"
|
id="PrivateResource"
|
||||||
message="The resource `@layout/exo_player_control_ffwd_button` is marked as private in androidx.media3:media3-ui:1.1.1"
|
message="The resource `@layout/exo_player_control_ffwd_button` is marked as private in androidx.media3:media3-ui:1.3.1"
|
||||||
errorLine1=" <include layout="@layout/exo_player_control_ffwd_button" />"
|
errorLine1=" <include layout="@layout/exo_player_control_ffwd_button" />"
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
|
@ -82,7 +104,7 @@
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="PrivateResource"
|
id="PrivateResource"
|
||||||
message="The resource `@dimen/exo_styled_bottom_bar_height` is marked as private in androidx.media3:media3-ui:1.1.1"
|
message="The resource `@dimen/exo_styled_bottom_bar_height` is marked as private in androidx.media3:media3-ui:1.3.1"
|
||||||
errorLine1=" android:layout_height="@dimen/exo_styled_bottom_bar_height""
|
errorLine1=" android:layout_height="@dimen/exo_styled_bottom_bar_height""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
|
@ -93,7 +115,7 @@
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="PrivateResource"
|
id="PrivateResource"
|
||||||
message="The resource `@dimen/exo_styled_bottom_bar_margin_top` is marked as private in androidx.media3:media3-ui:1.1.1"
|
message="The resource `@dimen/exo_styled_bottom_bar_margin_top` is marked as private in androidx.media3:media3-ui:1.3.1"
|
||||||
errorLine1=" android:layout_marginTop="@dimen/exo_styled_bottom_bar_margin_top""
|
errorLine1=" android:layout_marginTop="@dimen/exo_styled_bottom_bar_margin_top""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
|
@ -104,7 +126,7 @@
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="PrivateResource"
|
id="PrivateResource"
|
||||||
message="The resource `@color/exo_bottom_bar_background` is marked as private in androidx.media3:media3-ui:1.1.1"
|
message="The resource `@color/exo_bottom_bar_background` is marked as private in androidx.media3:media3-ui:1.3.1"
|
||||||
errorLine1=" android:background="@color/exo_bottom_bar_background""
|
errorLine1=" android:background="@color/exo_bottom_bar_background""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
|
@ -115,7 +137,7 @@
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="PrivateResource"
|
id="PrivateResource"
|
||||||
message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.1.1"
|
message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.3.1"
|
||||||
errorLine1=" android:paddingStart="@dimen/exo_styled_bottom_bar_time_padding""
|
errorLine1=" android:paddingStart="@dimen/exo_styled_bottom_bar_time_padding""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
|
@ -126,7 +148,7 @@
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="PrivateResource"
|
id="PrivateResource"
|
||||||
message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.1.1"
|
message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.3.1"
|
||||||
errorLine1=" android:paddingEnd="@dimen/exo_styled_bottom_bar_time_padding""
|
errorLine1=" android:paddingEnd="@dimen/exo_styled_bottom_bar_time_padding""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
|
@ -137,7 +159,7 @@
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="PrivateResource"
|
id="PrivateResource"
|
||||||
message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.1.1"
|
message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.3.1"
|
||||||
errorLine1=" android:paddingLeft="@dimen/exo_styled_bottom_bar_time_padding""
|
errorLine1=" android:paddingLeft="@dimen/exo_styled_bottom_bar_time_padding""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
|
@ -148,7 +170,7 @@
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="PrivateResource"
|
id="PrivateResource"
|
||||||
message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.1.1"
|
message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.3.1"
|
||||||
errorLine1=" android:paddingRight="@dimen/exo_styled_bottom_bar_time_padding""
|
errorLine1=" android:paddingRight="@dimen/exo_styled_bottom_bar_time_padding""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
|
@ -159,7 +181,7 @@
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="PrivateResource"
|
id="PrivateResource"
|
||||||
message="The resource `@dimen/exo_styled_progress_layout_height` is marked as private in androidx.media3:media3-ui:1.1.1"
|
message="The resource `@dimen/exo_styled_progress_layout_height` is marked as private in androidx.media3:media3-ui:1.3.1"
|
||||||
errorLine1=" android:layout_height="@dimen/exo_styled_progress_layout_height""
|
errorLine1=" android:layout_height="@dimen/exo_styled_progress_layout_height""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
|
@ -170,7 +192,7 @@
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="PrivateResource"
|
id="PrivateResource"
|
||||||
message="The resource `@dimen/exo_styled_progress_margin_bottom` is marked as private in androidx.media3:media3-ui:1.1.1"
|
message="The resource `@dimen/exo_styled_progress_margin_bottom` is marked as private in androidx.media3:media3-ui:1.3.1"
|
||||||
errorLine1=" android:layout_marginBottom="@dimen/exo_styled_progress_margin_bottom"/>"
|
errorLine1=" android:layout_marginBottom="@dimen/exo_styled_progress_margin_bottom"/>"
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
|
@ -181,7 +203,7 @@
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="PrivateResource"
|
id="PrivateResource"
|
||||||
message="The resource `@dimen/exo_styled_minimal_controls_margin_bottom` is marked as private in androidx.media3:media3-ui:1.1.1"
|
message="The resource `@dimen/exo_styled_minimal_controls_margin_bottom` is marked as private in androidx.media3:media3-ui:1.3.1"
|
||||||
errorLine1=" android:layout_marginBottom="@dimen/exo_styled_minimal_controls_margin_bottom""
|
errorLine1=" android:layout_marginBottom="@dimen/exo_styled_minimal_controls_margin_bottom""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
|
@ -208,7 +230,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/values/strings.xml"
|
||||||
line="328"
|
line="331"
|
||||||
column="5"/>
|
column="5"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -219,7 +241,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/values/strings.xml"
|
||||||
line="386"
|
line="389"
|
||||||
column="5"/>
|
column="5"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -230,7 +252,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/values/strings.xml"
|
||||||
line="562"
|
line="570"
|
||||||
column="5"/>
|
column="5"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -241,14 +263,14 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/values/strings.xml"
|
||||||
line="783"
|
line="790"
|
||||||
column="5"/>
|
column="5"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="UnusedTranslation"
|
id="UnusedTranslation"
|
||||||
message="The language `ber (Berber languages)` is present in this project, but not declared in the `localeConfig` resource"
|
message="The language `ber (Berber languages)` is present in this project, but not declared in the `localeConfig` resource"
|
||||||
errorLine1=" android:localeConfig="@xml/locales_config">"
|
errorLine1=" android:localeConfig="@xml/locales_config""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/AndroidManifest.xml"
|
file="src/main/AndroidManifest.xml"
|
||||||
|
@ -259,7 +281,7 @@
|
||||||
<issue
|
<issue
|
||||||
id="UnusedTranslation"
|
id="UnusedTranslation"
|
||||||
message="The language `el (Greek)` is present in this project, but not declared in the `localeConfig` resource"
|
message="The language `el (Greek)` is present in this project, but not declared in the `localeConfig` resource"
|
||||||
errorLine1=" android:localeConfig="@xml/locales_config">"
|
errorLine1=" android:localeConfig="@xml/locales_config""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/AndroidManifest.xml"
|
file="src/main/AndroidManifest.xml"
|
||||||
|
@ -270,7 +292,7 @@
|
||||||
<issue
|
<issue
|
||||||
id="UnusedTranslation"
|
id="UnusedTranslation"
|
||||||
message="The language `fi (Finnish)` is present in this project, but not declared in the `localeConfig` resource"
|
message="The language `fi (Finnish)` is present in this project, but not declared in the `localeConfig` resource"
|
||||||
errorLine1=" android:localeConfig="@xml/locales_config">"
|
errorLine1=" android:localeConfig="@xml/locales_config""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/AndroidManifest.xml"
|
file="src/main/AndroidManifest.xml"
|
||||||
|
@ -281,7 +303,7 @@
|
||||||
<issue
|
<issue
|
||||||
id="UnusedTranslation"
|
id="UnusedTranslation"
|
||||||
message="The language `fy (Western Frisian)` is present in this project, but not declared in the `localeConfig` resource"
|
message="The language `fy (Western Frisian)` is present in this project, but not declared in the `localeConfig` resource"
|
||||||
errorLine1=" android:localeConfig="@xml/locales_config">"
|
errorLine1=" android:localeConfig="@xml/locales_config""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/AndroidManifest.xml"
|
file="src/main/AndroidManifest.xml"
|
||||||
|
@ -292,7 +314,7 @@
|
||||||
<issue
|
<issue
|
||||||
id="UnusedTranslation"
|
id="UnusedTranslation"
|
||||||
message="The language `in (Indonesian)` is present in this project, but not declared in the `localeConfig` resource"
|
message="The language `in (Indonesian)` is present in this project, but not declared in the `localeConfig` resource"
|
||||||
errorLine1=" android:localeConfig="@xml/locales_config">"
|
errorLine1=" android:localeConfig="@xml/locales_config""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/AndroidManifest.xml"
|
file="src/main/AndroidManifest.xml"
|
||||||
|
@ -303,7 +325,7 @@
|
||||||
<issue
|
<issue
|
||||||
id="UnusedTranslation"
|
id="UnusedTranslation"
|
||||||
message="The language `lv (Latvian)` is present in this project, but not declared in the `localeConfig` resource"
|
message="The language `lv (Latvian)` is present in this project, but not declared in the `localeConfig` resource"
|
||||||
errorLine1=" android:localeConfig="@xml/locales_config">"
|
errorLine1=" android:localeConfig="@xml/locales_config""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/AndroidManifest.xml"
|
file="src/main/AndroidManifest.xml"
|
||||||
|
@ -314,7 +336,7 @@
|
||||||
<issue
|
<issue
|
||||||
id="UnusedTranslation"
|
id="UnusedTranslation"
|
||||||
message="The language `ml (Malayalam)` is present in this project, but not declared in the `localeConfig` resource"
|
message="The language `ml (Malayalam)` is present in this project, but not declared in the `localeConfig` resource"
|
||||||
errorLine1=" android:localeConfig="@xml/locales_config">"
|
errorLine1=" android:localeConfig="@xml/locales_config""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/AndroidManifest.xml"
|
file="src/main/AndroidManifest.xml"
|
||||||
|
@ -325,7 +347,7 @@
|
||||||
<issue
|
<issue
|
||||||
id="UnusedTranslation"
|
id="UnusedTranslation"
|
||||||
message="The language `si (Sinhala)` is present in this project, but not declared in the `localeConfig` resource"
|
message="The language `si (Sinhala)` is present in this project, but not declared in the `localeConfig` resource"
|
||||||
errorLine1=" android:localeConfig="@xml/locales_config">"
|
errorLine1=" android:localeConfig="@xml/locales_config""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/AndroidManifest.xml"
|
file="src/main/AndroidManifest.xml"
|
||||||
|
@ -336,7 +358,7 @@
|
||||||
<issue
|
<issue
|
||||||
id="UnusedTranslation"
|
id="UnusedTranslation"
|
||||||
message="The language `sk (Slovak)` is present in this project, but not declared in the `localeConfig` resource"
|
message="The language `sk (Slovak)` is present in this project, but not declared in the `localeConfig` resource"
|
||||||
errorLine1=" android:localeConfig="@xml/locales_config">"
|
errorLine1=" android:localeConfig="@xml/locales_config""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/AndroidManifest.xml"
|
file="src/main/AndroidManifest.xml"
|
||||||
|
@ -362,7 +384,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt"
|
file="src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt"
|
||||||
line="481"
|
line="532"
|
||||||
column="9"/>
|
column="9"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -373,7 +395,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/AccountAdapter.kt"
|
file="src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/AccountAdapter.kt"
|
||||||
line="80"
|
line="78"
|
||||||
column="9"/>
|
column="9"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -384,7 +406,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt"
|
file="src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt"
|
||||||
line="43"
|
line="45"
|
||||||
column="9"/>
|
column="9"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -395,7 +417,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt"
|
file="src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt"
|
||||||
line="49"
|
line="51"
|
||||||
column="9"/>
|
column="9"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -406,7 +428,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt"
|
file="src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt"
|
||||||
line="140"
|
line="157"
|
||||||
column="9"/>
|
column="9"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -417,7 +439,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/MutesAdapter.kt"
|
file="src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/MutesAdapter.kt"
|
||||||
line="107"
|
line="118"
|
||||||
column="9"/>
|
column="9"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -472,76 +494,10 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt"
|
file="src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt"
|
||||||
line="157"
|
line="161"
|
||||||
column="13"/>
|
column="13"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="ObsoleteSdkInt"
|
|
||||||
message="Unnecessary; SDK_INT is always >= 24"
|
|
||||||
errorLine1=" if (Build.VERSION.SDK_INT > 23) {"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt"
|
|
||||||
line="280"
|
|
||||||
column="13"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="ObsoleteSdkInt"
|
|
||||||
message="Unnecessary; SDK_INT is never < 24"
|
|
||||||
errorLine1=" if (Build.VERSION.SDK_INT <= 23 || player == null) {"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt"
|
|
||||||
line="289"
|
|
||||||
column="13"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="ObsoleteSdkInt"
|
|
||||||
message="Unnecessary; SDK_INT is never < 24"
|
|
||||||
errorLine1=" if (Build.VERSION.SDK_INT <= 23) {"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt"
|
|
||||||
line="310"
|
|
||||||
column="13"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="ObsoleteSdkInt"
|
|
||||||
message="Unnecessary; SDK_INT is always >= 24"
|
|
||||||
errorLine1=" if (Build.VERSION.SDK_INT > 23) {"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt"
|
|
||||||
line="322"
|
|
||||||
column="13"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="StringFormatTrivial"
|
|
||||||
message="This formatting string is trivial. Rather than using `String.format` to create your String, it will be more performant to concatenate your arguments with `+`. "
|
|
||||||
errorLine1=" (error) -> Log.e(TAG, String.format("Failed to %s account id %s", accept ? "accept" : "reject", id))"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java"
|
|
||||||
line="807"
|
|
||||||
column="61"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="StringFormatTrivial"
|
|
||||||
message="This formatting string is trivial. Rather than using `String.format` to create your String, it will be more performant to concatenate your arguments with `+`. "
|
|
||||||
errorLine1=" LinkHelper.openLink(requireContext(), String.format("https://%s/admin/reports/%s", accountManager.getActiveAccount().getDomain(), reportId));"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java"
|
|
||||||
line="828"
|
|
||||||
column="61"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="SmallSp"
|
id="SmallSp"
|
||||||
message="Avoid using sizes smaller than `11sp`: `8sp`"
|
message="Avoid using sizes smaller than `11sp`: `8sp`"
|
||||||
|
@ -589,12 +545,12 @@
|
||||||
<issue
|
<issue
|
||||||
id="ReportShortcutUsage"
|
id="ReportShortcutUsage"
|
||||||
message="Calling this method indicates use of dynamic shortcuts, but there are no calls to methods that track shortcut usage, such as `pushDynamicShortcut` or `reportShortcutUsed`. Calling these methods is recommended, as they track shortcut usage and allow launchers to adjust which shortcuts appear based on activation history. Please see https://developer.android.com/develop/ui/views/launch/shortcuts/managing-shortcuts#track-usage"
|
message="Calling this method indicates use of dynamic shortcuts, but there are no calls to methods that track shortcut usage, such as `pushDynamicShortcut` or `reportShortcutUsed`. Calling these methods is recommended, as they track shortcut usage and allow launchers to adjust which shortcuts appear based on activation history. Please see https://developer.android.com/develop/ui/views/launch/shortcuts/managing-shortcuts#track-usage"
|
||||||
errorLine1=" ShortcutManagerCompat.addDynamicShortcuts(context, listOf(shortcutInfo))"
|
errorLine1=" ShortcutManagerCompat.addDynamicShortcuts(context, listOf(shortcutInfo))"
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt"
|
file="src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt"
|
||||||
line="86"
|
line="96"
|
||||||
column="9"/>
|
column="13"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
|
@ -604,7 +560,7 @@
|
||||||
errorLine2=" ^">
|
errorLine2=" ^">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt"
|
file="src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt"
|
||||||
line="91"
|
line="95"
|
||||||
column="13"/>
|
column="13"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -615,7 +571,7 @@
|
||||||
errorLine2=" ^">
|
errorLine2=" ^">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt"
|
file="src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt"
|
||||||
line="91"
|
line="95"
|
||||||
column="50"/>
|
column="50"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -895,289 +851,36 @@
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="UnknownNullness"
|
id="RtlHardcoded"
|
||||||
message="Should explicitly declare type here since implicit type does not specify nullness"
|
message="Consider replacing `android:layout_marginLeft` with `android:layout_marginStart="8dp"` to better support right-to-left layouts"
|
||||||
errorLine1=" override fun onCreateView("
|
errorLine1=" android:layout_marginLeft="8dp""
|
||||||
errorLine2=" ~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt"
|
file="src/main/res/layout/item_list.xml"
|
||||||
line="53"
|
line="37"
|
||||||
column="18"/>
|
column="9"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="UnknownNullness"
|
id="RtlHardcoded"
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
message="Consider replacing `android:layout_marginLeft` with `android:layout_marginStart="8dp"` to better support right-to-left layouts"
|
||||||
errorLine1=" public abstract boolean deepEquals(NotificationViewData other);"
|
errorLine1=" android:layout_marginLeft="8dp""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java"
|
file="src/main/res/layout/item_list.xml"
|
||||||
line="43"
|
line="48"
|
||||||
column="40"/>
|
column="9"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="UnknownNullness"
|
id="RtlHardcoded"
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
message="Consider replacing `android:layout_marginLeft` with `android:layout_marginStart="8dp"` to better support right-to-left layouts"
|
||||||
errorLine1=" public Concrete(Notification.Type type, String id, TimelineAccount account,"
|
errorLine1=" android:layout_marginLeft="8dp""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java"
|
file="src/main/res/layout/item_list.xml"
|
||||||
line="54"
|
line="59"
|
||||||
column="25"/>
|
column="9"/>
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" public Concrete(Notification.Type type, String id, TimelineAccount account,"
|
|
||||||
errorLine2=" ~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java"
|
|
||||||
line="54"
|
|
||||||
column="49"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" public Concrete(Notification.Type type, String id, TimelineAccount account,"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java"
|
|
||||||
line="54"
|
|
||||||
column="60"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" public Notification.Type getType() {"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java"
|
|
||||||
line="63"
|
|
||||||
column="16"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" public String getId() {"
|
|
||||||
errorLine2=" ~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java"
|
|
||||||
line="67"
|
|
||||||
column="16"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" public TimelineAccount getAccount() {"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java"
|
|
||||||
line="71"
|
|
||||||
column="16"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" public boolean deepEquals(NotificationViewData o) {"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java"
|
|
||||||
line="91"
|
|
||||||
column="35"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" public Concrete copyWithStatus(@Nullable StatusViewData.Concrete statusViewData) {"
|
|
||||||
errorLine2=" ~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java"
|
|
||||||
line="108"
|
|
||||||
column="16"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" public boolean deepEquals(NotificationViewData other) {"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java"
|
|
||||||
line="132"
|
|
||||||
column="35"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" public NotificationsAdapter(String accountId,"
|
|
||||||
errorLine2=" ~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java"
|
|
||||||
line="98"
|
|
||||||
column="33"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" AdapterDataSource<NotificationViewData> dataSource,"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java"
|
|
||||||
line="99"
|
|
||||||
column="33"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" StatusDisplayOptions statusDisplayOptions,"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java"
|
|
||||||
line="100"
|
|
||||||
column="33"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" StatusActionListener statusListener,"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java"
|
|
||||||
line="101"
|
|
||||||
column="33"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" NotificationActionListener notificationActionListener,"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java"
|
|
||||||
line="102"
|
|
||||||
column="33"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" AccountActionListener accountActionListener) {"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java"
|
|
||||||
line="103"
|
|
||||||
column="33"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" void onViewAccount(String id);"
|
|
||||||
errorLine2=" ~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java"
|
|
||||||
line="340"
|
|
||||||
column="28"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" void onViewStatusForNotificationId(String notificationId);"
|
|
||||||
errorLine2=" ~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java"
|
|
||||||
line="342"
|
|
||||||
column="44"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" void onViewReport(String reportId);"
|
|
||||||
errorLine2=" ~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java"
|
|
||||||
line="344"
|
|
||||||
column="27"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" public static NotificationsFragment newInstance() {"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java"
|
|
||||||
line="200"
|
|
||||||
column="19"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" public void onMute(boolean mute, String id, int position, boolean notifications) {"
|
|
||||||
errorLine2=" ~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java"
|
|
||||||
line="789"
|
|
||||||
column="38"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" public void onBlock(boolean block, String id, int position) {"
|
|
||||||
errorLine2=" ~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java"
|
|
||||||
line="794"
|
|
||||||
column="40"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" public void onRespondToFollowRequest(boolean accept, String id, int position) {"
|
|
||||||
errorLine2=" ~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java"
|
|
||||||
line="799"
|
|
||||||
column="58"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" public void onViewStatusForNotificationId(String notificationId) {"
|
|
||||||
errorLine2=" ~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java"
|
|
||||||
line="812"
|
|
||||||
column="47"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UnknownNullness"
|
|
||||||
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
|
|
||||||
errorLine1=" public void onViewReport(String reportId) {"
|
|
||||||
errorLine2=" ~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java"
|
|
||||||
line="827"
|
|
||||||
column="30"/>
|
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
</issues>
|
</issues>
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
|
|
||||||
<!-- Newer dependencies are handled by Renovate, and don't need a warning -->
|
<!-- Newer dependencies are handled by Renovate, and don't need a warning -->
|
||||||
<issue id="GradleDependency" severity="ignore" />
|
<issue id="GradleDependency" severity="ignore" />
|
||||||
|
<issue id="NewerVersionAvailable" severity="ignore" />
|
||||||
|
|
||||||
<!-- Typographical punctuation is not something we care about at the moment -->
|
<!-- Typographical punctuation is not something we care about at the moment -->
|
||||||
<issue id="TypographyQuotes" severity="ignore" />
|
<issue id="TypographyQuotes" severity="ignore" />
|
||||||
|
|
|
@ -1,83 +1,44 @@
|
||||||
# GENERAL OPTIONS
|
# GENERAL OPTIONS
|
||||||
|
|
||||||
# turn on all optimizations except those that are known to cause problems on Android
|
|
||||||
-optimizations !code/simplification/cast,!field/*,!class/merging/*
|
|
||||||
-optimizationpasses 6
|
|
||||||
-allowaccessmodification
|
-allowaccessmodification
|
||||||
-dontpreverify
|
|
||||||
|
|
||||||
-dontusemixedcaseclassnames
|
# Preserve some attributes that may be required for reflection.
|
||||||
-dontskipnonpubliclibraryclasses
|
-keepattributes RuntimeVisible*Annotations, AnnotationDefault
|
||||||
-keepattributes *Annotation*
|
|
||||||
|
|
||||||
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
|
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
|
||||||
-keepclasseswithmembernames class * {
|
-keepclasseswithmembernames class * {
|
||||||
native <methods>;
|
native <methods>;
|
||||||
}
|
}
|
||||||
# keep setters in Views so that animations can still work.
|
|
||||||
# see http://proguard.sourceforge.net/manual/examples.html#beans
|
|
||||||
-keepclassmembers public class * extends android.view.View {
|
|
||||||
void set*(***);
|
|
||||||
*** get*();
|
|
||||||
}
|
|
||||||
# We want to keep methods in Activity that could be used in the XML attribute onClick
|
|
||||||
-keepclassmembers class * extends android.app.Activity {
|
|
||||||
public void *(android.view.View);
|
|
||||||
}
|
|
||||||
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
|
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
|
||||||
-keepclassmembers enum * {
|
-keepclassmembers,allowoptimization enum * {
|
||||||
public static **[] values();
|
public static **[] values();
|
||||||
public static ** valueOf(java.lang.String);
|
public static ** valueOf(java.lang.String);
|
||||||
}
|
}
|
||||||
|
|
||||||
-keepclassmembers class * implements android.os.Parcelable {
|
-keepclassmembers class * implements android.os.Parcelable {
|
||||||
public static final ** CREATOR;
|
public static final ** CREATOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
-keepclassmembers class **.R$* {
|
# Preserve annotated Javascript interface methods.
|
||||||
public static <fields>;
|
-keepclassmembers class * {
|
||||||
|
@android.webkit.JavascriptInterface <methods>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The support libraries contains references to newer platform versions.
|
||||||
|
# Don't warn about those in case this app is linking against an older
|
||||||
|
# platform version. We know about them, and they are safe.
|
||||||
|
-dontnote androidx.**
|
||||||
|
-dontwarn androidx.**
|
||||||
|
|
||||||
|
# This class is deprecated, but remains for backward compatibility.
|
||||||
|
-dontwarn android.util.FloatMath
|
||||||
|
|
||||||
|
# These classes are duplicated between android.jar and core-lambda-stubs.jar.
|
||||||
|
-dontnote java.lang.invoke.**
|
||||||
|
|
||||||
# TUSKY SPECIFIC OPTIONS
|
# TUSKY SPECIFIC OPTIONS
|
||||||
|
|
||||||
# keep members of our model classes, they are used in json de/serialization
|
|
||||||
-keepclassmembers class com.keylesspalace.tusky.entity.* { *; }
|
|
||||||
|
|
||||||
-keep public enum com.keylesspalace.tusky.entity.*$** {
|
|
||||||
**[] $VALUES;
|
|
||||||
public *;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclassmembers class com.keylesspalace.tusky.components.conversation.ConversationAccountEntity { *; }
|
|
||||||
-keepclassmembers class com.keylesspalace.tusky.db.DraftAttachment { *; }
|
|
||||||
|
|
||||||
-keep enum com.keylesspalace.tusky.db.DraftAttachment$Type {
|
|
||||||
public *;
|
|
||||||
}
|
|
||||||
|
|
||||||
# https://github.com/google/gson/blob/master/examples/android-proguard-example/proguard.cfg
|
|
||||||
|
|
||||||
# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
|
|
||||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
|
||||||
-keep class * extends com.google.gson.TypeAdapter
|
|
||||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
|
||||||
-keep class * implements com.google.gson.JsonSerializer
|
|
||||||
-keep class * implements com.google.gson.JsonDeserializer
|
|
||||||
|
|
||||||
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
|
|
||||||
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
|
|
||||||
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
|
|
||||||
|
|
||||||
# Retain generic signatures of classes used in MastodonApi so Retrofit works
|
|
||||||
-keep,allowobfuscation,allowshrinking class io.reactivex.rxjava3.core.Single
|
|
||||||
-keep,allowobfuscation,allowshrinking class retrofit2.Response
|
|
||||||
-keep,allowobfuscation,allowshrinking class kotlin.collections.List
|
|
||||||
-keep,allowobfuscation,allowshrinking class kotlin.collections.Map
|
|
||||||
-keep,allowobfuscation,allowshrinking class retrofit2.Call
|
|
||||||
|
|
||||||
# https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md#retrofit
|
|
||||||
-keepattributes Signature
|
|
||||||
-keep class kotlin.coroutines.Continuation
|
|
||||||
|
|
||||||
# preserve line numbers for crash reporting
|
# preserve line numbers for crash reporting
|
||||||
-keepattributes SourceFile,LineNumberTable
|
-keepattributes SourceFile,LineNumberTable
|
||||||
-renamesourcefileattribute SourceFile
|
-renamesourcefileattribute SourceFile
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,6 @@
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
import androidx.room.testing.MigrationTestHelper
|
import androidx.room.testing.MigrationTestHelper
|
||||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
|
@ -19,8 +18,7 @@ class MigrationsTest {
|
||||||
@Rule
|
@Rule
|
||||||
var helper: MigrationTestHelper = MigrationTestHelper(
|
var helper: MigrationTestHelper = MigrationTestHelper(
|
||||||
InstrumentationRegistry.getInstrumentation(),
|
InstrumentationRegistry.getInstrumentation(),
|
||||||
AppDatabase::class.java.canonicalName,
|
AppDatabase::class.java
|
||||||
FrameworkSQLiteOpenHelperFactory()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -18,24 +18,9 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/TuskyTheme"
|
android:theme="@style/TuskyTheme"
|
||||||
android:usesCleartextTraffic="false"
|
android:usesCleartextTraffic="false"
|
||||||
android:localeConfig="@xml/locales_config">
|
android:localeConfig="@xml/locales_config"
|
||||||
|
android:enableOnBackInvokedCallback="true">
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".SplashActivity"
|
|
||||||
android:theme="@style/SplashTheme"
|
|
||||||
android:exported="true">
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="android.app.shortcuts"
|
|
||||||
android:resource="@xml/share_shortcuts" />
|
|
||||||
|
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".components.login.LoginActivity"
|
android:name=".components.login.LoginActivity"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
@ -55,8 +40,14 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize"
|
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize"
|
||||||
android:exported="true">
|
android:exported="true"
|
||||||
|
android:theme="@style/SplashTheme">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
|
||||||
|
@ -100,6 +91,9 @@
|
||||||
<data android:mimeType="audio/*" />
|
<data android:mimeType="audio/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/share_shortcuts" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.service.chooser.chooser_target_service"
|
android:name="android.service.chooser.chooser_target_service"
|
||||||
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
|
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
|
||||||
|
@ -195,6 +189,7 @@
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service android:name=".service.SendStatusService"
|
<service android:name=".service.SendStatusService"
|
||||||
|
android:foregroundServiceType="shortService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -12,19 +9,21 @@ import android.text.method.LinkMovementMethod
|
||||||
import android.text.style.URLSpan
|
import android.text.style.URLSpan
|
||||||
import android.text.util.Linkify
|
import android.text.util.Linkify
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
||||||
import com.keylesspalace.tusky.databinding.ActivityAboutBinding
|
import com.keylesspalace.tusky.databinding.ActivityAboutBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
|
||||||
import com.keylesspalace.tusky.util.NoUnderlineURLSpan
|
import com.keylesspalace.tusky.util.NoUnderlineURLSpan
|
||||||
|
import com.keylesspalace.tusky.util.copyToClipboard
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import kotlinx.coroutines.launch
|
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class AboutActivity : BottomSheetActivity(), Injectable {
|
@AndroidEntryPoint
|
||||||
|
class AboutActivity : BottomSheetActivity() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var instanceInfoRepository: InstanceInfoRepository
|
lateinit var instanceInfoRepository: InstanceInfoRepository
|
||||||
|
|
||||||
|
@ -54,7 +53,7 @@ class AboutActivity : BottomSheetActivity(), Injectable {
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
accountManager.activeAccount?.let { account ->
|
accountManager.activeAccount?.let { account ->
|
||||||
val instanceInfo = instanceInfoRepository.getInstanceInfo()
|
val instanceInfo = instanceInfoRepository.getUpdatedInstanceInfoOrFallback()
|
||||||
binding.accountInfo.text = getString(
|
binding.accountInfo.text = getString(
|
||||||
R.string.about_account_info,
|
R.string.about_account_info,
|
||||||
account.username,
|
account.username,
|
||||||
|
@ -70,9 +69,15 @@ class AboutActivity : BottomSheetActivity(), Injectable {
|
||||||
binding.aboutPoweredByTusky.hide()
|
binding.aboutPoweredByTusky.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(R.string.about_tusky_license)
|
binding.aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(
|
||||||
binding.aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(R.string.about_project_site)
|
R.string.about_tusky_license
|
||||||
binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(R.string.about_bug_feature_request_site)
|
)
|
||||||
|
binding.aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(
|
||||||
|
R.string.about_project_site
|
||||||
|
)
|
||||||
|
binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(
|
||||||
|
R.string.about_bug_feature_request_site
|
||||||
|
)
|
||||||
|
|
||||||
binding.tuskyProfileButton.setOnClickListener {
|
binding.tuskyProfileButton.setOnClickListener {
|
||||||
viewUrl(BuildConfig.SUPPORT_ACCOUNT_URL)
|
viewUrl(BuildConfig.SUPPORT_ACCOUNT_URL)
|
||||||
|
@ -83,13 +88,11 @@ class AboutActivity : BottomSheetActivity(), Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.copyDeviceInfo.setOnClickListener {
|
binding.copyDeviceInfo.setOnClickListener {
|
||||||
val text = "${binding.versionTextView.text}\n\nDevice:\n\n${binding.deviceInfo.text}\n\nAccount:\n\n${binding.accountInfo.text}"
|
copyToClipboard(
|
||||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
"${binding.versionTextView.text}\n\nDevice:\n\n${binding.deviceInfo.text}\n\nAccount:\n\n${binding.accountInfo.text}",
|
||||||
val clip = ClipData.newPlainText("Tusky version information", text)
|
getString(R.string.about_copied),
|
||||||
clipboard.setPrimaryClip(clip)
|
"Tusky version information",
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
)
|
||||||
Toast.makeText(this, getString(R.string.about_copied), Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -25,48 +26,42 @@ import androidx.appcompat.widget.SearchView
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import com.keylesspalace.tusky.databinding.FragmentAccountsInListBinding
|
import com.keylesspalace.tusky.databinding.FragmentAccountsInListBinding
|
||||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
|
||||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
import com.keylesspalace.tusky.util.Either
|
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.loadAvatar
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.unsafeLazy
|
import com.keylesspalace.tusky.util.unsafeLazy
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
import com.keylesspalace.tusky.util.visible
|
||||||
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
|
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
|
||||||
import com.keylesspalace.tusky.viewmodel.State
|
import com.keylesspalace.tusky.viewmodel.State
|
||||||
import kotlinx.coroutines.launch
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
private typealias AccountInfo = Pair<TimelineAccount, Boolean>
|
private typealias AccountInfo = Pair<TimelineAccount, Boolean>
|
||||||
|
|
||||||
class AccountsInListFragment : DialogFragment(), Injectable {
|
@AndroidEntryPoint
|
||||||
|
class AccountsInListFragment : DialogFragment(R.layout.fragment_accounts_in_list) {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var viewModelFactory: ViewModelFactory
|
lateinit var preferences: SharedPreferences
|
||||||
|
|
||||||
private val viewModel: AccountsInListViewModel by viewModels { viewModelFactory }
|
private val viewModel: AccountsInListViewModel by viewModels()
|
||||||
private val binding by viewBinding(FragmentAccountsInListBinding::bind)
|
private val binding by viewBinding(FragmentAccountsInListBinding::bind)
|
||||||
|
|
||||||
private lateinit var listId: String
|
private lateinit var listId: String
|
||||||
private lateinit var listName: String
|
private lateinit var listName: String
|
||||||
private val adapter = Adapter()
|
|
||||||
private val searchAdapter = SearchAdapter()
|
|
||||||
|
|
||||||
private val radius by unsafeLazy { resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) }
|
private val radius by unsafeLazy { resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) }
|
||||||
private val pm by unsafeLazy { PreferenceManager.getDefaultSharedPreferences(requireContext()) }
|
|
||||||
private val animateAvatar by unsafeLazy { pm.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) }
|
|
||||||
private val animateEmojis by unsafeLazy { pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) }
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -82,15 +77,17 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
dialog?.apply {
|
dialog?.apply {
|
||||||
// Stretch dialog to the window
|
// Stretch dialog to the window
|
||||||
window?.setLayout(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)
|
window?.setLayout(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
||||||
return inflater.inflate(R.layout.fragment_accounts_in_list, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
val adapter = Adapter()
|
||||||
|
val searchAdapter = SearchAdapter()
|
||||||
|
|
||||||
binding.accountsRecycler.layoutManager = LinearLayoutManager(view.context)
|
binding.accountsRecycler.layoutManager = LinearLayoutManager(view.context)
|
||||||
binding.accountsRecycler.adapter = adapter
|
binding.accountsRecycler.adapter = adapter
|
||||||
|
|
||||||
|
@ -99,14 +96,14 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
viewModel.state.collect { state ->
|
viewModel.state.collect { state ->
|
||||||
adapter.submitList(state.accounts.asRightOrNull() ?: listOf())
|
adapter.submitList(state.accounts.getOrDefault(emptyList()))
|
||||||
|
|
||||||
when (state.accounts) {
|
state.accounts.fold(
|
||||||
is Either.Right -> binding.messageView.hide()
|
onSuccess = { binding.messageView.hide() },
|
||||||
is Either.Left -> handleError(state.accounts.value)
|
onFailure = { handleError(it) }
|
||||||
}
|
)
|
||||||
|
|
||||||
setupSearchView(state)
|
setupSearchView(searchAdapter, state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,13 +124,13 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupSearchView(state: State) {
|
private fun setupSearchView(searchAdapter: SearchAdapter, state: State) {
|
||||||
if (state.searchResult == null) {
|
if (state.searchResult == null) {
|
||||||
searchAdapter.submitList(listOf())
|
searchAdapter.submitList(listOf())
|
||||||
binding.accountsSearchRecycler.hide()
|
binding.accountsSearchRecycler.hide()
|
||||||
binding.accountsRecycler.show()
|
binding.accountsRecycler.show()
|
||||||
} else {
|
} else {
|
||||||
val listAccounts = state.accounts.asRightOrNull() ?: listOf()
|
val listAccounts = state.accounts.getOrDefault(emptyList())
|
||||||
val newList = state.searchResult.map { acc ->
|
val newList = state.searchResult.map { acc ->
|
||||||
acc to listAccounts.contains(acc)
|
acc to listAccounts.contains(acc)
|
||||||
}
|
}
|
||||||
|
@ -164,15 +161,27 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
||||||
return oldItem.id == newItem.id
|
return oldItem.id == newItem.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean {
|
override fun areContentsTheSame(
|
||||||
|
oldItem: TimelineAccount,
|
||||||
|
newItem: TimelineAccount
|
||||||
|
): Boolean {
|
||||||
return oldItem == newItem
|
return oldItem == newItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class Adapter : ListAdapter<TimelineAccount, BindingHolder<ItemFollowRequestBinding>>(AccountDiffer) {
|
inner class Adapter : ListAdapter<TimelineAccount, BindingHolder<ItemFollowRequestBinding>>(
|
||||||
|
AccountDiffer
|
||||||
|
) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
|
override fun onCreateViewHolder(
|
||||||
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): BindingHolder<ItemFollowRequestBinding> {
|
||||||
|
val binding = ItemFollowRequestBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
val holder = BindingHolder(binding)
|
val holder = BindingHolder(binding)
|
||||||
|
|
||||||
binding.notificationTextView.hide()
|
binding.notificationTextView.hide()
|
||||||
|
@ -186,10 +195,17 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
||||||
return holder
|
return holder
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BindingHolder<ItemFollowRequestBinding>, position: Int) {
|
override fun onBindViewHolder(
|
||||||
|
holder: BindingHolder<ItemFollowRequestBinding>,
|
||||||
|
position: Int
|
||||||
|
) {
|
||||||
val account = getItem(position)
|
val account = getItem(position)
|
||||||
|
val animateAvatar = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
|
||||||
|
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||||
|
val showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, false)
|
||||||
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis)
|
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis)
|
||||||
holder.binding.usernameTextView.text = account.username
|
holder.binding.usernameTextView.text = account.username
|
||||||
|
holder.binding.avatarBadge.visible(showBotOverlay && account.bot)
|
||||||
loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar)
|
loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -204,10 +220,19 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class SearchAdapter : ListAdapter<AccountInfo, BindingHolder<ItemFollowRequestBinding>>(SearchDiffer) {
|
inner class SearchAdapter : ListAdapter<AccountInfo, BindingHolder<ItemFollowRequestBinding>>(
|
||||||
|
SearchDiffer
|
||||||
|
) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
|
override fun onCreateViewHolder(
|
||||||
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): BindingHolder<ItemFollowRequestBinding> {
|
||||||
|
val binding = ItemFollowRequestBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
val holder = BindingHolder(binding)
|
val holder = BindingHolder(binding)
|
||||||
|
|
||||||
binding.notificationTextView.hide()
|
binding.notificationTextView.hide()
|
||||||
|
@ -224,9 +249,15 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
||||||
return holder
|
return holder
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BindingHolder<ItemFollowRequestBinding>, position: Int) {
|
override fun onBindViewHolder(
|
||||||
|
holder: BindingHolder<ItemFollowRequestBinding>,
|
||||||
|
position: Int
|
||||||
|
) {
|
||||||
val (account, inAList) = getItem(position)
|
val (account, inAList) = getItem(position)
|
||||||
|
|
||||||
|
val animateAvatar = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
|
||||||
|
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||||
|
|
||||||
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis)
|
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis)
|
||||||
holder.binding.usernameTextView.text = account.username
|
holder.binding.usernameTextView.text = account.username
|
||||||
loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar)
|
loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar)
|
||||||
|
|
|
@ -19,7 +19,6 @@ import android.app.ActivityManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
|
@ -34,46 +33,71 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.core.app.ActivityCompat;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import com.google.android.material.color.MaterialColors;
|
import com.google.android.material.color.MaterialColors;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import com.keylesspalace.tusky.adapter.AccountSelectionAdapter;
|
import com.keylesspalace.tusky.adapter.AccountSelectionAdapter;
|
||||||
import com.keylesspalace.tusky.components.login.LoginActivity;
|
import com.keylesspalace.tusky.components.login.LoginActivity;
|
||||||
import com.keylesspalace.tusky.db.AccountEntity;
|
import com.keylesspalace.tusky.db.entity.AccountEntity;
|
||||||
import com.keylesspalace.tusky.db.AccountManager;
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.di.Injectable;
|
import com.keylesspalace.tusky.di.PreferencesEntryPoint;
|
||||||
import com.keylesspalace.tusky.interfaces.AccountSelectionListener;
|
import com.keylesspalace.tusky.interfaces.AccountSelectionListener;
|
||||||
import com.keylesspalace.tusky.interfaces.PermissionRequester;
|
|
||||||
import com.keylesspalace.tusky.settings.AppTheme;
|
import com.keylesspalace.tusky.settings.AppTheme;
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys;
|
import com.keylesspalace.tusky.settings.PrefKeys;
|
||||||
|
import com.keylesspalace.tusky.util.ActivityConstants;
|
||||||
|
import com.keylesspalace.tusky.util.ActivityExtensions;
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import static com.keylesspalace.tusky.settings.PrefKeys.APP_THEME;
|
import static com.keylesspalace.tusky.settings.PrefKeys.APP_THEME;
|
||||||
|
|
||||||
public abstract class BaseActivity extends AppCompatActivity implements Injectable {
|
import dagger.hilt.EntryPoints;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All activities inheriting from BaseActivity must be annotated with @AndroidEntryPoint
|
||||||
|
*/
|
||||||
|
public abstract class BaseActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
public static final String OPEN_WITH_SLIDE_IN = "OPEN_WITH_SLIDE_IN";
|
||||||
|
|
||||||
private static final String TAG = "BaseActivity";
|
private static final String TAG = "BaseActivity";
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@NonNull
|
@NonNull
|
||||||
public AccountManager accountManager;
|
public AccountManager accountManager;
|
||||||
|
|
||||||
private static final int REQUESTER_NONE = Integer.MAX_VALUE;
|
@Inject
|
||||||
private HashMap<Integer, PermissionRequester> requesters;
|
@NonNull
|
||||||
|
public SharedPreferences preferences;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows overriding the default ViewModelProvider.Factory for testing purposes.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public ViewModelProvider.Factory viewModelProviderFactory = null;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
if (activityTransitionWasRequested()) {
|
||||||
|
ActivityExtensions.overrideActivityTransitionCompat(
|
||||||
|
this,
|
||||||
|
ActivityConstants.OVERRIDE_TRANSITION_OPEN,
|
||||||
|
R.anim.activity_open_enter,
|
||||||
|
R.anim.activity_open_exit
|
||||||
|
);
|
||||||
|
ActivityExtensions.overrideActivityTransitionCompat(
|
||||||
|
this,
|
||||||
|
ActivityConstants.OVERRIDE_TRANSITION_CLOSE,
|
||||||
|
R.anim.activity_close_enter,
|
||||||
|
R.anim.activity_close_exit
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* There isn't presently a way to globally change the theme of a whole application at
|
/* There isn't presently a way to globally change the theme of a whole application at
|
||||||
* runtime, just individual activities. So, each activity has to set its theme before any
|
* runtime, just individual activities. So, each activity has to set its theme before any
|
||||||
|
@ -82,6 +106,9 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
||||||
Log.d("activeTheme", theme);
|
Log.d("activeTheme", theme);
|
||||||
if (ThemeUtils.isBlack(getResources().getConfiguration(), theme)) {
|
if (ThemeUtils.isBlack(getResources().getConfiguration(), theme)) {
|
||||||
setTheme(R.style.TuskyBlackTheme);
|
setTheme(R.style.TuskyBlackTheme);
|
||||||
|
} else if (this instanceof MainActivity) {
|
||||||
|
// Replace the SplashTheme of MainActivity
|
||||||
|
setTheme(R.style.TuskyTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* set the taskdescription programmatically, the theme would turn it blue */
|
/* set the taskdescription programmatically, the theme would turn it blue */
|
||||||
|
@ -97,13 +124,16 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
||||||
if(requiresLogin()) {
|
if(requiresLogin()) {
|
||||||
redirectIfNotLoggedIn();
|
redirectIfNotLoggedIn();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
requesters = new HashMap<>();
|
private boolean activityTransitionWasRequested() {
|
||||||
|
return getIntent().getBooleanExtra(OPEN_WITH_SLIDE_IN, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void attachBaseContext(Context newBase) {
|
protected void attachBaseContext(Context newBase) {
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(newBase);
|
// injected preferences not yet available at this point of the lifecycle
|
||||||
|
SharedPreferences preferences = EntryPoints.get(newBase.getApplicationContext(), PreferencesEntryPoint.class).preferences();
|
||||||
|
|
||||||
// Scale text in the UI from PrefKeys.UI_TEXT_SCALE_RATIO
|
// Scale text in the UI from PrefKeys.UI_TEXT_SCALE_RATIO
|
||||||
float uiScaleRatio = preferences.getFloat(PrefKeys.UI_TEXT_SCALE_RATIO, 100F);
|
float uiScaleRatio = preferences.getFloat(PrefKeys.UI_TEXT_SCALE_RATIO, 100F);
|
||||||
|
@ -139,6 +169,13 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
||||||
super.attachBaseContext(fontScaleContext);
|
super.attachBaseContext(fontScaleContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
|
||||||
|
final ViewModelProvider.Factory factory = viewModelProviderFactory;
|
||||||
|
return (factory != null) ? factory : super.getDefaultViewModelProviderFactory();
|
||||||
|
}
|
||||||
|
|
||||||
protected boolean requiresLogin() {
|
protected boolean requiresLogin() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -166,11 +203,6 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
||||||
return style;
|
return style;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startActivityWithSlideInAnimation(@NonNull Intent intent) {
|
|
||||||
super.startActivity(intent);
|
|
||||||
overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||||
if (item.getItemId() == android.R.id.home) {
|
if (item.getItemId() == android.R.id.home) {
|
||||||
|
@ -180,22 +212,12 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void finish() {
|
|
||||||
super.finish();
|
|
||||||
overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void finishWithoutSlideOutAnimation() {
|
|
||||||
super.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void redirectIfNotLoggedIn() {
|
protected void redirectIfNotLoggedIn() {
|
||||||
AccountEntity account = accountManager.getActiveAccount();
|
AccountEntity account = accountManager.getActiveAccount();
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
Intent intent = new Intent(this, LoginActivity.class);
|
Intent intent = LoginActivity.getIntent(this, LoginActivity.MODE_DEFAULT);
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
startActivityWithSlideInAnimation(intent);
|
startActivity(intent);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -231,13 +253,17 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
||||||
if (!showActiveAccount && activeAccount != null) {
|
if (!showActiveAccount && activeAccount != null) {
|
||||||
accounts.remove(activeAccount);
|
accounts.remove(activeAccount);
|
||||||
}
|
}
|
||||||
AccountSelectionAdapter adapter = new AccountSelectionAdapter(this);
|
AccountSelectionAdapter adapter = new AccountSelectionAdapter(
|
||||||
|
this,
|
||||||
|
preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
|
||||||
|
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||||
|
);
|
||||||
adapter.addAll(accounts);
|
adapter.addAll(accounts);
|
||||||
|
|
||||||
new AlertDialog.Builder(this)
|
new AlertDialog.Builder(this)
|
||||||
.setTitle(dialogTitle)
|
.setTitle(dialogTitle)
|
||||||
.setAdapter(adapter, (dialogInterface, index) -> listener.onAccountSelected(accounts.get(index)))
|
.setAdapter(adapter, (dialogInterface, index) -> listener.onAccountSelected(accounts.get(index)))
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable String getOpenAsText() {
|
public @Nullable String getOpenAsText() {
|
||||||
|
@ -263,38 +289,6 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
||||||
Intent intent = MainActivity.redirectIntent(this, account.getId(), url);
|
Intent intent = MainActivity.redirectIntent(this, account.getId(), url);
|
||||||
|
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
finishWithoutSlideOutAnimation();
|
finish();
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
||||||
if (requesters.containsKey(requestCode)) {
|
|
||||||
PermissionRequester requester = requesters.remove(requestCode);
|
|
||||||
requester.onRequestPermissionsResult(permissions, grantResults);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void requestPermissions(@NonNull String[] permissions, @NonNull PermissionRequester requester) {
|
|
||||||
ArrayList<String> permissionsToRequest = new ArrayList<>();
|
|
||||||
for(String permission: permissions) {
|
|
||||||
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
permissionsToRequest.add(permission);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (permissionsToRequest.isEmpty()) {
|
|
||||||
int[] permissionsAlreadyGranted = new int[permissions.length];
|
|
||||||
requester.onRequestPermissionsResult(permissions, permissionsAlreadyGranted);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int newKey = requester == null ? REQUESTER_NONE : requesters.size();
|
|
||||||
if (newKey != REQUESTER_NONE) {
|
|
||||||
requesters.put(newKey, requester);
|
|
||||||
}
|
|
||||||
String[] permissionsCopy = new String[permissionsToRequest.size()];
|
|
||||||
permissionsToRequest.toArray(permissionsCopy);
|
|
||||||
ActivityCompat.requestPermissions(this, permissionsCopy, newKey);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,17 +22,17 @@ import android.view.View
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.lifecycleScope
|
||||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import autodispose2.autoDispose
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||||
import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity
|
import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.looksLikeMastodonUrl
|
import com.keylesspalace.tusky.util.looksLikeMastodonUrl
|
||||||
import com.keylesspalace.tusky.util.openLink
|
import com.keylesspalace.tusky.util.openLink
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/** this is the base class for all activities that open links
|
/** this is the base class for all activities that open links
|
||||||
* links are checked against the api if they are mastodon links so they can be opened in Tusky
|
* links are checked against the api if they are mastodon links so they can be opened in Tusky
|
||||||
|
@ -64,45 +64,48 @@ abstract class BottomSheetActivity : BaseActivity() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun viewUrl(url: String, lookupFallbackBehavior: PostLookupFallbackBehavior = PostLookupFallbackBehavior.OPEN_IN_BROWSER) {
|
open fun viewUrl(
|
||||||
|
url: String,
|
||||||
|
lookupFallbackBehavior: PostLookupFallbackBehavior = PostLookupFallbackBehavior.OPEN_IN_BROWSER
|
||||||
|
) {
|
||||||
if (!looksLikeMastodonUrl(url)) {
|
if (!looksLikeMastodonUrl(url)) {
|
||||||
openLink(url)
|
openLink(url)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mastodonApi.searchObservable(
|
lifecycleScope.launch {
|
||||||
query = url,
|
mastodonApi.search(
|
||||||
resolve = true
|
query = url,
|
||||||
).observeOn(AndroidSchedulers.mainThread())
|
resolve = true
|
||||||
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
|
).fold(
|
||||||
.subscribe(
|
onSuccess = { (accounts, statuses) ->
|
||||||
{ (accounts, statuses) ->
|
|
||||||
if (getCancelSearchRequested(url)) {
|
if (getCancelSearchRequested(url)) {
|
||||||
return@subscribe
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
onEndSearch(url)
|
onEndSearch(url)
|
||||||
|
|
||||||
if (statuses.isNotEmpty()) {
|
if (statuses.isNotEmpty()) {
|
||||||
viewThread(statuses[0].id, statuses[0].url)
|
viewThread(statuses[0].id, statuses[0].url)
|
||||||
return@subscribe
|
return@launch
|
||||||
}
|
}
|
||||||
accounts.firstOrNull { it.url.equals(url, ignoreCase = true) }?.let { account ->
|
accounts.firstOrNull { it.url.equals(url, ignoreCase = true) }?.let { account ->
|
||||||
// Some servers return (unrelated) accounts for url searches (#2804)
|
// Some servers return (unrelated) accounts for url searches (#2804)
|
||||||
// Verify that the account's url matches the query
|
// Verify that the account's url matches the query
|
||||||
viewAccount(account.id)
|
viewAccount(account.id)
|
||||||
return@subscribe
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
performUrlFallbackAction(url, lookupFallbackBehavior)
|
performUrlFallbackAction(url, lookupFallbackBehavior)
|
||||||
},
|
},
|
||||||
{
|
onFailure = {
|
||||||
if (!getCancelSearchRequested(url)) {
|
if (!getCancelSearchRequested(url)) {
|
||||||
onEndSearch(url)
|
onEndSearch(url)
|
||||||
performUrlFallbackAction(url, lookupFallbackBehavior)
|
performUrlFallbackAction(url, lookupFallbackBehavior)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
onBeginSearch(url)
|
onBeginSearch(url)
|
||||||
}
|
}
|
||||||
|
@ -121,10 +124,17 @@ abstract class BottomSheetActivity : BaseActivity() {
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithSlideInAnimation(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun performUrlFallbackAction(url: String, fallbackBehavior: PostLookupFallbackBehavior) {
|
protected open fun performUrlFallbackAction(
|
||||||
|
url: String,
|
||||||
|
fallbackBehavior: PostLookupFallbackBehavior
|
||||||
|
) {
|
||||||
when (fallbackBehavior) {
|
when (fallbackBehavior) {
|
||||||
PostLookupFallbackBehavior.OPEN_IN_BROWSER -> openLink(url)
|
PostLookupFallbackBehavior.OPEN_IN_BROWSER -> openLink(url)
|
||||||
PostLookupFallbackBehavior.DISPLAY_ERROR -> Toast.makeText(this, getString(R.string.post_lookup_error_format, url), Toast.LENGTH_SHORT).show()
|
PostLookupFallbackBehavior.DISPLAY_ERROR -> Toast.makeText(
|
||||||
|
this,
|
||||||
|
getString(R.string.post_lookup_error_format, url),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.core.widget.doAfterTextChanged
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
@ -43,8 +43,6 @@ import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
|
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
|
||||||
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
||||||
import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding
|
import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
|
||||||
import com.keylesspalace.tusky.util.Error
|
import com.keylesspalace.tusky.util.Error
|
||||||
import com.keylesspalace.tusky.util.Loading
|
import com.keylesspalace.tusky.util.Loading
|
||||||
import com.keylesspalace.tusky.util.Success
|
import com.keylesspalace.tusky.util.Success
|
||||||
|
@ -57,10 +55,12 @@ import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
import com.mikepenz.iconics.utils.sizeDp
|
import com.mikepenz.iconics.utils.sizeDp
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class EditProfileActivity : BaseActivity(), Injectable {
|
@AndroidEntryPoint
|
||||||
|
class EditProfileActivity : BaseActivity() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val AVATAR_SIZE = 400
|
const val AVATAR_SIZE = 400
|
||||||
|
@ -68,10 +68,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
const val HEADER_HEIGHT = 500
|
const val HEADER_HEIGHT = 500
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
private val viewModel: EditProfileViewModel by viewModels()
|
||||||
lateinit var viewModelFactory: ViewModelFactory
|
|
||||||
|
|
||||||
private val viewModel: EditProfileViewModel by viewModels { viewModelFactory }
|
|
||||||
|
|
||||||
private val binding by viewBinding(ActivityEditProfileBinding::inflate)
|
private val binding by viewBinding(ActivityEditProfileBinding::inflate)
|
||||||
|
|
||||||
|
@ -126,9 +123,17 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
binding.fieldList.layoutManager = LinearLayoutManager(this)
|
binding.fieldList.layoutManager = LinearLayoutManager(this)
|
||||||
binding.fieldList.adapter = accountFieldEditAdapter
|
binding.fieldList.adapter = accountFieldEditAdapter
|
||||||
|
|
||||||
val plusDrawable = IconicsDrawable(this, GoogleMaterial.Icon.gmd_add).apply { sizeDp = 12; colorInt = Color.WHITE }
|
val plusDrawable = IconicsDrawable(this, GoogleMaterial.Icon.gmd_add).apply {
|
||||||
|
sizeDp = 12
|
||||||
|
colorInt = Color.WHITE
|
||||||
|
}
|
||||||
|
|
||||||
binding.addFieldButton.setCompoundDrawablesRelativeWithIntrinsicBounds(plusDrawable, null, null, null)
|
binding.addFieldButton.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||||
|
plusDrawable,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
binding.addFieldButton.setOnClickListener {
|
binding.addFieldButton.setOnClickListener {
|
||||||
accountFieldEditAdapter.addField()
|
accountFieldEditAdapter.addField()
|
||||||
|
@ -143,52 +148,64 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
|
|
||||||
viewModel.obtainProfile()
|
viewModel.obtainProfile()
|
||||||
|
|
||||||
viewModel.profileData.observe(this) { profileRes ->
|
lifecycleScope.launch {
|
||||||
when (profileRes) {
|
viewModel.profileData.collect { profileRes ->
|
||||||
is Success -> {
|
if (profileRes == null) return@collect
|
||||||
val me = profileRes.data
|
when (profileRes) {
|
||||||
if (me != null) {
|
is Success -> {
|
||||||
binding.displayNameEditText.setText(me.displayName)
|
val me = profileRes.data
|
||||||
binding.noteEditText.setText(me.source?.note)
|
if (me != null) {
|
||||||
binding.lockedCheckBox.isChecked = me.locked
|
binding.displayNameEditText.setText(me.displayName)
|
||||||
|
binding.noteEditText.setText(me.source?.note)
|
||||||
|
binding.lockedCheckBox.isChecked = me.locked
|
||||||
|
|
||||||
accountFieldEditAdapter.setFields(me.source?.fields.orEmpty())
|
accountFieldEditAdapter.setFields(me.source?.fields.orEmpty())
|
||||||
binding.addFieldButton.isVisible =
|
binding.addFieldButton.isVisible =
|
||||||
(me.source?.fields?.size ?: 0) < maxAccountFields
|
(me.source?.fields?.size ?: 0) < maxAccountFields
|
||||||
|
|
||||||
if (viewModel.avatarData.value == null) {
|
if (viewModel.avatarData.value == null) {
|
||||||
Glide.with(this)
|
Glide.with(this@EditProfileActivity)
|
||||||
.load(me.avatar)
|
.load(me.avatar)
|
||||||
.placeholder(R.drawable.avatar_default)
|
.placeholder(R.drawable.avatar_default)
|
||||||
.transform(
|
.transform(
|
||||||
FitCenter(),
|
FitCenter(),
|
||||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
|
RoundedCorners(
|
||||||
)
|
resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)
|
||||||
.into(binding.avatarPreview)
|
)
|
||||||
}
|
)
|
||||||
|
.into(binding.avatarPreview)
|
||||||
|
}
|
||||||
|
|
||||||
if (viewModel.headerData.value == null) {
|
if (viewModel.headerData.value == null) {
|
||||||
Glide.with(this)
|
Glide.with(this@EditProfileActivity)
|
||||||
.load(me.header)
|
.load(me.header)
|
||||||
.into(binding.headerPreview)
|
.into(binding.headerPreview)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is Error -> {
|
||||||
|
Snackbar.make(
|
||||||
|
binding.avatarButton,
|
||||||
|
R.string.error_generic,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
)
|
||||||
|
.setAction(R.string.action_retry) {
|
||||||
|
viewModel.obtainProfile()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
is Loading -> { }
|
||||||
}
|
}
|
||||||
is Error -> {
|
|
||||||
Snackbar.make(binding.avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG)
|
|
||||||
.setAction(R.string.action_retry) {
|
|
||||||
viewModel.obtainProfile()
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
is Loading -> { }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
viewModel.instanceData.collect { instanceInfo ->
|
viewModel.instanceData.collect { instanceInfo ->
|
||||||
maxAccountFields = instanceInfo.maxFields
|
maxAccountFields = instanceInfo.maxFields
|
||||||
accountFieldEditAdapter.setFieldLimits(instanceInfo.maxFieldNameLength, instanceInfo.maxFieldValueLength)
|
accountFieldEditAdapter.setFieldLimits(
|
||||||
|
instanceInfo.maxFieldNameLength,
|
||||||
|
instanceInfo.maxFieldValueLength
|
||||||
|
)
|
||||||
binding.addFieldButton.isVisible =
|
binding.addFieldButton.isVisible =
|
||||||
accountFieldEditAdapter.itemCount < maxAccountFields
|
accountFieldEditAdapter.itemCount < maxAccountFields
|
||||||
}
|
}
|
||||||
|
@ -197,34 +214,50 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
observeImage(viewModel.avatarData, binding.avatarPreview, true)
|
observeImage(viewModel.avatarData, binding.avatarPreview, true)
|
||||||
observeImage(viewModel.headerData, binding.headerPreview, false)
|
observeImage(viewModel.headerData, binding.headerPreview, false)
|
||||||
|
|
||||||
viewModel.saveData.observe(
|
lifecycleScope.launch {
|
||||||
this
|
viewModel.saveData.collect {
|
||||||
) {
|
if (it == null) return@collect
|
||||||
when (it) {
|
when (it) {
|
||||||
is Success -> {
|
is Success -> {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
is Loading -> {
|
is Loading -> {
|
||||||
binding.saveProgressBar.visibility = View.VISIBLE
|
binding.saveProgressBar.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
is Error -> {
|
is Error -> {
|
||||||
onSaveFailure(it.errorMessage)
|
onSaveFailure(it.errorMessage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val onBackCallback = object : OnBackPressedCallback(enabled = true) {
|
binding.displayNameEditText.doAfterTextChanged {
|
||||||
override fun handleOnBackPressed() = checkForUnsavedChanges()
|
viewModel.dataChanged(currentProfileData)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.displayNameEditText.doAfterTextChanged {
|
||||||
|
viewModel.dataChanged(currentProfileData)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.lockedCheckBox.setOnCheckedChangeListener { _, _ ->
|
||||||
|
viewModel.dataChanged(currentProfileData)
|
||||||
|
}
|
||||||
|
|
||||||
|
accountFieldEditAdapter.onFieldsChanged = {
|
||||||
|
viewModel.dataChanged(currentProfileData)
|
||||||
|
}
|
||||||
|
|
||||||
|
val onBackCallback = object : OnBackPressedCallback(enabled = false) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
showUnsavedChangesDialog()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(this, onBackCallback)
|
onBackPressedDispatcher.addCallback(this, onBackCallback)
|
||||||
}
|
lifecycleScope.launch {
|
||||||
|
viewModel.isChanged.collect { dataWasChanged ->
|
||||||
fun checkForUnsavedChanges() {
|
onBackCallback.isEnabled = dataWasChanged
|
||||||
if (viewModel.hasUnsavedChanges(currentProfileData)) {
|
}
|
||||||
showUnsavedChangesDialog()
|
|
||||||
} else {
|
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,30 +269,30 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeImage(
|
private fun observeImage(
|
||||||
liveData: LiveData<Uri>,
|
flow: StateFlow<Uri?>,
|
||||||
imageView: ImageView,
|
imageView: ImageView,
|
||||||
roundedCorners: Boolean
|
roundedCorners: Boolean
|
||||||
) {
|
) {
|
||||||
liveData.observe(
|
lifecycleScope.launch {
|
||||||
this
|
flow.collect { imageUri ->
|
||||||
) { imageUri ->
|
|
||||||
|
|
||||||
// skipping all caches so we can always reuse the same uri
|
// skipping all caches so we can always reuse the same uri
|
||||||
val glide = Glide.with(imageView)
|
val glide = Glide.with(imageView)
|
||||||
.load(imageUri)
|
.load(imageUri)
|
||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
|
||||||
if (roundedCorners) {
|
if (roundedCorners) {
|
||||||
glide.transform(
|
glide.transform(
|
||||||
FitCenter(),
|
FitCenter(),
|
||||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
|
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
|
||||||
).into(imageView)
|
).into(imageView)
|
||||||
} else {
|
} else {
|
||||||
glide.into(imageView)
|
glide.into(imageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
imageView.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
imageView.show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,7 +351,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
|
|
||||||
private fun onPickFailure(throwable: Throwable?) {
|
private fun onPickFailure(throwable: Throwable?) {
|
||||||
Log.w("EditProfileActivity", "failed to pick media", throwable)
|
Log.w("EditProfileActivity", "failed to pick media", throwable)
|
||||||
Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
|
Snackbar.make(
|
||||||
|
binding.avatarButton,
|
||||||
|
R.string.error_media_upload_sending,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showUnsavedChangesDialog() = lifecycleScope.launch {
|
private fun showUnsavedChangesDialog() = lifecycleScope.launch {
|
||||||
|
|
|
@ -19,12 +19,17 @@ import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.RawRes
|
import androidx.annotation.RawRes
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.keylesspalace.tusky.databinding.ActivityLicenseBinding
|
import com.keylesspalace.tusky.databinding.ActivityLicenseBinding
|
||||||
import com.keylesspalace.tusky.util.closeQuietly
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStreamReader
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okio.buffer
|
||||||
|
import okio.source
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class LicenseActivity : BaseActivity() {
|
class LicenseActivity : BaseActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -44,23 +49,15 @@ class LicenseActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) {
|
private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) {
|
||||||
val sb = StringBuilder()
|
lifecycleScope.launch {
|
||||||
|
textView.text = withContext(Dispatchers.IO) {
|
||||||
val br = BufferedReader(InputStreamReader(resources.openRawResource(fileId)))
|
try {
|
||||||
|
resources.openRawResource(fileId).source().buffer().use { it.readUtf8() }
|
||||||
try {
|
} catch (e: IOException) {
|
||||||
var line: String? = br.readLine()
|
Log.w("LicenseActivity", e)
|
||||||
while (line != null) {
|
""
|
||||||
sb.append(line)
|
}
|
||||||
sb.append('\n')
|
|
||||||
line = br.readLine()
|
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.w("LicenseActivity", e)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
br.closeQuietly()
|
|
||||||
|
|
||||||
textView.text = sb.toString()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* Copyright 2017 Andrew Dawson
|
/* Copyright Tusky contributors
|
||||||
*
|
*
|
||||||
* This file is a part of Tusky.
|
* This file is a part of Tusky.
|
||||||
*
|
*
|
||||||
|
@ -23,9 +23,7 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.PopupMenu
|
import android.widget.PopupMenu
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
@ -35,16 +33,15 @@ import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.google.android.material.color.MaterialColors
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.databinding.ActivityListsBinding
|
import com.keylesspalace.tusky.databinding.ActivityListsBinding
|
||||||
import com.keylesspalace.tusky.databinding.DialogListBinding
|
import com.keylesspalace.tusky.databinding.DialogListBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.databinding.ItemListBinding
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
|
||||||
import com.keylesspalace.tusky.entity.MastoList
|
import com.keylesspalace.tusky.entity.MastoList
|
||||||
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.util.visible
|
import com.keylesspalace.tusky.util.visible
|
||||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel
|
import com.keylesspalace.tusky.viewmodel.ListsViewModel
|
||||||
|
@ -54,28 +51,15 @@ import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.ERROR_OTHER
|
||||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.INITIAL
|
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.INITIAL
|
||||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADED
|
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADED
|
||||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADING
|
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADING
|
||||||
import com.mikepenz.iconics.IconicsDrawable
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
|
||||||
import com.mikepenz.iconics.utils.sizeDp
|
|
||||||
import dagger.android.DispatchingAndroidInjector
|
|
||||||
import dagger.android.HasAndroidInjector
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
// TODO use the ListSelectionFragment (and/or its adapter or binding) here; but keep the LoadingState from here (?)
|
||||||
* Created by charlag on 1/4/18.
|
|
||||||
*/
|
|
||||||
|
|
||||||
class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
@AndroidEntryPoint
|
||||||
|
class ListsActivity : BaseActivity() {
|
||||||
|
|
||||||
@Inject
|
private val viewModel: ListsViewModel by viewModels()
|
||||||
lateinit var viewModelFactory: ViewModelFactory
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
|
|
||||||
|
|
||||||
private val viewModel: ListsViewModel by viewModels { viewModelFactory }
|
|
||||||
|
|
||||||
private val binding by viewBinding(ActivityListsBinding::inflate)
|
private val binding by viewBinding(ActivityListsBinding::inflate)
|
||||||
|
|
||||||
|
@ -214,9 +198,9 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onListSelected(listId: String, listTitle: String) {
|
private fun onListSelected(list: MastoList) {
|
||||||
startActivityWithSlideInAnimation(
|
startActivityWithSlideInAnimation(
|
||||||
StatusListActivity.newListIntent(this, listId, listTitle)
|
StatusListActivity.newListIntent(this, list.id, list.title)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,47 +239,38 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class ListsAdapter :
|
private inner class ListsAdapter :
|
||||||
ListAdapter<MastoList, ListsAdapter.ListViewHolder>(ListsDiffer) {
|
ListAdapter<MastoList, BindingHolder<ItemListBinding>>(ListsDiffer) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
|
override fun onCreateViewHolder(
|
||||||
return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false)
|
parent: ViewGroup,
|
||||||
.let(this::ListViewHolder)
|
viewType: Int
|
||||||
.apply {
|
): BindingHolder<ItemListBinding> {
|
||||||
val iconColor = MaterialColors.getColor(nameTextView, android.R.attr.textColorTertiary)
|
return BindingHolder(ItemListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||||
val context = nameTextView.context
|
}
|
||||||
val icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_list).apply { sizeDp = 20; colorInt = iconColor }
|
|
||||||
|
|
||||||
nameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
|
override fun onBindViewHolder(holder: BindingHolder<ItemListBinding>, position: Int) {
|
||||||
|
val item = getItem(position)
|
||||||
|
holder.binding.listName.text = item.title
|
||||||
|
|
||||||
|
holder.binding.moreButton.apply {
|
||||||
|
visible(true)
|
||||||
|
setOnClickListener {
|
||||||
|
onMore(item, holder.binding.moreButton)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
|
|
||||||
holder.nameTextView.text = getItem(position).title
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class ListViewHolder(view: View) :
|
|
||||||
RecyclerView.ViewHolder(view),
|
|
||||||
View.OnClickListener {
|
|
||||||
val nameTextView: TextView = view.findViewById(R.id.list_name_textview)
|
|
||||||
val moreButton: ImageButton = view.findViewById(R.id.editListButton)
|
|
||||||
|
|
||||||
init {
|
|
||||||
view.setOnClickListener(this)
|
|
||||||
moreButton.setOnClickListener(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
holder.itemView.setOnClickListener {
|
||||||
if (v == itemView) {
|
onListSelected(item)
|
||||||
val list = getItem(bindingAdapterPosition)
|
|
||||||
onListSelected(list.id, list.title)
|
|
||||||
} else {
|
|
||||||
onMore(getItem(bindingAdapterPosition), v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPickedDialogName(name: String, listId: String?, exclusive: Boolean, replyPolicy: String) {
|
private fun onPickedDialogName(
|
||||||
|
name: String,
|
||||||
|
listId: String?,
|
||||||
|
exclusive: Boolean,
|
||||||
|
replyPolicy: String
|
||||||
|
) {
|
||||||
if (listId == null) {
|
if (listId == null) {
|
||||||
viewModel.createNewList(name, exclusive, replyPolicy)
|
viewModel.createNewList(name, exclusive, replyPolicy)
|
||||||
} else {
|
} else {
|
||||||
|
@ -303,8 +278,6 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun androidInjector() = dispatchingAndroidInjector
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newIntent(context: Context) = Intent(context, ListsActivity::class.java)
|
fun newIntent(context: Context) = Intent(context, ListsActivity::class.java)
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,14 +45,12 @@ import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.IntentCompat
|
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.core.view.forEach
|
import androidx.core.view.forEach
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import androidx.viewpager2.widget.MarginPageTransformer
|
import androidx.viewpager2.widget.MarginPageTransformer
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
@ -79,17 +77,18 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
||||||
import com.keylesspalace.tusky.components.login.LoginActivity
|
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
|
||||||
import com.keylesspalace.tusky.components.notifications.disableAllNotifications
|
|
||||||
import com.keylesspalace.tusky.components.notifications.enablePushNotificationsWithFallback
|
|
||||||
import com.keylesspalace.tusky.components.notifications.showMigrationNoticeIfNecessary
|
|
||||||
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
||||||
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity
|
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity
|
||||||
import com.keylesspalace.tusky.components.search.SearchActivity
|
import com.keylesspalace.tusky.components.search.SearchActivity
|
||||||
|
import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper
|
||||||
|
import com.keylesspalace.tusky.components.systemnotifications.disableAllNotifications
|
||||||
|
import com.keylesspalace.tusky.components.systemnotifications.enablePushNotificationsWithFallback
|
||||||
|
import com.keylesspalace.tusky.components.systemnotifications.showMigrationNoticeIfNecessary
|
||||||
import com.keylesspalace.tusky.components.trending.TrendingActivity
|
import com.keylesspalace.tusky.components.trending.TrendingActivity
|
||||||
import com.keylesspalace.tusky.databinding.ActivityMainBinding
|
import com.keylesspalace.tusky.databinding.ActivityMainBinding
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
|
||||||
import com.keylesspalace.tusky.db.DraftsAlert
|
import com.keylesspalace.tusky.db.DraftsAlert
|
||||||
|
import com.keylesspalace.tusky.db.entity.AccountEntity
|
||||||
|
import com.keylesspalace.tusky.di.ApplicationScope
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.entity.Notification
|
import com.keylesspalace.tusky.entity.Notification
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
@ -101,14 +100,17 @@ import com.keylesspalace.tusky.pager.MainPagerAdapter
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.usecase.DeveloperToolsUseCase
|
import com.keylesspalace.tusky.usecase.DeveloperToolsUseCase
|
||||||
import com.keylesspalace.tusky.usecase.LogoutUsecase
|
import com.keylesspalace.tusky.usecase.LogoutUsecase
|
||||||
|
import com.keylesspalace.tusky.util.ActivityConstants
|
||||||
|
import com.keylesspalace.tusky.util.ShareShortcutHelper
|
||||||
import com.keylesspalace.tusky.util.deleteStaleCachedMedia
|
import com.keylesspalace.tusky.util.deleteStaleCachedMedia
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.getDimension
|
import com.keylesspalace.tusky.util.getDimension
|
||||||
|
import com.keylesspalace.tusky.util.getParcelableExtraCompat
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.overrideActivityTransitionCompat
|
||||||
import com.keylesspalace.tusky.util.reduceSwipeSensitivity
|
import com.keylesspalace.tusky.util.reduceSwipeSensitivity
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.unsafeLazy
|
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
|
||||||
import com.keylesspalace.tusky.util.updateShortcut
|
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.mikepenz.iconics.IconicsDrawable
|
import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
|
@ -137,17 +139,17 @@ import com.mikepenz.materialdrawer.util.addItems
|
||||||
import com.mikepenz.materialdrawer.util.addItemsAtPosition
|
import com.mikepenz.materialdrawer.util.addItemsAtPosition
|
||||||
import com.mikepenz.materialdrawer.util.updateBadge
|
import com.mikepenz.materialdrawer.util.updateBadge
|
||||||
import com.mikepenz.materialdrawer.widget.AccountHeaderView
|
import com.mikepenz.materialdrawer.widget.AccountHeaderView
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dagger.android.HasAndroidInjector
|
import dagger.hilt.android.migration.OptionalInject
|
||||||
import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE
|
import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector, MenuProvider {
|
@OptionalInject
|
||||||
@Inject
|
@AndroidEntryPoint
|
||||||
lateinit var androidInjector: DispatchingAndroidInjector<Any>
|
class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var eventHub: EventHub
|
lateinit var eventHub: EventHub
|
||||||
|
@ -164,6 +166,13 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var developerToolsUseCase: DeveloperToolsUseCase
|
lateinit var developerToolsUseCase: DeveloperToolsUseCase
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var shareShortcutHelper: ShareShortcutHelper
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ApplicationScope
|
||||||
|
lateinit var externalScope: CoroutineScope
|
||||||
|
|
||||||
private val binding by viewBinding(ActivityMainBinding::inflate)
|
private val binding by viewBinding(ActivityMainBinding::inflate)
|
||||||
|
|
||||||
private lateinit var header: AccountHeaderView
|
private lateinit var header: AccountHeaderView
|
||||||
|
@ -172,8 +181,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
|
|
||||||
private var unreadAnnouncementsCount = 0
|
private var unreadAnnouncementsCount = 0
|
||||||
|
|
||||||
private val preferences by unsafeLazy { PreferenceManager.getDefaultSharedPreferences(this) }
|
|
||||||
|
|
||||||
// We need to know if the emoji pack has been changed
|
// We need to know if the emoji pack has been changed
|
||||||
private var selectedEmojiPack: String? = null
|
private var selectedEmojiPack: String? = null
|
||||||
|
|
||||||
|
@ -185,12 +192,31 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
|
|
||||||
private var directMessageTab: TabLayout.Tab? = null
|
private var directMessageTab: TabLayout.Tab? = null
|
||||||
|
|
||||||
|
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
binding.viewPager.currentItem = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// Newer Android versions don't need to install the compat Splash Screen
|
||||||
|
// and it can cause theming bugs.
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||||
|
installSplashScreen()
|
||||||
|
}
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val activeAccount = accountManager.activeAccount
|
// will be redirected to LoginActivity by BaseActivity
|
||||||
?: return // will be redirected to LoginActivity by BaseActivity
|
val activeAccount = accountManager.activeAccount ?: return
|
||||||
|
|
||||||
|
if (explodeAnimationWasRequested()) {
|
||||||
|
overrideActivityTransitionCompat(
|
||||||
|
ActivityConstants.OVERRIDE_TRANSITION_OPEN,
|
||||||
|
R.anim.explode,
|
||||||
|
R.anim.activity_open_exit
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var showNotificationTab = false
|
var showNotificationTab = false
|
||||||
|
|
||||||
|
@ -199,7 +225,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
val notificationId = intent.getIntExtra(NOTIFICATION_ID, -1)
|
val notificationId = intent.getIntExtra(NOTIFICATION_ID, -1)
|
||||||
if (notificationId != -1) {
|
if (notificationId != -1) {
|
||||||
// opened from a notification action, cancel the notification
|
// opened from a notification action, cancel the notification
|
||||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
val notificationManager = getSystemService(
|
||||||
|
NOTIFICATION_SERVICE
|
||||||
|
) as NotificationManager
|
||||||
notificationManager.cancel(intent.getStringExtra(NOTIFICATION_TAG), notificationId)
|
notificationManager.cancel(intent.getStringExtra(NOTIFICATION_TAG), notificationId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +281,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
// user clicked a notification, show follow requests for type FOLLOW_REQUEST,
|
// user clicked a notification, show follow requests for type FOLLOW_REQUEST,
|
||||||
// otherwise show notification tab
|
// otherwise show notification tab
|
||||||
if (intent.getStringExtra(NOTIFICATION_TYPE) == Notification.Type.FOLLOW_REQUEST.name) {
|
if (intent.getStringExtra(NOTIFICATION_TYPE) == Notification.Type.FOLLOW_REQUEST.name) {
|
||||||
val intent = AccountListActivity.newIntent(this, AccountListActivity.Type.FOLLOW_REQUESTS)
|
val intent = AccountListActivity.newIntent(
|
||||||
|
this,
|
||||||
|
AccountListActivity.Type.FOLLOW_REQUESTS
|
||||||
|
)
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithSlideInAnimation(intent)
|
||||||
} else {
|
} else {
|
||||||
showNotificationTab = true
|
showNotificationTab = true
|
||||||
|
@ -293,8 +324,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
setupDrawer(
|
setupDrawer(
|
||||||
savedInstanceState,
|
savedInstanceState,
|
||||||
addSearchButton = hideTopToolbar,
|
addSearchButton = hideTopToolbar,
|
||||||
addTrendingTagsButton = !accountManager.activeAccount!!.tabPreferences.hasTab(TRENDING_TAGS),
|
addTrendingTagsButton = !accountManager.activeAccount!!.tabPreferences.hasTab(
|
||||||
addTrendingStatusesButton = !accountManager.activeAccount!!.tabPreferences.hasTab(TRENDING_STATUSES),
|
TRENDING_TAGS
|
||||||
|
),
|
||||||
|
addTrendingStatusesButton = !accountManager.activeAccount!!.tabPreferences.hasTab(
|
||||||
|
TRENDING_STATUSES
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Fetch user info while we're doing other things. This has to be done after setting up the
|
/* Fetch user info while we're doing other things. This has to be done after setting up the
|
||||||
|
@ -320,7 +355,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
refreshMainDrawerItems(
|
refreshMainDrawerItems(
|
||||||
addSearchButton = hideTopToolbar,
|
addSearchButton = hideTopToolbar,
|
||||||
addTrendingTagsButton = !event.newTabs.hasTab(TRENDING_TAGS),
|
addTrendingTagsButton = !event.newTabs.hasTab(TRENDING_TAGS),
|
||||||
addTrendingStatusesButton = !event.newTabs.hasTab(TRENDING_STATUSES),
|
addTrendingStatusesButton = !event.newTabs.hasTab(TRENDING_STATUSES)
|
||||||
)
|
)
|
||||||
|
|
||||||
setupTabs(false)
|
setupTabs(false)
|
||||||
|
@ -333,7 +368,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
directMessageTab?.let {
|
directMessageTab?.let {
|
||||||
if (event.accountId == activeAccount.accountId) {
|
if (event.accountId == activeAccount.accountId) {
|
||||||
val hasDirectMessageNotification =
|
val hasDirectMessageNotification =
|
||||||
event.notifications.any { it.type == Notification.Type.MENTION && it.status?.visibility == Status.Visibility.DIRECT }
|
event.notifications.any {
|
||||||
|
it.type == Notification.Type.MENTION && it.status?.visibility == Status.Visibility.DIRECT
|
||||||
|
}
|
||||||
|
|
||||||
if (hasDirectMessageNotification) {
|
if (hasDirectMessageNotification) {
|
||||||
showDirectMessageBadge(true)
|
showDirectMessageBadge(true)
|
||||||
|
@ -355,31 +392,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Schedulers.io().scheduleDirect {
|
externalScope.launch(Dispatchers.IO) {
|
||||||
// Flush old media that was cached for sharing
|
// Flush old media that was cached for sharing
|
||||||
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky"))
|
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky"))
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedEmojiPack = preferences.getString(EMOJI_PREFERENCE, "")
|
selectedEmojiPack = preferences.getString(EMOJI_PREFERENCE, "")
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(
|
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||||
this,
|
|
||||||
object : OnBackPressedCallback(true) {
|
|
||||||
override fun handleOnBackPressed() {
|
|
||||||
when {
|
|
||||||
binding.mainDrawerLayout.isOpen -> {
|
|
||||||
binding.mainDrawerLayout.close()
|
|
||||||
}
|
|
||||||
binding.viewPager.currentItem != 0 -> {
|
|
||||||
binding.viewPager.currentItem = 0
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
Build.VERSION.SDK_INT >= 33 &&
|
Build.VERSION.SDK_INT >= 33 &&
|
||||||
|
@ -427,7 +447,13 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
|
|
||||||
// If the main toolbar is hidden then there's no space in the top/bottomNav to show
|
// If the main toolbar is hidden then there's no space in the top/bottomNav to show
|
||||||
// the menu items as icons, so forceably disable them
|
// the menu items as icons, so forceably disable them
|
||||||
if (!binding.mainToolbar.isVisible) menu.forEach { it.setShowAsAction(SHOW_AS_ACTION_NEVER) }
|
if (!binding.mainToolbar.isVisible) {
|
||||||
|
menu.forEach {
|
||||||
|
it.setShowAsAction(
|
||||||
|
SHOW_AS_ACTION_NEVER
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemSelected(item: MenuItem): Boolean {
|
override fun onMenuItemSelected(item: MenuItem): Boolean {
|
||||||
|
@ -454,12 +480,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||||
super.onStart()
|
// Allow software back press to be properly dispatched to drawer layout
|
||||||
// For some reason the navigation drawer is opened when the activity is recreated
|
val handled = when (event.action) {
|
||||||
if (binding.mainDrawerLayout.isOpen) {
|
KeyEvent.ACTION_DOWN -> binding.mainDrawerLayout.onKeyDown(event.keyCode, event)
|
||||||
binding.mainDrawerLayout.closeDrawer(GravityCompat.START, false)
|
KeyEvent.ACTION_UP -> binding.mainDrawerLayout.onKeyUp(event.keyCode, event)
|
||||||
|
else -> false
|
||||||
}
|
}
|
||||||
|
return handled || super.dispatchKeyEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
|
@ -503,7 +531,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun forwardToComposeActivity(intent: Intent) {
|
private fun forwardToComposeActivity(intent: Intent) {
|
||||||
val composeOptions = IntentCompat.getParcelableExtra(intent, COMPOSE_OPTIONS, ComposeActivity.ComposeOptions::class.java)
|
val composeOptions =
|
||||||
|
intent.getParcelableExtraCompat<ComposeActivity.ComposeOptions>(COMPOSE_OPTIONS)
|
||||||
|
|
||||||
val composeIntent = if (composeOptions != null) {
|
val composeIntent = if (composeOptions != null) {
|
||||||
ComposeActivity.startIntent(this, composeOptions)
|
ComposeActivity.startIntent(this, composeOptions)
|
||||||
|
@ -523,7 +552,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
savedInstanceState: Bundle?,
|
savedInstanceState: Bundle?,
|
||||||
addSearchButton: Boolean,
|
addSearchButton: Boolean,
|
||||||
addTrendingTagsButton: Boolean,
|
addTrendingTagsButton: Boolean,
|
||||||
addTrendingStatusesButton: Boolean,
|
addTrendingStatusesButton: Boolean
|
||||||
) {
|
) {
|
||||||
val drawerOpenClickListener = View.OnClickListener { binding.mainDrawerLayout.open() }
|
val drawerOpenClickListener = View.OnClickListener { binding.mainDrawerLayout.open() }
|
||||||
|
|
||||||
|
@ -553,7 +582,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
header.currentProfileName.ellipsize = TextUtils.TruncateAt.END
|
header.currentProfileName.ellipsize = TextUtils.TruncateAt.END
|
||||||
|
|
||||||
header.accountHeaderBackground.setColorFilter(getColor(R.color.headerBackgroundFilter))
|
header.accountHeaderBackground.setColorFilter(getColor(R.color.headerBackgroundFilter))
|
||||||
header.accountHeaderBackground.setBackgroundColor(MaterialColors.getColor(header, R.attr.colorBackgroundAccent))
|
header.accountHeaderBackground.setBackgroundColor(
|
||||||
|
MaterialColors.getColor(header, R.attr.colorBackgroundAccent)
|
||||||
|
)
|
||||||
val animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
|
val animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
|
||||||
|
|
||||||
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
|
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
|
||||||
|
@ -589,7 +620,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
refreshMainDrawerItems(
|
refreshMainDrawerItems(
|
||||||
addSearchButton = addSearchButton,
|
addSearchButton = addSearchButton,
|
||||||
addTrendingTagsButton = addTrendingTagsButton,
|
addTrendingTagsButton = addTrendingTagsButton,
|
||||||
addTrendingStatusesButton = addTrendingStatusesButton,
|
addTrendingStatusesButton = addTrendingStatusesButton
|
||||||
)
|
)
|
||||||
setSavedInstance(savedInstanceState)
|
setSavedInstance(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
@ -598,7 +629,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
private fun refreshMainDrawerItems(
|
private fun refreshMainDrawerItems(
|
||||||
addSearchButton: Boolean,
|
addSearchButton: Boolean,
|
||||||
addTrendingTagsButton: Boolean,
|
addTrendingTagsButton: Boolean,
|
||||||
addTrendingStatusesButton: Boolean,
|
addTrendingStatusesButton: Boolean
|
||||||
) {
|
) {
|
||||||
binding.mainDrawer.apply {
|
binding.mainDrawer.apply {
|
||||||
itemAdapter.clear()
|
itemAdapter.clear()
|
||||||
|
@ -823,8 +854,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
else -> getString(tabs[position].text)
|
else -> getString(tabs[position].text)
|
||||||
}
|
}
|
||||||
if (tabs[position].id == DIRECT) {
|
if (tabs[position].id == DIRECT) {
|
||||||
tab.orCreateBadge
|
val badge = tab.orCreateBadge
|
||||||
tab.badge?.isVisible = accountManager.activeAccount?.hasDirectMessageBadge ?: false
|
badge.isVisible = accountManager.activeAccount?.hasDirectMessageBadge ?: false
|
||||||
|
badge.backgroundColor = MaterialColors.getColor(binding.mainDrawer, com.google.android.material.R.attr.colorPrimary)
|
||||||
directMessageTab = tab
|
directMessageTab = tab
|
||||||
}
|
}
|
||||||
}.also { it.attach() }
|
}.also { it.attach() }
|
||||||
|
@ -852,6 +884,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
|
|
||||||
onTabSelectedListener = object : OnTabSelectedListener {
|
onTabSelectedListener = object : OnTabSelectedListener {
|
||||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||||
|
onBackPressedCallback.isEnabled = tab.position > 0
|
||||||
|
|
||||||
binding.mainToolbar.title = tab.contentDescription
|
binding.mainToolbar.title = tab.contentDescription
|
||||||
|
|
||||||
refreshComposeButtonState(tabAdapter, tab.position)
|
refreshComposeButtonState(tabAdapter, tab.position)
|
||||||
|
@ -884,7 +918,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
|
|
||||||
supportActionBar?.title = tabs[position].title(this@MainActivity)
|
supportActionBar?.title = tabs[position].title(this@MainActivity)
|
||||||
binding.mainToolbar.setOnClickListener {
|
binding.mainToolbar.setOnClickListener {
|
||||||
(tabAdapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect()
|
(
|
||||||
|
tabAdapter.getFragment(
|
||||||
|
activeTabLayout.selectedTabPosition
|
||||||
|
) as? ReselectableFragment
|
||||||
|
)?.onReselect()
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProfiles()
|
updateProfiles()
|
||||||
|
@ -915,7 +953,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
// open LoginActivity to add new account
|
// open LoginActivity to add new account
|
||||||
if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) {
|
if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) {
|
||||||
startActivityWithSlideInAnimation(LoginActivity.getIntent(this, LoginActivity.MODE_ADDITIONAL_LOGIN))
|
startActivityWithSlideInAnimation(
|
||||||
|
LoginActivity.getIntent(this, LoginActivity.MODE_ADDITIONAL_LOGIN)
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// change Account
|
// change Account
|
||||||
|
@ -927,15 +967,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
cacheUpdater.stop()
|
cacheUpdater.stop()
|
||||||
accountManager.setActiveAccount(newSelectedId)
|
accountManager.setActiveAccount(newSelectedId)
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
intent.putExtra(OPEN_WITH_EXPLODE_ANIMATION, true)
|
||||||
if (forward != null) {
|
if (forward != null) {
|
||||||
intent.type = forward.type
|
intent.type = forward.type
|
||||||
intent.action = forward.action
|
intent.action = forward.action
|
||||||
intent.putExtras(forward)
|
intent.putExtras(forward)
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
finishWithoutSlideOutAnimation()
|
finish()
|
||||||
overridePendingTransition(R.anim.explode, R.anim.explode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun logout() {
|
private fun logout() {
|
||||||
|
@ -958,7 +997,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT)
|
LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT)
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
finishWithoutSlideOutAnimation()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
@ -986,10 +1025,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
loadDrawerAvatar(me.avatar, false)
|
loadDrawerAvatar(me.avatar, false)
|
||||||
|
|
||||||
accountManager.updateActiveAccount(me)
|
accountManager.updateActiveAccount(me)
|
||||||
NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this)
|
NotificationHelper.createNotificationChannelsForAccount(
|
||||||
|
accountManager.activeAccount!!,
|
||||||
|
this
|
||||||
|
)
|
||||||
|
|
||||||
// Setup push notifications
|
// Setup push notifications
|
||||||
showMigrationNoticeIfNecessary(this, binding.mainCoordinatorLayout, binding.composeButton, accountManager)
|
showMigrationNoticeIfNecessary(
|
||||||
|
this,
|
||||||
|
binding.mainCoordinatorLayout,
|
||||||
|
binding.composeButton,
|
||||||
|
accountManager
|
||||||
|
)
|
||||||
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
|
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
enablePushNotificationsWithFallback(this@MainActivity, mastodonApi, accountManager)
|
enablePushNotificationsWithFallback(this@MainActivity, mastodonApi, accountManager)
|
||||||
|
@ -999,7 +1046,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProfiles()
|
updateProfiles()
|
||||||
updateShortcut(this, accountManager.activeAccount!!)
|
shareShortcutHelper.updateShortcuts()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("CheckResult")
|
@SuppressLint("CheckResult")
|
||||||
|
@ -1024,7 +1071,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
Glide.with(this)
|
Glide.with(this)
|
||||||
.asDrawable()
|
.asDrawable()
|
||||||
.load(avatarUrl)
|
.load(avatarUrl)
|
||||||
.transform(RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)))
|
.transform(
|
||||||
|
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
|
||||||
|
)
|
||||||
.apply {
|
.apply {
|
||||||
if (showPlaceholder) placeholder(R.drawable.avatar_default)
|
if (showPlaceholder) placeholder(R.drawable.avatar_default)
|
||||||
}
|
}
|
||||||
|
@ -1054,7 +1103,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
Glide.with(this)
|
Glide.with(this)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.load(avatarUrl)
|
.load(avatarUrl)
|
||||||
.transform(RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)))
|
.transform(
|
||||||
|
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
|
||||||
|
)
|
||||||
.apply {
|
.apply {
|
||||||
if (showPlaceholder) placeholder(R.drawable.avatar_default)
|
if (showPlaceholder) placeholder(R.drawable.avatar_default)
|
||||||
}
|
}
|
||||||
|
@ -1101,7 +1152,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateAnnouncementsBadge() {
|
private fun updateAnnouncementsBadge() {
|
||||||
binding.mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString()))
|
binding.mainDrawer.updateBadge(
|
||||||
|
DRAWER_ITEM_ANNOUNCEMENTS,
|
||||||
|
StringHolder(
|
||||||
|
if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateProfiles() {
|
private fun updateProfiles() {
|
||||||
|
@ -1128,18 +1184,22 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
header.clear()
|
header.clear()
|
||||||
header.profiles = profiles
|
header.profiles = profiles
|
||||||
header.setActiveProfile(accountManager.activeAccount!!.id)
|
header.setActiveProfile(accountManager.activeAccount!!.id)
|
||||||
binding.mainToolbar.subtitle = if (accountManager.shouldDisplaySelfUsername(this)) {
|
binding.mainToolbar.subtitle = if (accountManager.shouldDisplaySelfUsername()) {
|
||||||
accountManager.activeAccount!!.fullName
|
accountManager.activeAccount!!.fullName
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun explodeAnimationWasRequested(): Boolean {
|
||||||
|
return intent.getBooleanExtra(OPEN_WITH_EXPLODE_ANIMATION, false)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getActionButton() = binding.composeButton
|
override fun getActionButton() = binding.composeButton
|
||||||
|
|
||||||
override fun androidInjector() = androidInjector
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
const val OPEN_WITH_EXPLODE_ANIMATION = "explode"
|
||||||
|
|
||||||
private const val TAG = "MainActivity" // logging tag
|
private const val TAG = "MainActivity" // logging tag
|
||||||
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13
|
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13
|
||||||
private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14
|
private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14
|
||||||
|
@ -1165,7 +1225,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
* Switches the active account to the accountId and takes the user to the correct place according to the notification they clicked
|
* Switches the active account to the accountId and takes the user to the correct place according to the notification they clicked
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun openNotificationIntent(context: Context, tuskyAccountId: Long, type: Notification.Type): Intent {
|
fun openNotificationIntent(
|
||||||
|
context: Context,
|
||||||
|
tuskyAccountId: Long,
|
||||||
|
type: Notification.Type
|
||||||
|
): Intent {
|
||||||
return accountSwitchIntent(context, tuskyAccountId).apply {
|
return accountSwitchIntent(context, tuskyAccountId).apply {
|
||||||
putExtra(NOTIFICATION_TYPE, type.name)
|
putExtra(NOTIFICATION_TYPE, type.name)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
/* Copyright 2018 Conny Duck
|
|
||||||
*
|
|
||||||
* This file is a part of Tusky.
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
||||||
* Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import com.keylesspalace.tusky.components.login.LoginActivity
|
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@SuppressLint("CustomSplashScreen")
|
|
||||||
class SplashActivity : AppCompatActivity(), Injectable {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var accountManager: AccountManager
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
/** Determine whether the user is currently logged in, and if so go ahead and load the
|
|
||||||
* timeline. Otherwise, start the activity_login screen. */
|
|
||||||
val intent = if (accountManager.activeAccount != null) {
|
|
||||||
Intent(this, MainActivity::class.java)
|
|
||||||
} else {
|
|
||||||
LoginActivity.getIntent(this, LoginActivity.MODE_DEFAULT)
|
|
||||||
}
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -35,21 +35,21 @@ import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
|
||||||
import com.keylesspalace.tusky.entity.Filter
|
import com.keylesspalace.tusky.entity.Filter
|
||||||
import com.keylesspalace.tusky.entity.FilterV1
|
import com.keylesspalace.tusky.entity.FilterV1
|
||||||
import com.keylesspalace.tusky.util.isHttpNotFound
|
import com.keylesspalace.tusky.util.isHttpNotFound
|
||||||
|
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dagger.android.HasAndroidInjector
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
@AndroidEntryPoint
|
||||||
|
class StatusListActivity : BottomSheetActivity() {
|
||||||
@Inject
|
|
||||||
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var eventHub: EventHub
|
lateinit var eventHub: EventHub
|
||||||
|
|
||||||
private val binding: ActivityStatuslistBinding by viewBinding(ActivityStatuslistBinding::inflate)
|
private val binding: ActivityStatuslistBinding by viewBinding(
|
||||||
|
ActivityStatuslistBinding::inflate
|
||||||
|
)
|
||||||
private lateinit var kind: Kind
|
private lateinit var kind: Kind
|
||||||
private var hashtag: String? = null
|
private var hashtag: String? = null
|
||||||
private var followTagItem: MenuItem? = null
|
private var followTagItem: MenuItem? = null
|
||||||
|
@ -136,10 +136,18 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
followTagItem?.isVisible = false
|
followTagItem?.isVisible = false
|
||||||
unfollowTagItem?.isVisible = true
|
unfollowTagItem?.isVisible = true
|
||||||
|
|
||||||
Snackbar.make(binding.root, getString(R.string.following_hashtag_success_format, tag), Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(
|
||||||
|
binding.root,
|
||||||
|
getString(R.string.following_hashtag_success_format, tag),
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Snackbar.make(binding.root, getString(R.string.error_following_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(
|
||||||
|
binding.root,
|
||||||
|
getString(R.string.error_following_hashtag_format, tag),
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
Log.e(TAG, "Failed to follow #$tag", it)
|
Log.e(TAG, "Failed to follow #$tag", it)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -158,10 +166,18 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
followTagItem?.isVisible = true
|
followTagItem?.isVisible = true
|
||||||
unfollowTagItem?.isVisible = false
|
unfollowTagItem?.isVisible = false
|
||||||
|
|
||||||
Snackbar.make(binding.root, getString(R.string.unfollowing_hashtag_success_format, tag), Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(
|
||||||
|
binding.root,
|
||||||
|
getString(R.string.unfollowing_hashtag_success_format, tag),
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Snackbar.make(binding.root, getString(R.string.error_unfollowing_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(
|
||||||
|
binding.root,
|
||||||
|
getString(R.string.error_unfollowing_hashtag_format, tag),
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
Log.e(TAG, "Failed to unfollow #$tag", it)
|
Log.e(TAG, "Failed to unfollow #$tag", it)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -200,8 +216,8 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
}
|
}
|
||||||
updateTagMuteState(mutedFilterV1 != null)
|
updateTagMuteState(mutedFilterV1 != null)
|
||||||
},
|
},
|
||||||
{ throwable ->
|
{ throwable2 ->
|
||||||
Log.e(TAG, "Error getting filters: $throwable")
|
Log.e(TAG, "Error getting filters: $throwable2")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -238,7 +254,12 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
expiresInSeconds = null
|
expiresInSeconds = null
|
||||||
).fold(
|
).fold(
|
||||||
{ filter ->
|
{ filter ->
|
||||||
if (mastodonApi.addFilterKeyword(filterId = filter.id, keyword = hashedTag, wholeWord = true).isSuccess) {
|
if (mastodonApi.addFilterKeyword(
|
||||||
|
filterId = filter.id,
|
||||||
|
keyword = hashedTag,
|
||||||
|
wholeWord = true
|
||||||
|
).isSuccess
|
||||||
|
) {
|
||||||
// must be requested again; otherwise does not contain the keyword (but server does)
|
// must be requested again; otherwise does not contain the keyword (but server does)
|
||||||
mutedFilter = mastodonApi.getFilter(filter.id).getOrNull()
|
mutedFilter = mastodonApi.getFilter(filter.id).getOrNull()
|
||||||
|
|
||||||
|
@ -246,7 +267,11 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
eventHub.dispatch(PreferenceChangedEvent(filter.context[0]))
|
eventHub.dispatch(PreferenceChangedEvent(filter.context[0]))
|
||||||
filterCreateSuccess = true
|
filterCreateSuccess = true
|
||||||
} else {
|
} else {
|
||||||
Snackbar.make(binding.root, getString(R.string.error_muting_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(
|
||||||
|
binding.root,
|
||||||
|
getString(R.string.error_muting_hashtag_format, tag),
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
Log.e(TAG, "Failed to mute #$tag")
|
Log.e(TAG, "Failed to mute #$tag")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -264,13 +289,21 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
eventHub.dispatch(PreferenceChangedEvent(filter.context[0]))
|
eventHub.dispatch(PreferenceChangedEvent(filter.context[0]))
|
||||||
filterCreateSuccess = true
|
filterCreateSuccess = true
|
||||||
},
|
},
|
||||||
{ throwable ->
|
{ throwable2 ->
|
||||||
Snackbar.make(binding.root, getString(R.string.error_muting_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(
|
||||||
Log.e(TAG, "Failed to mute #$tag", throwable)
|
binding.root,
|
||||||
|
getString(R.string.error_muting_hashtag_format, tag),
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
Log.e(TAG, "Failed to mute #$tag", throwable2)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Snackbar.make(binding.root, getString(R.string.error_muting_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(
|
||||||
|
binding.root,
|
||||||
|
getString(R.string.error_muting_hashtag_format, tag),
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
Log.e(TAG, "Failed to mute #$tag", throwable)
|
Log.e(TAG, "Failed to mute #$tag", throwable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -278,7 +311,11 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
|
|
||||||
if (filterCreateSuccess) {
|
if (filterCreateSuccess) {
|
||||||
updateTagMuteState(true)
|
updateTagMuteState(true)
|
||||||
Snackbar.make(binding.root, getString(R.string.muting_hashtag_success_format, tag), Snackbar.LENGTH_LONG).apply {
|
Snackbar.make(
|
||||||
|
binding.root,
|
||||||
|
getString(R.string.muting_hashtag_success_format, tag),
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).apply {
|
||||||
setAction(R.string.action_view_filter) {
|
setAction(R.string.action_view_filter) {
|
||||||
val intent = if (mutedFilter != null) {
|
val intent = if (mutedFilter != null) {
|
||||||
Intent(this@StatusListActivity, EditFilterActivity::class.java).apply {
|
Intent(this@StatusListActivity, EditFilterActivity::class.java).apply {
|
||||||
|
@ -339,10 +376,18 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
mutedFilterV1 = null
|
mutedFilterV1 = null
|
||||||
mutedFilter = null
|
mutedFilter = null
|
||||||
|
|
||||||
Snackbar.make(binding.root, getString(R.string.unmuting_hashtag_success_format, tag), Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(
|
||||||
|
binding.root,
|
||||||
|
getString(R.string.unmuting_hashtag_success_format, tag),
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
},
|
},
|
||||||
{ throwable ->
|
{ throwable ->
|
||||||
Snackbar.make(binding.root, getString(R.string.error_unmuting_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(
|
||||||
|
binding.root,
|
||||||
|
getString(R.string.error_unmuting_hashtag_format, tag),
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
Log.e(TAG, "Failed to unmute #$tag", throwable)
|
Log.e(TAG, "Failed to unmute #$tag", throwable)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -351,8 +396,6 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun androidInjector() = dispatchingAndroidInjector
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val EXTRA_KIND = "kind"
|
private const val EXTRA_KIND = "kind"
|
||||||
|
|
|
@ -20,10 +20,10 @@ import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
||||||
|
import com.keylesspalace.tusky.components.notifications.NotificationsFragment
|
||||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
||||||
import com.keylesspalace.tusky.components.trending.TrendingTagsFragment
|
import com.keylesspalace.tusky.components.trending.TrendingTagsFragment
|
||||||
import com.keylesspalace.tusky.fragment.NotificationsFragment
|
|
||||||
import java.util.Objects
|
import java.util.Objects
|
||||||
|
|
||||||
/** this would be a good case for a sealed class, but that does not work nice with Room */
|
/** this would be a good case for a sealed class, but that does not work nice with Room */
|
||||||
|
@ -104,7 +104,11 @@ fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabD
|
||||||
id = TRENDING_STATUSES,
|
id = TRENDING_STATUSES,
|
||||||
text = R.string.title_public_trending_statuses,
|
text = R.string.title_public_trending_statuses,
|
||||||
icon = R.drawable.ic_hot_24dp,
|
icon = R.drawable.ic_hot_24dp,
|
||||||
fragment = { TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES) }
|
fragment = {
|
||||||
|
TimelineFragment.newInstance(
|
||||||
|
TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
HASHTAG -> TabData(
|
HASHTAG -> TabData(
|
||||||
id = HASHTAG,
|
id = HASHTAG,
|
||||||
|
@ -112,13 +116,22 @@ fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabD
|
||||||
icon = R.drawable.ic_hashtag,
|
icon = R.drawable.ic_hashtag,
|
||||||
fragment = { args -> TimelineFragment.newHashtagInstance(args) },
|
fragment = { args -> TimelineFragment.newHashtagInstance(args) },
|
||||||
arguments = arguments,
|
arguments = arguments,
|
||||||
title = { context -> arguments.joinToString(separator = " ") { context.getString(R.string.title_tag, it) } }
|
title = { context ->
|
||||||
|
arguments.joinToString(separator = " ") {
|
||||||
|
context.getString(R.string.title_tag, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
LIST -> TabData(
|
LIST -> TabData(
|
||||||
id = LIST,
|
id = LIST,
|
||||||
text = R.string.list,
|
text = R.string.list,
|
||||||
icon = R.drawable.ic_list,
|
icon = R.drawable.ic_list,
|
||||||
fragment = { args -> TimelineFragment.newInstance(TimelineViewModel.Kind.LIST, args.getOrNull(0).orEmpty()) },
|
fragment = { args ->
|
||||||
|
TimelineFragment.newInstance(
|
||||||
|
TimelineViewModel.Kind.LIST,
|
||||||
|
args.getOrNull(0).orEmpty()
|
||||||
|
)
|
||||||
|
},
|
||||||
arguments = arguments,
|
arguments = arguments,
|
||||||
title = { arguments.getOrNull(1).orEmpty() }
|
title = { arguments.getOrNull(1).orEmpty() }
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,18 +15,10 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.ProgressBar
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.widget.AppCompatEditText
|
import androidx.appcompat.widget.AppCompatEditText
|
||||||
|
@ -38,34 +30,28 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
|
||||||
import at.connyduck.sparkbutton.helpers.Utils
|
import at.connyduck.sparkbutton.helpers.Utils
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import com.google.android.material.transition.MaterialArcMotion
|
import com.google.android.material.transition.MaterialArcMotion
|
||||||
import com.google.android.material.transition.MaterialContainerTransform
|
import com.google.android.material.transition.MaterialContainerTransform
|
||||||
import com.keylesspalace.tusky.adapter.ItemInteractionListener
|
import com.keylesspalace.tusky.adapter.ItemInteractionListener
|
||||||
import com.keylesspalace.tusky.adapter.TabAdapter
|
import com.keylesspalace.tusky.adapter.TabAdapter
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent
|
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent
|
||||||
|
import com.keylesspalace.tusky.components.account.list.ListSelectionFragment
|
||||||
import com.keylesspalace.tusky.databinding.ActivityTabPreferenceBinding
|
import com.keylesspalace.tusky.databinding.ActivityTabPreferenceBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
|
||||||
import com.keylesspalace.tusky.entity.MastoList
|
import com.keylesspalace.tusky.entity.MastoList
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.getDimension
|
|
||||||
import com.keylesspalace.tusky.util.hide
|
|
||||||
import com.keylesspalace.tusky.util.show
|
|
||||||
import com.keylesspalace.tusky.util.unsafeLazy
|
import com.keylesspalace.tusky.util.unsafeLazy
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.util.visible
|
import com.keylesspalace.tusky.util.visible
|
||||||
import kotlinx.coroutines.CoroutineStart
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.awaitCancellation
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListener {
|
@AndroidEntryPoint
|
||||||
|
class TabPreferenceActivity : BaseActivity(), ItemInteractionListener, ListSelectionFragment.ListSelectionListener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var mastodonApi: MastodonApi
|
lateinit var mastodonApi: MastodonApi
|
||||||
|
@ -82,9 +68,13 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
||||||
|
|
||||||
private var tabsChanged = false
|
private var tabsChanged = false
|
||||||
|
|
||||||
private val selectedItemElevation by unsafeLazy { resources.getDimension(R.dimen.selected_drag_item_elevation) }
|
private val selectedItemElevation by unsafeLazy {
|
||||||
|
resources.getDimension(R.dimen.selected_drag_item_elevation)
|
||||||
|
}
|
||||||
|
|
||||||
private val hashtagRegex by unsafeLazy { Pattern.compile("([\\w_]*[\\p{Alpha}_][\\w_]*)", Pattern.CASE_INSENSITIVE) }
|
private val hashtagRegex by unsafeLazy {
|
||||||
|
Pattern.compile("([\\w_]*[\\p{Alpha}_][\\w_]*)", Pattern.CASE_INSENSITIVE)
|
||||||
|
}
|
||||||
|
|
||||||
private val onFabDismissedCallback = object : OnBackPressedCallback(false) {
|
private val onFabDismissedCallback = object : OnBackPressedCallback(false) {
|
||||||
override fun handleOnBackPressed() {
|
override fun handleOnBackPressed() {
|
||||||
|
@ -109,14 +99,19 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
||||||
currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT)
|
currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT)
|
||||||
binding.currentTabsRecyclerView.adapter = currentTabsAdapter
|
binding.currentTabsRecyclerView.adapter = currentTabsAdapter
|
||||||
binding.currentTabsRecyclerView.layoutManager = LinearLayoutManager(this)
|
binding.currentTabsRecyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
binding.currentTabsRecyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
binding.currentTabsRecyclerView.addItemDecoration(
|
||||||
|
DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
|
||||||
|
)
|
||||||
|
|
||||||
addTabAdapter = TabAdapter(listOf(createTabDataFromId(DIRECT)), true, this)
|
addTabAdapter = TabAdapter(listOf(createTabDataFromId(DIRECT)), true, this)
|
||||||
binding.addTabRecyclerView.adapter = addTabAdapter
|
binding.addTabRecyclerView.adapter = addTabAdapter
|
||||||
binding.addTabRecyclerView.layoutManager = LinearLayoutManager(this)
|
binding.addTabRecyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
|
|
||||||
touchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
|
touchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
|
||||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
override fun getMovementFlags(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder
|
||||||
|
): Int {
|
||||||
return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.END)
|
return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.END)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,7 +123,11 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
||||||
return MIN_TAB_COUNT < currentTabs.size
|
return MIN_TAB_COUNT < currentTabs.size
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
|
override fun onMove(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
target: RecyclerView.ViewHolder
|
||||||
|
): Boolean {
|
||||||
val temp = currentTabs[viewHolder.bindingAdapterPosition]
|
val temp = currentTabs[viewHolder.bindingAdapterPosition]
|
||||||
currentTabs[viewHolder.bindingAdapterPosition] = currentTabs[target.bindingAdapterPosition]
|
currentTabs[viewHolder.bindingAdapterPosition] = currentTabs[target.bindingAdapterPosition]
|
||||||
currentTabs[target.bindingAdapterPosition] = temp
|
currentTabs[target.bindingAdapterPosition] = temp
|
||||||
|
@ -148,7 +147,10 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
override fun clearView(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder
|
||||||
|
) {
|
||||||
super.clearView(recyclerView, viewHolder)
|
super.clearView(recyclerView, viewHolder)
|
||||||
viewHolder.itemView.elevation = 0f
|
viewHolder.itemView.elevation = 0f
|
||||||
}
|
}
|
||||||
|
@ -267,81 +269,24 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
||||||
editText.requestFocus()
|
editText.requestFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var listSelectDialog: ListSelectionFragment? = null
|
||||||
|
|
||||||
private fun showSelectListDialog() {
|
private fun showSelectListDialog() {
|
||||||
val adapter = object : ArrayAdapter<MastoList>(this, android.R.layout.simple_list_item_1) {
|
listSelectDialog = ListSelectionFragment.newInstance(null)
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
listSelectDialog?.show(supportFragmentManager, null)
|
||||||
val view = super.getView(position, convertView, parent)
|
|
||||||
getItem(position)?.let { item -> (view as TextView).text = item.title }
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val statusLayout = LinearLayout(this)
|
return
|
||||||
statusLayout.gravity = Gravity.CENTER
|
|
||||||
val progress = ProgressBar(this)
|
|
||||||
val preferredPadding = getDimension(this, androidx.appcompat.R.attr.dialogPreferredPadding)
|
|
||||||
progress.setPadding(preferredPadding, 0, preferredPadding, 0)
|
|
||||||
progress.visible(false)
|
|
||||||
|
|
||||||
val noListsText = TextView(this)
|
|
||||||
noListsText.setPadding(preferredPadding, 0, preferredPadding, 0)
|
|
||||||
noListsText.text = getText(R.string.select_list_empty)
|
|
||||||
noListsText.visible(false)
|
|
||||||
|
|
||||||
statusLayout.addView(progress)
|
|
||||||
statusLayout.addView(noListsText)
|
|
||||||
|
|
||||||
val dialogBuilder = AlertDialog.Builder(this)
|
|
||||||
.setTitle(R.string.select_list_title)
|
|
||||||
.setNeutralButton(R.string.select_list_manage) { _, _ ->
|
|
||||||
val listIntent = Intent(applicationContext, ListsActivity::class.java)
|
|
||||||
startActivity(listIntent)
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.setView(statusLayout)
|
|
||||||
.setAdapter(adapter) { _, position ->
|
|
||||||
adapter.getItem(position)?.let { item ->
|
|
||||||
val newTab = createTabDataFromId(LIST, listOf(item.id, item.title))
|
|
||||||
currentTabs.add(newTab)
|
|
||||||
currentTabsAdapter.notifyItemInserted(currentTabs.size - 1)
|
|
||||||
updateAvailableTabs()
|
|
||||||
saveTabs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val showProgressBarJob = getProgressBarJob(progress, 500)
|
|
||||||
showProgressBarJob.start()
|
|
||||||
|
|
||||||
val dialog = dialogBuilder.show()
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
mastodonApi.getLists().fold(
|
|
||||||
{ lists ->
|
|
||||||
showProgressBarJob.cancel()
|
|
||||||
adapter.addAll(lists)
|
|
||||||
if (lists.isEmpty()) {
|
|
||||||
noListsText.show()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ throwable ->
|
|
||||||
dialog.hide()
|
|
||||||
Log.e("TabPreferenceActivity", "failed to load lists", throwable)
|
|
||||||
Snackbar.make(binding.root, R.string.error_list_load, Snackbar.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getProgressBarJob(progressView: View, delayMs: Long) = this.lifecycleScope.launch(
|
override fun onListSelected(list: MastoList) {
|
||||||
start = CoroutineStart.LAZY
|
listSelectDialog?.dismiss()
|
||||||
) {
|
listSelectDialog = null
|
||||||
try {
|
|
||||||
delay(delayMs)
|
val newTab = createTabDataFromId(LIST, listOf(list.id, list.title))
|
||||||
progressView.show()
|
currentTabs.add(newTab)
|
||||||
awaitCancellation()
|
currentTabsAdapter.notifyItemInserted(currentTabs.size - 1)
|
||||||
} finally {
|
updateAvailableTabs()
|
||||||
progressView.hide()
|
saveTabs()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateHashtag(input: CharSequence?): Boolean {
|
private fun validateHashtag(input: CharSequence?): Boolean {
|
||||||
|
@ -377,7 +322,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
||||||
addableTabs.add(trendingTagsTab)
|
addableTabs.add(trendingTagsTab)
|
||||||
}
|
}
|
||||||
val bookmarksTab = createTabDataFromId(BOOKMARKS)
|
val bookmarksTab = createTabDataFromId(BOOKMARKS)
|
||||||
if (!currentTabs.contains(trendingTagsTab)) {
|
if (!currentTabs.contains(bookmarksTab)) {
|
||||||
addableTabs.add(bookmarksTab)
|
addableTabs.add(bookmarksTab)
|
||||||
}
|
}
|
||||||
val trendingStatusesTab = createTabDataFromId(TRENDING_STATUSES)
|
val trendingStatusesTab = createTabDataFromId(TRENDING_STATUSES)
|
||||||
|
|
|
@ -18,13 +18,13 @@ package com.keylesspalace.tusky
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.hilt.work.HiltWorkerFactory
|
||||||
|
import androidx.work.Configuration
|
||||||
import androidx.work.Constraints
|
import androidx.work.Constraints
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import androidx.work.PeriodicWorkRequestBuilder
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import autodispose2.AutoDisposePlugins
|
import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
|
||||||
import com.keylesspalace.tusky.di.AppInjector
|
|
||||||
import com.keylesspalace.tusky.settings.AppTheme
|
import com.keylesspalace.tusky.settings.AppTheme
|
||||||
import com.keylesspalace.tusky.settings.NEW_INSTALL_SCHEMA_VERSION
|
import com.keylesspalace.tusky.settings.NEW_INSTALL_SCHEMA_VERSION
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
|
@ -33,30 +33,26 @@ import com.keylesspalace.tusky.settings.SCHEMA_VERSION
|
||||||
import com.keylesspalace.tusky.util.LocaleManager
|
import com.keylesspalace.tusky.util.LocaleManager
|
||||||
import com.keylesspalace.tusky.util.setAppNightMode
|
import com.keylesspalace.tusky.util.setAppNightMode
|
||||||
import com.keylesspalace.tusky.worker.PruneCacheWorker
|
import com.keylesspalace.tusky.worker.PruneCacheWorker
|
||||||
import com.keylesspalace.tusky.worker.WorkerFactory
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import dagger.android.DispatchingAndroidInjector
|
|
||||||
import dagger.android.HasAndroidInjector
|
|
||||||
import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
|
import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
|
||||||
import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper
|
import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper
|
||||||
import de.c1710.filemojicompat_ui.helpers.EmojiPreference
|
import de.c1710.filemojicompat_ui.helpers.EmojiPreference
|
||||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
|
||||||
import org.conscrypt.Conscrypt
|
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import org.conscrypt.Conscrypt
|
||||||
|
|
||||||
class TuskyApplication : Application(), HasAndroidInjector {
|
@HiltAndroidApp
|
||||||
@Inject
|
class TuskyApplication : Application(), Configuration.Provider {
|
||||||
lateinit var androidInjector: DispatchingAndroidInjector<Any>
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var workerFactory: WorkerFactory
|
lateinit var workerFactory: HiltWorkerFactory
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var localeManager: LocaleManager
|
lateinit var localeManager: LocaleManager
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var sharedPreferences: SharedPreferences
|
lateinit var preferences: SharedPreferences
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
// Uncomment me to get StrictMode violation logs
|
// Uncomment me to get StrictMode violation logs
|
||||||
|
@ -73,12 +69,11 @@ class TuskyApplication : Application(), HasAndroidInjector {
|
||||||
|
|
||||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||||
|
|
||||||
AutoDisposePlugins.setHideProxies(false) // a small performance optimization
|
|
||||||
|
|
||||||
AppInjector.init(this)
|
|
||||||
|
|
||||||
// Migrate shared preference keys and defaults from version to version.
|
// Migrate shared preference keys and defaults from version to version.
|
||||||
val oldVersion = sharedPreferences.getInt(PrefKeys.SCHEMA_VERSION, NEW_INSTALL_SCHEMA_VERSION)
|
val oldVersion = preferences.getInt(
|
||||||
|
PrefKeys.SCHEMA_VERSION,
|
||||||
|
NEW_INSTALL_SCHEMA_VERSION
|
||||||
|
)
|
||||||
if (oldVersion != SCHEMA_VERSION) {
|
if (oldVersion != SCHEMA_VERSION) {
|
||||||
upgradeSharedPreferences(oldVersion, SCHEMA_VERSION)
|
upgradeSharedPreferences(oldVersion, SCHEMA_VERSION)
|
||||||
}
|
}
|
||||||
|
@ -89,24 +84,13 @@ class TuskyApplication : Application(), HasAndroidInjector {
|
||||||
EmojiPackHelper.init(this, DefaultEmojiPackList.get(this), allowPackImports = false)
|
EmojiPackHelper.init(this, DefaultEmojiPackList.get(this), allowPackImports = false)
|
||||||
|
|
||||||
// init night mode
|
// init night mode
|
||||||
val theme = sharedPreferences.getString(APP_THEME, AppTheme.DEFAULT.value)
|
val theme = preferences.getString(APP_THEME, AppTheme.DEFAULT.value)
|
||||||
setAppNightMode(theme)
|
setAppNightMode(theme)
|
||||||
|
|
||||||
localeManager.setLocale()
|
localeManager.setLocale()
|
||||||
|
|
||||||
RxJavaPlugins.setErrorHandler {
|
|
||||||
Log.w("RxJava", "undeliverable exception", it)
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationHelper.createWorkerNotificationChannel(this)
|
NotificationHelper.createWorkerNotificationChannel(this)
|
||||||
|
|
||||||
WorkManager.initialize(
|
|
||||||
this,
|
|
||||||
androidx.work.Configuration.Builder()
|
|
||||||
.setWorkerFactory(workerFactory)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Prune the database every ~ 12 hours when the device is idle.
|
// Prune the database every ~ 12 hours when the device is idle.
|
||||||
val pruneCacheWorker = PeriodicWorkRequestBuilder<PruneCacheWorker>(12, TimeUnit.HOURS)
|
val pruneCacheWorker = PeriodicWorkRequestBuilder<PruneCacheWorker>(12, TimeUnit.HOURS)
|
||||||
.setConstraints(Constraints.Builder().setRequiresDeviceIdle(true).build())
|
.setConstraints(Constraints.Builder().setRequiresDeviceIdle(true).build())
|
||||||
|
@ -118,11 +102,14 @@ class TuskyApplication : Application(), HasAndroidInjector {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun androidInjector() = androidInjector
|
override val workManagerConfiguration: Configuration
|
||||||
|
get() = Configuration.Builder()
|
||||||
|
.setWorkerFactory(workerFactory)
|
||||||
|
.build()
|
||||||
|
|
||||||
private fun upgradeSharedPreferences(oldVersion: Int, newVersion: Int) {
|
private fun upgradeSharedPreferences(oldVersion: Int, newVersion: Int) {
|
||||||
Log.d(TAG, "Upgrading shared preferences: $oldVersion -> $newVersion")
|
Log.d(TAG, "Upgrading shared preferences: $oldVersion -> $newVersion")
|
||||||
val editor = sharedPreferences.edit()
|
val editor = preferences.edit()
|
||||||
|
|
||||||
if (oldVersion < 2023022701) {
|
if (oldVersion < 2023022701) {
|
||||||
// These preferences are (now) handled in AccountPreferenceHandler. Remove them from shared for clarity.
|
// These preferences are (now) handled in AccountPreferenceHandler. Remove them from shared for clarity.
|
||||||
|
@ -132,20 +119,25 @@ class TuskyApplication : Application(), HasAndroidInjector {
|
||||||
editor.remove(PrefKeys.MEDIA_PREVIEW_ENABLED)
|
editor.remove(PrefKeys.MEDIA_PREVIEW_ENABLED)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldVersion < 2023072401) {
|
|
||||||
// The notifications filter / clear options are shown on a menu, not a separate bar,
|
|
||||||
// the preference to display them is not needed.
|
|
||||||
editor.remove(PrefKeys.Deprecated.SHOW_NOTIFICATIONS_FILTER)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldVersion != NEW_INSTALL_SCHEMA_VERSION && oldVersion < 2023082301) {
|
if (oldVersion != NEW_INSTALL_SCHEMA_VERSION && oldVersion < 2023082301) {
|
||||||
// Default value for appTheme is now THEME_SYSTEM. If the user is upgrading and
|
// Default value for appTheme is now THEME_SYSTEM. If the user is upgrading and
|
||||||
// didn't have an explicit preference set use the previous default, so the
|
// didn't have an explicit preference set use the previous default, so the
|
||||||
// theme does not unexpectedly change.
|
// theme does not unexpectedly change.
|
||||||
if (!sharedPreferences.contains(APP_THEME)) {
|
if (!preferences.contains(APP_THEME)) {
|
||||||
editor.putString(APP_THEME, AppTheme.NIGHT.value)
|
editor.putString(APP_THEME, AppTheme.NIGHT.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < 2023112001) {
|
||||||
|
editor.remove(PrefKeys.TAB_FILTER_HOME_REPLIES)
|
||||||
|
editor.remove(PrefKeys.TAB_FILTER_HOME_BOOSTS)
|
||||||
|
editor.remove(PrefKeys.TAB_SHOW_HOME_SELF_BOOSTS)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldVersion < 2024060201) {
|
||||||
|
editor.remove(PrefKeys.Deprecated.FAB_HIDE)
|
||||||
|
}
|
||||||
|
|
||||||
editor.putInt(PrefKeys.SCHEMA_VERSION, newVersion)
|
editor.putInt(PrefKeys.SCHEMA_VERSION, newVersion)
|
||||||
editor.apply()
|
editor.apply()
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,8 @@ import android.Manifest
|
||||||
import android.animation.Animator
|
import android.animation.Animator
|
||||||
import android.animation.AnimatorListenerAdapter
|
import android.animation.AnimatorListenerAdapter
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
@ -38,17 +35,14 @@ import android.view.View
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.app.ShareCompat
|
import androidx.core.app.ShareCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.content.IntentCompat
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider
|
|
||||||
import autodispose2.autoDispose
|
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.request.FutureTarget
|
|
||||||
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID
|
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID
|
||||||
import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity
|
import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity
|
||||||
import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding
|
import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding
|
||||||
|
@ -57,26 +51,28 @@ import com.keylesspalace.tusky.fragment.ViewImageFragment
|
||||||
import com.keylesspalace.tusky.fragment.ViewVideoFragment
|
import com.keylesspalace.tusky.fragment.ViewVideoFragment
|
||||||
import com.keylesspalace.tusky.pager.ImagePagerAdapter
|
import com.keylesspalace.tusky.pager.ImagePagerAdapter
|
||||||
import com.keylesspalace.tusky.pager.SingleImagePagerAdapter
|
import com.keylesspalace.tusky.pager.SingleImagePagerAdapter
|
||||||
|
import com.keylesspalace.tusky.util.copyToClipboard
|
||||||
|
import com.keylesspalace.tusky.util.getParcelableArrayListExtraCompat
|
||||||
import com.keylesspalace.tusky.util.getTemporaryMediaFilename
|
import com.keylesspalace.tusky.util.getTemporaryMediaFilename
|
||||||
|
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
|
||||||
|
import com.keylesspalace.tusky.util.submitAsync
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dagger.android.HasAndroidInjector
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.rxjava3.core.Single
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
|
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
|
||||||
|
|
||||||
class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.PhotoActionsListener, ViewVideoFragment.VideoActionsListener {
|
@AndroidEntryPoint
|
||||||
@Inject
|
class ViewMediaActivity :
|
||||||
lateinit var androidInjector: DispatchingAndroidInjector<Any>
|
BaseActivity(),
|
||||||
|
ViewImageFragment.PhotoActionsListener,
|
||||||
|
ViewVideoFragment.VideoActionsListener {
|
||||||
|
|
||||||
private val binding by viewBinding(ActivityViewMediaBinding::inflate)
|
private val binding by viewBinding(ActivityViewMediaBinding::inflate)
|
||||||
|
|
||||||
|
@ -90,6 +86,19 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
|
||||||
private val toolbarVisibilityListeners = mutableListOf<ToolbarVisibilityListener>()
|
private val toolbarVisibilityListeners = mutableListOf<ToolbarVisibilityListener>()
|
||||||
private var imageUrl: String? = null
|
private var imageUrl: String? = null
|
||||||
|
|
||||||
|
private val requestDownloadMediaPermissionLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||||
|
if (isGranted) {
|
||||||
|
downloadMedia()
|
||||||
|
} else {
|
||||||
|
showErrorDialog(
|
||||||
|
binding.toolbar,
|
||||||
|
R.string.error_media_download_permission,
|
||||||
|
R.string.action_retry
|
||||||
|
) { requestDownloadMedia() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun addToolbarVisibilityListener(listener: ToolbarVisibilityListener): Function0<Boolean> {
|
fun addToolbarVisibilityListener(listener: ToolbarVisibilityListener): Function0<Boolean> {
|
||||||
this.toolbarVisibilityListeners.add(listener)
|
this.toolbarVisibilityListeners.add(listener)
|
||||||
listener(isToolbarVisible)
|
listener(isToolbarVisible)
|
||||||
|
@ -103,7 +112,7 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
|
||||||
supportPostponeEnterTransition()
|
supportPostponeEnterTransition()
|
||||||
|
|
||||||
// Gather the parameters.
|
// Gather the parameters.
|
||||||
attachments = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_ATTACHMENTS, AttachmentViewData::class.java)
|
attachments = intent.getParcelableArrayListExtraCompat(EXTRA_ATTACHMENTS)
|
||||||
val initialPosition = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0)
|
val initialPosition = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0)
|
||||||
|
|
||||||
// Adapter is actually of existential type PageAdapter & SharedElementsTransitionListener
|
// Adapter is actually of existential type PageAdapter & SharedElementsTransitionListener
|
||||||
|
@ -215,7 +224,11 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
|
||||||
private fun downloadMedia() {
|
private fun downloadMedia() {
|
||||||
val url = imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url
|
val url = imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url
|
||||||
val filename = Uri.parse(url).lastPathSegment
|
val filename = Uri.parse(url).lastPathSegment
|
||||||
Toast.makeText(applicationContext, resources.getString(R.string.download_image, filename), Toast.LENGTH_SHORT).show()
|
Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
resources.getString(R.string.download_image, filename),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
|
||||||
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
val request = DownloadManager.Request(Uri.parse(url))
|
val request = DownloadManager.Request(Uri.parse(url))
|
||||||
|
@ -225,17 +238,7 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
|
||||||
|
|
||||||
private fun requestDownloadMedia() {
|
private fun requestDownloadMedia() {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
|
requestDownloadMediaPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
downloadMedia()
|
|
||||||
} else {
|
|
||||||
showErrorDialog(
|
|
||||||
binding.toolbar,
|
|
||||||
R.string.error_media_download_permission,
|
|
||||||
R.string.action_retry
|
|
||||||
) { requestDownloadMedia() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
downloadMedia()
|
downloadMedia()
|
||||||
}
|
}
|
||||||
|
@ -243,13 +246,16 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
|
||||||
|
|
||||||
private fun onOpenStatus() {
|
private fun onOpenStatus() {
|
||||||
val attach = attachments!![binding.viewPager.currentItem]
|
val attach = attachments!![binding.viewPager.currentItem]
|
||||||
startActivityWithSlideInAnimation(ViewThreadActivity.startIntent(this, attach.statusId, attach.statusUrl))
|
startActivityWithSlideInAnimation(
|
||||||
|
ViewThreadActivity.startIntent(this, attach.statusId, attach.statusUrl)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyLink() {
|
private fun copyLink() {
|
||||||
val url = imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url
|
copyToClipboard(
|
||||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url,
|
||||||
clipboard.setPrimaryClip(ClipData.newPlainText(null, url))
|
getString(R.string.url_copied),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shareMedia() {
|
private fun shareMedia() {
|
||||||
|
@ -276,7 +282,9 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
|
||||||
private fun shareFile(file: File, mimeType: String?) {
|
private fun shareFile(file: File, mimeType: String?) {
|
||||||
ShareCompat.IntentBuilder(this)
|
ShareCompat.IntentBuilder(this)
|
||||||
.setType(mimeType)
|
.setType(mimeType)
|
||||||
.addStream(FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file))
|
.addStream(
|
||||||
|
FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file)
|
||||||
|
)
|
||||||
.setChooserTitle(R.string.send_media_to)
|
.setChooserTitle(R.string.send_media_to)
|
||||||
.startChooser()
|
.startChooser()
|
||||||
}
|
}
|
||||||
|
@ -287,46 +295,37 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
|
||||||
isCreating = true
|
isCreating = true
|
||||||
binding.progressBarShare.visibility = View.VISIBLE
|
binding.progressBarShare.visibility = View.VISIBLE
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
val file = File(directory, getTemporaryMediaFilename("png"))
|
|
||||||
val futureTask: FutureTarget<Bitmap> =
|
lifecycleScope.launch {
|
||||||
Glide.with(applicationContext).asBitmap().load(Uri.parse(url)).submit()
|
val file = File(directory, getTemporaryMediaFilename("png"))
|
||||||
Single.fromCallable {
|
val result = try {
|
||||||
val bitmap = futureTask.get()
|
val bitmap =
|
||||||
try {
|
Glide.with(applicationContext).asBitmap().load(Uri.parse(url)).submitAsync()
|
||||||
val stream = FileOutputStream(file)
|
try {
|
||||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
|
FileOutputStream(file).use { stream ->
|
||||||
stream.close()
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
|
||||||
return@fromCallable true
|
|
||||||
} catch (fnfe: FileNotFoundException) {
|
|
||||||
Log.e(TAG, "Error writing temporary media.")
|
|
||||||
} catch (ioe: IOException) {
|
|
||||||
Log.e(TAG, "Error writing temporary media.")
|
|
||||||
}
|
|
||||||
return@fromCallable false
|
|
||||||
}
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.doOnDispose {
|
|
||||||
futureTask.cancel(true)
|
|
||||||
}
|
|
||||||
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
|
|
||||||
.subscribe(
|
|
||||||
{ result ->
|
|
||||||
Log.d(TAG, "Download image result: $result")
|
|
||||||
isCreating = false
|
|
||||||
invalidateOptionsMenu()
|
|
||||||
binding.progressBarShare.visibility = View.GONE
|
|
||||||
if (result) {
|
|
||||||
shareFile(file, "image/png")
|
|
||||||
}
|
}
|
||||||
},
|
true
|
||||||
{ error ->
|
} catch (ioe: IOException) {
|
||||||
isCreating = false
|
// FileNotFoundException is covered by IOException
|
||||||
invalidateOptionsMenu()
|
Log.e(TAG, "Error writing temporary media.")
|
||||||
binding.progressBarShare.visibility = View.GONE
|
false
|
||||||
Log.e(TAG, "Failed to download image", error)
|
}.also { result -> Log.d(TAG, "Download image result: $result") }
|
||||||
|
} catch (error: Throwable) {
|
||||||
|
if (error is CancellationException) {
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
)
|
Log.e(TAG, "Failed to download image", error)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
isCreating = false
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
binding.progressBarShare.visibility = View.GONE
|
||||||
|
if (result) {
|
||||||
|
shareFile(file, "image/png")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shareMediaFile(directory: File, url: String) {
|
private fun shareMediaFile(directory: File, url: String) {
|
||||||
|
@ -357,8 +356,6 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun androidInjector() = androidInjector
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val EXTRA_ATTACHMENTS = "attachments"
|
private const val EXTRA_ATTACHMENTS = "attachments"
|
||||||
private const val EXTRA_ATTACHMENT_INDEX = "index"
|
private const val EXTRA_ATTACHMENT_INDEX = "index"
|
||||||
|
@ -366,7 +363,11 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
|
||||||
private const val TAG = "ViewMediaActivity"
|
private const val TAG = "ViewMediaActivity"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun newIntent(context: Context?, attachments: List<AttachmentViewData>, index: Int): Intent {
|
fun newIntent(
|
||||||
|
context: Context?,
|
||||||
|
attachments: List<AttachmentViewData>,
|
||||||
|
index: Int
|
||||||
|
): Intent {
|
||||||
val intent = Intent(context, ViewMediaActivity::class.java)
|
val intent = Intent(context, ViewMediaActivity::class.java)
|
||||||
intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, ArrayList(attachments))
|
intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, ArrayList(attachments))
|
||||||
intent.putExtra(EXTRA_ATTACHMENT_INDEX, index)
|
intent.putExtra(EXTRA_ATTACHMENT_INDEX, index)
|
||||||
|
|
|
@ -24,7 +24,9 @@ import com.keylesspalace.tusky.entity.StringField
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
import com.keylesspalace.tusky.util.fixTextSelection
|
import com.keylesspalace.tusky.util.fixTextSelection
|
||||||
|
|
||||||
class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditFieldBinding>>() {
|
class AccountFieldEditAdapter(
|
||||||
|
var onFieldsChanged: () -> Unit = { }
|
||||||
|
) : RecyclerView.Adapter<BindingHolder<ItemEditFieldBinding>>() {
|
||||||
|
|
||||||
private val fieldData = mutableListOf<MutableStringPair>()
|
private val fieldData = mutableListOf<MutableStringPair>()
|
||||||
private var maxNameLength: Int? = null
|
private var maxNameLength: Int? = null
|
||||||
|
@ -62,8 +64,15 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
|
||||||
|
|
||||||
override fun getItemCount() = fieldData.size
|
override fun getItemCount() = fieldData.size
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemEditFieldBinding> {
|
override fun onCreateViewHolder(
|
||||||
val binding = ItemEditFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): BindingHolder<ItemEditFieldBinding> {
|
||||||
|
val binding = ItemEditFieldBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
return BindingHolder(binding)
|
return BindingHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,10 +92,12 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
|
||||||
|
|
||||||
holder.binding.accountFieldNameText.doAfterTextChanged { newText ->
|
holder.binding.accountFieldNameText.doAfterTextChanged { newText ->
|
||||||
fieldData.getOrNull(holder.bindingAdapterPosition)?.first = newText.toString()
|
fieldData.getOrNull(holder.bindingAdapterPosition)?.first = newText.toString()
|
||||||
|
onFieldsChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.binding.accountFieldValueText.doAfterTextChanged { newText ->
|
holder.binding.accountFieldValueText.doAfterTextChanged { newText ->
|
||||||
fieldData.getOrNull(holder.bindingAdapterPosition)?.second = newText.toString()
|
fieldData.getOrNull(holder.bindingAdapterPosition)?.second = newText.toString()
|
||||||
|
onFieldsChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the textview contents are selectable
|
// Ensure the textview contents are selectable
|
||||||
|
|
|
@ -20,15 +20,20 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ItemAutocompleteAccountBinding
|
import com.keylesspalace.tusky.databinding.ItemAutocompleteAccountBinding
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.entity.AccountEntity
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.loadAvatar
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
|
||||||
class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(context, R.layout.item_autocomplete_account) {
|
class AccountSelectionAdapter(
|
||||||
|
context: Context,
|
||||||
|
private val animateAvatars: Boolean,
|
||||||
|
private val animateEmojis: Boolean
|
||||||
|
) : ArrayAdapter<AccountEntity>(
|
||||||
|
context,
|
||||||
|
R.layout.item_autocomplete_account
|
||||||
|
) {
|
||||||
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
val binding = if (convertView == null) {
|
val binding = if (convertView == null) {
|
||||||
|
@ -39,17 +44,13 @@ class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(co
|
||||||
|
|
||||||
val account = getItem(position)
|
val account = getItem(position)
|
||||||
if (account != null) {
|
if (account != null) {
|
||||||
val pm = PreferenceManager.getDefaultSharedPreferences(binding.avatar.context)
|
|
||||||
val animateEmojis = pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
|
||||||
|
|
||||||
binding.username.text = account.fullName
|
binding.username.text = account.fullName
|
||||||
binding.displayName.text = account.displayName.emojify(account.emojis, binding.displayName, animateEmojis)
|
binding.displayName.text = account.displayName.emojify(account.emojis, binding.displayName, animateEmojis)
|
||||||
binding.avatarBadge.visibility = View.GONE // We never want to display the bot badge here
|
binding.avatarBadge.visibility = View.GONE // We never want to display the bot badge here
|
||||||
|
|
||||||
val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp)
|
val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp)
|
||||||
val animateAvatar = pm.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
|
|
||||||
|
|
||||||
loadAvatar(account.profilePictureUrl, binding.avatar, avatarRadius, animateAvatar)
|
loadAvatar(account.profilePictureUrl, binding.avatar, avatarRadius, animateAvatars)
|
||||||
}
|
}
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
|
|
|
@ -31,13 +31,20 @@ class EmojiAdapter(
|
||||||
private val animate: Boolean
|
private val animate: Boolean
|
||||||
) : RecyclerView.Adapter<BindingHolder<ItemEmojiButtonBinding>>() {
|
) : RecyclerView.Adapter<BindingHolder<ItemEmojiButtonBinding>>() {
|
||||||
|
|
||||||
private val emojiList: List<Emoji> = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker }
|
private val emojiList: List<Emoji> = emojiList.filter { emoji -> emoji.visibleInPicker }
|
||||||
.sortedBy { it.shortcode.lowercase(Locale.ROOT) }
|
.sortedBy { it.shortcode.lowercase(Locale.ROOT) }
|
||||||
|
|
||||||
override fun getItemCount() = emojiList.size
|
override fun getItemCount() = emojiList.size
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemEmojiButtonBinding> {
|
override fun onCreateViewHolder(
|
||||||
val binding = ItemEmojiButtonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): BindingHolder<ItemEmojiButtonBinding> {
|
||||||
|
val binding = ItemEmojiButtonBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
return BindingHolder(binding)
|
return BindingHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,15 +16,17 @@
|
||||||
package com.keylesspalace.tusky.adapter
|
package com.keylesspalace.tusky.adapter
|
||||||
|
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableString
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.text.style.StyleSpan
|
import android.text.style.StyleSpan
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.components.notifications.NotificationsViewHolder
|
||||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
||||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||||
|
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.loadAvatar
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
@ -33,12 +35,28 @@ import com.keylesspalace.tusky.util.setClickableText
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.unicodeWrap
|
import com.keylesspalace.tusky.util.unicodeWrap
|
||||||
import com.keylesspalace.tusky.util.visible
|
import com.keylesspalace.tusky.util.visible
|
||||||
|
import com.keylesspalace.tusky.viewdata.NotificationViewData
|
||||||
|
|
||||||
class FollowRequestViewHolder(
|
class FollowRequestViewHolder(
|
||||||
private val binding: ItemFollowRequestBinding,
|
private val binding: ItemFollowRequestBinding,
|
||||||
|
private val accountListener: AccountActionListener,
|
||||||
private val linkListener: LinkListener,
|
private val linkListener: LinkListener,
|
||||||
private val showHeader: Boolean
|
private val showHeader: Boolean
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root), NotificationsViewHolder {
|
||||||
|
|
||||||
|
override fun bind(
|
||||||
|
viewData: NotificationViewData.Concrete,
|
||||||
|
payloads: List<*>,
|
||||||
|
statusDisplayOptions: StatusDisplayOptions
|
||||||
|
) {
|
||||||
|
setupWithAccount(
|
||||||
|
viewData.account,
|
||||||
|
statusDisplayOptions.animateAvatars,
|
||||||
|
statusDisplayOptions.animateEmojis,
|
||||||
|
statusDisplayOptions.showBotOverlay
|
||||||
|
)
|
||||||
|
setupActionListener(accountListener, viewData.account.id)
|
||||||
|
}
|
||||||
|
|
||||||
fun setupWithAccount(
|
fun setupWithAccount(
|
||||||
account: TimelineAccount,
|
account: TimelineAccount,
|
||||||
|
@ -47,16 +65,26 @@ class FollowRequestViewHolder(
|
||||||
showBotOverlay: Boolean
|
showBotOverlay: Boolean
|
||||||
) {
|
) {
|
||||||
val wrappedName = account.name.unicodeWrap()
|
val wrappedName = account.name.unicodeWrap()
|
||||||
val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, animateEmojis)
|
val emojifiedName: CharSequence = wrappedName.emojify(
|
||||||
|
account.emojis,
|
||||||
|
binding.displayNameTextView,
|
||||||
|
animateEmojis
|
||||||
|
)
|
||||||
binding.displayNameTextView.text = emojifiedName
|
binding.displayNameTextView.text = emojifiedName
|
||||||
if (showHeader) {
|
if (showHeader) {
|
||||||
val wholeMessage: String = itemView.context.getString(R.string.notification_follow_request_format, wrappedName)
|
val wholeMessage: String = itemView.context.getString(
|
||||||
binding.notificationTextView.text = SpannableStringBuilder(wholeMessage).apply {
|
R.string.notification_follow_request_format,
|
||||||
|
wrappedName
|
||||||
|
)
|
||||||
|
binding.notificationTextView.text = SpannableString(wholeMessage).apply {
|
||||||
setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
}.emojify(account.emojis, itemView, animateEmojis)
|
}.emojify(account.emojis, binding.notificationTextView, animateEmojis)
|
||||||
}
|
}
|
||||||
binding.notificationTextView.visible(showHeader)
|
binding.notificationTextView.visible(showHeader)
|
||||||
val formattedUsername = itemView.context.getString(R.string.post_username_format, account.username)
|
val formattedUsername = itemView.context.getString(
|
||||||
|
R.string.post_username_format,
|
||||||
|
account.username
|
||||||
|
)
|
||||||
binding.usernameTextView.text = formattedUsername
|
binding.usernameTextView.text = formattedUsername
|
||||||
if (account.note.isEmpty()) {
|
if (account.note.isEmpty()) {
|
||||||
binding.accountNote.hide()
|
binding.accountNote.hide()
|
||||||
|
@ -67,7 +95,9 @@ class FollowRequestViewHolder(
|
||||||
.emojify(account.emojis, binding.accountNote, animateEmojis)
|
.emojify(account.emojis, binding.accountNote, animateEmojis)
|
||||||
setClickableText(binding.accountNote, emojifiedNote, emptyList(), null, linkListener)
|
setClickableText(binding.accountNote, emojifiedNote, emptyList(), null, linkListener)
|
||||||
}
|
}
|
||||||
val avatarRadius = binding.avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
val avatarRadius = binding.avatar.context.resources.getDimensionPixelSize(
|
||||||
|
R.dimen.avatar_radius_48dp
|
||||||
|
)
|
||||||
loadAvatar(account.avatar, binding.avatar, avatarRadius, animateAvatar)
|
loadAvatar(account.avatar, binding.avatar, avatarRadius, animateAvatar)
|
||||||
binding.avatarBadge.visible(showBotOverlay && account.bot)
|
binding.avatarBadge.visible(showBotOverlay && account.bot)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,11 @@ import com.keylesspalace.tusky.util.getTuskyDisplayName
|
||||||
import com.keylesspalace.tusky.util.modernLanguageCode
|
import com.keylesspalace.tusky.util.modernLanguageCode
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : ArrayAdapter<Locale>(context, resource, locales) {
|
class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : ArrayAdapter<Locale>(
|
||||||
|
context,
|
||||||
|
resource,
|
||||||
|
locales
|
||||||
|
) {
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
return (super.getView(position, convertView, parent) as TextView).apply {
|
return (super.getView(position, convertView, parent) as TextView).apply {
|
||||||
setTextColor(MaterialColors.getColor(this, android.R.attr.textColorTertiary))
|
setTextColor(MaterialColors.getColor(this, android.R.attr.textColorTertiary))
|
||||||
|
|
|
@ -1,708 +0,0 @@
|
||||||
/* Copyright 2021 Tusky Contributors
|
|
||||||
*
|
|
||||||
* This file is a part of Tusky.
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
||||||
* Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky.adapter;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.PorterDuff;
|
|
||||||
import android.graphics.Typeface;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.text.InputFilter;
|
|
||||||
import android.text.SpannableStringBuilder;
|
|
||||||
import android.text.Spanned;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.text.style.StyleSpan;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.ColorRes;
|
|
||||||
import androidx.annotation.DrawableRes;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
|
||||||
import com.keylesspalace.tusky.R;
|
|
||||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
|
|
||||||
import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding;
|
|
||||||
import com.keylesspalace.tusky.entity.Emoji;
|
|
||||||
import com.keylesspalace.tusky.entity.Notification;
|
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
|
||||||
import com.keylesspalace.tusky.entity.TimelineAccount;
|
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
|
||||||
import com.keylesspalace.tusky.util.AbsoluteTimeFormatter;
|
|
||||||
import com.keylesspalace.tusky.util.CardViewMode;
|
|
||||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
|
||||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
|
||||||
import com.keylesspalace.tusky.util.LinkHelper;
|
|
||||||
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
|
|
||||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
|
||||||
import com.keylesspalace.tusky.util.StringUtils;
|
|
||||||
import com.keylesspalace.tusky.util.TimestampUtils;
|
|
||||||
import com.keylesspalace.tusky.viewdata.NotificationViewData;
|
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import at.connyduck.sparkbutton.helpers.Utils;
|
|
||||||
|
|
||||||
public class NotificationsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements LinkListener{
|
|
||||||
|
|
||||||
public interface AdapterDataSource<T> {
|
|
||||||
int getItemCount();
|
|
||||||
|
|
||||||
T getItemAt(int pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static final int VIEW_TYPE_STATUS = 0;
|
|
||||||
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 1;
|
|
||||||
private static final int VIEW_TYPE_FOLLOW = 2;
|
|
||||||
private static final int VIEW_TYPE_FOLLOW_REQUEST = 3;
|
|
||||||
private static final int VIEW_TYPE_PLACEHOLDER = 4;
|
|
||||||
private static final int VIEW_TYPE_REPORT = 5;
|
|
||||||
private static final int VIEW_TYPE_UNKNOWN = 6;
|
|
||||||
|
|
||||||
private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE};
|
|
||||||
private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0];
|
|
||||||
|
|
||||||
private final String accountId;
|
|
||||||
private StatusDisplayOptions statusDisplayOptions;
|
|
||||||
private final StatusActionListener statusListener;
|
|
||||||
private final NotificationActionListener notificationActionListener;
|
|
||||||
private final AccountActionListener accountActionListener;
|
|
||||||
private final AdapterDataSource<NotificationViewData> dataSource;
|
|
||||||
private final AbsoluteTimeFormatter absoluteTimeFormatter = new AbsoluteTimeFormatter();
|
|
||||||
|
|
||||||
public NotificationsAdapter(String accountId,
|
|
||||||
AdapterDataSource<NotificationViewData> dataSource,
|
|
||||||
StatusDisplayOptions statusDisplayOptions,
|
|
||||||
StatusActionListener statusListener,
|
|
||||||
NotificationActionListener notificationActionListener,
|
|
||||||
AccountActionListener accountActionListener) {
|
|
||||||
|
|
||||||
this.accountId = accountId;
|
|
||||||
this.dataSource = dataSource;
|
|
||||||
this.statusDisplayOptions = statusDisplayOptions;
|
|
||||||
this.statusListener = statusListener;
|
|
||||||
this.notificationActionListener = notificationActionListener;
|
|
||||||
this.accountActionListener = accountActionListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
|
||||||
switch (viewType) {
|
|
||||||
case VIEW_TYPE_STATUS: {
|
|
||||||
View view = inflater
|
|
||||||
.inflate(R.layout.item_status, parent, false);
|
|
||||||
return new StatusViewHolder(view);
|
|
||||||
}
|
|
||||||
case VIEW_TYPE_STATUS_NOTIFICATION: {
|
|
||||||
View view = inflater
|
|
||||||
.inflate(R.layout.item_status_notification, parent, false);
|
|
||||||
return new StatusNotificationViewHolder(view, statusDisplayOptions, absoluteTimeFormatter);
|
|
||||||
}
|
|
||||||
case VIEW_TYPE_FOLLOW: {
|
|
||||||
View view = inflater
|
|
||||||
.inflate(R.layout.item_follow, parent, false);
|
|
||||||
return new FollowViewHolder(view, statusDisplayOptions);
|
|
||||||
}
|
|
||||||
case VIEW_TYPE_FOLLOW_REQUEST: {
|
|
||||||
ItemFollowRequestBinding binding = ItemFollowRequestBinding.inflate(inflater, parent, false);
|
|
||||||
return new FollowRequestViewHolder(binding, this, true);
|
|
||||||
}
|
|
||||||
case VIEW_TYPE_PLACEHOLDER: {
|
|
||||||
View view = inflater
|
|
||||||
.inflate(R.layout.item_status_placeholder, parent, false);
|
|
||||||
return new PlaceholderViewHolder(view);
|
|
||||||
}
|
|
||||||
case VIEW_TYPE_REPORT: {
|
|
||||||
ItemReportNotificationBinding binding = ItemReportNotificationBinding.inflate(inflater, parent, false);
|
|
||||||
return new ReportNotificationViewHolder(binding);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
case VIEW_TYPE_UNKNOWN: {
|
|
||||||
View view = new View(parent.getContext());
|
|
||||||
view.setLayoutParams(
|
|
||||||
new ViewGroup.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
Utils.dpToPx(parent.getContext(), 24)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return new RecyclerView.ViewHolder(view) {
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
|
||||||
bindViewHolder(viewHolder, position, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @NonNull List<Object> payloads) {
|
|
||||||
bindViewHolder(viewHolder, position, payloads);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @Nullable List<Object> payloads) {
|
|
||||||
Object payloadForHolder = payloads != null && !payloads.isEmpty() ? payloads.get(0) : null;
|
|
||||||
if (position < this.dataSource.getItemCount()) {
|
|
||||||
NotificationViewData notification = dataSource.getItemAt(position);
|
|
||||||
if (notification instanceof NotificationViewData.Placeholder) {
|
|
||||||
if (payloadForHolder == null) {
|
|
||||||
NotificationViewData.Placeholder placeholder = ((NotificationViewData.Placeholder) notification);
|
|
||||||
PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder;
|
|
||||||
holder.setup(statusListener, placeholder.isLoading());
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
NotificationViewData.Concrete concreteNotification =
|
|
||||||
(NotificationViewData.Concrete) notification;
|
|
||||||
switch (viewHolder.getItemViewType()) {
|
|
||||||
case VIEW_TYPE_STATUS: {
|
|
||||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
|
||||||
StatusViewData.Concrete status = concreteNotification.getStatusViewData();
|
|
||||||
if (status == null) {
|
|
||||||
/* in some very rare cases servers sends null status even though they should not,
|
|
||||||
* we have to handle it somehow */
|
|
||||||
holder.showStatusContent(false);
|
|
||||||
} else {
|
|
||||||
if (payloads == null) {
|
|
||||||
holder.showStatusContent(true);
|
|
||||||
}
|
|
||||||
holder.setupWithStatus(status, statusListener, statusDisplayOptions, payloadForHolder);
|
|
||||||
}
|
|
||||||
if (concreteNotification.getType() == Notification.Type.POLL) {
|
|
||||||
holder.setPollInfo(accountId.equals(concreteNotification.getAccount().getId()));
|
|
||||||
} else {
|
|
||||||
holder.hideStatusInfo();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case VIEW_TYPE_STATUS_NOTIFICATION: {
|
|
||||||
StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder;
|
|
||||||
StatusViewData.Concrete statusViewData = concreteNotification.getStatusViewData();
|
|
||||||
if (payloadForHolder == null) {
|
|
||||||
if (statusViewData == null) {
|
|
||||||
/* in some very rare cases servers sends null status even though they should not,
|
|
||||||
* we have to handle it somehow */
|
|
||||||
holder.showNotificationContent(false);
|
|
||||||
} else {
|
|
||||||
holder.showNotificationContent(true);
|
|
||||||
|
|
||||||
Status status = statusViewData.getActionable();
|
|
||||||
holder.setDisplayName(status.getAccount().getDisplayName(), status.getAccount().getEmojis());
|
|
||||||
holder.setUsername(status.getAccount().getUsername());
|
|
||||||
holder.setCreatedAt(status.getCreatedAt());
|
|
||||||
|
|
||||||
if (concreteNotification.getType() == Notification.Type.STATUS ||
|
|
||||||
concreteNotification.getType() == Notification.Type.UPDATE) {
|
|
||||||
holder.setAvatar(status.getAccount().getAvatar(), status.getAccount().getBot());
|
|
||||||
} else {
|
|
||||||
holder.setAvatars(status.getAccount().getAvatar(),
|
|
||||||
concreteNotification.getAccount().getAvatar());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.setMessage(concreteNotification, statusListener);
|
|
||||||
holder.setupButtons(notificationActionListener,
|
|
||||||
concreteNotification.getAccount().getId(),
|
|
||||||
concreteNotification.getId());
|
|
||||||
} else {
|
|
||||||
if (payloadForHolder instanceof List)
|
|
||||||
for (Object item : (List<?>) payloadForHolder) {
|
|
||||||
if (StatusBaseViewHolder.Key.KEY_CREATED.equals(item) && statusViewData != null) {
|
|
||||||
holder.setCreatedAt(statusViewData.getStatus().getActionableStatus().getCreatedAt());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case VIEW_TYPE_FOLLOW: {
|
|
||||||
if (payloadForHolder == null) {
|
|
||||||
FollowViewHolder holder = (FollowViewHolder) viewHolder;
|
|
||||||
holder.setMessage(concreteNotification.getAccount(), concreteNotification.getType() == Notification.Type.SIGN_UP);
|
|
||||||
holder.setupButtons(notificationActionListener, concreteNotification.getAccount().getId());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case VIEW_TYPE_FOLLOW_REQUEST: {
|
|
||||||
if (payloadForHolder == null) {
|
|
||||||
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
|
|
||||||
holder.setupWithAccount(concreteNotification.getAccount(), statusDisplayOptions.animateAvatars(), statusDisplayOptions.animateEmojis(), statusDisplayOptions.showBotOverlay());
|
|
||||||
holder.setupActionListener(accountActionListener, concreteNotification.getAccount().getId());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case VIEW_TYPE_REPORT: {
|
|
||||||
if (payloadForHolder == null) {
|
|
||||||
ReportNotificationViewHolder holder = (ReportNotificationViewHolder) viewHolder;
|
|
||||||
holder.setupWithReport(concreteNotification.getAccount(), concreteNotification.getReport(), statusDisplayOptions.animateAvatars(), statusDisplayOptions.animateEmojis());
|
|
||||||
holder.setupActionListener(notificationActionListener, concreteNotification.getReport().getTargetAccount().getId(), concreteNotification.getAccount().getId(), concreteNotification.getReport().getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return dataSource.getItemCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMediaPreviewEnabled(boolean mediaPreviewEnabled) {
|
|
||||||
this.statusDisplayOptions = statusDisplayOptions.copy(
|
|
||||||
statusDisplayOptions.animateAvatars(),
|
|
||||||
mediaPreviewEnabled,
|
|
||||||
statusDisplayOptions.useAbsoluteTime(),
|
|
||||||
statusDisplayOptions.showBotOverlay(),
|
|
||||||
statusDisplayOptions.useBlurhash(),
|
|
||||||
CardViewMode.NONE,
|
|
||||||
statusDisplayOptions.confirmReblogs(),
|
|
||||||
statusDisplayOptions.confirmFavourites(),
|
|
||||||
statusDisplayOptions.hideStats(),
|
|
||||||
statusDisplayOptions.animateEmojis(),
|
|
||||||
statusDisplayOptions.showStatsInline(),
|
|
||||||
statusDisplayOptions.showSensitiveMedia(),
|
|
||||||
statusDisplayOptions.openSpoiler()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isMediaPreviewEnabled() {
|
|
||||||
return this.statusDisplayOptions.mediaPreviewEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemViewType(int position) {
|
|
||||||
NotificationViewData notification = dataSource.getItemAt(position);
|
|
||||||
if (notification instanceof NotificationViewData.Concrete) {
|
|
||||||
NotificationViewData.Concrete concrete = ((NotificationViewData.Concrete) notification);
|
|
||||||
switch (concrete.getType()) {
|
|
||||||
case MENTION:
|
|
||||||
case POLL: {
|
|
||||||
return VIEW_TYPE_STATUS;
|
|
||||||
}
|
|
||||||
case STATUS:
|
|
||||||
case FAVOURITE:
|
|
||||||
case REBLOG:
|
|
||||||
case UPDATE: {
|
|
||||||
return VIEW_TYPE_STATUS_NOTIFICATION;
|
|
||||||
}
|
|
||||||
case FOLLOW:
|
|
||||||
case SIGN_UP: {
|
|
||||||
return VIEW_TYPE_FOLLOW;
|
|
||||||
}
|
|
||||||
case FOLLOW_REQUEST: {
|
|
||||||
return VIEW_TYPE_FOLLOW_REQUEST;
|
|
||||||
}
|
|
||||||
case REPORT: {
|
|
||||||
return VIEW_TYPE_REPORT;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return VIEW_TYPE_UNKNOWN;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (notification instanceof NotificationViewData.Placeholder) {
|
|
||||||
return VIEW_TYPE_PLACEHOLDER;
|
|
||||||
} else {
|
|
||||||
throw new AssertionError("Unknown notification type");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface NotificationActionListener {
|
|
||||||
void onViewAccount(String id);
|
|
||||||
|
|
||||||
void onViewStatusForNotificationId(String notificationId);
|
|
||||||
|
|
||||||
void onViewReport(String reportId);
|
|
||||||
|
|
||||||
void onExpandedChange(boolean expanded, int position);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the status {@link android.widget.ToggleButton} responsible for collapsing long
|
|
||||||
* status content is interacted with.
|
|
||||||
*
|
|
||||||
* @param isCollapsed Whether the status content is shown in a collapsed state or fully.
|
|
||||||
* @param position The position of the status in the list.
|
|
||||||
*/
|
|
||||||
void onNotificationContentCollapsedChange(boolean isCollapsed, int position);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class FollowViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
private final TextView message;
|
|
||||||
private final TextView usernameView;
|
|
||||||
private final TextView displayNameView;
|
|
||||||
private final ImageView avatar;
|
|
||||||
private final StatusDisplayOptions statusDisplayOptions;
|
|
||||||
|
|
||||||
FollowViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) {
|
|
||||||
super(itemView);
|
|
||||||
message = itemView.findViewById(R.id.notification_text);
|
|
||||||
usernameView = itemView.findViewById(R.id.notification_username);
|
|
||||||
displayNameView = itemView.findViewById(R.id.notification_display_name);
|
|
||||||
avatar = itemView.findViewById(R.id.notification_avatar);
|
|
||||||
this.statusDisplayOptions = statusDisplayOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setMessage(TimelineAccount account, Boolean isSignUp) {
|
|
||||||
Context context = message.getContext();
|
|
||||||
|
|
||||||
String format = context.getString(isSignUp ? R.string.notification_sign_up_format : R.string.notification_follow_format);
|
|
||||||
String wrappedDisplayName = StringUtils.unicodeWrap(account.getName());
|
|
||||||
String wholeMessage = String.format(format, wrappedDisplayName);
|
|
||||||
CharSequence emojifiedMessage = CustomEmojiHelper.emojify(
|
|
||||||
wholeMessage, account.getEmojis(), message, statusDisplayOptions.animateEmojis()
|
|
||||||
);
|
|
||||||
message.setText(emojifiedMessage);
|
|
||||||
|
|
||||||
String username = context.getString(R.string.post_username_format, account.getUsername());
|
|
||||||
usernameView.setText(username);
|
|
||||||
|
|
||||||
CharSequence emojifiedDisplayName = CustomEmojiHelper.emojify(
|
|
||||||
wrappedDisplayName, account.getEmojis(), usernameView, statusDisplayOptions.animateEmojis()
|
|
||||||
);
|
|
||||||
|
|
||||||
displayNameView.setText(emojifiedDisplayName);
|
|
||||||
|
|
||||||
int avatarRadius = avatar.getContext().getResources()
|
|
||||||
.getDimensionPixelSize(R.dimen.avatar_radius_42dp);
|
|
||||||
|
|
||||||
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius,
|
|
||||||
statusDisplayOptions.animateAvatars(), null);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupButtons(final NotificationActionListener listener, final String accountId) {
|
|
||||||
itemView.setOnClickListener(v -> listener.onViewAccount(accountId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class StatusNotificationViewHolder extends RecyclerView.ViewHolder
|
|
||||||
implements View.OnClickListener {
|
|
||||||
|
|
||||||
private final View container;
|
|
||||||
private final TextView message;
|
|
||||||
// private final View statusNameBar;
|
|
||||||
private final TextView displayName;
|
|
||||||
private final TextView username;
|
|
||||||
private final TextView timestampInfo;
|
|
||||||
private final TextView statusContent;
|
|
||||||
private final ImageView statusAvatar;
|
|
||||||
private final ImageView notificationAvatar;
|
|
||||||
private final TextView contentWarningDescriptionTextView;
|
|
||||||
private final Button contentWarningButton;
|
|
||||||
private final Button contentCollapseButton; // TODO: This code SHOULD be based on StatusBaseViewHolder
|
|
||||||
private final StatusDisplayOptions statusDisplayOptions;
|
|
||||||
private final AbsoluteTimeFormatter absoluteTimeFormatter;
|
|
||||||
|
|
||||||
private String accountId;
|
|
||||||
private String notificationId;
|
|
||||||
private NotificationActionListener notificationActionListener;
|
|
||||||
private StatusViewData.Concrete statusViewData;
|
|
||||||
|
|
||||||
private final int avatarRadius48dp;
|
|
||||||
private final int avatarRadius36dp;
|
|
||||||
private final int avatarRadius24dp;
|
|
||||||
|
|
||||||
StatusNotificationViewHolder(
|
|
||||||
View itemView,
|
|
||||||
StatusDisplayOptions statusDisplayOptions,
|
|
||||||
AbsoluteTimeFormatter absoluteTimeFormatter
|
|
||||||
) {
|
|
||||||
super(itemView);
|
|
||||||
message = itemView.findViewById(R.id.notification_top_text);
|
|
||||||
// statusNameBar = itemView.findViewById(R.id.status_name_bar);
|
|
||||||
displayName = itemView.findViewById(R.id.status_display_name);
|
|
||||||
username = itemView.findViewById(R.id.status_username);
|
|
||||||
timestampInfo = itemView.findViewById(R.id.status_meta_info);
|
|
||||||
statusContent = itemView.findViewById(R.id.notification_content);
|
|
||||||
statusAvatar = itemView.findViewById(R.id.notification_status_avatar);
|
|
||||||
notificationAvatar = itemView.findViewById(R.id.notification_notification_avatar);
|
|
||||||
contentWarningDescriptionTextView = itemView.findViewById(R.id.notification_content_warning_description);
|
|
||||||
contentWarningButton = itemView.findViewById(R.id.notification_content_warning_button);
|
|
||||||
contentCollapseButton = itemView.findViewById(R.id.button_toggle_notification_content);
|
|
||||||
|
|
||||||
container = itemView.findViewById(R.id.notification_container);
|
|
||||||
|
|
||||||
this.statusDisplayOptions = statusDisplayOptions;
|
|
||||||
this.absoluteTimeFormatter = absoluteTimeFormatter;
|
|
||||||
|
|
||||||
int darkerFilter = Color.rgb(123, 123, 123);
|
|
||||||
statusAvatar.setColorFilter(darkerFilter, PorterDuff.Mode.MULTIPLY);
|
|
||||||
notificationAvatar.setColorFilter(darkerFilter, PorterDuff.Mode.MULTIPLY);
|
|
||||||
|
|
||||||
itemView.setOnClickListener(this);
|
|
||||||
message.setOnClickListener(this);
|
|
||||||
statusContent.setOnClickListener(this);
|
|
||||||
|
|
||||||
this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp);
|
|
||||||
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
|
|
||||||
this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showNotificationContent(boolean show) {
|
|
||||||
// statusNameBar.setVisibility(show ? View.VISIBLE : View.GONE);
|
|
||||||
contentWarningDescriptionTextView.setVisibility(show ? View.VISIBLE : View.GONE);
|
|
||||||
contentWarningButton.setVisibility(show ? View.VISIBLE : View.GONE);
|
|
||||||
statusContent.setVisibility(show ? View.VISIBLE : View.GONE);
|
|
||||||
statusAvatar.setVisibility(show ? View.VISIBLE : View.GONE);
|
|
||||||
notificationAvatar.setVisibility(show ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setDisplayName(String name, List<Emoji> emojis) {
|
|
||||||
CharSequence emojifiedName = CustomEmojiHelper.emojify(name, emojis, displayName, statusDisplayOptions.animateEmojis());
|
|
||||||
displayName.setText(emojifiedName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setUsername(String name) {
|
|
||||||
Context context = username.getContext();
|
|
||||||
String format = context.getString(R.string.post_username_format);
|
|
||||||
String usernameText = String.format(format, name);
|
|
||||||
username.setText(usernameText);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setCreatedAt(@Nullable Date createdAt) {
|
|
||||||
if (statusDisplayOptions.useAbsoluteTime()) {
|
|
||||||
timestampInfo.setText(absoluteTimeFormatter.format(createdAt, true));
|
|
||||||
} else {
|
|
||||||
// This is the visible timestampInfo.
|
|
||||||
String readout;
|
|
||||||
/* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m"
|
|
||||||
* as 17 meters instead of minutes. */
|
|
||||||
CharSequence readoutAloud;
|
|
||||||
if (createdAt != null) {
|
|
||||||
long then = createdAt.getTime();
|
|
||||||
long now = new Date().getTime();
|
|
||||||
readout = TimestampUtils.getRelativeTimeSpanString(timestampInfo.getContext(), then, now);
|
|
||||||
readoutAloud = android.text.format.DateUtils.getRelativeTimeSpanString(then, now,
|
|
||||||
android.text.format.DateUtils.SECOND_IN_MILLIS,
|
|
||||||
android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE);
|
|
||||||
} else {
|
|
||||||
// unknown minutes~
|
|
||||||
readout = "?m";
|
|
||||||
readoutAloud = "? minutes";
|
|
||||||
}
|
|
||||||
timestampInfo.setText(readout);
|
|
||||||
timestampInfo.setContentDescription(readoutAloud);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Drawable getIconWithColor(Context context, @DrawableRes int drawable, @ColorRes int color) {
|
|
||||||
Drawable icon = ContextCompat.getDrawable(context, drawable);
|
|
||||||
if (icon != null) {
|
|
||||||
icon.setColorFilter(context.getColor(color), PorterDuff.Mode.SRC_ATOP);
|
|
||||||
}
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener) {
|
|
||||||
this.statusViewData = notificationViewData.getStatusViewData();
|
|
||||||
|
|
||||||
String displayName = StringUtils.unicodeWrap(notificationViewData.getAccount().getName());
|
|
||||||
Notification.Type type = notificationViewData.getType();
|
|
||||||
|
|
||||||
Context context = message.getContext();
|
|
||||||
String format;
|
|
||||||
Drawable icon;
|
|
||||||
switch (type) {
|
|
||||||
default:
|
|
||||||
case FAVOURITE: {
|
|
||||||
icon = getIconWithColor(context, R.drawable.ic_star_24dp, R.color.tusky_orange);
|
|
||||||
format = context.getString(R.string.notification_favourite_format);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case REBLOG: {
|
|
||||||
icon = getIconWithColor(context, R.drawable.ic_repeat_24dp, R.color.tusky_blue);
|
|
||||||
format = context.getString(R.string.notification_reblog_format);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case STATUS: {
|
|
||||||
icon = getIconWithColor(context, R.drawable.ic_home_24dp, R.color.tusky_blue);
|
|
||||||
format = context.getString(R.string.notification_subscription_format);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case UPDATE: {
|
|
||||||
icon = getIconWithColor(context, R.drawable.ic_edit_24dp, R.color.tusky_blue);
|
|
||||||
format = context.getString(R.string.notification_update_format);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
message.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
|
|
||||||
String wholeMessage = String.format(format, displayName);
|
|
||||||
final SpannableStringBuilder str = new SpannableStringBuilder(wholeMessage);
|
|
||||||
int displayNameIndex = format.indexOf("%s");
|
|
||||||
str.setSpan(
|
|
||||||
new StyleSpan(Typeface.BOLD),
|
|
||||||
displayNameIndex,
|
|
||||||
displayNameIndex + displayName.length(),
|
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
||||||
);
|
|
||||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
|
||||||
str, notificationViewData.getAccount().getEmojis(), message, statusDisplayOptions.animateEmojis()
|
|
||||||
);
|
|
||||||
message.setText(emojifiedText);
|
|
||||||
|
|
||||||
if (statusViewData != null) {
|
|
||||||
boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getStatus().getSpoilerText());
|
|
||||||
contentWarningDescriptionTextView.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
|
||||||
contentWarningButton.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
|
||||||
if (statusViewData.isExpanded()) {
|
|
||||||
contentWarningButton.setText(R.string.post_content_warning_show_less);
|
|
||||||
} else {
|
|
||||||
contentWarningButton.setText(R.string.post_content_warning_show_more);
|
|
||||||
}
|
|
||||||
|
|
||||||
contentWarningButton.setOnClickListener(view -> {
|
|
||||||
if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) {
|
|
||||||
notificationActionListener.onExpandedChange(!statusViewData.isExpanded(), getBindingAdapterPosition());
|
|
||||||
}
|
|
||||||
statusContent.setVisibility(statusViewData.isExpanded() ? View.GONE : View.VISIBLE);
|
|
||||||
});
|
|
||||||
|
|
||||||
setupContentAndSpoiler(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupButtons(final NotificationActionListener listener, final String accountId,
|
|
||||||
final String notificationId) {
|
|
||||||
this.notificationActionListener = listener;
|
|
||||||
this.accountId = accountId;
|
|
||||||
this.notificationId = notificationId;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setAvatar(@Nullable String statusAvatarUrl, boolean isBot) {
|
|
||||||
statusAvatar.setPaddingRelative(0, 0, 0, 0);
|
|
||||||
|
|
||||||
ImageLoadingHelper.loadAvatar(statusAvatarUrl,
|
|
||||||
statusAvatar, avatarRadius48dp, statusDisplayOptions.animateAvatars(), null);
|
|
||||||
|
|
||||||
if (statusDisplayOptions.showBotOverlay() && isBot) {
|
|
||||||
notificationAvatar.setVisibility(View.VISIBLE);
|
|
||||||
Glide.with(notificationAvatar)
|
|
||||||
.load(R.drawable.bot_badge)
|
|
||||||
.into(notificationAvatar);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
notificationAvatar.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setAvatars(@Nullable String statusAvatarUrl, @Nullable String notificationAvatarUrl) {
|
|
||||||
int padding = Utils.dpToPx(statusAvatar.getContext(), 12);
|
|
||||||
statusAvatar.setPaddingRelative(0, 0, padding, padding);
|
|
||||||
|
|
||||||
ImageLoadingHelper.loadAvatar(statusAvatarUrl,
|
|
||||||
statusAvatar, avatarRadius36dp, statusDisplayOptions.animateAvatars(), null);
|
|
||||||
|
|
||||||
notificationAvatar.setVisibility(View.VISIBLE);
|
|
||||||
ImageLoadingHelper.loadAvatar(notificationAvatarUrl, notificationAvatar,
|
|
||||||
avatarRadius24dp, statusDisplayOptions.animateAvatars(), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
if (notificationActionListener == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (v == container || v == statusContent) {
|
|
||||||
notificationActionListener.onViewStatusForNotificationId(notificationId);
|
|
||||||
}
|
|
||||||
else if (v == message) {
|
|
||||||
notificationActionListener.onViewAccount(accountId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupContentAndSpoiler(final LinkListener listener) {
|
|
||||||
|
|
||||||
boolean shouldShowContentIfSpoiler = statusViewData.isExpanded();
|
|
||||||
boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getStatus(). getSpoilerText());
|
|
||||||
if (!shouldShowContentIfSpoiler && hasSpoiler) {
|
|
||||||
statusContent.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
statusContent.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
Spanned content = statusViewData.getContent();
|
|
||||||
List<Emoji> emojis = statusViewData.getActionable().getEmojis();
|
|
||||||
|
|
||||||
if (statusViewData.isCollapsible() && (statusViewData.isExpanded() || !hasSpoiler)) {
|
|
||||||
contentCollapseButton.setOnClickListener(view -> {
|
|
||||||
int position = getBindingAdapterPosition();
|
|
||||||
if (position != RecyclerView.NO_POSITION && notificationActionListener != null) {
|
|
||||||
notificationActionListener.onNotificationContentCollapsedChange(!statusViewData.isCollapsed(), position);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
contentCollapseButton.setVisibility(View.VISIBLE);
|
|
||||||
if (statusViewData.isCollapsed()) {
|
|
||||||
contentCollapseButton.setText(R.string.post_content_warning_show_more);
|
|
||||||
statusContent.setFilters(COLLAPSE_INPUT_FILTER);
|
|
||||||
} else {
|
|
||||||
contentCollapseButton.setText(R.string.post_content_warning_show_less);
|
|
||||||
statusContent.setFilters(NO_INPUT_FILTER);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
contentCollapseButton.setVisibility(View.GONE);
|
|
||||||
statusContent.setFilters(NO_INPUT_FILTER);
|
|
||||||
}
|
|
||||||
|
|
||||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
|
||||||
content, emojis, statusContent, statusDisplayOptions.animateEmojis()
|
|
||||||
);
|
|
||||||
LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getActionable().getMentions(), statusViewData.getActionable().getTags(), listener);
|
|
||||||
|
|
||||||
CharSequence emojifiedContentWarning = CustomEmojiHelper.emojify(
|
|
||||||
statusViewData.getStatus().getSpoilerText(),
|
|
||||||
statusViewData.getActionable().getEmojis(),
|
|
||||||
contentWarningDescriptionTextView,
|
|
||||||
statusDisplayOptions.animateEmojis()
|
|
||||||
);
|
|
||||||
contentWarningDescriptionTextView.setText(emojifiedContentWarning);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewTag(@NonNull String tag) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewAccount(@NonNull String id) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewUrl(@NonNull String url) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,54 +14,34 @@
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
package com.keylesspalace.tusky.adapter
|
package com.keylesspalace.tusky.adapter
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.keylesspalace.tusky.databinding.ItemStatusPlaceholderBinding
|
||||||
import com.google.android.material.progressindicator.CircularProgressIndicatorSpec
|
|
||||||
import com.google.android.material.progressindicator.IndeterminateDrawable
|
|
||||||
import com.keylesspalace.tusky.R
|
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||||
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.util.visible
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Placeholder for different timelines.
|
* Placeholder for missing parts in timelines.
|
||||||
*
|
*
|
||||||
* Displays a "Load more" button for a particular status ID, or a
|
* Displays a "Load more" button to load the gap, or a
|
||||||
* circular progress wheel if the status' page is being loaded.
|
* circular progress bar if the missing page is being loaded.
|
||||||
*
|
|
||||||
* The user can only have one "Load more" operation in progress at
|
|
||||||
* a time (determined by the adapter), so the contents of the view
|
|
||||||
* and the enabled state is driven by that.
|
|
||||||
*/
|
*/
|
||||||
class PlaceholderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
class PlaceholderViewHolder(
|
||||||
private val loadMoreButton: MaterialButton = itemView.findViewById(R.id.button_load_more)
|
private val binding: ItemStatusPlaceholderBinding,
|
||||||
private val drawable = IndeterminateDrawable.createCircularDrawable(
|
listener: StatusActionListener
|
||||||
itemView.context,
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
CircularProgressIndicatorSpec(itemView.context, null)
|
|
||||||
)
|
|
||||||
|
|
||||||
fun setup(listener: StatusActionListener, loading: Boolean) {
|
init {
|
||||||
itemView.isEnabled = !loading
|
binding.loadMoreButton.setOnClickListener {
|
||||||
loadMoreButton.isEnabled = !loading
|
binding.loadMoreButton.hide()
|
||||||
|
binding.loadMoreProgressBar.show()
|
||||||
if (loading) {
|
|
||||||
loadMoreButton.text = ""
|
|
||||||
loadMoreButton.icon = drawable
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMoreButton.text = itemView.context.getString(R.string.load_more_placeholder_text)
|
|
||||||
loadMoreButton.icon = null
|
|
||||||
|
|
||||||
// To allow the user to click anywhere in the layout to load more content set the click
|
|
||||||
// listener on the parent layout instead of loadMoreButton.
|
|
||||||
//
|
|
||||||
// See the comments in item_status_placeholder.xml for more details.
|
|
||||||
itemView.setOnClickListener {
|
|
||||||
itemView.isEnabled = false
|
|
||||||
loadMoreButton.isEnabled = false
|
|
||||||
loadMoreButton.icon = drawable
|
|
||||||
loadMoreButton.text = ""
|
|
||||||
listener.onLoadMore(bindingAdapterPosition)
|
listener.onLoadMore(bindingAdapterPosition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setup(loading: Boolean) {
|
||||||
|
binding.loadMoreButton.visible(!loading)
|
||||||
|
binding.loadMoreProgressBar.visible(loading)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,10 @@ class PollAdapter : RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
||||||
.map { pollOptions.indexOf(it) }
|
.map { pollOptions.indexOf(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemPollBinding> {
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): BindingHolder<ItemPollBinding> {
|
||||||
val binding = ItemPollBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding = ItemPollBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return BindingHolder(binding)
|
return BindingHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,11 @@ class PreviewPollOptionsAdapter : RecyclerView.Adapter<PreviewViewHolder>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PreviewViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PreviewViewHolder {
|
||||||
return PreviewViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_poll_preview_option, parent, false))
|
return PreviewViewHolder(
|
||||||
|
LayoutInflater.from(
|
||||||
|
parent.context
|
||||||
|
).inflate(R.layout.item_poll_preview_option, parent, false)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = options.size
|
override fun getItemCount() = options.size
|
||||||
|
|
|
@ -9,6 +9,7 @@ import android.graphics.drawable.Drawable;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.format.DateUtils;
|
import android.text.format.DateUtils;
|
||||||
|
import android.view.Gravity;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
@ -17,14 +18,14 @@ import android.widget.ImageButton;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes;
|
import androidx.annotation.DrawableRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
import androidx.appcompat.widget.PopupMenu;
|
import androidx.appcompat.widget.PopupMenu;
|
||||||
|
import androidx.appcompat.widget.TooltipCompat;
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.core.text.HtmlCompat;
|
import androidx.core.text.HtmlCompat;
|
||||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
@ -48,6 +49,7 @@ import com.keylesspalace.tusky.entity.Filter;
|
||||||
import com.keylesspalace.tusky.entity.FilterResult;
|
import com.keylesspalace.tusky.entity.FilterResult;
|
||||||
import com.keylesspalace.tusky.entity.HashTag;
|
import com.keylesspalace.tusky.entity.HashTag;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
import com.keylesspalace.tusky.entity.Translation;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.util.AbsoluteTimeFormatter;
|
import com.keylesspalace.tusky.util.AbsoluteTimeFormatter;
|
||||||
import com.keylesspalace.tusky.util.AttachmentHelper;
|
import com.keylesspalace.tusky.util.AttachmentHelper;
|
||||||
|
@ -56,6 +58,7 @@ import com.keylesspalace.tusky.util.CompositeWithOpaqueBackground;
|
||||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||||
import com.keylesspalace.tusky.util.LinkHelper;
|
import com.keylesspalace.tusky.util.LinkHelper;
|
||||||
|
import com.keylesspalace.tusky.util.LocaleUtilsKt;
|
||||||
import com.keylesspalace.tusky.util.NumberUtils;
|
import com.keylesspalace.tusky.util.NumberUtils;
|
||||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||||
import com.keylesspalace.tusky.util.TimestampUtils;
|
import com.keylesspalace.tusky.util.TimestampUtils;
|
||||||
|
@ -66,6 +69,7 @@ import com.keylesspalace.tusky.viewdata.PollOptionViewData;
|
||||||
import com.keylesspalace.tusky.viewdata.PollViewData;
|
import com.keylesspalace.tusky.viewdata.PollViewData;
|
||||||
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
|
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
import com.keylesspalace.tusky.viewdata.TranslationViewData;
|
||||||
|
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -120,6 +124,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
protected final TextView filteredPlaceholderLabel;
|
protected final TextView filteredPlaceholderLabel;
|
||||||
protected final Button filteredPlaceholderShowButton;
|
protected final Button filteredPlaceholderShowButton;
|
||||||
protected final ConstraintLayout statusContainer;
|
protected final ConstraintLayout statusContainer;
|
||||||
|
private final TextView translationStatusView;
|
||||||
|
private final Button untranslateButton;
|
||||||
|
|
||||||
|
|
||||||
private final NumberFormat numberFormat = NumberFormat.getNumberInstance();
|
private final NumberFormat numberFormat = NumberFormat.getNumberInstance();
|
||||||
private final AbsoluteTimeFormatter absoluteTimeFormatter = new AbsoluteTimeFormatter();
|
private final AbsoluteTimeFormatter absoluteTimeFormatter = new AbsoluteTimeFormatter();
|
||||||
|
@ -151,10 +158,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning);
|
sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning);
|
||||||
sensitiveMediaShow = itemView.findViewById(R.id.status_sensitive_media_button);
|
sensitiveMediaShow = itemView.findViewById(R.id.status_sensitive_media_button);
|
||||||
mediaLabels = new TextView[]{
|
mediaLabels = new TextView[]{
|
||||||
itemView.findViewById(R.id.status_media_label_0),
|
itemView.findViewById(R.id.status_media_label_0),
|
||||||
itemView.findViewById(R.id.status_media_label_1),
|
itemView.findViewById(R.id.status_media_label_1),
|
||||||
itemView.findViewById(R.id.status_media_label_2),
|
itemView.findViewById(R.id.status_media_label_2),
|
||||||
itemView.findViewById(R.id.status_media_label_3)
|
itemView.findViewById(R.id.status_media_label_3)
|
||||||
};
|
};
|
||||||
mediaDescriptions = new CharSequence[mediaLabels.length];
|
mediaDescriptions = new CharSequence[mediaLabels.length];
|
||||||
contentWarningDescription = itemView.findViewById(R.id.status_content_warning_description);
|
contentWarningDescription = itemView.findViewById(R.id.status_content_warning_description);
|
||||||
|
@ -182,6 +189,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
pollOptions.setLayoutManager(new LinearLayoutManager(pollOptions.getContext()));
|
pollOptions.setLayoutManager(new LinearLayoutManager(pollOptions.getContext()));
|
||||||
((DefaultItemAnimator) pollOptions.getItemAnimator()).setSupportsChangeAnimations(false);
|
((DefaultItemAnimator) pollOptions.getItemAnimator()).setSupportsChangeAnimations(false);
|
||||||
|
|
||||||
|
translationStatusView = itemView.findViewById(R.id.status_translation_status);
|
||||||
|
untranslateButton = itemView.findViewById(R.id.status_button_untranslate);
|
||||||
|
|
||||||
this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp);
|
this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp);
|
||||||
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
|
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
|
||||||
this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp);
|
this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp);
|
||||||
|
@ -191,9 +201,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
TouchDelegateHelper.expandTouchSizeToFillRow((ViewGroup) itemView, CollectionsKt.listOfNotNull(replyButton, reblogButton, favouriteButton, bookmarkButton, moreButton));
|
TouchDelegateHelper.expandTouchSizeToFillRow((ViewGroup) itemView, CollectionsKt.listOfNotNull(replyButton, reblogButton, favouriteButton, bookmarkButton, moreButton));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setDisplayName(@NonNull String name, @Nullable List<Emoji> customEmojis, @NonNull StatusDisplayOptions statusDisplayOptions) {
|
protected void setDisplayName(@NonNull String name, @NonNull List<Emoji> customEmojis, @NonNull StatusDisplayOptions statusDisplayOptions) {
|
||||||
CharSequence emojifiedName = CustomEmojiHelper.emojify(
|
CharSequence emojifiedName = CustomEmojiHelper.emojify(
|
||||||
name, customEmojis, displayName, statusDisplayOptions.animateEmojis()
|
name, customEmojis, displayName, statusDisplayOptions.animateEmojis()
|
||||||
);
|
);
|
||||||
displayName.setText(emojifiedName);
|
displayName.setText(emojifiedName);
|
||||||
}
|
}
|
||||||
|
@ -213,7 +223,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
final @NonNull StatusActionListener listener) {
|
final @NonNull StatusActionListener listener) {
|
||||||
|
|
||||||
Status actionable = status.getActionable();
|
Status actionable = status.getActionable();
|
||||||
String spoilerText = actionable.getSpoilerText();
|
String spoilerText = status.getSpoilerText();
|
||||||
List<Emoji> emojis = actionable.getEmojis();
|
List<Emoji> emojis = actionable.getEmojis();
|
||||||
|
|
||||||
boolean sensitive = !TextUtils.isEmpty(spoilerText);
|
boolean sensitive = !TextUtils.isEmpty(spoilerText);
|
||||||
|
@ -221,7 +231,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
if (sensitive) {
|
if (sensitive) {
|
||||||
CharSequence emojiSpoiler = CustomEmojiHelper.emojify(
|
CharSequence emojiSpoiler = CustomEmojiHelper.emojify(
|
||||||
spoilerText, emojis, contentWarningDescription, statusDisplayOptions.animateEmojis()
|
spoilerText, emojis, contentWarningDescription, statusDisplayOptions.animateEmojis()
|
||||||
);
|
);
|
||||||
contentWarningDescription.setText(emojiSpoiler);
|
contentWarningDescription.setText(emojiSpoiler);
|
||||||
contentWarningDescription.setVisibility(View.VISIBLE);
|
contentWarningDescription.setVisibility(View.VISIBLE);
|
||||||
|
@ -271,9 +281,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
Status actionable = status.getActionable();
|
Status actionable = status.getActionable();
|
||||||
Spanned content = status.getContent();
|
Spanned content = status.getContent();
|
||||||
List<Status.Mention> mentions = actionable.getMentions();
|
List<Status.Mention> mentions = actionable.getMentions();
|
||||||
List<HashTag> tags =actionable.getTags();
|
List<HashTag> tags = actionable.getTags();
|
||||||
List<Emoji> emojis = actionable.getEmojis();
|
List<Emoji> emojis = actionable.getEmojis();
|
||||||
PollViewData poll = PollViewDataKt.toViewData(actionable.getPoll());
|
PollViewData poll = PollViewDataKt.toViewData(status.getPoll());
|
||||||
|
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content, statusDisplayOptions.animateEmojis());
|
CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content, statusDisplayOptions.animateEmojis());
|
||||||
|
@ -304,7 +314,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setAvatar(String url,
|
private void setAvatar(String url,
|
||||||
@Nullable String rebloggedUrl,
|
@Nullable String rebloggedUrl,
|
||||||
boolean isBot,
|
boolean isBot,
|
||||||
StatusDisplayOptions statusDisplayOptions) {
|
StatusDisplayOptions statusDisplayOptions) {
|
||||||
|
|
||||||
|
@ -315,8 +325,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
if (statusDisplayOptions.showBotOverlay() && isBot) {
|
if (statusDisplayOptions.showBotOverlay() && isBot) {
|
||||||
avatarInset.setVisibility(View.VISIBLE);
|
avatarInset.setVisibility(View.VISIBLE);
|
||||||
Glide.with(avatarInset)
|
Glide.with(avatarInset)
|
||||||
.load(R.drawable.bot_badge)
|
.load(R.drawable.bot_badge)
|
||||||
.into(avatarInset);
|
.into(avatarInset);
|
||||||
} else {
|
} else {
|
||||||
avatarInset.setVisibility(View.GONE);
|
avatarInset.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
@ -330,7 +340,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
avatarInset.setVisibility(View.VISIBLE);
|
avatarInset.setVisibility(View.VISIBLE);
|
||||||
avatarInset.setBackground(null);
|
avatarInset.setBackground(null);
|
||||||
ImageLoadingHelper.loadAvatar(rebloggedUrl, avatarInset, avatarRadius24dp,
|
ImageLoadingHelper.loadAvatar(rebloggedUrl, avatarInset, avatarRadius24dp,
|
||||||
statusDisplayOptions.animateAvatars(), null);
|
statusDisplayOptions.animateAvatars(), null);
|
||||||
|
|
||||||
avatarRadius = avatarRadius36dp;
|
avatarRadius = avatarRadius36dp;
|
||||||
}
|
}
|
||||||
|
@ -383,8 +393,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
long then = createdAt.getTime();
|
long then = createdAt.getTime();
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
return DateUtils.getRelativeTimeSpanString(then, now,
|
return DateUtils.getRelativeTimeSpanString(then, now,
|
||||||
DateUtils.SECOND_IN_MILLIS,
|
DateUtils.SECOND_IN_MILLIS,
|
||||||
DateUtils.FORMAT_ABBREV_RELATIVE);
|
DateUtils.FORMAT_ABBREV_RELATIVE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -467,9 +477,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
imageView.removeFocalPoint();
|
imageView.removeFocalPoint();
|
||||||
|
|
||||||
Glide.with(imageView)
|
Glide.with(imageView)
|
||||||
.load(placeholder)
|
.load(placeholder)
|
||||||
.centerInside()
|
.centerInside()
|
||||||
.into(imageView);
|
.into(imageView);
|
||||||
} else {
|
} else {
|
||||||
Focus focus = meta != null ? meta.getFocus() : null;
|
Focus focus = meta != null ? meta.getFocus() : null;
|
||||||
|
|
||||||
|
@ -477,29 +487,29 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
imageView.setFocalPoint(focus);
|
imageView.setFocalPoint(focus);
|
||||||
|
|
||||||
Glide.with(imageView.getContext())
|
Glide.with(imageView.getContext())
|
||||||
.load(previewUrl)
|
.load(previewUrl)
|
||||||
.placeholder(placeholder)
|
.placeholder(placeholder)
|
||||||
.centerInside()
|
.centerInside()
|
||||||
.addListener(imageView)
|
.addListener(imageView)
|
||||||
.into(imageView);
|
.into(imageView);
|
||||||
} else {
|
} else {
|
||||||
imageView.removeFocalPoint();
|
imageView.removeFocalPoint();
|
||||||
|
|
||||||
Glide.with(imageView)
|
Glide.with(imageView)
|
||||||
.load(previewUrl)
|
.load(previewUrl)
|
||||||
.placeholder(placeholder)
|
.placeholder(placeholder)
|
||||||
.centerInside()
|
.centerInside()
|
||||||
.into(imageView);
|
.into(imageView);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setMediaPreviews(
|
protected void setMediaPreviews(
|
||||||
final @NonNull List<Attachment> attachments,
|
final @NonNull List<Attachment> attachments,
|
||||||
boolean sensitive,
|
boolean sensitive,
|
||||||
final @NonNull StatusActionListener listener,
|
final @NonNull StatusActionListener listener,
|
||||||
boolean showingContent,
|
boolean showingContent,
|
||||||
boolean useBlurhash
|
boolean useBlurhash
|
||||||
) {
|
) {
|
||||||
|
|
||||||
mediaPreview.setVisibility(View.VISIBLE);
|
mediaPreview.setVisibility(View.VISIBLE);
|
||||||
|
@ -518,20 +528,22 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadImage(
|
loadImage(
|
||||||
imageView,
|
imageView,
|
||||||
showingContent ? previewUrl : null,
|
showingContent ? previewUrl : null,
|
||||||
attachment.getMeta(),
|
attachment.getMeta(),
|
||||||
useBlurhash ? attachment.getBlurhash() : null
|
useBlurhash ? attachment.getBlurhash() : null
|
||||||
);
|
);
|
||||||
|
|
||||||
final Attachment.Type type = attachment.getType();
|
final Attachment.Type type = attachment.getType();
|
||||||
if (showingContent && (type == Attachment.Type.VIDEO || type == Attachment.Type.GIFV)) {
|
if (showingContent && (type == Attachment.Type.VIDEO || type == Attachment.Type.GIFV)) {
|
||||||
imageView.setForeground(ContextCompat.getDrawable(itemView.getContext(), R.drawable.play_indicator_overlay));
|
imageView.setForegroundGravity(Gravity.CENTER);
|
||||||
|
imageView.setForeground(AppCompatResources.getDrawable(itemView.getContext(), R.drawable.ic_play_indicator));
|
||||||
} else {
|
} else {
|
||||||
imageView.setForeground(null);
|
imageView.setForeground(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
setAttachmentClickListener(imageView, listener, i, attachment, true);
|
final CharSequence formattedDescription = AttachmentHelper.getFormattedDescription(attachment, imageView.getContext());
|
||||||
|
setAttachmentClickListener(imageView, listener, i, formattedDescription, true);
|
||||||
|
|
||||||
if (sensitive) {
|
if (sensitive) {
|
||||||
sensitiveMediaWarning.setText(R.string.post_sensitive_media_title);
|
sensitiveMediaWarning.setText(R.string.post_sensitive_media_title);
|
||||||
|
@ -583,8 +595,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
private void updateMediaLabel(int index, boolean sensitive, boolean showingContent) {
|
private void updateMediaLabel(int index, boolean sensitive, boolean showingContent) {
|
||||||
Context context = itemView.getContext();
|
Context context = itemView.getContext();
|
||||||
CharSequence label = (sensitive && !showingContent) ?
|
CharSequence label = (sensitive && !showingContent) ?
|
||||||
context.getString(R.string.post_sensitive_media_title) :
|
context.getString(R.string.post_sensitive_media_title) :
|
||||||
mediaDescriptions[index];
|
mediaDescriptions[index];
|
||||||
mediaLabels[index].setText(label);
|
mediaLabels[index].setText(label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -601,17 +613,17 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
// Set the icon next to the label.
|
// Set the icon next to the label.
|
||||||
int drawableId = getLabelIcon(attachments.get(0).getType());
|
int drawableId = getLabelIcon(attachments.get(0).getType());
|
||||||
mediaLabel.setCompoundDrawablesWithIntrinsicBounds(drawableId, 0, 0, 0);
|
mediaLabel.setCompoundDrawablesRelativeWithIntrinsicBounds(drawableId, 0, 0, 0);
|
||||||
|
|
||||||
setAttachmentClickListener(mediaLabel, listener, i, attachment, false);
|
setAttachmentClickListener(mediaLabel, listener, i, mediaDescriptions[i], false);
|
||||||
} else {
|
} else {
|
||||||
mediaLabel.setVisibility(View.GONE);
|
mediaLabel.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setAttachmentClickListener(View view, @NonNull StatusActionListener listener,
|
private void setAttachmentClickListener(@NonNull View view, @NonNull StatusActionListener listener,
|
||||||
int index, Attachment attachment, boolean animateTransition) {
|
int index, CharSequence description, boolean animateTransition) {
|
||||||
view.setOnClickListener(v -> {
|
view.setOnClickListener(v -> {
|
||||||
int position = getBindingAdapterPosition();
|
int position = getBindingAdapterPosition();
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
|
@ -622,11 +634,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
view.setOnLongClickListener(v -> {
|
TooltipCompat.setTooltipText(view, description);
|
||||||
CharSequence description = AttachmentHelper.getFormattedDescription(attachment, view.getContext());
|
|
||||||
Toast.makeText(view.getContext(), description, Toast.LENGTH_LONG).show();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void hideSensitiveMediaWarning() {
|
protected void hideSensitiveMediaWarning() {
|
||||||
|
@ -727,7 +735,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
popup.setOnMenuItemClickListener(item -> {
|
popup.setOnMenuItemClickListener(item -> {
|
||||||
listener.onReblog(!buttonState, position);
|
listener.onReblog(!buttonState, position);
|
||||||
if(!buttonState) {
|
if (!buttonState) {
|
||||||
reblogButton.playAnimation();
|
reblogButton.playAnimation();
|
||||||
reblogButton.setChecked(true);
|
reblogButton.setChecked(true);
|
||||||
}
|
}
|
||||||
|
@ -749,7 +757,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
popup.setOnMenuItemClickListener(item -> {
|
popup.setOnMenuItemClickListener(item -> {
|
||||||
listener.onFavourite(!buttonState, position);
|
listener.onFavourite(!buttonState, position);
|
||||||
if(!buttonState) {
|
if (!buttonState) {
|
||||||
favouriteButton.playAnimation();
|
favouriteButton.playAnimation();
|
||||||
favouriteButton.setChecked(true);
|
favouriteButton.setChecked(true);
|
||||||
}
|
}
|
||||||
|
@ -775,16 +783,16 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
setIsReply(actionable.getInReplyToId() != null);
|
setIsReply(actionable.getInReplyToId() != null);
|
||||||
setReplyCount(actionable.getRepliesCount(), statusDisplayOptions.showStatsInline());
|
setReplyCount(actionable.getRepliesCount(), statusDisplayOptions.showStatsInline());
|
||||||
setAvatar(actionable.getAccount().getAvatar(), status.getRebloggedAvatar(),
|
setAvatar(actionable.getAccount().getAvatar(), status.getRebloggedAvatar(),
|
||||||
actionable.getAccount().getBot(), statusDisplayOptions);
|
actionable.getAccount().getBot(), statusDisplayOptions);
|
||||||
setReblogged(actionable.getReblogged());
|
setReblogged(actionable.getReblogged());
|
||||||
setFavourited(actionable.getFavourited());
|
setFavourited(actionable.getFavourited());
|
||||||
setBookmarked(actionable.getBookmarked());
|
setBookmarked(actionable.getBookmarked());
|
||||||
List<Attachment> attachments = actionable.getAttachments();
|
List<Attachment> attachments = status.getAttachments();
|
||||||
boolean sensitive = actionable.getSensitive();
|
boolean sensitive = actionable.getSensitive();
|
||||||
if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) {
|
if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) {
|
||||||
setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash());
|
setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash());
|
||||||
|
|
||||||
if (attachments.size() == 0) {
|
if (attachments.isEmpty()) {
|
||||||
hideSensitiveMediaWarning();
|
hideSensitiveMediaWarning();
|
||||||
}
|
}
|
||||||
// Hide the unused label.
|
// Hide the unused label.
|
||||||
|
@ -801,8 +809,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
setupCard(status, status.isExpanded(), statusDisplayOptions.cardViewMode(), statusDisplayOptions, listener);
|
setupCard(status, status.isExpanded(), statusDisplayOptions.cardViewMode(), statusDisplayOptions, listener);
|
||||||
|
|
||||||
setupButtons(listener, actionable.getAccount().getId(), status.getContent().toString(),
|
setupButtons(listener, actionable.getAccount().getId(), status.getContent().toString(),
|
||||||
statusDisplayOptions);
|
statusDisplayOptions);
|
||||||
setRebloggingEnabled(actionable.rebloggingAllowed(), actionable.getVisibility());
|
|
||||||
|
setTranslationStatus(status, listener);
|
||||||
|
|
||||||
|
setRebloggingEnabled(actionable.isRebloggingAllowed(), actionable.getVisibility());
|
||||||
|
|
||||||
setSpoilerAndContent(status, statusDisplayOptions, listener);
|
setSpoilerAndContent(status, statusDisplayOptions, listener);
|
||||||
|
|
||||||
|
@ -827,6 +838,29 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setTranslationStatus(StatusViewData.Concrete status, StatusActionListener listener) {
|
||||||
|
var translationViewData = status.getTranslation();
|
||||||
|
if (translationViewData != null) {
|
||||||
|
if (translationViewData instanceof TranslationViewData.Loaded) {
|
||||||
|
Translation translation = ((TranslationViewData.Loaded) translationViewData).getData();
|
||||||
|
translationStatusView.setVisibility(View.VISIBLE);
|
||||||
|
var langName = LocaleUtilsKt.localeNameForUntrustedISO639LangCode(translation.getDetectedSourceLanguage());
|
||||||
|
translationStatusView.setText(translationStatusView.getContext().getString(R.string.label_translated, langName, translation.getProvider()));
|
||||||
|
untranslateButton.setVisibility(View.VISIBLE);
|
||||||
|
untranslateButton.setOnClickListener((v) -> listener.onUntranslate(getBindingAdapterPosition()));
|
||||||
|
} else {
|
||||||
|
translationStatusView.setVisibility(View.VISIBLE);
|
||||||
|
translationStatusView.setText(R.string.label_translating);
|
||||||
|
untranslateButton.setVisibility(View.GONE);
|
||||||
|
untranslateButton.setOnClickListener(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
translationStatusView.setVisibility(View.GONE);
|
||||||
|
untranslateButton.setVisibility(View.GONE);
|
||||||
|
untranslateButton.setOnClickListener(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void setupFilterPlaceholder(StatusViewData.Concrete status, StatusActionListener listener, StatusDisplayOptions displayOptions) {
|
private void setupFilterPlaceholder(StatusViewData.Concrete status, StatusActionListener listener, StatusDisplayOptions displayOptions) {
|
||||||
if (status.getFilterAction() != Filter.Action.WARN) {
|
if (status.getFilterAction() != Filter.Action.WARN) {
|
||||||
showFilteredPlaceholder(false);
|
showFilteredPlaceholder(false);
|
||||||
|
@ -845,7 +879,14 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredPlaceholderLabel.setText(itemView.getContext().getString(R.string.status_filter_placeholder_label_format, matchedFilter.getTitle()));
|
final String matchedFilterTitle;
|
||||||
|
if (matchedFilter == null) {
|
||||||
|
matchedFilterTitle = "";
|
||||||
|
} else {
|
||||||
|
matchedFilterTitle = matchedFilter.getTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredPlaceholderLabel.setText(itemView.getContext().getString(R.string.status_filter_placeholder_label_format, matchedFilterTitle));
|
||||||
filteredPlaceholderShowButton.setOnClickListener(view -> listener.clearWarningAction(getBindingAdapterPosition()));
|
filteredPlaceholderShowButton.setOnClickListener(view -> listener.clearWarningAction(getBindingAdapterPosition()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -864,61 +905,91 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
Status actionable = status.getActionable();
|
Status actionable = status.getActionable();
|
||||||
|
|
||||||
String description = context.getString(R.string.description_status,
|
String description = context.getString(R.string.description_status,
|
||||||
actionable.getAccount().getDisplayName(),
|
// 1 display_name
|
||||||
getContentWarningDescription(context, status),
|
actionable.getAccount().getDisplayName(),
|
||||||
(TextUtils.isEmpty(actionable.getSpoilerText()) || !actionable.getSensitive() || status.isExpanded() ? status.getContent() : ""),
|
// 2 CW?
|
||||||
getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions),
|
getContentWarningDescription(context, status),
|
||||||
actionable.getEditedAt() != null ? context.getString(R.string.description_post_edited) : "",
|
// 3 content?
|
||||||
getReblogDescription(context, status),
|
(TextUtils.isEmpty(status.getSpoilerText()) || !actionable.getSensitive() || status.isExpanded() ? status.getContent() : ""),
|
||||||
actionable.getAccount().getUsername(),
|
// 4 date
|
||||||
actionable.getReblogged() ? context.getString(R.string.description_post_reblogged) : "",
|
getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions),
|
||||||
actionable.getFavourited() ? context.getString(R.string.description_post_favourited) : "",
|
// 5 edited?
|
||||||
actionable.getBookmarked() ? context.getString(R.string.description_post_bookmarked) : "",
|
actionable.getEditedAt() != null ? context.getString(R.string.description_post_edited) : "",
|
||||||
getMediaDescription(context, status),
|
// 6 reposted_by?
|
||||||
getVisibilityDescription(context, actionable.getVisibility()),
|
getReblogDescription(context, status),
|
||||||
getFavsText(context, actionable.getFavouritesCount()),
|
// 7 username
|
||||||
getReblogsText(context, actionable.getReblogsCount()),
|
actionable.getAccount().getUsername(),
|
||||||
getPollDescription(status, context, statusDisplayOptions)
|
// 8 reposted
|
||||||
|
actionable.getReblogged() ? context.getString(R.string.description_post_reblogged) : "",
|
||||||
|
// 9 favorited
|
||||||
|
actionable.getFavourited() ? context.getString(R.string.description_post_favourited) : "",
|
||||||
|
// 10 bookmarked
|
||||||
|
actionable.getBookmarked() ? context.getString(R.string.description_post_bookmarked) : "",
|
||||||
|
// 11 media
|
||||||
|
getMediaDescription(context, status),
|
||||||
|
// 12 visibility
|
||||||
|
getVisibilityDescription(context, actionable.getVisibility()),
|
||||||
|
// 13 fav_number
|
||||||
|
getFavsText(context, actionable.getFavouritesCount()),
|
||||||
|
// 14 reblog_number
|
||||||
|
getReblogsText(context, actionable.getReblogsCount()),
|
||||||
|
// 15 poll?
|
||||||
|
getPollDescription(status, context, statusDisplayOptions),
|
||||||
|
// 16 translated?
|
||||||
|
getTranslatedDescription(context, status.getTranslation())
|
||||||
);
|
);
|
||||||
itemView.setContentDescription(description);
|
itemView.setContentDescription(description);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getTranslatedDescription(Context context, TranslationViewData translationViewData) {
|
||||||
|
if (translationViewData == null) {
|
||||||
|
return "";
|
||||||
|
} else if (translationViewData instanceof TranslationViewData.Loading) {
|
||||||
|
return context.getString(R.string.label_translating);
|
||||||
|
} else {
|
||||||
|
Translation translation = ((TranslationViewData.Loaded) translationViewData).getData();
|
||||||
|
var langName = LocaleUtilsKt.localeNameForUntrustedISO639LangCode(translation.getDetectedSourceLanguage());
|
||||||
|
return context.getString(R.string.label_translated, langName, translation.getProvider());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static CharSequence getReblogDescription(Context context,
|
private static CharSequence getReblogDescription(Context context,
|
||||||
@NonNull StatusViewData.Concrete status) {
|
@NonNull StatusViewData.Concrete status) {
|
||||||
|
@Nullable
|
||||||
Status reblog = status.getRebloggingStatus();
|
Status reblog = status.getRebloggingStatus();
|
||||||
if (reblog != null) {
|
if (reblog != null) {
|
||||||
return context
|
return context
|
||||||
.getString(R.string.post_boosted_format, reblog.getAccount().getUsername());
|
.getString(R.string.post_boosted_format, reblog.getAccount().getUsername());
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CharSequence getMediaDescription(Context context,
|
private static CharSequence getMediaDescription(Context context,
|
||||||
@NonNull StatusViewData.Concrete status) {
|
@NonNull StatusViewData.Concrete viewData) {
|
||||||
if (status.getActionable().getAttachments().isEmpty()) {
|
if (viewData.getAttachments().isEmpty()) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
StringBuilder mediaDescriptions = CollectionsKt.fold(
|
StringBuilder mediaDescriptions = CollectionsKt.fold(
|
||||||
status.getActionable().getAttachments(),
|
viewData.getAttachments(),
|
||||||
new StringBuilder(),
|
new StringBuilder(),
|
||||||
(builder, a) -> {
|
(builder, a) -> {
|
||||||
if (a.getDescription() == null) {
|
if (a.getDescription() == null) {
|
||||||
String placeholder =
|
String placeholder =
|
||||||
context.getString(R.string.description_post_media_no_description_placeholder);
|
context.getString(R.string.description_post_media_no_description_placeholder);
|
||||||
return builder.append(placeholder);
|
return builder.append(placeholder);
|
||||||
} else {
|
} else {
|
||||||
builder.append("; ");
|
builder.append("; ");
|
||||||
return builder.append(a.getDescription());
|
return builder.append(a.getDescription());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return context.getString(R.string.description_post_media, mediaDescriptions);
|
return context.getString(R.string.description_post_media, mediaDescriptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CharSequence getContentWarningDescription(Context context,
|
private static CharSequence getContentWarningDescription(Context context,
|
||||||
@NonNull StatusViewData.Concrete status) {
|
@NonNull StatusViewData.Concrete status) {
|
||||||
if (!TextUtils.isEmpty(status.getActionable().getSpoilerText())) {
|
if (!TextUtils.isEmpty(status.getSpoilerText())) {
|
||||||
return context.getString(R.string.description_post_cw, status.getActionable().getSpoilerText());
|
return context.getString(R.string.description_post_cw, status.getSpoilerText());
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -954,7 +1025,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
private CharSequence getPollDescription(@NonNull StatusViewData.Concrete status,
|
private CharSequence getPollDescription(@NonNull StatusViewData.Concrete status,
|
||||||
Context context,
|
Context context,
|
||||||
StatusDisplayOptions statusDisplayOptions) {
|
StatusDisplayOptions statusDisplayOptions) {
|
||||||
PollViewData poll = PollViewDataKt.toViewData(status.getActionable().getPoll());
|
PollViewData poll = PollViewDataKt.toViewData(status.getPoll());
|
||||||
if (poll == null) {
|
if (poll == null) {
|
||||||
return "";
|
return "";
|
||||||
} else {
|
} else {
|
||||||
|
@ -969,29 +1040,21 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
args[4] = getPollInfoText(System.currentTimeMillis(), poll, statusDisplayOptions,
|
args[4] = getPollInfoText(System.currentTimeMillis(), poll, statusDisplayOptions,
|
||||||
context);
|
context);
|
||||||
return context.getString(R.string.description_poll, args);
|
return context.getString(R.string.description_poll, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
protected CharSequence getFavsText(@NonNull Context context, int count) {
|
protected CharSequence getFavsText(@NonNull Context context, int count) {
|
||||||
if (count > 0) {
|
String countString = numberFormat.format(count);
|
||||||
String countString = numberFormat.format(count);
|
return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.favs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY);
|
||||||
return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.favs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY);
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
protected CharSequence getReblogsText(@NonNull Context context, int count) {
|
protected CharSequence getReblogsText(@NonNull Context context, int count) {
|
||||||
if (count > 0) {
|
String countString = numberFormat.format(count);
|
||||||
String countString = numberFormat.format(count);
|
return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.reblogs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY);
|
||||||
return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.reblogs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY);
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupPoll(PollViewData poll, List<Emoji> emojis,
|
private void setupPoll(PollViewData poll, List<Emoji> emojis,
|
||||||
|
@ -1014,26 +1077,26 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
pollAdapter.setup(
|
pollAdapter.setup(
|
||||||
poll.getOptions(),
|
poll.getOptions(),
|
||||||
poll.getVotesCount(),
|
poll.getVotesCount(),
|
||||||
poll.getVotersCount(),
|
poll.getVotersCount(),
|
||||||
emojis,
|
emojis,
|
||||||
PollAdapter.RESULT,
|
PollAdapter.RESULT,
|
||||||
viewThreadListener,
|
viewThreadListener,
|
||||||
statusDisplayOptions.animateEmojis()
|
statusDisplayOptions.animateEmojis()
|
||||||
);
|
);
|
||||||
|
|
||||||
pollButton.setVisibility(View.GONE);
|
pollButton.setVisibility(View.GONE);
|
||||||
} else {
|
} else {
|
||||||
// voting possible
|
// voting possible
|
||||||
pollAdapter.setup(
|
pollAdapter.setup(
|
||||||
poll.getOptions(),
|
poll.getOptions(),
|
||||||
poll.getVotesCount(),
|
poll.getVotesCount(),
|
||||||
poll.getVotersCount(),
|
poll.getVotersCount(),
|
||||||
emojis,
|
emojis,
|
||||||
poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE,
|
poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE,
|
||||||
null,
|
null,
|
||||||
statusDisplayOptions.animateEmojis()
|
statusDisplayOptions.animateEmojis()
|
||||||
);
|
);
|
||||||
|
|
||||||
pollButton.setVisibility(View.VISIBLE);
|
pollButton.setVisibility(View.VISIBLE);
|
||||||
|
@ -1086,11 +1149,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setupCard(
|
protected void setupCard(
|
||||||
final @NonNull StatusViewData.Concrete status,
|
final @NonNull StatusViewData.Concrete status,
|
||||||
boolean expanded,
|
boolean expanded,
|
||||||
final @NonNull CardViewMode cardViewMode,
|
final @NonNull CardViewMode cardViewMode,
|
||||||
final @NonNull StatusDisplayOptions statusDisplayOptions,
|
final @NonNull StatusDisplayOptions statusDisplayOptions,
|
||||||
final @NonNull StatusActionListener listener
|
final @NonNull StatusActionListener listener
|
||||||
) {
|
) {
|
||||||
if (cardView == null) {
|
if (cardView == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -1100,11 +1163,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
final Card card = actionable.getCard();
|
final Card card = actionable.getCard();
|
||||||
|
|
||||||
if (cardViewMode != CardViewMode.NONE &&
|
if (cardViewMode != CardViewMode.NONE &&
|
||||||
actionable.getAttachments().size() == 0 &&
|
actionable.getAttachments().isEmpty() &&
|
||||||
actionable.getPoll() == null &&
|
actionable.getPoll() == null &&
|
||||||
card != null &&
|
card != null &&
|
||||||
!TextUtils.isEmpty(card.getUrl()) &&
|
!TextUtils.isEmpty(card.getUrl()) &&
|
||||||
(!actionable.getSensitive() || expanded) &&
|
(TextUtils.isEmpty(actionable.getSpoilerText()) || expanded) &&
|
||||||
(!status.isCollapsible() || !status.isCollapsed())) {
|
(!status.isCollapsible() || !status.isCollapsed())) {
|
||||||
|
|
||||||
cardView.setVisibility(View.VISIBLE);
|
cardView.setVisibility(View.VISIBLE);
|
||||||
|
@ -1128,14 +1191,14 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
if (statusDisplayOptions.mediaPreviewEnabled() && !actionable.getSensitive() && !TextUtils.isEmpty(card.getImage())) {
|
if (statusDisplayOptions.mediaPreviewEnabled() && !actionable.getSensitive() && !TextUtils.isEmpty(card.getImage())) {
|
||||||
|
|
||||||
int radius = cardImage.getContext().getResources()
|
int radius = cardImage.getContext().getResources()
|
||||||
.getDimensionPixelSize(R.dimen.card_radius);
|
.getDimensionPixelSize(R.dimen.card_radius);
|
||||||
ShapeAppearanceModel.Builder cardImageShape = ShapeAppearanceModel.builder();
|
ShapeAppearanceModel.Builder cardImageShape = ShapeAppearanceModel.builder();
|
||||||
|
|
||||||
if (card.getWidth() > card.getHeight()) {
|
if (card.getWidth() > card.getHeight()) {
|
||||||
cardView.setOrientation(LinearLayout.VERTICAL);
|
cardView.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
cardImage.getLayoutParams().height = cardImage.getContext().getResources()
|
cardImage.getLayoutParams().height = cardImage.getContext().getResources()
|
||||||
.getDimensionPixelSize(R.dimen.card_image_vertical_height);
|
.getDimensionPixelSize(R.dimen.card_image_vertical_height);
|
||||||
cardImage.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
cardImage.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||||
|
@ -1145,7 +1208,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
cardView.setOrientation(LinearLayout.HORIZONTAL);
|
cardView.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
cardImage.getLayoutParams().width = cardImage.getContext().getResources()
|
cardImage.getLayoutParams().width = cardImage.getContext().getResources()
|
||||||
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
|
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
|
||||||
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||||
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
cardImageShape.setTopLeftCorner(CornerFamily.ROUNDED, radius);
|
cardImageShape.setTopLeftCorner(CornerFamily.ROUNDED, radius);
|
||||||
|
@ -1157,40 +1220,40 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
cardImage.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
cardImage.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||||
|
|
||||||
RequestBuilder<Drawable> builder = Glide.with(cardImage.getContext())
|
RequestBuilder<Drawable> builder = Glide.with(cardImage.getContext())
|
||||||
.load(card.getImage())
|
.load(card.getImage())
|
||||||
.dontTransform();
|
.dontTransform();
|
||||||
if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) {
|
if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) {
|
||||||
builder = builder.placeholder(decodeBlurHash(card.getBlurhash()));
|
builder = builder.placeholder(decodeBlurHash(card.getBlurhash()));
|
||||||
}
|
}
|
||||||
builder.into(cardImage);
|
builder.into(cardImage);
|
||||||
} else if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) {
|
} else if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) {
|
||||||
int radius = cardImage.getContext().getResources()
|
int radius = cardImage.getContext().getResources()
|
||||||
.getDimensionPixelSize(R.dimen.card_radius);
|
.getDimensionPixelSize(R.dimen.card_radius);
|
||||||
|
|
||||||
cardView.setOrientation(LinearLayout.HORIZONTAL);
|
cardView.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
cardImage.getLayoutParams().width = cardImage.getContext().getResources()
|
cardImage.getLayoutParams().width = cardImage.getContext().getResources()
|
||||||
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
|
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
|
||||||
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||||
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
|
|
||||||
ShapeAppearanceModel cardImageShape = ShapeAppearanceModel.builder()
|
ShapeAppearanceModel cardImageShape = ShapeAppearanceModel.builder()
|
||||||
.setTopLeftCorner(CornerFamily.ROUNDED, radius)
|
.setTopLeftCorner(CornerFamily.ROUNDED, radius)
|
||||||
.setBottomLeftCorner(CornerFamily.ROUNDED, radius)
|
.setBottomLeftCorner(CornerFamily.ROUNDED, radius)
|
||||||
.build();
|
.build();
|
||||||
cardImage.setShapeAppearanceModel(cardImageShape);
|
cardImage.setShapeAppearanceModel(cardImageShape);
|
||||||
|
|
||||||
cardImage.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
cardImage.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||||
|
|
||||||
Glide.with(cardImage.getContext())
|
Glide.with(cardImage.getContext())
|
||||||
.load(decodeBlurHash(card.getBlurhash()))
|
.load(decodeBlurHash(card.getBlurhash()))
|
||||||
.dontTransform()
|
.dontTransform()
|
||||||
.into(cardImage);
|
.into(cardImage);
|
||||||
} else {
|
} else {
|
||||||
cardView.setOrientation(LinearLayout.HORIZONTAL);
|
cardView.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
cardImage.getLayoutParams().width = cardImage.getContext().getResources()
|
cardImage.getLayoutParams().width = cardImage.getContext().getResources()
|
||||||
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
|
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
|
||||||
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||||
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
|
|
||||||
|
@ -1199,8 +1262,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
cardImage.setScaleType(ImageView.ScaleType.CENTER);
|
cardImage.setScaleType(ImageView.ScaleType.CENTER);
|
||||||
|
|
||||||
Glide.with(cardImage.getContext())
|
Glide.with(cardImage.getContext())
|
||||||
.load(R.drawable.card_image_placeholder)
|
.load(R.drawable.card_image_placeholder)
|
||||||
.into(cardImage);
|
.into(cardImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
View.OnClickListener visitLink = v -> listener.onViewUrl(card.getUrl());
|
View.OnClickListener visitLink = v -> listener.onViewUrl(card.getUrl());
|
||||||
|
@ -1208,8 +1271,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
cardView.setOnClickListener(visitLink);
|
cardView.setOnClickListener(visitLink);
|
||||||
// View embedded photos in our image viewer instead of opening the browser
|
// View embedded photos in our image viewer instead of opening the browser
|
||||||
cardImage.setOnClickListener(card.getType().equals(Card.TYPE_PHOTO) && !TextUtils.isEmpty(card.getEmbedUrl()) ?
|
cardImage.setOnClickListener(card.getType().equals(Card.TYPE_PHOTO) && !TextUtils.isEmpty(card.getEmbedUrl()) ?
|
||||||
v -> cardView.getContext().startActivity(ViewMediaActivity.newSingleImageIntent(cardView.getContext(), card.getEmbedUrl())) :
|
v -> cardView.getContext().startActivity(ViewMediaActivity.newSingleImageIntent(cardView.getContext(), card.getEmbedUrl())) :
|
||||||
visitLink);
|
visitLink);
|
||||||
|
|
||||||
cardView.setClipToOutline(true);
|
cardView.setClipToOutline(true);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -57,8 +57,8 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||||
|
|
||||||
if (visibilityIcon != null) {
|
if (visibilityIcon != null) {
|
||||||
ImageSpan visibilityIconSpan = new ImageSpan(
|
ImageSpan visibilityIconSpan = new ImageSpan(
|
||||||
visibilityIcon,
|
visibilityIcon,
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? DynamicDrawableSpan.ALIGN_CENTER : DynamicDrawableSpan.ALIGN_BASELINE
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? DynamicDrawableSpan.ALIGN_CENTER : DynamicDrawableSpan.ALIGN_BASELINE
|
||||||
);
|
);
|
||||||
sb.setSpan(visibilityIconSpan, 0, visibilityString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
sb.setSpan(visibilityIconSpan, 0, visibilityString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,6 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||||
|
|
||||||
Date createdAt = status.getCreatedAt();
|
Date createdAt = status.getCreatedAt();
|
||||||
if (createdAt != null) {
|
if (createdAt != null) {
|
||||||
|
|
||||||
sb.append(" ");
|
sb.append(" ");
|
||||||
sb.append(dateFormat.format(createdAt));
|
sb.append(dateFormat.format(createdAt));
|
||||||
}
|
}
|
||||||
|
@ -95,10 +94,16 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String language = status.getLanguage();
|
||||||
|
|
||||||
|
if (language != null) {
|
||||||
|
sb.append(metadataJoiner);
|
||||||
|
sb.append(language.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
Status.Application app = status.getApplication();
|
Status.Application app = status.getApplication();
|
||||||
|
|
||||||
if (app != null) {
|
if (app != null) {
|
||||||
|
|
||||||
sb.append(metadataJoiner);
|
sb.append(metadataJoiner);
|
||||||
|
|
||||||
if (app.getWebsite() != null) {
|
if (app.getWebsite() != null) {
|
||||||
|
@ -114,25 +119,8 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setReblogAndFavCount(int reblogCount, int favCount, StatusActionListener listener) {
|
private void setReblogAndFavCount(int reblogCount, int favCount, StatusActionListener listener) {
|
||||||
|
reblogs.setText(getReblogsText(reblogs.getContext(), reblogCount));
|
||||||
if (reblogCount > 0) {
|
favourites.setText(getFavsText(favourites.getContext(), favCount));
|
||||||
reblogs.setText(getReblogsText(reblogs.getContext(), reblogCount));
|
|
||||||
reblogs.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
reblogs.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
if (favCount > 0) {
|
|
||||||
favourites.setText(getFavsText(favourites.getContext(), favCount));
|
|
||||||
favourites.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
favourites.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reblogs.getVisibility() == View.GONE && favourites.getVisibility() == View.GONE) {
|
|
||||||
infoDivider.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
infoDivider.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
reblogs.setOnClickListener(v -> {
|
reblogs.setOnClickListener(v -> {
|
||||||
int position = getBindingAdapterPosition();
|
int position = getBindingAdapterPosition();
|
||||||
|
@ -155,8 +143,8 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||||
@Nullable Object payloads) {
|
@Nullable Object payloads) {
|
||||||
// We never collapse statuses in the detail view
|
// We never collapse statuses in the detail view
|
||||||
StatusViewData.Concrete uncollapsedStatus = (status.isCollapsible() && status.isCollapsed()) ?
|
StatusViewData.Concrete uncollapsedStatus = (status.isCollapsible() && status.isCollapsed()) ?
|
||||||
status.copyWithCollapsed(false) :
|
status.copyWithCollapsed(false) :
|
||||||
status;
|
status;
|
||||||
|
|
||||||
super.setupWithStatus(uncollapsedStatus, listener, statusDisplayOptions, payloads);
|
super.setupWithStatus(uncollapsedStatus, listener, statusDisplayOptions, payloads);
|
||||||
setupCard(uncollapsedStatus, status.isExpanded(), CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status
|
setupCard(uncollapsedStatus, status.isExpanded(), CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status
|
||||||
|
@ -165,7 +153,7 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||||
|
|
||||||
if (!statusDisplayOptions.hideStats()) {
|
if (!statusDisplayOptions.hideStats()) {
|
||||||
setReblogAndFavCount(actionable.getReblogsCount(),
|
setReblogAndFavCount(actionable.getReblogsCount(),
|
||||||
actionable.getFavouritesCount(), listener);
|
actionable.getFavouritesCount(), listener);
|
||||||
} else {
|
} else {
|
||||||
hideQuantitativeStats();
|
hideQuantitativeStats();
|
||||||
}
|
}
|
||||||
|
@ -197,7 +185,7 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
final Drawable visibilityDrawable = AppCompatResources.getDrawable(
|
final Drawable visibilityDrawable = AppCompatResources.getDrawable(
|
||||||
this.metaInfo.getContext(), visibilityIcon
|
this.metaInfo.getContext(), visibilityIcon
|
||||||
);
|
);
|
||||||
if (visibilityDrawable == null) {
|
if (visibilityDrawable == null) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -205,10 +193,10 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||||
|
|
||||||
final int size = (int) this.metaInfo.getTextSize();
|
final int size = (int) this.metaInfo.getTextSize();
|
||||||
visibilityDrawable.setBounds(
|
visibilityDrawable.setBounds(
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
size,
|
size,
|
||||||
size
|
size
|
||||||
);
|
);
|
||||||
visibilityDrawable.setTint(this.metaInfo.getCurrentTextColor());
|
visibilityDrawable.setTint(this.metaInfo.getCurrentTextColor());
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,11 @@ class TabAdapter(
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ViewBinding> {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ViewBinding> {
|
||||||
val binding = if (small) {
|
val binding = if (small) {
|
||||||
ItemTabPreferenceSmallBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
ItemTabPreferenceSmallBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
ItemTabPreferenceBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
ItemTabPreferenceBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,52 +1,75 @@
|
||||||
package com.keylesspalace.tusky.appstore
|
package com.keylesspalace.tusky.appstore
|
||||||
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.squareup.moshi.adapter
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the database cache in response to events.
|
||||||
|
* This is important for the home timeline and notifications to be up to date.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
class CacheUpdater @Inject constructor(
|
class CacheUpdater @Inject constructor(
|
||||||
eventHub: EventHub,
|
eventHub: EventHub,
|
||||||
accountManager: AccountManager,
|
accountManager: AccountManager,
|
||||||
appDatabase: AppDatabase,
|
appDatabase: AppDatabase,
|
||||||
gson: Gson
|
moshi: Moshi
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
init {
|
private val timelineDao = appDatabase.timelineDao()
|
||||||
val timelineDao = appDatabase.timelineDao()
|
private val statusDao = appDatabase.timelineStatusDao()
|
||||||
|
private val notificationsDao = appDatabase.notificationsDao()
|
||||||
|
|
||||||
|
init {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
eventHub.events.collect { event ->
|
eventHub.events.collect { event ->
|
||||||
val accountId = accountManager.activeAccount?.id ?: return@collect
|
val tuskyAccountId = accountManager.activeAccount?.id ?: return@collect
|
||||||
when (event) {
|
when (event) {
|
||||||
is StatusChangedEvent -> {
|
is StatusChangedEvent -> statusDao.update(
|
||||||
val status = event.status
|
tuskyAccountId = tuskyAccountId,
|
||||||
timelineDao.update(
|
status = event.status,
|
||||||
accountId = accountId,
|
moshi = moshi
|
||||||
status = status,
|
)
|
||||||
gson = gson
|
|
||||||
)
|
is UnfollowEvent -> timelineDao.removeStatusesAndReblogsByUser(tuskyAccountId, event.accountId)
|
||||||
|
|
||||||
|
is BlockEvent -> removeAllByUser(tuskyAccountId, event.accountId)
|
||||||
|
is MuteEvent -> removeAllByUser(tuskyAccountId, event.accountId)
|
||||||
|
|
||||||
|
is DomainMuteEvent -> {
|
||||||
|
timelineDao.deleteAllFromInstance(tuskyAccountId, event.instance)
|
||||||
|
notificationsDao.deleteAllFromInstance(tuskyAccountId, event.instance)
|
||||||
}
|
}
|
||||||
is UnfollowEvent ->
|
|
||||||
timelineDao.removeAllByUser(accountId, event.accountId)
|
is StatusDeletedEvent -> {
|
||||||
is StatusDeletedEvent ->
|
timelineDao.deleteAllWithStatus(tuskyAccountId, event.statusId)
|
||||||
timelineDao.delete(accountId, event.statusId)
|
notificationsDao.deleteAllWithStatus(tuskyAccountId, event.statusId)
|
||||||
|
}
|
||||||
|
|
||||||
is PollVoteEvent -> {
|
is PollVoteEvent -> {
|
||||||
val pollString = gson.toJson(event.poll)
|
val pollString = moshi.adapter<Poll>().toJson(event.poll)
|
||||||
timelineDao.setVoted(accountId, event.statusId, pollString)
|
statusDao.setVoted(tuskyAccountId, event.statusId, pollString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun removeAllByUser(tuskyAccountId: Long, accountId: String) {
|
||||||
|
timelineDao.removeAllByUser(tuskyAccountId, accountId)
|
||||||
|
notificationsDao.removeAllByUser(tuskyAccountId, accountId)
|
||||||
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
this.scope.cancel()
|
this.scope.cancel()
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import com.keylesspalace.tusky.TabData
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.entity.Notification
|
import com.keylesspalace.tusky.entity.Notification
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
|
import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
|
||||||
data class StatusChangedEvent(val status: Status) : Event
|
data class StatusChangedEvent(val status: Status) : Event
|
||||||
|
@ -13,7 +14,7 @@ data class BlockEvent(val accountId: String) : Event
|
||||||
data class MuteEvent(val accountId: String) : Event
|
data class MuteEvent(val accountId: String) : Event
|
||||||
data class StatusDeletedEvent(val statusId: String) : Event
|
data class StatusDeletedEvent(val statusId: String) : Event
|
||||||
data class StatusComposedEvent(val status: Status) : Event
|
data class StatusComposedEvent(val status: Status) : Event
|
||||||
data class StatusScheduledEvent(val status: Status) : Event
|
data class StatusScheduledEvent(val scheduledStatus: ScheduledStatus) : Event
|
||||||
data class ProfileEditedEvent(val newProfileData: Account) : Event
|
data class ProfileEditedEvent(val newProfileData: Account) : Event
|
||||||
data class PreferenceChangedEvent(val preferenceKey: String) : Event
|
data class PreferenceChangedEvent(val preferenceKey: String) : Event
|
||||||
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Event
|
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Event
|
||||||
|
@ -21,6 +22,9 @@ data class PollVoteEvent(val statusId: String, val poll: Poll) : Event
|
||||||
data class DomainMuteEvent(val instance: String) : Event
|
data class DomainMuteEvent(val instance: String) : Event
|
||||||
data class AnnouncementReadEvent(val announcementId: String) : Event
|
data class AnnouncementReadEvent(val announcementId: String) : Event
|
||||||
data class FilterUpdatedEvent(val filterContext: List<String>) : Event
|
data class FilterUpdatedEvent(val filterContext: List<String>) : Event
|
||||||
data class NewNotificationsEvent(val accountId: String, val notifications: List<Notification>) : Event
|
data class NewNotificationsEvent(
|
||||||
|
val accountId: String,
|
||||||
|
val notifications: List<Notification>
|
||||||
|
) : Event
|
||||||
data class ConversationsLoadingEvent(val accountId: String) : Event
|
data class ConversationsLoadingEvent(val accountId: String) : Event
|
||||||
data class NotificationsLoadingEvent(val accountId: String) : Event
|
data class NotificationsLoadingEvent(val accountId: String) : Event
|
||||||
|
|
|
@ -1,30 +1,20 @@
|
||||||
package com.keylesspalace.tusky.appstore
|
package com.keylesspalace.tusky.appstore
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Observable
|
|
||||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
|
||||||
interface Event
|
interface Event
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class EventHub @Inject constructor() {
|
class EventHub @Inject constructor() {
|
||||||
|
|
||||||
private val sharedEventFlow: MutableSharedFlow<Event> = MutableSharedFlow()
|
private val _events = MutableSharedFlow<Event>()
|
||||||
val events: Flow<Event> = sharedEventFlow
|
val events: SharedFlow<Event> = _events.asSharedFlow()
|
||||||
|
|
||||||
// TODO remove this old stuff as soon as NotificationsFragment is Kotlin
|
|
||||||
private val eventsSubject = PublishSubject.create<Event>()
|
|
||||||
val eventsObservable: Observable<Event> = eventsSubject
|
|
||||||
|
|
||||||
suspend fun dispatch(event: Event) {
|
suspend fun dispatch(event: Event) {
|
||||||
sharedEventFlow.emit(event)
|
_events.emit(event)
|
||||||
eventsSubject.onNext(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dispatchOld(event: Event) {
|
|
||||||
eventsSubject.onNext(event)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,14 +16,11 @@
|
||||||
package com.keylesspalace.tusky.components.account
|
package com.keylesspalace.tusky.components.account
|
||||||
|
|
||||||
import android.animation.ArgbEvaluator
|
import android.animation.ArgbEvaluator
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.graphics.drawable.LayerDrawable
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
|
@ -48,7 +45,7 @@ import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsCompat.Type.systemBars
|
import androidx.core.view.WindowInsetsCompat.Type.systemBars
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.core.widget.doAfterTextChanged
|
import androidx.core.widget.doAfterTextChanged
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.viewpager2.widget.MarginPageTransformer
|
import androidx.viewpager2.widget.MarginPageTransformer
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
@ -66,14 +63,13 @@ import com.keylesspalace.tusky.EditProfileActivity
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.StatusListActivity
|
import com.keylesspalace.tusky.StatusListActivity
|
||||||
import com.keylesspalace.tusky.ViewMediaActivity
|
import com.keylesspalace.tusky.ViewMediaActivity
|
||||||
import com.keylesspalace.tusky.components.account.list.ListsForAccountFragment
|
import com.keylesspalace.tusky.components.account.list.ListSelectionFragment
|
||||||
import com.keylesspalace.tusky.components.accountlist.AccountListActivity
|
import com.keylesspalace.tusky.components.accountlist.AccountListActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.components.report.ReportActivity
|
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||||
import com.keylesspalace.tusky.databinding.ActivityAccountBinding
|
import com.keylesspalace.tusky.databinding.ActivityAccountBinding
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
|
||||||
import com.keylesspalace.tusky.db.DraftsAlert
|
import com.keylesspalace.tusky.db.DraftsAlert
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.db.entity.AccountEntity
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.entity.Relationship
|
import com.keylesspalace.tusky.entity.Relationship
|
||||||
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
||||||
|
@ -84,6 +80,7 @@ import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.Error
|
import com.keylesspalace.tusky.util.Error
|
||||||
import com.keylesspalace.tusky.util.Loading
|
import com.keylesspalace.tusky.util.Loading
|
||||||
import com.keylesspalace.tusky.util.Success
|
import com.keylesspalace.tusky.util.Success
|
||||||
|
import com.keylesspalace.tusky.util.copyToClipboard
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.getDomain
|
import com.keylesspalace.tusky.util.getDomain
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
@ -92,7 +89,7 @@ import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||||
import com.keylesspalace.tusky.util.reduceSwipeSensitivity
|
import com.keylesspalace.tusky.util.reduceSwipeSensitivity
|
||||||
import com.keylesspalace.tusky.util.setClickableText
|
import com.keylesspalace.tusky.util.setClickableText
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.unsafeLazy
|
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.util.visible
|
import com.keylesspalace.tusky.util.visible
|
||||||
import com.keylesspalace.tusky.view.showMuteAccountDialog
|
import com.keylesspalace.tusky.view.showMuteAccountDialog
|
||||||
|
@ -100,34 +97,27 @@ import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
import com.mikepenz.iconics.utils.sizeDp
|
import com.mikepenz.iconics.utils.sizeDp
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dagger.android.HasAndroidInjector
|
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider, HasAndroidInjector, LinkListener {
|
@AndroidEntryPoint
|
||||||
|
class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider, LinkListener {
|
||||||
@Inject
|
|
||||||
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var viewModelFactory: ViewModelFactory
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var draftsAlert: DraftsAlert
|
lateinit var draftsAlert: DraftsAlert
|
||||||
|
|
||||||
private val viewModel: AccountViewModel by viewModels { viewModelFactory }
|
private val viewModel: AccountViewModel by viewModels()
|
||||||
|
|
||||||
private val binding: ActivityAccountBinding by viewBinding(ActivityAccountBinding::inflate)
|
private val binding: ActivityAccountBinding by viewBinding(ActivityAccountBinding::inflate)
|
||||||
|
|
||||||
private lateinit var accountFieldAdapter: AccountFieldAdapter
|
private lateinit var accountFieldAdapter: AccountFieldAdapter
|
||||||
|
|
||||||
private val preferences by unsafeLazy { PreferenceManager.getDefaultSharedPreferences(this) }
|
|
||||||
|
|
||||||
private var followState: FollowState = FollowState.NOT_FOLLOWING
|
private var followState: FollowState = FollowState.NOT_FOLLOWING
|
||||||
private var blocking: Boolean = false
|
private var blocking: Boolean = false
|
||||||
private var muting: Boolean = false
|
private var muting: Boolean = false
|
||||||
|
@ -139,8 +129,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
private var animateAvatar: Boolean = false
|
private var animateAvatar: Boolean = false
|
||||||
private var animateEmojis: Boolean = false
|
private var animateEmojis: Boolean = false
|
||||||
|
|
||||||
// fields for scroll animation
|
// for scroll animation
|
||||||
private var hideFab: Boolean = false
|
|
||||||
private var oldOffset: Int = 0
|
private var oldOffset: Int = 0
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
|
@ -178,10 +167,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
// Obtain information to fill out the profile.
|
// Obtain information to fill out the profile.
|
||||||
viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!)
|
viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!)
|
||||||
|
|
||||||
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
|
animateAvatar = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
|
||||||
animateAvatar = sharedPrefs.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
|
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||||
animateEmojis = sharedPrefs.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
|
||||||
hideFab = sharedPrefs.getBoolean(PrefKeys.FAB_HIDE, false)
|
|
||||||
|
|
||||||
handleWindowInsets()
|
handleWindowInsets()
|
||||||
setupToolbar()
|
setupToolbar()
|
||||||
|
@ -246,7 +233,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
}
|
}
|
||||||
|
|
||||||
// If wellbeing mode is enabled, follow stats and posts count should be hidden
|
// If wellbeing mode is enabled, follow stats and posts count should be hidden
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false)
|
val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false)
|
||||||
|
|
||||||
if (wellbeingEnabled) {
|
if (wellbeingEnabled) {
|
||||||
|
@ -267,9 +253,18 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
binding.accountFragmentViewPager.adapter = adapter
|
binding.accountFragmentViewPager.adapter = adapter
|
||||||
binding.accountFragmentViewPager.offscreenPageLimit = 2
|
binding.accountFragmentViewPager.offscreenPageLimit = 2
|
||||||
|
|
||||||
val pageTitles = arrayOf(getString(R.string.title_posts), getString(R.string.title_posts_with_replies), getString(R.string.title_posts_pinned), getString(R.string.title_media))
|
val pageTitles =
|
||||||
|
arrayOf(
|
||||||
|
getString(R.string.title_posts),
|
||||||
|
getString(R.string.title_posts_with_replies),
|
||||||
|
getString(R.string.title_posts_pinned),
|
||||||
|
getString(R.string.title_media)
|
||||||
|
)
|
||||||
|
|
||||||
TabLayoutMediator(binding.accountTabLayout, binding.accountFragmentViewPager) { tab, position ->
|
TabLayoutMediator(
|
||||||
|
binding.accountTabLayout,
|
||||||
|
binding.accountFragmentViewPager
|
||||||
|
) { tab, position ->
|
||||||
tab.text = pageTitles[position]
|
tab.text = pageTitles[position]
|
||||||
}.attach()
|
}.attach()
|
||||||
|
|
||||||
|
@ -301,7 +296,15 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
val right = insets.getInsets(systemBars()).right
|
val right = insets.getInsets(systemBars()).right
|
||||||
val bottom = insets.getInsets(systemBars()).bottom
|
val bottom = insets.getInsets(systemBars()).bottom
|
||||||
val left = insets.getInsets(systemBars()).left
|
val left = insets.getInsets(systemBars()).left
|
||||||
binding.accountCoordinatorLayout.updatePadding(right = right, bottom = bottom, left = left)
|
binding.accountCoordinatorLayout.updatePadding(
|
||||||
|
right = right,
|
||||||
|
bottom = bottom,
|
||||||
|
left = left
|
||||||
|
)
|
||||||
|
binding.swipeToRefreshLayout.setProgressViewEndTarget(
|
||||||
|
false,
|
||||||
|
top + resources.getDimensionPixelSize(R.dimen.account_swiperefresh_distance)
|
||||||
|
)
|
||||||
|
|
||||||
WindowInsetsCompat.CONSUMED
|
WindowInsetsCompat.CONSUMED
|
||||||
}
|
}
|
||||||
|
@ -318,30 +321,24 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
|
|
||||||
val appBarElevation = resources.getDimension(R.dimen.actionbar_elevation)
|
val appBarElevation = resources.getDimension(R.dimen.actionbar_elevation)
|
||||||
|
|
||||||
val toolbarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation)
|
val toolbarBackground = MaterialShapeDrawable.createWithElevationOverlay(
|
||||||
|
this,
|
||||||
|
appBarElevation
|
||||||
|
)
|
||||||
toolbarBackground.fillColor = ColorStateList.valueOf(Color.TRANSPARENT)
|
toolbarBackground.fillColor = ColorStateList.valueOf(Color.TRANSPARENT)
|
||||||
binding.accountToolbar.background = toolbarBackground
|
binding.accountToolbar.background = toolbarBackground
|
||||||
|
|
||||||
// Provide a non-transparent background to the navigation and overflow icons to ensure
|
binding.accountToolbar.setNavigationIcon(R.drawable.ic_arrow_back_with_background)
|
||||||
// they remain visible over whatever the profile background image might be.
|
binding.accountToolbar.setOverflowIcon(
|
||||||
val backgroundCircle = AppCompatResources.getDrawable(this, R.drawable.background_circle)!!
|
AppCompatResources.getDrawable(this, R.drawable.ic_more_with_background)
|
||||||
backgroundCircle.alpha = 210 // Any lower than this and the backgrounds interfere
|
|
||||||
binding.accountToolbar.navigationIcon = LayerDrawable(
|
|
||||||
arrayOf(
|
|
||||||
backgroundCircle,
|
|
||||||
binding.accountToolbar.navigationIcon
|
|
||||||
)
|
|
||||||
)
|
|
||||||
binding.accountToolbar.overflowIcon = LayerDrawable(
|
|
||||||
arrayOf(
|
|
||||||
backgroundCircle,
|
|
||||||
binding.accountToolbar.overflowIcon
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.accountHeaderInfoContainer.background = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation)
|
binding.accountHeaderInfoContainer.background = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation)
|
||||||
|
|
||||||
val avatarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation).apply {
|
val avatarBackground = MaterialShapeDrawable.createWithElevationOverlay(
|
||||||
|
this,
|
||||||
|
appBarElevation
|
||||||
|
).apply {
|
||||||
fillColor = ColorStateList.valueOf(toolbarColor)
|
fillColor = ColorStateList.valueOf(toolbarColor)
|
||||||
elevation = appBarElevation
|
elevation = appBarElevation
|
||||||
shapeAppearanceModel = ShapeAppearanceModel.builder()
|
shapeAppearanceModel = ShapeAppearanceModel.builder()
|
||||||
|
@ -365,15 +362,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
supportActionBar?.setDisplayShowTitleEnabled(false)
|
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hideFab && !blocking) {
|
|
||||||
if (verticalOffset > oldOffset) {
|
|
||||||
binding.accountFloatingActionButton.show()
|
|
||||||
}
|
|
||||||
if (verticalOffset < oldOffset) {
|
|
||||||
binding.accountFloatingActionButton.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val scaledAvatarSize = (avatarSize + verticalOffset) / avatarSize
|
val scaledAvatarSize = (avatarSize + verticalOffset) / avatarSize
|
||||||
|
|
||||||
binding.accountAvatarImageView.scaleX = scaledAvatarSize
|
binding.accountAvatarImageView.scaleX = scaledAvatarSize
|
||||||
|
@ -381,11 +369,17 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
|
|
||||||
binding.accountAvatarImageView.visible(scaledAvatarSize > 0)
|
binding.accountAvatarImageView.visible(scaledAvatarSize > 0)
|
||||||
|
|
||||||
val transparencyPercent = (abs(verticalOffset) / titleVisibleHeight.toFloat()).coerceAtMost(1f)
|
val transparencyPercent = (abs(verticalOffset) / titleVisibleHeight.toFloat()).coerceAtMost(
|
||||||
|
1f
|
||||||
|
)
|
||||||
|
|
||||||
window.statusBarColor = argbEvaluator.evaluate(transparencyPercent, statusBarColorTransparent, statusBarColorOpaque) as Int
|
window.statusBarColor = argbEvaluator.evaluate(transparencyPercent, statusBarColorTransparent, statusBarColorOpaque) as Int
|
||||||
|
|
||||||
val evaluatedToolbarColor = argbEvaluator.evaluate(transparencyPercent, Color.TRANSPARENT, toolbarColor) as Int
|
val evaluatedToolbarColor = argbEvaluator.evaluate(
|
||||||
|
transparencyPercent,
|
||||||
|
Color.TRANSPARENT,
|
||||||
|
toolbarColor
|
||||||
|
) as Int
|
||||||
|
|
||||||
toolbarBackground.fillColor = ColorStateList.valueOf(evaluatedToolbarColor)
|
toolbarBackground.fillColor = ColorStateList.valueOf(evaluatedToolbarColor)
|
||||||
|
|
||||||
|
@ -403,31 +397,46 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
* Subscribe to data loaded at the view model
|
* Subscribe to data loaded at the view model
|
||||||
*/
|
*/
|
||||||
private fun subscribeObservables() {
|
private fun subscribeObservables() {
|
||||||
viewModel.accountData.observe(this) {
|
lifecycleScope.launch {
|
||||||
when (it) {
|
viewModel.accountData.collect {
|
||||||
is Success -> onAccountChanged(it.data)
|
if (it == null) return@collect
|
||||||
is Error -> {
|
when (it) {
|
||||||
Snackbar.make(binding.accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
|
is Success -> onAccountChanged(it.data)
|
||||||
|
is Error -> {
|
||||||
|
Snackbar.make(
|
||||||
|
binding.accountCoordinatorLayout,
|
||||||
|
R.string.error_generic,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
)
|
||||||
|
.setAction(R.string.action_retry) { viewModel.refresh() }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
is Loading -> { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.relationshipData.collect {
|
||||||
|
val relation = it?.data
|
||||||
|
if (relation != null) {
|
||||||
|
onRelationshipChanged(relation)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it is Error) {
|
||||||
|
Snackbar.make(
|
||||||
|
binding.accountCoordinatorLayout,
|
||||||
|
R.string.error_generic,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
)
|
||||||
.setAction(R.string.action_retry) { viewModel.refresh() }
|
.setAction(R.string.action_retry) { viewModel.refresh() }
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
is Loading -> { }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
viewModel.relationshipData.observe(this) {
|
lifecycleScope.launch {
|
||||||
val relation = it?.data
|
viewModel.noteSaved.collect {
|
||||||
if (relation != null) {
|
binding.saveNoteInfo.visible(it, View.INVISIBLE)
|
||||||
onRelationshipChanged(relation)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (it is Error) {
|
|
||||||
Snackbar.make(binding.accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
|
|
||||||
.setAction(R.string.action_retry) { viewModel.refresh() }
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModel.noteSaved.observe(this) {
|
|
||||||
binding.saveNoteInfo.visible(it, View.INVISIBLE)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Post failed" dialog should display in this activity
|
// "Post failed" dialog should display in this activity
|
||||||
|
@ -444,10 +453,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
*/
|
*/
|
||||||
private fun setupRefreshLayout() {
|
private fun setupRefreshLayout() {
|
||||||
binding.swipeToRefreshLayout.setOnRefreshListener { onRefresh() }
|
binding.swipeToRefreshLayout.setOnRefreshListener { onRefresh() }
|
||||||
viewModel.isRefreshing.observe(
|
lifecycleScope.launch {
|
||||||
this
|
viewModel.isRefreshing.collect { isRefreshing ->
|
||||||
) { isRefreshing ->
|
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
|
||||||
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
|
}
|
||||||
}
|
}
|
||||||
binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||||
}
|
}
|
||||||
|
@ -463,21 +472,24 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
for (view in listOf(binding.accountUsernameTextView, binding.accountDisplayNameTextView)) {
|
for (view in listOf(binding.accountUsernameTextView, binding.accountDisplayNameTextView)) {
|
||||||
view.setOnLongClickListener {
|
view.setOnLongClickListener {
|
||||||
loadedAccount?.let { loadedAccount ->
|
loadedAccount?.let { loadedAccount ->
|
||||||
val fullUsername = getFullUsername(loadedAccount)
|
copyToClipboard(
|
||||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
getFullUsername(loadedAccount),
|
||||||
clipboard.setPrimaryClip(ClipData.newPlainText(null, fullUsername))
|
getString(R.string.account_username_copied),
|
||||||
Snackbar.make(binding.root, getString(R.string.account_username_copied), Snackbar.LENGTH_SHORT)
|
)
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val emojifiedNote = account.note.parseAsMastodonHtml().emojify(account.emojis, binding.accountNoteTextView, animateEmojis)
|
val emojifiedNote = account.note.parseAsMastodonHtml().emojify(
|
||||||
|
account.emojis,
|
||||||
|
binding.accountNoteTextView,
|
||||||
|
animateEmojis
|
||||||
|
)
|
||||||
setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this)
|
setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this)
|
||||||
|
|
||||||
accountFieldAdapter.fields = account.fields.orEmpty()
|
accountFieldAdapter.fields = account.fields
|
||||||
accountFieldAdapter.emojis = account.emojis.orEmpty()
|
accountFieldAdapter.emojis = account.emojis
|
||||||
accountFieldAdapter.notifyDataSetChanged()
|
accountFieldAdapter.notifyDataSetChanged()
|
||||||
|
|
||||||
binding.accountLockedImageView.visible(account.locked)
|
binding.accountLockedImageView.visible(account.locked)
|
||||||
|
@ -503,7 +515,13 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
val isLight = resources.getBoolean(R.bool.lightNavigationBar)
|
val isLight = resources.getBoolean(R.bool.lightNavigationBar)
|
||||||
|
|
||||||
if (loadedAccount?.bot == true) {
|
if (loadedAccount?.bot == true) {
|
||||||
val badgeView = getBadge(getColor(R.color.tusky_grey_50), R.drawable.ic_bot_24dp, getString(R.string.profile_badge_bot_text), isLight)
|
val badgeView =
|
||||||
|
getBadge(
|
||||||
|
getColor(R.color.tusky_grey_50),
|
||||||
|
R.drawable.ic_bot_24dp,
|
||||||
|
getString(R.string.profile_badge_bot_text),
|
||||||
|
isLight
|
||||||
|
)
|
||||||
binding.accountBadgeContainer.addView(badgeView)
|
binding.accountBadgeContainer.addView(badgeView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -612,7 +630,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
*/
|
*/
|
||||||
private fun updateRemoteAccount() {
|
private fun updateRemoteAccount() {
|
||||||
loadedAccount?.let { account ->
|
loadedAccount?.let { account ->
|
||||||
if (account.isRemote()) {
|
if (account.isRemote) {
|
||||||
binding.accountRemoveView.show()
|
binding.accountRemoveView.show()
|
||||||
binding.accountRemoveView.setOnClickListener {
|
binding.accountRemoveView.setOnClickListener {
|
||||||
openLink(account.url)
|
openLink(account.url)
|
||||||
|
@ -634,6 +652,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
binding.accountFloatingActionButton.setOnClickListener { mention() }
|
binding.accountFloatingActionButton.setOnClickListener { mention() }
|
||||||
|
|
||||||
binding.accountFollowButton.setOnClickListener {
|
binding.accountFollowButton.setOnClickListener {
|
||||||
|
val confirmFollows = preferences.getBoolean(PrefKeys.CONFIRM_FOLLOWS, false)
|
||||||
if (viewModel.isSelf) {
|
if (viewModel.isSelf) {
|
||||||
val intent = Intent(this@AccountActivity, EditProfileActivity::class.java)
|
val intent = Intent(this@AccountActivity, EditProfileActivity::class.java)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
@ -647,7 +666,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
|
|
||||||
when (followState) {
|
when (followState) {
|
||||||
FollowState.NOT_FOLLOWING -> {
|
FollowState.NOT_FOLLOWING -> {
|
||||||
viewModel.changeFollowState()
|
if (confirmFollows) {
|
||||||
|
showFollowWarningDialog()
|
||||||
|
} else {
|
||||||
|
viewModel.changeFollowState()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
FollowState.REQUESTED -> {
|
FollowState.REQUESTED -> {
|
||||||
showFollowRequestPendingDialog()
|
showFollowRequestPendingDialog()
|
||||||
|
@ -674,7 +697,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
showingReblogs = relation.showingReblogs
|
showingReblogs = relation.showingReblogs
|
||||||
|
|
||||||
// If wellbeing mode is enabled, "follows you" text should not be visible
|
// If wellbeing mode is enabled, "follows you" text should not be visible
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false)
|
val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false)
|
||||||
|
|
||||||
binding.accountFollowsYouTextView.visible(relation.followedBy && !wellbeingEnabled)
|
binding.accountFollowsYouTextView.visible(relation.followedBy && !wellbeingEnabled)
|
||||||
|
@ -867,13 +889,23 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showFollowWarningDialog() {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.dialog_follow_warning)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() }
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun toggleBlockDomain(instance: String) {
|
private fun toggleBlockDomain(instance: String) {
|
||||||
if (blockingDomain) {
|
if (blockingDomain) {
|
||||||
viewModel.unblockDomain(instance)
|
viewModel.unblockDomain(instance)
|
||||||
} else {
|
} else {
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setMessage(getString(R.string.mute_domain_warning, instance))
|
.setMessage(getString(R.string.mute_domain_warning, instance))
|
||||||
.setPositiveButton(getString(R.string.mute_domain_warning_dialog_ok)) { _, _ -> viewModel.blockDomain(instance) }
|
.setPositiveButton(
|
||||||
|
getString(R.string.mute_domain_warning_dialog_ok)
|
||||||
|
) { _, _ -> viewModel.blockDomain(instance) }
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
@ -966,7 +998,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
sendIntent.action = Intent.ACTION_SEND
|
sendIntent.action = Intent.ACTION_SEND
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, url)
|
sendIntent.putExtra(Intent.EXTRA_TEXT, url)
|
||||||
sendIntent.type = "text/plain"
|
sendIntent.type = "text/plain"
|
||||||
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_account_link_to)))
|
startActivity(
|
||||||
|
Intent.createChooser(
|
||||||
|
sendIntent,
|
||||||
|
resources.getText(R.string.send_account_link_to)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -978,7 +1015,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
sendIntent.action = Intent.ACTION_SEND
|
sendIntent.action = Intent.ACTION_SEND
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, fullUsername)
|
sendIntent.putExtra(Intent.EXTRA_TEXT, fullUsername)
|
||||||
sendIntent.type = "text/plain"
|
sendIntent.type = "text/plain"
|
||||||
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_account_username_to)))
|
startActivity(
|
||||||
|
Intent.createChooser(
|
||||||
|
sendIntent,
|
||||||
|
resources.getText(R.string.send_account_username_to)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -991,7 +1033,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.action_add_or_remove_from_list -> {
|
R.id.action_add_or_remove_from_list -> {
|
||||||
ListsForAccountFragment.newInstance(viewModel.accountId).show(supportFragmentManager, null)
|
ListSelectionFragment.newInstance(viewModel.accountId).show(supportFragmentManager, null)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.action_mute_domain -> {
|
R.id.action_mute_domain -> {
|
||||||
|
@ -1009,7 +1051,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
}
|
}
|
||||||
R.id.action_report -> {
|
R.id.action_report -> {
|
||||||
loadedAccount?.let { loadedAccount ->
|
loadedAccount?.let { loadedAccount ->
|
||||||
startActivity(ReportActivity.getIntent(this, viewModel.accountId, loadedAccount.username))
|
startActivity(
|
||||||
|
ReportActivity.getIntent(this, viewModel.accountId, loadedAccount.username)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -1026,7 +1070,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFullUsername(account: Account): String {
|
private fun getFullUsername(account: Account): String {
|
||||||
return if (account.isRemote()) {
|
return if (account.isRemote) {
|
||||||
"@" + account.username
|
"@" + account.username
|
||||||
} else {
|
} else {
|
||||||
val localUsername = account.localUsername
|
val localUsername = account.localUsername
|
||||||
|
@ -1047,7 +1091,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
// text color with maximum contrast
|
// text color with maximum contrast
|
||||||
val textColor = if (isLight) Color.BLACK else Color.WHITE
|
val textColor = if (isLight) Color.BLACK else Color.WHITE
|
||||||
// badge color with 50% transparency so it blends in with the theme background
|
// badge color with 50% transparency so it blends in with the theme background
|
||||||
val backgroundColor = Color.argb(128, Color.red(baseColor), Color.green(baseColor), Color.blue(baseColor))
|
val backgroundColor = Color.argb(
|
||||||
|
128,
|
||||||
|
Color.red(baseColor),
|
||||||
|
Color.green(baseColor),
|
||||||
|
Color.blue(baseColor)
|
||||||
|
)
|
||||||
// a color between the text color and the badge color
|
// a color between the text color and the badge color
|
||||||
val outlineColor = ColorUtils.blendARGB(textColor, baseColor, 0.7f)
|
val outlineColor = ColorUtils.blendARGB(textColor, baseColor, 0.7f)
|
||||||
|
|
||||||
|
@ -1076,8 +1125,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
||||||
return badge
|
return badge
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun androidInjector() = dispatchingAndroidInjector
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val KEY_ACCOUNT_ID = "id"
|
private const val KEY_ACCOUNT_ID = "id"
|
||||||
|
|
|
@ -38,8 +38,15 @@ class AccountFieldAdapter(
|
||||||
|
|
||||||
override fun getItemCount() = fields.size
|
override fun getItemCount() = fields.size
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAccountFieldBinding> {
|
override fun onCreateViewHolder(
|
||||||
val binding = ItemAccountFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): BindingHolder<ItemAccountFieldBinding> {
|
||||||
|
val binding = ItemAccountFieldBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
return BindingHolder(binding)
|
return BindingHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,11 +58,20 @@ class AccountFieldAdapter(
|
||||||
val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis)
|
val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis)
|
||||||
nameTextView.text = emojifiedName
|
nameTextView.text = emojifiedName
|
||||||
|
|
||||||
val emojifiedValue = field.value.parseAsMastodonHtml().emojify(emojis, valueTextView, animateEmojis)
|
val emojifiedValue = field.value.parseAsMastodonHtml().emojify(
|
||||||
|
emojis,
|
||||||
|
valueTextView,
|
||||||
|
animateEmojis
|
||||||
|
)
|
||||||
setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener)
|
setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener)
|
||||||
|
|
||||||
if (field.verifiedAt != null) {
|
if (field.verifiedAt != null) {
|
||||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
|
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
R.drawable.ic_check_circle,
|
||||||
|
0
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,11 @@ class AccountPagerAdapter(
|
||||||
override fun createFragment(position: Int): Fragment {
|
override fun createFragment(position: Int): Fragment {
|
||||||
return when (position) {
|
return when (position) {
|
||||||
0 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER, accountId, false)
|
0 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER, accountId, false)
|
||||||
1 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_WITH_REPLIES, accountId, false)
|
1 -> TimelineFragment.newInstance(
|
||||||
|
TimelineViewModel.Kind.USER_WITH_REPLIES,
|
||||||
|
accountId,
|
||||||
|
false
|
||||||
|
)
|
||||||
2 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_PINNED, accountId, false)
|
2 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_PINNED, accountId, false)
|
||||||
3 -> AccountMediaFragment.newInstance(accountId)
|
3 -> AccountMediaFragment.newInstance(accountId)
|
||||||
else -> throw AssertionError("Page $position is out of AccountPagerAdapter bounds")
|
else -> throw AssertionError("Page $position is out of AccountPagerAdapter bounds")
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.keylesspalace.tusky.components.account
|
package com.keylesspalace.tusky.components.account
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
|
@ -20,23 +19,38 @@ import com.keylesspalace.tusky.util.Loading
|
||||||
import com.keylesspalace.tusky.util.Resource
|
import com.keylesspalace.tusky.util.Resource
|
||||||
import com.keylesspalace.tusky.util.Success
|
import com.keylesspalace.tusky.util.Success
|
||||||
import com.keylesspalace.tusky.util.getDomain
|
import com.keylesspalace.tusky.util.getDomain
|
||||||
import kotlinx.coroutines.Job
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
class AccountViewModel @Inject constructor(
|
class AccountViewModel @Inject constructor(
|
||||||
private val mastodonApi: MastodonApi,
|
private val mastodonApi: MastodonApi,
|
||||||
private val eventHub: EventHub,
|
private val eventHub: EventHub,
|
||||||
accountManager: AccountManager
|
accountManager: AccountManager
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val accountData = MutableLiveData<Resource<Account>>()
|
private val _accountData = MutableStateFlow(null as Resource<Account>?)
|
||||||
val relationshipData = MutableLiveData<Resource<Relationship>>()
|
val accountData: StateFlow<Resource<Account>?> = _accountData.asStateFlow()
|
||||||
|
|
||||||
val noteSaved = MutableLiveData<Boolean>()
|
private val _relationshipData = MutableStateFlow(null as Resource<Relationship>?)
|
||||||
|
val relationshipData: StateFlow<Resource<Relationship>?> = _relationshipData.asStateFlow()
|
||||||
|
|
||||||
|
private val _noteSaved = MutableStateFlow(false)
|
||||||
|
val noteSaved: StateFlow<Boolean> = _noteSaved.asStateFlow()
|
||||||
|
|
||||||
|
private val _isRefreshing = MutableSharedFlow<Boolean>(1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
|
val isRefreshing: SharedFlow<Boolean> = _isRefreshing.asSharedFlow()
|
||||||
|
|
||||||
val isRefreshing = MutableLiveData<Boolean>().apply { value = false }
|
|
||||||
private var isDataLoading = false
|
private var isDataLoading = false
|
||||||
|
|
||||||
lateinit var accountId: String
|
lateinit var accountId: String
|
||||||
|
@ -55,17 +69,17 @@ class AccountViewModel @Inject constructor(
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
eventHub.events.collect { event ->
|
eventHub.events.collect { event ->
|
||||||
if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) {
|
if (event is ProfileEditedEvent && event.newProfileData.id == _accountData.value?.data?.id) {
|
||||||
accountData.postValue(Success(event.newProfileData))
|
_accountData.value = Success(event.newProfileData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun obtainAccount(reload: Boolean = false) {
|
private fun obtainAccount(reload: Boolean = false) {
|
||||||
if (accountData.value == null || reload) {
|
if (_accountData.value == null || reload) {
|
||||||
isDataLoading = true
|
isDataLoading = true
|
||||||
accountData.postValue(Loading())
|
_accountData.value = Loading()
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
mastodonApi.account(accountId)
|
mastodonApi.account(accountId)
|
||||||
|
@ -74,15 +88,15 @@ class AccountViewModel @Inject constructor(
|
||||||
domain = getDomain(account.url)
|
domain = getDomain(account.url)
|
||||||
isFromOwnDomain = domain == activeAccount.domain
|
isFromOwnDomain = domain == activeAccount.domain
|
||||||
|
|
||||||
accountData.postValue(Success(account))
|
_accountData.value = Success(account)
|
||||||
isDataLoading = false
|
isDataLoading = false
|
||||||
isRefreshing.postValue(false)
|
_isRefreshing.emit(false)
|
||||||
},
|
},
|
||||||
{ t ->
|
{ t ->
|
||||||
Log.w(TAG, "failed obtaining account", t)
|
Log.w(TAG, "failed obtaining account", t)
|
||||||
accountData.postValue(Error(cause = t))
|
_accountData.value = Error(cause = t)
|
||||||
isDataLoading = false
|
isDataLoading = false
|
||||||
isRefreshing.postValue(false)
|
_isRefreshing.emit(false)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -90,18 +104,25 @@ class AccountViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun obtainRelationship(reload: Boolean = false) {
|
private fun obtainRelationship(reload: Boolean = false) {
|
||||||
if (relationshipData.value == null || reload) {
|
if (_relationshipData.value == null || reload) {
|
||||||
relationshipData.postValue(Loading())
|
_relationshipData.value = Loading()
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
mastodonApi.relationships(listOf(accountId))
|
mastodonApi.relationships(listOf(accountId))
|
||||||
.fold(
|
.fold(
|
||||||
{ relationships ->
|
{ relationships ->
|
||||||
relationshipData.postValue(if (relationships.isNotEmpty()) Success(relationships[0]) else Error())
|
_relationshipData.value =
|
||||||
|
if (relationships.isNotEmpty()) {
|
||||||
|
Success(
|
||||||
|
relationships[0]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Error()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ t ->
|
{ t ->
|
||||||
Log.w(TAG, "failed obtaining relationships", t)
|
Log.w(TAG, "failed obtaining relationships", t)
|
||||||
relationshipData.postValue(Error(cause = t))
|
_relationshipData.value = Error(cause = t)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -109,7 +130,7 @@ class AccountViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeFollowState() {
|
fun changeFollowState() {
|
||||||
val relationship = relationshipData.value?.data
|
val relationship = _relationshipData.value?.data
|
||||||
if (relationship?.following == true || relationship?.requested == true) {
|
if (relationship?.following == true || relationship?.requested == true) {
|
||||||
changeRelationship(RelationShipAction.UNFOLLOW)
|
changeRelationship(RelationShipAction.UNFOLLOW)
|
||||||
} else {
|
} else {
|
||||||
|
@ -118,7 +139,7 @@ class AccountViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeBlockState() {
|
fun changeBlockState() {
|
||||||
if (relationshipData.value?.data?.blocking == true) {
|
if (_relationshipData.value?.data?.blocking == true) {
|
||||||
changeRelationship(RelationShipAction.UNBLOCK)
|
changeRelationship(RelationShipAction.UNBLOCK)
|
||||||
} else {
|
} else {
|
||||||
changeRelationship(RelationShipAction.BLOCK)
|
changeRelationship(RelationShipAction.BLOCK)
|
||||||
|
@ -134,9 +155,9 @@ class AccountViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeSubscribingState() {
|
fun changeSubscribingState() {
|
||||||
val relationship = relationshipData.value?.data
|
val relationship = _relationshipData.value?.data
|
||||||
if (relationship?.notifying == true || /* Mastodon 3.3.0rc1 */
|
if (relationship?.notifying == true || // Mastodon 3.3.0rc1
|
||||||
relationship?.subscribing == true /* Pleroma */
|
relationship?.subscribing == true // Pleroma
|
||||||
) {
|
) {
|
||||||
changeRelationship(RelationShipAction.UNSUBSCRIBE)
|
changeRelationship(RelationShipAction.UNSUBSCRIBE)
|
||||||
} else {
|
} else {
|
||||||
|
@ -148,9 +169,9 @@ class AccountViewModel @Inject constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
mastodonApi.blockDomain(instance).fold({
|
mastodonApi.blockDomain(instance).fold({
|
||||||
eventHub.dispatch(DomainMuteEvent(instance))
|
eventHub.dispatch(DomainMuteEvent(instance))
|
||||||
val relation = relationshipData.value?.data
|
val relation = _relationshipData.value?.data
|
||||||
if (relation != null) {
|
if (relation != null) {
|
||||||
relationshipData.postValue(Success(relation.copy(blockingDomain = true)))
|
_relationshipData.value = Success(relation.copy(blockingDomain = true))
|
||||||
}
|
}
|
||||||
}, { e ->
|
}, { e ->
|
||||||
Log.e(TAG, "Error muting $instance", e)
|
Log.e(TAG, "Error muting $instance", e)
|
||||||
|
@ -161,9 +182,9 @@ class AccountViewModel @Inject constructor(
|
||||||
fun unblockDomain(instance: String) {
|
fun unblockDomain(instance: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
mastodonApi.unblockDomain(instance).fold({
|
mastodonApi.unblockDomain(instance).fold({
|
||||||
val relation = relationshipData.value?.data
|
val relation = _relationshipData.value?.data
|
||||||
if (relation != null) {
|
if (relation != null) {
|
||||||
relationshipData.postValue(Success(relation.copy(blockingDomain = false)))
|
_relationshipData.value = Success(relation.copy(blockingDomain = false))
|
||||||
}
|
}
|
||||||
}, { e ->
|
}, { e ->
|
||||||
Log.e(TAG, "Error unmuting $instance", e)
|
Log.e(TAG, "Error unmuting $instance", e)
|
||||||
|
@ -172,7 +193,7 @@ class AccountViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeShowReblogsState() {
|
fun changeShowReblogsState() {
|
||||||
if (relationshipData.value?.data?.showingReblogs == true) {
|
if (_relationshipData.value?.data?.showingReblogs == true) {
|
||||||
changeRelationship(RelationShipAction.FOLLOW, false)
|
changeRelationship(RelationShipAction.FOLLOW, false)
|
||||||
} else {
|
} else {
|
||||||
changeRelationship(RelationShipAction.FOLLOW, true)
|
changeRelationship(RelationShipAction.FOLLOW, true)
|
||||||
|
@ -187,9 +208,9 @@ class AccountViewModel @Inject constructor(
|
||||||
parameter: Boolean? = null,
|
parameter: Boolean? = null,
|
||||||
duration: Int? = null
|
duration: Int? = null
|
||||||
) = viewModelScope.launch {
|
) = viewModelScope.launch {
|
||||||
val relation = relationshipData.value?.data
|
val relation = _relationshipData.value?.data
|
||||||
val account = accountData.value?.data
|
val account = _accountData.value?.data
|
||||||
val isMastodon = relationshipData.value?.data?.notifying != null
|
val isMastodon = _relationshipData.value?.data?.notifying != null
|
||||||
|
|
||||||
if (relation != null && account != null) {
|
if (relation != null && account != null) {
|
||||||
// optimistically post new state for faster response
|
// optimistically post new state for faster response
|
||||||
|
@ -222,7 +243,7 @@ class AccountViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
relationshipData.postValue(Loading(newRelation))
|
_relationshipData.value = Loading(newRelation)
|
||||||
}
|
}
|
||||||
|
|
||||||
val relationshipCall = when (relationshipAction) {
|
val relationshipCall = when (relationshipAction) {
|
||||||
|
@ -257,7 +278,7 @@ class AccountViewModel @Inject constructor(
|
||||||
|
|
||||||
relationshipCall.fold(
|
relationshipCall.fold(
|
||||||
{ relationship ->
|
{ relationship ->
|
||||||
relationshipData.postValue(Success(relationship))
|
_relationshipData.value = Success(relationship)
|
||||||
|
|
||||||
when (relationshipAction) {
|
when (relationshipAction) {
|
||||||
RelationShipAction.UNFOLLOW -> eventHub.dispatch(UnfollowEvent(accountId))
|
RelationShipAction.UNFOLLOW -> eventHub.dispatch(UnfollowEvent(accountId))
|
||||||
|
@ -268,22 +289,22 @@ class AccountViewModel @Inject constructor(
|
||||||
},
|
},
|
||||||
{ t ->
|
{ t ->
|
||||||
Log.w(TAG, "failed loading relationship", t)
|
Log.w(TAG, "failed loading relationship", t)
|
||||||
relationshipData.postValue(Error(relation, cause = t))
|
_relationshipData.value = Error(relation, cause = t)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun noteChanged(newNote: String) {
|
fun noteChanged(newNote: String) {
|
||||||
noteSaved.postValue(false)
|
_noteSaved.value = false
|
||||||
noteUpdateJob?.cancel()
|
noteUpdateJob?.cancel()
|
||||||
noteUpdateJob = viewModelScope.launch {
|
noteUpdateJob = viewModelScope.launch {
|
||||||
delay(1500)
|
delay(1500)
|
||||||
mastodonApi.updateAccountNote(accountId, newNote)
|
mastodonApi.updateAccountNote(accountId, newNote)
|
||||||
.fold(
|
.fold(
|
||||||
{
|
{
|
||||||
noteSaved.postValue(true)
|
_noteSaved.value = true
|
||||||
delay(4000)
|
delay(4000)
|
||||||
noteSaved.postValue(false)
|
_noteSaved.value = false
|
||||||
},
|
},
|
||||||
{ t ->
|
{ t ->
|
||||||
Log.w(TAG, "Error updating note", t)
|
Log.w(TAG, "Error updating note", t)
|
||||||
|
@ -315,7 +336,14 @@ class AccountViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class RelationShipAction {
|
enum class RelationShipAction {
|
||||||
FOLLOW, UNFOLLOW, BLOCK, UNBLOCK, MUTE, UNMUTE, SUBSCRIBE, UNSUBSCRIBE
|
FOLLOW,
|
||||||
|
UNFOLLOW,
|
||||||
|
BLOCK,
|
||||||
|
UNBLOCK,
|
||||||
|
MUTE,
|
||||||
|
UNMUTE,
|
||||||
|
SUBSCRIBE,
|
||||||
|
UNSUBSCRIBE
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -0,0 +1,245 @@
|
||||||
|
/* Copyright Tusky Contributors
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.components.account.list
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.keylesspalace.tusky.ListsActivity
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.databinding.FragmentListsListBinding
|
||||||
|
import com.keylesspalace.tusky.databinding.ItemListBinding
|
||||||
|
import com.keylesspalace.tusky.entity.MastoList
|
||||||
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.util.visible
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.awaitCancellation
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class ListSelectionFragment : DialogFragment() {
|
||||||
|
|
||||||
|
interface ListSelectionListener {
|
||||||
|
fun onListSelected(list: MastoList)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val viewModel: ListsForAccountViewModel by viewModels()
|
||||||
|
|
||||||
|
private var selectListener: ListSelectionListener? = null
|
||||||
|
private var accountId: String? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
selectListener = context as? ListSelectionListener
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle)
|
||||||
|
accountId = requireArguments().getString(ARG_ACCOUNT_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val context = requireContext()
|
||||||
|
|
||||||
|
val binding = FragmentListsListBinding.inflate(layoutInflater)
|
||||||
|
val adapter = Adapter()
|
||||||
|
binding.listsView.adapter = adapter
|
||||||
|
|
||||||
|
val dialogBuilder = AlertDialog.Builder(context)
|
||||||
|
.setView(binding.root)
|
||||||
|
.setTitle(R.string.select_list_title)
|
||||||
|
.setNeutralButton(R.string.select_list_manage) { _, _ ->
|
||||||
|
val listIntent = Intent(context, ListsActivity::class.java)
|
||||||
|
startActivity(listIntent)
|
||||||
|
}
|
||||||
|
.setNegativeButton(if (accountId != null) R.string.button_done else android.R.string.cancel, null)
|
||||||
|
|
||||||
|
val dialog = dialogBuilder.create()
|
||||||
|
|
||||||
|
val showProgressBarJob = getProgressBarJob(binding.progressBar, 500)
|
||||||
|
showProgressBarJob.start()
|
||||||
|
|
||||||
|
// TODO change this to a (single) LoadState like elsewhere?
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.states.collectLatest { states ->
|
||||||
|
binding.progressBar.hide()
|
||||||
|
showProgressBarJob.cancel()
|
||||||
|
if (states.isEmpty()) {
|
||||||
|
binding.messageView.show()
|
||||||
|
binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.no_lists)
|
||||||
|
} else {
|
||||||
|
binding.listsView.show()
|
||||||
|
adapter.submitList(states)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.loadError.collectLatest { error ->
|
||||||
|
Log.e(TAG, "failed to load lists", error)
|
||||||
|
binding.progressBar.hide()
|
||||||
|
showProgressBarJob.cancel()
|
||||||
|
binding.listsView.hide()
|
||||||
|
binding.messageView.apply {
|
||||||
|
show()
|
||||||
|
setup(error) { load(binding) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.actionError.collectLatest { error ->
|
||||||
|
when (error.type) {
|
||||||
|
ActionError.Type.ADD -> {
|
||||||
|
Snackbar.make(
|
||||||
|
binding.root,
|
||||||
|
R.string.failed_to_add_to_list,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
)
|
||||||
|
.setAction(R.string.action_retry) {
|
||||||
|
viewModel.addAccountToList(accountId!!, error.listId)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
ActionError.Type.REMOVE -> {
|
||||||
|
Snackbar.make(
|
||||||
|
binding.root,
|
||||||
|
R.string.failed_to_remove_from_list,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
)
|
||||||
|
.setAction(R.string.action_retry) {
|
||||||
|
viewModel.removeAccountFromList(accountId!!, error.listId)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
load(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getProgressBarJob(progressView: View, delayMs: Long) = this.lifecycleScope.launch(
|
||||||
|
start = CoroutineStart.LAZY
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
delay(delayMs)
|
||||||
|
progressView.show()
|
||||||
|
awaitCancellation()
|
||||||
|
} finally {
|
||||||
|
progressView.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun load(binding: FragmentListsListBinding) {
|
||||||
|
binding.progressBar.show()
|
||||||
|
binding.listsView.hide()
|
||||||
|
binding.messageView.hide()
|
||||||
|
viewModel.load(accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private object Differ : DiffUtil.ItemCallback<AccountListState>() {
|
||||||
|
override fun areItemsTheSame(
|
||||||
|
oldItem: AccountListState,
|
||||||
|
newItem: AccountListState
|
||||||
|
): Boolean {
|
||||||
|
return oldItem.list.id == newItem.list.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(
|
||||||
|
oldItem: AccountListState,
|
||||||
|
newItem: AccountListState
|
||||||
|
): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class Adapter :
|
||||||
|
ListAdapter<AccountListState, BindingHolder<ItemListBinding>>(Differ) {
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): BindingHolder<ItemListBinding> {
|
||||||
|
return BindingHolder(ItemListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: BindingHolder<ItemListBinding>, position: Int) {
|
||||||
|
val item = getItem(position)
|
||||||
|
holder.binding.listName.text = item.list.title
|
||||||
|
accountId?.let { accountId ->
|
||||||
|
holder.binding.addButton.apply {
|
||||||
|
visible(!item.includesAccount)
|
||||||
|
setOnClickListener {
|
||||||
|
viewModel.addAccountToList(accountId, item.list.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder.binding.removeButton.apply {
|
||||||
|
visible(item.includesAccount)
|
||||||
|
setOnClickListener {
|
||||||
|
viewModel.removeAccountFromList(accountId, item.list.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.itemView.setOnClickListener {
|
||||||
|
selectListener?.onListSelected(item.list)
|
||||||
|
|
||||||
|
accountId?.let { accountId ->
|
||||||
|
if (item.includesAccount) {
|
||||||
|
viewModel.removeAccountFromList(accountId, item.list.id)
|
||||||
|
} else {
|
||||||
|
viewModel.addAccountToList(accountId, item.list.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "ListsListFragment"
|
||||||
|
private const val ARG_ACCOUNT_ID = "accountId"
|
||||||
|
|
||||||
|
fun newInstance(accountId: String?): ListSelectionFragment {
|
||||||
|
val args = Bundle().apply {
|
||||||
|
putString(ARG_ACCOUNT_ID, accountId)
|
||||||
|
}
|
||||||
|
return ListSelectionFragment().apply { arguments = args }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,200 +0,0 @@
|
||||||
/* Copyright 2022 kyori19
|
|
||||||
*
|
|
||||||
* This file is a part of Tusky.
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
||||||
* Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
||||||
* see <http://www.gnu.org/licenses>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.account.list
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import com.keylesspalace.tusky.R
|
|
||||||
import com.keylesspalace.tusky.databinding.FragmentListsForAccountBinding
|
|
||||||
import com.keylesspalace.tusky.databinding.ItemAddOrRemoveFromListBinding
|
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
|
||||||
import com.keylesspalace.tusky.util.hide
|
|
||||||
import com.keylesspalace.tusky.util.show
|
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
|
||||||
import com.keylesspalace.tusky.util.visible
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class ListsForAccountFragment : DialogFragment(), Injectable {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var viewModelFactory: ViewModelFactory
|
|
||||||
|
|
||||||
private val viewModel: ListsForAccountViewModel by viewModels { viewModelFactory }
|
|
||||||
private val binding by viewBinding(FragmentListsForAccountBinding::bind)
|
|
||||||
|
|
||||||
private val adapter = Adapter()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle)
|
|
||||||
|
|
||||||
viewModel.setup(requireArguments().getString(ARG_ACCOUNT_ID)!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
dialog?.apply {
|
|
||||||
window?.setLayout(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
return inflater.inflate(R.layout.fragment_lists_for_account, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
binding.listsView.layoutManager = LinearLayoutManager(view.context)
|
|
||||||
binding.listsView.adapter = adapter
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewModel.states.collectLatest { states ->
|
|
||||||
binding.progressBar.hide()
|
|
||||||
if (states.isEmpty()) {
|
|
||||||
binding.messageView.show()
|
|
||||||
binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.no_lists) {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.listsView.show()
|
|
||||||
adapter.submitList(states)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewModel.loadError.collectLatest { error ->
|
|
||||||
binding.progressBar.hide()
|
|
||||||
binding.listsView.hide()
|
|
||||||
binding.messageView.apply {
|
|
||||||
show()
|
|
||||||
setup(error) { load() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewModel.actionError.collectLatest { error ->
|
|
||||||
when (error.type) {
|
|
||||||
ActionError.Type.ADD -> {
|
|
||||||
Snackbar.make(binding.root, R.string.failed_to_add_to_list, Snackbar.LENGTH_LONG)
|
|
||||||
.setAction(R.string.action_retry) {
|
|
||||||
viewModel.addAccountToList(error.listId)
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
ActionError.Type.REMOVE -> {
|
|
||||||
Snackbar.make(binding.root, R.string.failed_to_remove_from_list, Snackbar.LENGTH_LONG)
|
|
||||||
.setAction(R.string.action_retry) {
|
|
||||||
viewModel.removeAccountFromList(error.listId)
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.doneButton.setOnClickListener {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun load() {
|
|
||||||
binding.progressBar.show()
|
|
||||||
binding.listsView.hide()
|
|
||||||
binding.messageView.hide()
|
|
||||||
viewModel.load()
|
|
||||||
}
|
|
||||||
|
|
||||||
private object Differ : DiffUtil.ItemCallback<AccountListState>() {
|
|
||||||
override fun areItemsTheSame(
|
|
||||||
oldItem: AccountListState,
|
|
||||||
newItem: AccountListState
|
|
||||||
): Boolean {
|
|
||||||
return oldItem.list.id == newItem.list.id
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(
|
|
||||||
oldItem: AccountListState,
|
|
||||||
newItem: AccountListState
|
|
||||||
): Boolean {
|
|
||||||
return oldItem == newItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class Adapter :
|
|
||||||
ListAdapter<AccountListState, BindingHolder<ItemAddOrRemoveFromListBinding>>(Differ) {
|
|
||||||
override fun onCreateViewHolder(
|
|
||||||
parent: ViewGroup,
|
|
||||||
viewType: Int
|
|
||||||
): BindingHolder<ItemAddOrRemoveFromListBinding> {
|
|
||||||
val binding =
|
|
||||||
ItemAddOrRemoveFromListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
|
||||||
return BindingHolder(binding)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BindingHolder<ItemAddOrRemoveFromListBinding>, position: Int) {
|
|
||||||
val item = getItem(position)
|
|
||||||
holder.binding.listNameView.text = item.list.title
|
|
||||||
holder.binding.addButton.apply {
|
|
||||||
visible(!item.includesAccount)
|
|
||||||
setOnClickListener {
|
|
||||||
viewModel.addAccountToList(item.list.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
holder.binding.removeButton.apply {
|
|
||||||
visible(item.includesAccount)
|
|
||||||
setOnClickListener {
|
|
||||||
viewModel.removeAccountFromList(item.list.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val ARG_ACCOUNT_ID = "accountId"
|
|
||||||
|
|
||||||
fun newInstance(accountId: String): ListsForAccountFragment {
|
|
||||||
val args = Bundle().apply {
|
|
||||||
putString(ARG_ACCOUNT_ID, accountId)
|
|
||||||
}
|
|
||||||
return ListsForAccountFragment().apply { arguments = args }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -24,14 +24,14 @@ import at.connyduck.calladapter.networkresult.onSuccess
|
||||||
import at.connyduck.calladapter.networkresult.runCatching
|
import at.connyduck.calladapter.networkresult.runCatching
|
||||||
import com.keylesspalace.tusky.entity.MastoList
|
import com.keylesspalace.tusky.entity.MastoList
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
data class AccountListState(
|
data class AccountListState(
|
||||||
val list: MastoList,
|
val list: MastoList,
|
||||||
|
@ -49,40 +49,36 @@ data class ActionError(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class ListsForAccountViewModel @Inject constructor(
|
class ListsForAccountViewModel @Inject constructor(
|
||||||
private val mastodonApi: MastodonApi
|
private val mastodonApi: MastodonApi
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private lateinit var accountId: String
|
|
||||||
|
|
||||||
private val _states = MutableSharedFlow<List<AccountListState>>(1)
|
private val _states = MutableSharedFlow<List<AccountListState>>(1)
|
||||||
val states: SharedFlow<List<AccountListState>> = _states
|
val states: SharedFlow<List<AccountListState>> = _states.asSharedFlow()
|
||||||
|
|
||||||
private val _loadError = MutableSharedFlow<Throwable>(1)
|
private val _loadError = MutableSharedFlow<Throwable>(1)
|
||||||
val loadError: SharedFlow<Throwable> = _loadError
|
val loadError: SharedFlow<Throwable> = _loadError.asSharedFlow()
|
||||||
|
|
||||||
private val _actionError = MutableSharedFlow<ActionError>(1)
|
private val _actionError = MutableSharedFlow<ActionError>(1)
|
||||||
val actionError: SharedFlow<ActionError> = _actionError
|
val actionError: SharedFlow<ActionError> = _actionError.asSharedFlow()
|
||||||
|
|
||||||
fun setup(accountId: String) {
|
fun load(accountId: String?) {
|
||||||
this.accountId = accountId
|
|
||||||
}
|
|
||||||
|
|
||||||
fun load() {
|
|
||||||
_loadError.resetReplayCache()
|
_loadError.resetReplayCache()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
val (all, includes) = listOf(
|
val all = mastodonApi.getLists().getOrThrow()
|
||||||
async { mastodonApi.getLists() },
|
var includes: List<MastoList> = emptyList()
|
||||||
async { mastodonApi.getListsIncludesAccount(accountId) }
|
if (accountId != null) {
|
||||||
).awaitAll()
|
includes = mastodonApi.getListsIncludesAccount(accountId).getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
_states.emit(
|
_states.emit(
|
||||||
all.getOrThrow().map { list ->
|
all.map { listState ->
|
||||||
AccountListState(
|
AccountListState(
|
||||||
list = list,
|
list = listState,
|
||||||
includesAccount = includes.getOrThrow().any { it.id == list.id }
|
includesAccount = includes.any { it.id == listState.id }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -93,7 +89,9 @@ class ListsForAccountViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addAccountToList(listId: String) {
|
// TODO there is no "progress" visible for these
|
||||||
|
|
||||||
|
fun addAccountToList(accountId: String, listId: String) {
|
||||||
_actionError.resetReplayCache()
|
_actionError.resetReplayCache()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
mastodonApi.addAccountToList(listId, listOf(accountId))
|
mastodonApi.addAccountToList(listId, listOf(accountId))
|
||||||
|
@ -114,7 +112,7 @@ class ListsForAccountViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAccountFromList(listId: String) {
|
fun removeAccountFromList(accountId: String, listId: String) {
|
||||||
_actionError.resetReplayCache()
|
_actionError.resetReplayCache()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
mastodonApi.deleteAccountFromList(listId, listOf(accountId))
|
mastodonApi.deleteAccountFromList(listId, listOf(accountId))
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.account.media
|
package com.keylesspalace.tusky.components.account.media
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
|
@ -28,15 +29,12 @@ import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.ViewMediaActivity
|
import com.keylesspalace.tusky.ViewMediaActivity
|
||||||
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
|
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.interfaces.RefreshableFragment
|
import com.keylesspalace.tusky.interfaces.RefreshableFragment
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
|
@ -49,30 +47,31 @@ import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
import com.mikepenz.iconics.utils.sizeDp
|
import com.mikepenz.iconics.utils.sizeDp
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment with multiple columns of media previews for the specified account.
|
* Fragment with multiple columns of media previews for the specified account.
|
||||||
*/
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
class AccountMediaFragment :
|
class AccountMediaFragment :
|
||||||
Fragment(R.layout.fragment_timeline),
|
Fragment(R.layout.fragment_timeline),
|
||||||
RefreshableFragment,
|
RefreshableFragment,
|
||||||
MenuProvider,
|
MenuProvider {
|
||||||
Injectable {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var viewModelFactory: ViewModelFactory
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var accountManager: AccountManager
|
lateinit var accountManager: AccountManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var preferences: SharedPreferences
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentTimelineBinding::bind)
|
private val binding by viewBinding(FragmentTimelineBinding::bind)
|
||||||
|
|
||||||
private val viewModel: AccountMediaViewModel by viewModels { viewModelFactory }
|
private val viewModel: AccountMediaViewModel by viewModels()
|
||||||
|
|
||||||
private lateinit var adapter: AccountMediaGridAdapter
|
private var adapter: AccountMediaGridAdapter? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -82,19 +81,23 @@ class AccountMediaFragment :
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||||
|
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(view.context)
|
|
||||||
val useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true)
|
val useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true)
|
||||||
|
|
||||||
adapter = AccountMediaGridAdapter(
|
val adapter = AccountMediaGridAdapter(
|
||||||
useBlurhash = useBlurhash,
|
useBlurhash = useBlurhash,
|
||||||
context = view.context,
|
context = view.context,
|
||||||
onAttachmentClickListener = ::onAttachmentClick
|
onAttachmentClickListener = ::onAttachmentClick
|
||||||
)
|
)
|
||||||
|
this.adapter = adapter
|
||||||
|
|
||||||
val columnCount = view.context.resources.getInteger(R.integer.profile_media_column_count)
|
val columnCount = view.context.resources.getInteger(R.integer.profile_media_column_count)
|
||||||
val imageSpacing = view.context.resources.getDimensionPixelSize(R.dimen.profile_media_spacing)
|
val imageSpacing = view.context.resources.getDimensionPixelSize(
|
||||||
|
R.dimen.profile_media_spacing
|
||||||
|
)
|
||||||
|
|
||||||
binding.recyclerView.addItemDecoration(GridSpacingItemDecoration(columnCount, imageSpacing, 0))
|
binding.recyclerView.addItemDecoration(
|
||||||
|
GridSpacingItemDecoration(columnCount, imageSpacing, 0)
|
||||||
|
)
|
||||||
|
|
||||||
binding.recyclerView.layoutManager = GridLayoutManager(view.context, columnCount)
|
binding.recyclerView.layoutManager = GridLayoutManager(view.context, columnCount)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
@ -124,7 +127,11 @@ class AccountMediaFragment :
|
||||||
is LoadState.NotLoading -> {
|
is LoadState.NotLoading -> {
|
||||||
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
|
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
|
||||||
binding.statusView.show()
|
binding.statusView.show()
|
||||||
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null)
|
binding.statusView.setup(
|
||||||
|
R.drawable.elephant_friend_empty,
|
||||||
|
R.string.message_empty,
|
||||||
|
null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is LoadState.Error -> {
|
is LoadState.Error -> {
|
||||||
|
@ -139,6 +146,12 @@ class AccountMediaFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
// Clear the adapter to prevent leaking the View
|
||||||
|
adapter = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
menuInflater.inflate(R.menu.fragment_account_media, menu)
|
menuInflater.inflate(R.menu.fragment_account_media, menu)
|
||||||
menu.findItem(R.id.action_refresh)?.apply {
|
menu.findItem(R.id.action_refresh)?.apply {
|
||||||
|
@ -175,11 +188,19 @@ class AccountMediaFragment :
|
||||||
Attachment.Type.GIFV,
|
Attachment.Type.GIFV,
|
||||||
Attachment.Type.VIDEO,
|
Attachment.Type.VIDEO,
|
||||||
Attachment.Type.AUDIO -> {
|
Attachment.Type.AUDIO -> {
|
||||||
val intent = ViewMediaActivity.newIntent(context, attachmentsFromSameStatus, currentIndex)
|
val intent = ViewMediaActivity.newIntent(
|
||||||
|
context,
|
||||||
|
attachmentsFromSameStatus,
|
||||||
|
currentIndex
|
||||||
|
)
|
||||||
if (activity != null) {
|
if (activity != null) {
|
||||||
val url = selected.attachment.url
|
val url = selected.attachment.url
|
||||||
ViewCompat.setTransitionName(view, url)
|
ViewCompat.setTransitionName(view, url)
|
||||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, url)
|
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||||
|
requireActivity(),
|
||||||
|
view,
|
||||||
|
url
|
||||||
|
)
|
||||||
startActivity(intent, options.toBundle())
|
startActivity(intent, options.toBundle())
|
||||||
} else {
|
} else {
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
@ -192,7 +213,7 @@ class AccountMediaFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refreshContent() {
|
override fun refreshContent() {
|
||||||
adapter.refresh()
|
adapter?.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -5,8 +5,8 @@ import android.graphics.Color
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.appcompat.widget.TooltipCompat
|
||||||
import androidx.core.view.setPadding
|
import androidx.core.view.setPadding
|
||||||
import androidx.paging.PagingDataAdapter
|
import androidx.paging.PagingDataAdapter
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
@ -21,7 +21,7 @@ import com.keylesspalace.tusky.util.getFormattedDescription
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||||
import java.util.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
class AccountMediaGridAdapter(
|
class AccountMediaGridAdapter(
|
||||||
private val useBlurhash: Boolean,
|
private val useBlurhash: Boolean,
|
||||||
|
@ -29,27 +29,49 @@ class AccountMediaGridAdapter(
|
||||||
private val onAttachmentClickListener: (AttachmentViewData, View) -> Unit
|
private val onAttachmentClickListener: (AttachmentViewData, View) -> Unit
|
||||||
) : PagingDataAdapter<AttachmentViewData, BindingHolder<ItemAccountMediaBinding>>(
|
) : PagingDataAdapter<AttachmentViewData, BindingHolder<ItemAccountMediaBinding>>(
|
||||||
object : DiffUtil.ItemCallback<AttachmentViewData>() {
|
object : DiffUtil.ItemCallback<AttachmentViewData>() {
|
||||||
override fun areItemsTheSame(oldItem: AttachmentViewData, newItem: AttachmentViewData): Boolean {
|
override fun areItemsTheSame(
|
||||||
|
oldItem: AttachmentViewData,
|
||||||
|
newItem: AttachmentViewData
|
||||||
|
): Boolean {
|
||||||
return oldItem.attachment.id == newItem.attachment.id
|
return oldItem.attachment.id == newItem.attachment.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: AttachmentViewData, newItem: AttachmentViewData): Boolean {
|
override fun areContentsTheSame(
|
||||||
|
oldItem: AttachmentViewData,
|
||||||
|
newItem: AttachmentViewData
|
||||||
|
): Boolean {
|
||||||
return oldItem == newItem
|
return oldItem == newItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val baseItemBackgroundColor = MaterialColors.getColor(context, com.google.android.material.R.attr.colorSurface, Color.BLACK)
|
private val baseItemBackgroundColor = MaterialColors.getColor(
|
||||||
private val videoIndicator = AppCompatResources.getDrawable(context, R.drawable.ic_play_indicator)
|
context,
|
||||||
private val mediaHiddenDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_hide_media_24dp)
|
com.google.android.material.R.attr.colorSurface,
|
||||||
|
Color.BLACK
|
||||||
|
)
|
||||||
|
private val videoIndicator = AppCompatResources.getDrawable(
|
||||||
|
context,
|
||||||
|
R.drawable.ic_play_indicator
|
||||||
|
)
|
||||||
|
private val mediaHiddenDrawable = AppCompatResources.getDrawable(
|
||||||
|
context,
|
||||||
|
R.drawable.ic_hide_media_24dp
|
||||||
|
)
|
||||||
|
|
||||||
private val itemBgBaseHSV = FloatArray(3)
|
private val itemBgBaseHSV = FloatArray(3)
|
||||||
private val random = Random()
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAccountMediaBinding> {
|
override fun onCreateViewHolder(
|
||||||
val binding = ItemAccountMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): BindingHolder<ItemAccountMediaBinding> {
|
||||||
|
val binding = ItemAccountMediaBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
Color.colorToHSV(baseItemBackgroundColor, itemBgBaseHSV)
|
Color.colorToHSV(baseItemBackgroundColor, itemBgBaseHSV)
|
||||||
itemBgBaseHSV[2] = itemBgBaseHSV[2] + random.nextFloat() / 3f - 1f / 6f
|
itemBgBaseHSV[2] = itemBgBaseHSV[2] + Random.nextFloat() / 3f - 1f / 6f
|
||||||
binding.root.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV))
|
binding.root.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV))
|
||||||
return BindingHolder(binding)
|
return BindingHolder(binding)
|
||||||
}
|
}
|
||||||
|
@ -71,7 +93,11 @@ class AccountMediaGridAdapter(
|
||||||
if (item.attachment.type == Attachment.Type.AUDIO) {
|
if (item.attachment.type == Attachment.Type.AUDIO) {
|
||||||
overlay.hide()
|
overlay.hide()
|
||||||
|
|
||||||
imageView.setPadding(context.resources.getDimensionPixelSize(R.dimen.profile_media_audio_icon_padding))
|
imageView.setPadding(
|
||||||
|
context.resources.getDimensionPixelSize(
|
||||||
|
R.dimen.profile_media_audio_icon_padding
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
Glide.with(imageView)
|
Glide.with(imageView)
|
||||||
.load(R.drawable.ic_music_box_preview_24dp)
|
.load(R.drawable.ic_music_box_preview_24dp)
|
||||||
|
@ -115,11 +141,7 @@ class AccountMediaGridAdapter(
|
||||||
onAttachmentClickListener(item, imageView)
|
onAttachmentClickListener(item, imageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.binding.root.setOnLongClickListener { view ->
|
TooltipCompat.setTooltipText(holder.binding.root, imageView.contentDescription)
|
||||||
val description = item.attachment.getFormattedDescription(view.context)
|
|
||||||
Toast.makeText(view.context, description, Toast.LENGTH_LONG).show()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ import androidx.paging.LoadType
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import androidx.paging.RemoteMediator
|
import androidx.paging.RemoteMediator
|
||||||
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.entity.AccountEntity
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
|
@ -59,7 +59,15 @@ class AccountMediaRemoteMediator(
|
||||||
}
|
}
|
||||||
|
|
||||||
val attachments = statuses.flatMap { status ->
|
val attachments = statuses.flatMap { status ->
|
||||||
AttachmentViewData.list(status, activeAccount.alwaysShowSensitiveMedia ?: false)
|
status.attachments.map { attachment ->
|
||||||
|
AttachmentViewData(
|
||||||
|
attachment = attachment,
|
||||||
|
statusId = status.id,
|
||||||
|
statusUrl = status.url.orEmpty(),
|
||||||
|
sensitive = status.sensitive,
|
||||||
|
isRevealed = activeAccount.alwaysShowSensitiveMedia || !status.sensitive
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadType == LoadType.REFRESH) {
|
if (loadType == LoadType.REFRESH) {
|
||||||
|
|
|
@ -24,10 +24,12 @@ import androidx.paging.cachedIn
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
class AccountMediaViewModel @Inject constructor(
|
class AccountMediaViewModel @Inject constructor(
|
||||||
private val accountManager: AccountManager,
|
accountManager: AccountManager,
|
||||||
api: MastodonApi
|
api: MastodonApi
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
|
|
@ -22,14 +22,11 @@ import androidx.fragment.app.commit
|
||||||
import com.keylesspalace.tusky.BottomSheetActivity
|
import com.keylesspalace.tusky.BottomSheetActivity
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ActivityAccountListBinding
|
import com.keylesspalace.tusky.databinding.ActivityAccountListBinding
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import com.keylesspalace.tusky.util.getSerializableExtraCompat
|
||||||
import dagger.android.HasAndroidInjector
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class AccountListActivity : BottomSheetActivity(), HasAndroidInjector {
|
@AndroidEntryPoint
|
||||||
|
class AccountListActivity : BottomSheetActivity() {
|
||||||
@Inject
|
|
||||||
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
|
|
||||||
|
|
||||||
enum class Type {
|
enum class Type {
|
||||||
FOLLOWS,
|
FOLLOWS,
|
||||||
|
@ -46,7 +43,7 @@ class AccountListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
val binding = ActivityAccountListBinding.inflate(layoutInflater)
|
val binding = ActivityAccountListBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
val type = intent.getSerializableExtra(EXTRA_TYPE) as Type
|
val type = intent.getSerializableExtraCompat<Type>(EXTRA_TYPE)!!
|
||||||
val id: String? = intent.getStringExtra(EXTRA_ID)
|
val id: String? = intent.getStringExtra(EXTRA_ID)
|
||||||
|
|
||||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||||
|
@ -69,8 +66,6 @@ class AccountListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun androidInjector() = dispatchingAndroidInjector
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val EXTRA_TYPE = "type"
|
private const val EXTRA_TYPE = "type"
|
||||||
private const val EXTRA_ID = "id"
|
private const val EXTRA_ID = "id"
|
||||||
|
|
|
@ -15,23 +15,19 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.accountlist
|
package com.keylesspalace.tusky.components.accountlist
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
|
||||||
import autodispose2.autoDispose
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.BaseActivity
|
|
||||||
import com.keylesspalace.tusky.BottomSheetActivity
|
import com.keylesspalace.tusky.BottomSheetActivity
|
||||||
import com.keylesspalace.tusky.PostLookupFallbackBehavior
|
import com.keylesspalace.tusky.PostLookupFallbackBehavior
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
|
@ -46,7 +42,6 @@ import com.keylesspalace.tusky.components.accountlist.adapter.FollowRequestsHead
|
||||||
import com.keylesspalace.tusky.components.accountlist.adapter.MutesAdapter
|
import com.keylesspalace.tusky.components.accountlist.adapter.MutesAdapter
|
||||||
import com.keylesspalace.tusky.databinding.FragmentAccountListBinding
|
import com.keylesspalace.tusky.databinding.FragmentAccountListBinding
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
|
||||||
import com.keylesspalace.tusky.entity.Relationship
|
import com.keylesspalace.tusky.entity.Relationship
|
||||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
|
@ -54,20 +49,23 @@ import com.keylesspalace.tusky.interfaces.LinkListener
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.HttpHeaderLink
|
import com.keylesspalace.tusky.util.HttpHeaderLink
|
||||||
|
import com.keylesspalace.tusky.util.getSerializableCompat
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener
|
import com.keylesspalace.tusky.view.EndlessOnScrollListener
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class AccountListFragment :
|
class AccountListFragment :
|
||||||
Fragment(R.layout.fragment_account_list),
|
Fragment(R.layout.fragment_account_list),
|
||||||
AccountActionListener,
|
AccountActionListener,
|
||||||
LinkListener,
|
LinkListener {
|
||||||
Injectable {
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var api: MastodonApi
|
lateinit var api: MastodonApi
|
||||||
|
@ -75,19 +73,21 @@ class AccountListFragment :
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var accountManager: AccountManager
|
lateinit var accountManager: AccountManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var preferences: SharedPreferences
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentAccountListBinding::bind)
|
private val binding by viewBinding(FragmentAccountListBinding::bind)
|
||||||
|
|
||||||
private lateinit var type: Type
|
private lateinit var type: Type
|
||||||
private var id: String? = null
|
private var id: String? = null
|
||||||
|
|
||||||
private lateinit var scrollListener: EndlessOnScrollListener
|
private var adapter: AccountAdapter<*>? = null
|
||||||
private lateinit var adapter: AccountAdapter<*>
|
|
||||||
private var fetching = false
|
private var fetching = false
|
||||||
private var bottomId: String? = null
|
private var bottomId: String? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
type = requireArguments().getSerializable(ARG_TYPE) as Type
|
type = requireArguments().getSerializableCompat(ARG_TYPE)!!
|
||||||
id = requireArguments().getString(ARG_ID)
|
id = requireArguments().getString(ARG_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,19 +96,17 @@ class AccountListFragment :
|
||||||
val layoutManager = LinearLayoutManager(view.context)
|
val layoutManager = LinearLayoutManager(view.context)
|
||||||
binding.recyclerView.layoutManager = layoutManager
|
binding.recyclerView.layoutManager = layoutManager
|
||||||
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
binding.recyclerView.addItemDecoration(
|
||||||
|
DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)
|
||||||
|
)
|
||||||
|
|
||||||
binding.swipeRefreshLayout.setOnRefreshListener { fetchAccounts() }
|
val animateAvatar = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
|
||||||
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||||
|
val showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true)
|
||||||
val pm = PreferenceManager.getDefaultSharedPreferences(view.context)
|
|
||||||
val animateAvatar = pm.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
|
|
||||||
val animateEmojis = pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
|
||||||
val showBotOverlay = pm.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true)
|
|
||||||
|
|
||||||
val activeAccount = accountManager.activeAccount!!
|
val activeAccount = accountManager.activeAccount!!
|
||||||
|
|
||||||
adapter = when (type) {
|
val adapter = when (type) {
|
||||||
Type.BLOCKS -> BlocksAdapter(this, animateAvatar, animateEmojis, showBotOverlay)
|
Type.BLOCKS -> BlocksAdapter(this, animateAvatar, animateEmojis, showBotOverlay)
|
||||||
Type.MUTES -> MutesAdapter(this, animateAvatar, animateEmojis, showBotOverlay)
|
Type.MUTES -> MutesAdapter(this, animateAvatar, animateEmojis, showBotOverlay)
|
||||||
Type.FOLLOW_REQUESTS -> {
|
Type.FOLLOW_REQUESTS -> {
|
||||||
|
@ -116,40 +114,49 @@ class AccountListFragment :
|
||||||
instanceName = activeAccount.domain,
|
instanceName = activeAccount.domain,
|
||||||
accountLocked = activeAccount.locked
|
accountLocked = activeAccount.locked
|
||||||
)
|
)
|
||||||
val followRequestsAdapter = FollowRequestsAdapter(this, this, animateAvatar, animateEmojis, showBotOverlay)
|
val followRequestsAdapter =
|
||||||
|
FollowRequestsAdapter(this, this, animateAvatar, animateEmojis, showBotOverlay)
|
||||||
binding.recyclerView.adapter = ConcatAdapter(headerAdapter, followRequestsAdapter)
|
binding.recyclerView.adapter = ConcatAdapter(headerAdapter, followRequestsAdapter)
|
||||||
followRequestsAdapter
|
followRequestsAdapter
|
||||||
}
|
}
|
||||||
else -> FollowAdapter(this, animateAvatar, animateEmojis, showBotOverlay)
|
else -> FollowAdapter(this, animateAvatar, animateEmojis, showBotOverlay)
|
||||||
}
|
}
|
||||||
|
this.adapter = adapter
|
||||||
if (binding.recyclerView.adapter == null) {
|
if (binding.recyclerView.adapter == null) {
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollListener = object : EndlessOnScrollListener(layoutManager) {
|
val scrollListener = object : EndlessOnScrollListener(layoutManager) {
|
||||||
override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) {
|
override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) {
|
||||||
if (bottomId == null) {
|
if (bottomId == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fetchAccounts(bottomId)
|
fetchAccounts(adapter, bottomId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.recyclerView.addOnScrollListener(scrollListener)
|
binding.recyclerView.addOnScrollListener(scrollListener)
|
||||||
|
|
||||||
fetchAccounts()
|
binding.swipeRefreshLayout.setOnRefreshListener { fetchAccounts(adapter) }
|
||||||
|
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||||
|
|
||||||
|
fetchAccounts(adapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
// Clear the adapter to prevent leaking the View
|
||||||
|
adapter = null
|
||||||
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewTag(tag: String) {
|
override fun onViewTag(tag: String) {
|
||||||
(activity as BaseActivity?)
|
activity?.startActivityWithSlideInAnimation(
|
||||||
?.startActivityWithSlideInAnimation(StatusListActivity.newHashtagIntent(requireContext(), tag))
|
StatusListActivity.newHashtagIntent(requireContext(), tag)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewAccount(id: String) {
|
override fun onViewAccount(id: String) {
|
||||||
(activity as BaseActivity?)?.let {
|
activity?.startActivityWithSlideInAnimation(AccountActivity.getIntent(requireContext(), id))
|
||||||
val intent = AccountActivity.getIntent(it, id)
|
|
||||||
it.startActivityWithSlideInAnimation(intent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewUrl(url: String) {
|
override fun onViewUrl(url: String) {
|
||||||
|
@ -225,7 +232,11 @@ class AccountListFragment :
|
||||||
val unblockedUser = blocksAdapter.removeItem(position)
|
val unblockedUser = blocksAdapter.removeItem(position)
|
||||||
|
|
||||||
if (unblockedUser != null) {
|
if (unblockedUser != null) {
|
||||||
Snackbar.make(binding.recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG)
|
Snackbar.make(
|
||||||
|
binding.recyclerView,
|
||||||
|
R.string.confirmation_unblocked,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
)
|
||||||
.setAction(R.string.action_undo) {
|
.setAction(R.string.action_undo) {
|
||||||
blocksAdapter.addItem(unblockedUser, position)
|
blocksAdapter.addItem(unblockedUser, position)
|
||||||
onBlock(true, id, position)
|
onBlock(true, id, position)
|
||||||
|
@ -243,30 +254,26 @@ class AccountListFragment :
|
||||||
Log.e(TAG, "Failed to $verb account accountId $accountId")
|
Log.e(TAG, "Failed to $verb account accountId $accountId")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRespondToFollowRequest(
|
override fun onRespondToFollowRequest(accept: Boolean, id: String, position: Int) {
|
||||||
accept: Boolean,
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
accountId: String,
|
if (accept) {
|
||||||
position: Int
|
api.authorizeFollowRequest(id)
|
||||||
) {
|
} else {
|
||||||
if (accept) {
|
api.rejectFollowRequest(id)
|
||||||
api.authorizeFollowRequest(accountId)
|
}.fold(
|
||||||
} else {
|
onSuccess = {
|
||||||
api.rejectFollowRequest(accountId)
|
|
||||||
}.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
|
||||||
.subscribe(
|
|
||||||
{
|
|
||||||
onRespondToFollowRequestSuccess(position)
|
onRespondToFollowRequestSuccess(position)
|
||||||
},
|
},
|
||||||
{ throwable ->
|
onFailure = { throwable ->
|
||||||
val verb = if (accept) {
|
val verb = if (accept) {
|
||||||
"accept"
|
"accept"
|
||||||
} else {
|
} else {
|
||||||
"reject"
|
"reject"
|
||||||
}
|
}
|
||||||
Log.e(TAG, "Failed to $verb account id $accountId.", throwable)
|
Log.e(TAG, "Failed to $verb account id $id.", throwable)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onRespondToFollowRequestSuccess(position: Int) {
|
private fun onRespondToFollowRequestSuccess(position: Int) {
|
||||||
|
@ -302,7 +309,7 @@ class AccountListFragment :
|
||||||
return requireNotNull(id) { "id must not be null for type " + type.name }
|
return requireNotNull(id) { "id must not be null for type " + type.name }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchAccounts(fromId: String? = null) {
|
private fun fetchAccounts(adapter: AccountAdapter<*>, fromId: String? = null) {
|
||||||
if (fetching) {
|
if (fetching) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -318,26 +325,36 @@ class AccountListFragment :
|
||||||
val response = getFetchCallByListType(fromId)
|
val response = getFetchCallByListType(fromId)
|
||||||
|
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
onFetchAccountsFailure(Exception(response.message()))
|
onFetchAccountsFailure(adapter, Exception(response.message()))
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val accountList = response.body()
|
val accountList = response.body()
|
||||||
|
|
||||||
if (accountList == null) {
|
if (accountList == null) {
|
||||||
onFetchAccountsFailure(Exception(response.message()))
|
onFetchAccountsFailure(adapter, Exception(response.message()))
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val linkHeader = response.headers()["Link"]
|
val linkHeader = response.headers()["Link"]
|
||||||
onFetchAccountsSuccess(accountList, linkHeader)
|
onFetchAccountsSuccess(adapter, accountList, linkHeader)
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
onFetchAccountsFailure(exception)
|
if (exception is CancellationException) {
|
||||||
|
// Scope is cancelled, probably because the fragment is destroyed.
|
||||||
|
// We must not touch any views anymore, so rethrow the exception.
|
||||||
|
// (CancellationException in a cancelled scope is normal and will be ignored)
|
||||||
|
throw exception
|
||||||
|
}
|
||||||
|
onFetchAccountsFailure(adapter, exception)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFetchAccountsSuccess(accounts: List<TimelineAccount>, linkHeader: String?) {
|
private fun onFetchAccountsSuccess(
|
||||||
|
adapter: AccountAdapter<*>,
|
||||||
|
accounts: List<TimelineAccount>,
|
||||||
|
linkHeader: String?
|
||||||
|
) {
|
||||||
adapter.setBottomLoading(false)
|
adapter.setBottomLoading(false)
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
|
|
||||||
|
@ -352,7 +369,7 @@ class AccountListFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
if (adapter is MutesAdapter) {
|
if (adapter is MutesAdapter) {
|
||||||
fetchRelationships(accounts.map { it.id })
|
fetchRelationships(adapter, accounts.map { it.id })
|
||||||
}
|
}
|
||||||
|
|
||||||
bottomId = fromId
|
bottomId = fromId
|
||||||
|
@ -371,23 +388,30 @@ class AccountListFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchRelationships(ids: List<String>) {
|
private fun fetchRelationships(mutesAdapter: MutesAdapter, ids: List<String>) {
|
||||||
lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
api.relationships(ids)
|
api.relationships(ids)
|
||||||
.fold(::onFetchRelationshipsSuccess) { throwable ->
|
.fold(
|
||||||
Log.e(TAG, "Fetch failure for relationships of accounts: $ids", throwable)
|
onSuccess = { relationships ->
|
||||||
}
|
onFetchRelationshipsSuccess(mutesAdapter, relationships)
|
||||||
|
},
|
||||||
|
onFailure = { throwable ->
|
||||||
|
Log.e(TAG, "Fetch failure for relationships of accounts: $ids", throwable)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFetchRelationshipsSuccess(relationships: List<Relationship>) {
|
private fun onFetchRelationshipsSuccess(
|
||||||
val mutesAdapter = adapter as MutesAdapter
|
mutesAdapter: MutesAdapter,
|
||||||
|
relationships: List<Relationship>
|
||||||
|
) {
|
||||||
val mutingNotificationsMap = HashMap<String, Boolean>()
|
val mutingNotificationsMap = HashMap<String, Boolean>()
|
||||||
relationships.map { mutingNotificationsMap.put(it.id, it.mutingNotifications) }
|
relationships.map { mutingNotificationsMap.put(it.id, it.mutingNotifications) }
|
||||||
mutesAdapter.updateMutingNotificationsMap(mutingNotificationsMap)
|
mutesAdapter.updateMutingNotificationsMap(mutingNotificationsMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFetchAccountsFailure(throwable: Throwable) {
|
private fun onFetchAccountsFailure(adapter: AccountAdapter<*>, throwable: Throwable) {
|
||||||
fetching = false
|
fetching = false
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
Log.e(TAG, "Fetch failure", throwable)
|
Log.e(TAG, "Fetch failure", throwable)
|
||||||
|
@ -396,7 +420,7 @@ class AccountListFragment :
|
||||||
binding.messageView.show()
|
binding.messageView.show()
|
||||||
binding.messageView.setup(throwable) {
|
binding.messageView.setup(throwable) {
|
||||||
binding.messageView.hide()
|
binding.messageView.hide()
|
||||||
this.fetchAccounts(null)
|
this.fetchAccounts(adapter, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import com.keylesspalace.tusky.databinding.ItemFooterBinding
|
||||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
import com.keylesspalace.tusky.util.removeDuplicates
|
import com.keylesspalace.tusky.util.removeDuplicatesTo
|
||||||
|
|
||||||
/** Generic adapter with bottom loading indicator. */
|
/** Generic adapter with bottom loading indicator. */
|
||||||
abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructor(
|
abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructor(
|
||||||
|
@ -60,9 +60,7 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createFooterViewHolder(
|
private fun createFooterViewHolder(parent: ViewGroup): RecyclerView.ViewHolder {
|
||||||
parent: ViewGroup
|
|
||||||
): RecyclerView.ViewHolder {
|
|
||||||
val binding = ItemFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding = ItemFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return BindingHolder(binding)
|
return BindingHolder(binding)
|
||||||
}
|
}
|
||||||
|
@ -76,7 +74,7 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(newAccounts: List<TimelineAccount>) {
|
fun update(newAccounts: List<TimelineAccount>) {
|
||||||
accountList = removeDuplicates(newAccounts)
|
accountList = newAccounts.removeDuplicatesTo(ArrayList())
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,16 +39,27 @@ class BlocksAdapter(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
override fun createAccountViewHolder(parent: ViewGroup): BindingHolder<ItemBlockedUserBinding> {
|
override fun createAccountViewHolder(parent: ViewGroup): BindingHolder<ItemBlockedUserBinding> {
|
||||||
val binding = ItemBlockedUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding = ItemBlockedUserBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
return BindingHolder(binding)
|
return BindingHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindAccountViewHolder(viewHolder: BindingHolder<ItemBlockedUserBinding>, position: Int) {
|
override fun onBindAccountViewHolder(
|
||||||
|
viewHolder: BindingHolder<ItemBlockedUserBinding>,
|
||||||
|
position: Int
|
||||||
|
) {
|
||||||
val account = accountList[position]
|
val account = accountList[position]
|
||||||
val binding = viewHolder.binding
|
val binding = viewHolder.binding
|
||||||
val context = binding.root.context
|
val context = binding.root.context
|
||||||
|
|
||||||
val emojifiedName = account.name.emojify(account.emojis, binding.blockedUserDisplayName, animateEmojis)
|
val emojifiedName = account.name.emojify(
|
||||||
|
account.emojis,
|
||||||
|
binding.blockedUserDisplayName,
|
||||||
|
animateEmojis
|
||||||
|
)
|
||||||
binding.blockedUserDisplayName.text = emojifiedName
|
binding.blockedUserDisplayName.text = emojifiedName
|
||||||
val formattedUsername = context.getString(R.string.post_username_format, account.username)
|
val formattedUsername = context.getString(R.string.post_username_format, account.username)
|
||||||
binding.blockedUserUsername.text = formattedUsername
|
binding.blockedUserUsername.text = formattedUsername
|
||||||
|
|
|
@ -44,6 +44,7 @@ class FollowRequestsAdapter(
|
||||||
)
|
)
|
||||||
return FollowRequestViewHolder(
|
return FollowRequestViewHolder(
|
||||||
binding,
|
binding,
|
||||||
|
accountActionListener,
|
||||||
linkListener,
|
linkListener,
|
||||||
showHeader = false
|
showHeader = false
|
||||||
)
|
)
|
||||||
|
|
|
@ -27,12 +27,22 @@ class FollowRequestsHeaderAdapter(
|
||||||
private val accountLocked: Boolean
|
private val accountLocked: Boolean
|
||||||
) : RecyclerView.Adapter<BindingHolder<ItemFollowRequestsHeaderBinding>>() {
|
) : RecyclerView.Adapter<BindingHolder<ItemFollowRequestsHeaderBinding>>() {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestsHeaderBinding> {
|
override fun onCreateViewHolder(
|
||||||
val binding = ItemFollowRequestsHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): BindingHolder<ItemFollowRequestsHeaderBinding> {
|
||||||
|
val binding = ItemFollowRequestsHeaderBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
return BindingHolder(binding)
|
return BindingHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(viewHolder: BindingHolder<ItemFollowRequestsHeaderBinding>, position: Int) {
|
override fun onBindViewHolder(
|
||||||
|
viewHolder: BindingHolder<ItemFollowRequestsHeaderBinding>,
|
||||||
|
position: Int
|
||||||
|
) {
|
||||||
viewHolder.binding.root.text = viewHolder.binding.root.context.getString(R.string.follow_requests_info, instanceName)
|
viewHolder.binding.root.text = viewHolder.binding.root.context.getString(R.string.follow_requests_info, instanceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,18 +42,29 @@ class MutesAdapter(
|
||||||
private val mutingNotificationsMap = HashMap<String, Boolean>()
|
private val mutingNotificationsMap = HashMap<String, Boolean>()
|
||||||
|
|
||||||
override fun createAccountViewHolder(parent: ViewGroup): BindingHolder<ItemMutedUserBinding> {
|
override fun createAccountViewHolder(parent: ViewGroup): BindingHolder<ItemMutedUserBinding> {
|
||||||
val binding = ItemMutedUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding = ItemMutedUserBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
return BindingHolder(binding)
|
return BindingHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindAccountViewHolder(viewHolder: BindingHolder<ItemMutedUserBinding>, position: Int) {
|
override fun onBindAccountViewHolder(
|
||||||
|
viewHolder: BindingHolder<ItemMutedUserBinding>,
|
||||||
|
position: Int
|
||||||
|
) {
|
||||||
val account = accountList[position]
|
val account = accountList[position]
|
||||||
val binding = viewHolder.binding
|
val binding = viewHolder.binding
|
||||||
val context = binding.root.context
|
val context = binding.root.context
|
||||||
|
|
||||||
val mutingNotifications = mutingNotificationsMap[account.id]
|
val mutingNotifications = mutingNotificationsMap[account.id]
|
||||||
|
|
||||||
val emojifiedName = account.name.emojify(account.emojis, binding.mutedUserDisplayName, animateEmojis)
|
val emojifiedName = account.name.emojify(
|
||||||
|
account.emojis,
|
||||||
|
binding.mutedUserDisplayName,
|
||||||
|
animateEmojis
|
||||||
|
)
|
||||||
binding.mutedUserDisplayName.text = emojifiedName
|
binding.mutedUserDisplayName.text = emojifiedName
|
||||||
|
|
||||||
val formattedUsername = context.getString(R.string.post_username_format, account.username)
|
val formattedUsername = context.getString(R.string.post_username_format, account.username)
|
||||||
|
|
|
@ -16,8 +16,9 @@
|
||||||
package com.keylesspalace.tusky.components.announcements
|
package com.keylesspalace.tusky.components.announcements
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableString
|
||||||
import android.view.ContextThemeWrapper
|
import android.view.ContextThemeWrapper
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -25,6 +26,7 @@ import android.view.ViewGroup
|
||||||
import androidx.core.view.size
|
import androidx.core.view.size
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.request.target.Target
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ItemAnnouncementBinding
|
import com.keylesspalace.tusky.databinding.ItemAnnouncementBinding
|
||||||
|
@ -33,11 +35,12 @@ import com.keylesspalace.tusky.interfaces.LinkListener
|
||||||
import com.keylesspalace.tusky.util.AbsoluteTimeFormatter
|
import com.keylesspalace.tusky.util.AbsoluteTimeFormatter
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
import com.keylesspalace.tusky.util.EmojiSpan
|
import com.keylesspalace.tusky.util.EmojiSpan
|
||||||
|
import com.keylesspalace.tusky.util.clearEmojiTargets
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||||
import com.keylesspalace.tusky.util.setClickableText
|
import com.keylesspalace.tusky.util.setClickableText
|
||||||
|
import com.keylesspalace.tusky.util.setEmojiTargets
|
||||||
import com.keylesspalace.tusky.util.visible
|
import com.keylesspalace.tusky.util.visible
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
|
|
||||||
interface AnnouncementActionListener : LinkListener {
|
interface AnnouncementActionListener : LinkListener {
|
||||||
fun openReactionPicker(announcementId: String, target: View)
|
fun openReactionPicker(announcementId: String, target: View)
|
||||||
|
@ -54,8 +57,15 @@ class AnnouncementAdapter(
|
||||||
|
|
||||||
private val absoluteTimeFormatter = AbsoluteTimeFormatter()
|
private val absoluteTimeFormatter = AbsoluteTimeFormatter()
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAnnouncementBinding> {
|
override fun onCreateViewHolder(
|
||||||
val binding = ItemAnnouncementBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): BindingHolder<ItemAnnouncementBinding> {
|
||||||
|
val binding = ItemAnnouncementBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
return BindingHolder(binding)
|
return BindingHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +79,11 @@ class AnnouncementAdapter(
|
||||||
val chips = holder.binding.chipGroup
|
val chips = holder.binding.chipGroup
|
||||||
val addReactionChip = holder.binding.addReactionChip
|
val addReactionChip = holder.binding.addReactionChip
|
||||||
|
|
||||||
val emojifiedText: CharSequence = item.content.parseAsMastodonHtml().emojify(item.emojis, text, animateEmojis)
|
val emojifiedText: CharSequence = item.content.parseAsMastodonHtml().emojify(
|
||||||
|
item.emojis,
|
||||||
|
text,
|
||||||
|
animateEmojis
|
||||||
|
)
|
||||||
|
|
||||||
setClickableText(text, emojifiedText, item.mentions, item.tags, listener)
|
setClickableText(text, emojifiedText, item.mentions, item.tags, listener)
|
||||||
|
|
||||||
|
@ -84,6 +98,11 @@ class AnnouncementAdapter(
|
||||||
// hide button if announcement badge limit is already reached
|
// hide button if announcement badge limit is already reached
|
||||||
addReactionChip.visible(item.reactions.size < 8)
|
addReactionChip.visible(item.reactions.size < 8)
|
||||||
|
|
||||||
|
val requestManager = Glide.with(chips)
|
||||||
|
|
||||||
|
chips.clearEmojiTargets()
|
||||||
|
val targets = ArrayList<Target<Drawable>>(item.reactions.size)
|
||||||
|
|
||||||
item.reactions.forEachIndexed { i, reaction ->
|
item.reactions.forEachIndexed { i, reaction ->
|
||||||
(
|
(
|
||||||
chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
|
chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
|
||||||
|
@ -99,17 +118,25 @@ class AnnouncementAdapter(
|
||||||
} else {
|
} else {
|
||||||
// we set the EmojiSpan on a space, because otherwise the Chip won't have the right size
|
// we set the EmojiSpan on a space, because otherwise the Chip won't have the right size
|
||||||
// https://github.com/tuskyapp/Tusky/issues/2308
|
// https://github.com/tuskyapp/Tusky/issues/2308
|
||||||
val spanBuilder = SpannableStringBuilder(" ${reaction.count}")
|
val spannable = SpannableString(" ${reaction.count}")
|
||||||
val span = EmojiSpan(WeakReference(this))
|
val span = EmojiSpan(this)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
span.contentDescription = reaction.name
|
span.contentDescription = reaction.name
|
||||||
}
|
}
|
||||||
spanBuilder.setSpan(span, 0, 1, 0)
|
val target = span.createGlideTarget(this, animateEmojis)
|
||||||
Glide.with(this)
|
spannable.setSpan(span, 0, 1, 0)
|
||||||
|
requestManager
|
||||||
.asDrawable()
|
.asDrawable()
|
||||||
.load(if (animateEmojis) { reaction.url } else { reaction.staticUrl })
|
.load(
|
||||||
.into(span.getTarget(animateEmojis))
|
if (animateEmojis) {
|
||||||
this.text = spanBuilder
|
reaction.url
|
||||||
|
} else {
|
||||||
|
reaction.staticUrl
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.into(target)
|
||||||
|
targets.add(target)
|
||||||
|
this.text = spannable
|
||||||
}
|
}
|
||||||
|
|
||||||
isChecked = reaction.me
|
isChecked = reaction.me
|
||||||
|
@ -128,11 +155,18 @@ class AnnouncementAdapter(
|
||||||
chips.removeViewAt(item.reactions.size)
|
chips.removeViewAt(item.reactions.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store Glide targets for later cancellation
|
||||||
|
chips.setEmojiTargets(targets)
|
||||||
|
|
||||||
addReactionChip.setOnClickListener {
|
addReactionChip.setOnClickListener {
|
||||||
listener.openReactionPicker(item.id, it)
|
listener.openReactionPicker(item.id, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onViewRecycled(holder: BindingHolder<ItemAnnouncementBinding>) {
|
||||||
|
holder.binding.chipGroup.clearEmojiTargets()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getItemCount() = items.size
|
override fun getItemCount() = items.size
|
||||||
|
|
||||||
fun updateList(items: List<Announcement>) {
|
fun updateList(items: List<Announcement>) {
|
||||||
|
|
|
@ -17,7 +17,6 @@ package com.keylesspalace.tusky.components.announcements
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
|
@ -26,7 +25,7 @@ import android.view.View
|
||||||
import android.widget.PopupWindow
|
import android.widget.PopupWindow
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
|
@ -36,14 +35,13 @@ import com.keylesspalace.tusky.StatusListActivity
|
||||||
import com.keylesspalace.tusky.adapter.EmojiAdapter
|
import com.keylesspalace.tusky.adapter.EmojiAdapter
|
||||||
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
|
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
|
||||||
import com.keylesspalace.tusky.databinding.ActivityAnnouncementsBinding
|
import com.keylesspalace.tusky.databinding.ActivityAnnouncementsBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.Error
|
import com.keylesspalace.tusky.util.Error
|
||||||
import com.keylesspalace.tusky.util.Loading
|
import com.keylesspalace.tusky.util.Loading
|
||||||
import com.keylesspalace.tusky.util.Success
|
import com.keylesspalace.tusky.util.Success
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
|
||||||
import com.keylesspalace.tusky.util.unsafeLazy
|
import com.keylesspalace.tusky.util.unsafeLazy
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.view.EmojiPicker
|
import com.keylesspalace.tusky.view.EmojiPicker
|
||||||
|
@ -51,19 +49,17 @@ import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
import com.mikepenz.iconics.utils.sizeDp
|
import com.mikepenz.iconics.utils.sizeDp
|
||||||
import javax.inject.Inject
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class AnnouncementsActivity :
|
class AnnouncementsActivity :
|
||||||
BottomSheetActivity(),
|
BottomSheetActivity(),
|
||||||
AnnouncementActionListener,
|
AnnouncementActionListener,
|
||||||
OnEmojiSelectedListener,
|
OnEmojiSelectedListener,
|
||||||
MenuProvider,
|
MenuProvider {
|
||||||
Injectable {
|
|
||||||
|
|
||||||
@Inject
|
private val viewModel: AnnouncementsViewModel by viewModels()
|
||||||
lateinit var viewModelFactory: ViewModelFactory
|
|
||||||
|
|
||||||
private val viewModel: AnnouncementsViewModel by viewModels { viewModelFactory }
|
|
||||||
|
|
||||||
private val binding by viewBinding(ActivityAnnouncementsBinding::inflate)
|
private val binding by viewBinding(ActivityAnnouncementsBinding::inflate)
|
||||||
|
|
||||||
|
@ -102,7 +98,6 @@ class AnnouncementsActivity :
|
||||||
val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
|
val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
|
||||||
binding.announcementsList.addItemDecoration(divider)
|
binding.announcementsList.addItemDecoration(divider)
|
||||||
|
|
||||||
val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false)
|
val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false)
|
||||||
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||||
|
|
||||||
|
@ -110,35 +105,46 @@ class AnnouncementsActivity :
|
||||||
|
|
||||||
binding.announcementsList.adapter = adapter
|
binding.announcementsList.adapter = adapter
|
||||||
|
|
||||||
viewModel.announcements.observe(this) {
|
lifecycleScope.launch {
|
||||||
when (it) {
|
viewModel.announcements.collect {
|
||||||
is Success -> {
|
if (it == null) return@collect
|
||||||
binding.progressBar.hide()
|
when (it) {
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
is Success -> {
|
||||||
if (it.data.isNullOrEmpty()) {
|
binding.progressBar.hide()
|
||||||
binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_announcements)
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
binding.errorMessageView.show()
|
if (it.data.isNullOrEmpty()) {
|
||||||
} else {
|
binding.errorMessageView.setup(
|
||||||
|
R.drawable.elephant_friend_empty,
|
||||||
|
R.string.no_announcements
|
||||||
|
)
|
||||||
|
binding.errorMessageView.show()
|
||||||
|
} else {
|
||||||
|
binding.errorMessageView.hide()
|
||||||
|
}
|
||||||
|
adapter.updateList(it.data ?: listOf())
|
||||||
|
}
|
||||||
|
is Loading -> {
|
||||||
binding.errorMessageView.hide()
|
binding.errorMessageView.hide()
|
||||||
}
|
}
|
||||||
adapter.updateList(it.data ?: listOf())
|
is Error -> {
|
||||||
}
|
binding.progressBar.hide()
|
||||||
is Loading -> {
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
binding.errorMessageView.hide()
|
binding.errorMessageView.setup(
|
||||||
}
|
R.drawable.errorphant_error,
|
||||||
is Error -> {
|
R.string.error_generic
|
||||||
binding.progressBar.hide()
|
) {
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
refreshAnnouncements()
|
||||||
binding.errorMessageView.setup(R.drawable.errorphant_error, R.string.error_generic) {
|
}
|
||||||
refreshAnnouncements()
|
binding.errorMessageView.show()
|
||||||
}
|
}
|
||||||
binding.errorMessageView.show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.emojis.observe(this) {
|
lifecycleScope.launch {
|
||||||
picker.adapter = EmojiAdapter(it, this, animateEmojis)
|
viewModel.emoji.collect {
|
||||||
|
picker.adapter = EmojiAdapter(it, this@AnnouncementsActivity, animateEmojis)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.load()
|
viewModel.load()
|
||||||
|
|
|
@ -16,8 +16,6 @@
|
||||||
package com.keylesspalace.tusky.components.announcements
|
package com.keylesspalace.tusky.components.announcements
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
|
@ -31,40 +29,47 @@ import com.keylesspalace.tusky.util.Error
|
||||||
import com.keylesspalace.tusky.util.Loading
|
import com.keylesspalace.tusky.util.Loading
|
||||||
import com.keylesspalace.tusky.util.Resource
|
import com.keylesspalace.tusky.util.Resource
|
||||||
import com.keylesspalace.tusky.util.Success
|
import com.keylesspalace.tusky.util.Success
|
||||||
import kotlinx.coroutines.launch
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
class AnnouncementsViewModel @Inject constructor(
|
class AnnouncementsViewModel @Inject constructor(
|
||||||
private val instanceInfoRepo: InstanceInfoRepository,
|
private val instanceInfoRepo: InstanceInfoRepository,
|
||||||
private val mastodonApi: MastodonApi,
|
private val mastodonApi: MastodonApi,
|
||||||
private val eventHub: EventHub
|
private val eventHub: EventHub
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val announcementsMutable = MutableLiveData<Resource<List<Announcement>>>()
|
private val _announcements = MutableStateFlow(null as Resource<List<Announcement>>?)
|
||||||
val announcements: LiveData<Resource<List<Announcement>>> = announcementsMutable
|
val announcements: StateFlow<Resource<List<Announcement>>?> = _announcements.asStateFlow()
|
||||||
|
|
||||||
private val emojisMutable = MutableLiveData<List<Emoji>>()
|
private val _emoji = MutableStateFlow(emptyList<Emoji>())
|
||||||
val emojis: LiveData<List<Emoji>> = emojisMutable
|
val emoji: StateFlow<List<Emoji>> = _emoji.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
emojisMutable.postValue(instanceInfoRepo.getEmojis())
|
_emoji.value = instanceInfoRepo.getEmojis()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load() {
|
fun load() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
announcementsMutable.postValue(Loading())
|
_announcements.value = Loading()
|
||||||
mastodonApi.listAnnouncements()
|
mastodonApi.listAnnouncements()
|
||||||
.fold(
|
.fold(
|
||||||
{
|
{
|
||||||
announcementsMutable.postValue(Success(it))
|
_announcements.value = Success(it)
|
||||||
it.filter { announcement -> !announcement.read }
|
it.filter { announcement -> !announcement.read }
|
||||||
.forEach { announcement ->
|
.forEach { announcement ->
|
||||||
mastodonApi.dismissAnnouncement(announcement.id)
|
mastodonApi.dismissAnnouncement(announcement.id)
|
||||||
.fold(
|
.fold(
|
||||||
{
|
{
|
||||||
eventHub.dispatch(AnnouncementReadEvent(announcement.id))
|
eventHub.dispatch(
|
||||||
|
AnnouncementReadEvent(announcement.id)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{ throwable ->
|
{ throwable ->
|
||||||
Log.d(
|
Log.d(
|
||||||
|
@ -77,7 +82,7 @@ class AnnouncementsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
announcementsMutable.postValue(Error(cause = it))
|
_announcements.value = Error(cause = it)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -88,9 +93,9 @@ class AnnouncementsViewModel @Inject constructor(
|
||||||
mastodonApi.addAnnouncementReaction(announcementId, name)
|
mastodonApi.addAnnouncementReaction(announcementId, name)
|
||||||
.fold(
|
.fold(
|
||||||
{
|
{
|
||||||
announcementsMutable.postValue(
|
_announcements.value =
|
||||||
Success(
|
Success(
|
||||||
announcements.value!!.data!!.map { announcement ->
|
announcements.value?.data?.map { announcement ->
|
||||||
if (announcement.id == announcementId) {
|
if (announcement.id == announcementId) {
|
||||||
announcement.copy(
|
announcement.copy(
|
||||||
reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) {
|
reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) {
|
||||||
|
@ -107,7 +112,7 @@ class AnnouncementsViewModel @Inject constructor(
|
||||||
} else {
|
} else {
|
||||||
listOf(
|
listOf(
|
||||||
*announcement.reactions.toTypedArray(),
|
*announcement.reactions.toTypedArray(),
|
||||||
emojis.value!!.find { emoji -> emoji.shortcode == name }!!.run {
|
emoji.value.find { emoji -> emoji.shortcode == name }!!.run {
|
||||||
Announcement.Reaction(
|
Announcement.Reaction(
|
||||||
name,
|
name,
|
||||||
1,
|
1,
|
||||||
|
@ -124,7 +129,6 @@ class AnnouncementsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Log.w(TAG, "Failed to add reaction to the announcement.", it)
|
Log.w(TAG, "Failed to add reaction to the announcement.", it)
|
||||||
|
@ -138,7 +142,7 @@ class AnnouncementsViewModel @Inject constructor(
|
||||||
mastodonApi.removeAnnouncementReaction(announcementId, name)
|
mastodonApi.removeAnnouncementReaction(announcementId, name)
|
||||||
.fold(
|
.fold(
|
||||||
{
|
{
|
||||||
announcementsMutable.postValue(
|
_announcements.value =
|
||||||
Success(
|
Success(
|
||||||
announcements.value!!.data!!.map { announcement ->
|
announcements.value!!.data!!.map { announcement ->
|
||||||
if (announcement.id == announcementId) {
|
if (announcement.id == announcementId) {
|
||||||
|
@ -163,7 +167,6 @@ class AnnouncementsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Log.w(TAG, "Failed to remove reaction from the announcement.", it)
|
Log.w(TAG, "Failed to remove reaction from the announcement.", it)
|
||||||
|
|
|
@ -21,10 +21,7 @@ import android.content.ClipData
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.PorterDuff
|
|
||||||
import android.graphics.PorterDuffColorFilter
|
|
||||||
import android.icu.text.BreakIterator
|
import android.icu.text.BreakIterator
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
@ -50,12 +47,8 @@ import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.content.IntentCompat
|
|
||||||
import androidx.core.content.res.use
|
import androidx.core.content.res.use
|
||||||
import androidx.core.os.BundleCompat
|
|
||||||
import androidx.core.view.ContentInfoCompat
|
import androidx.core.view.ContentInfoCompat
|
||||||
import androidx.core.view.OnReceiveContentListener
|
import androidx.core.view.OnReceiveContentListener
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
|
@ -63,13 +56,13 @@ import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.doAfterTextChanged
|
import androidx.core.widget.doAfterTextChanged
|
||||||
import androidx.core.widget.doOnTextChanged
|
import androidx.core.widget.doOnTextChanged
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
import com.canhub.cropper.CropImage
|
import com.canhub.cropper.CropImage
|
||||||
import com.canhub.cropper.CropImageContract
|
import com.canhub.cropper.CropImageContract
|
||||||
import com.canhub.cropper.options
|
import com.canhub.cropper.options
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.BaseActivity
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
|
@ -86,10 +79,8 @@ import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener
|
||||||
import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView
|
import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView
|
||||||
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
||||||
import com.keylesspalace.tusky.databinding.ActivityComposeBinding
|
import com.keylesspalace.tusky.databinding.ActivityComposeBinding
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.entity.AccountEntity
|
||||||
import com.keylesspalace.tusky.db.DraftAttachment
|
import com.keylesspalace.tusky.db.entity.DraftAttachment
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
import com.keylesspalace.tusky.entity.NewPoll
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
|
@ -99,48 +90,51 @@ import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys.APP_THEME
|
import com.keylesspalace.tusky.settings.PrefKeys.APP_THEME
|
||||||
import com.keylesspalace.tusky.util.MentionSpan
|
import com.keylesspalace.tusky.util.MentionSpan
|
||||||
import com.keylesspalace.tusky.util.PickMediaFiles
|
import com.keylesspalace.tusky.util.PickMediaFiles
|
||||||
|
import com.keylesspalace.tusky.util.defaultFinders
|
||||||
import com.keylesspalace.tusky.util.getInitialLanguages
|
import com.keylesspalace.tusky.util.getInitialLanguages
|
||||||
import com.keylesspalace.tusky.util.getLocaleList
|
import com.keylesspalace.tusky.util.getLocaleList
|
||||||
import com.keylesspalace.tusky.util.getMediaSize
|
import com.keylesspalace.tusky.util.getMediaSize
|
||||||
|
import com.keylesspalace.tusky.util.getParcelableArrayListExtraCompat
|
||||||
|
import com.keylesspalace.tusky.util.getParcelableCompat
|
||||||
|
import com.keylesspalace.tusky.util.getParcelableExtraCompat
|
||||||
|
import com.keylesspalace.tusky.util.getSerializableCompat
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.highlightSpans
|
import com.keylesspalace.tusky.util.highlightSpans
|
||||||
import com.keylesspalace.tusky.util.loadAvatar
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
import com.keylesspalace.tusky.util.modernLanguageCode
|
import com.keylesspalace.tusky.util.modernLanguageCode
|
||||||
import com.keylesspalace.tusky.util.setDrawableTint
|
import com.keylesspalace.tusky.util.setDrawableTint
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.unsafeLazy
|
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.util.visible
|
import com.keylesspalace.tusky.util.visible
|
||||||
import com.mikepenz.iconics.IconicsDrawable
|
import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
import com.mikepenz.iconics.utils.sizeDp
|
import com.mikepenz.iconics.utils.sizeDp
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import dagger.hilt.android.migration.OptionalInject
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import java.text.DecimalFormat
|
|
||||||
import java.util.Locale
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
|
@OptionalInject
|
||||||
|
@AndroidEntryPoint
|
||||||
class ComposeActivity :
|
class ComposeActivity :
|
||||||
BaseActivity(),
|
BaseActivity(),
|
||||||
ComposeOptionsListener,
|
ComposeOptionsListener,
|
||||||
ComposeAutoCompleteAdapter.AutocompletionProvider,
|
ComposeAutoCompleteAdapter.AutocompletionProvider,
|
||||||
OnEmojiSelectedListener,
|
OnEmojiSelectedListener,
|
||||||
Injectable,
|
|
||||||
OnReceiveContentListener,
|
OnReceiveContentListener,
|
||||||
ComposeScheduleView.OnTimeSetListener,
|
ComposeScheduleView.OnTimeSetListener,
|
||||||
CaptionDialog.Listener {
|
CaptionDialog.Listener {
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var viewModelFactory: ViewModelFactory
|
|
||||||
|
|
||||||
private lateinit var composeOptionsBehavior: BottomSheetBehavior<*>
|
private lateinit var composeOptionsBehavior: BottomSheetBehavior<*>
|
||||||
private lateinit var addMediaBehavior: BottomSheetBehavior<*>
|
private lateinit var addMediaBehavior: BottomSheetBehavior<*>
|
||||||
private lateinit var emojiBehavior: BottomSheetBehavior<*>
|
private lateinit var emojiBehavior: BottomSheetBehavior<*>
|
||||||
|
@ -151,26 +145,53 @@ class ComposeActivity :
|
||||||
|
|
||||||
private var photoUploadUri: Uri? = null
|
private var photoUploadUri: Uri? = null
|
||||||
|
|
||||||
private val preferences by unsafeLazy { PreferenceManager.getDefaultSharedPreferences(this) }
|
@VisibleForTesting
|
||||||
|
var highlightFinders = defaultFinders
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
var maximumTootCharacters = InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT
|
var maximumTootCharacters = InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT
|
||||||
var charactersReservedPerUrl = InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL
|
var charactersReservedPerUrl = InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL
|
||||||
|
|
||||||
private val viewModel: ComposeViewModel by viewModels { viewModelFactory }
|
private val viewModel: ComposeViewModel by viewModels()
|
||||||
|
|
||||||
private val binding by viewBinding(ActivityComposeBinding::inflate)
|
private val binding by viewBinding(ActivityComposeBinding::inflate)
|
||||||
|
|
||||||
private var maxUploadMediaNumber = InstanceInfoRepository.DEFAULT_MAX_MEDIA_ATTACHMENTS
|
private var maxUploadMediaNumber = InstanceInfoRepository.DEFAULT_MAX_MEDIA_ATTACHMENTS
|
||||||
|
|
||||||
private val takePicture = registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
|
private val takePictureLauncher =
|
||||||
if (success) {
|
registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
|
||||||
pickMedia(photoUploadUri!!)
|
if (success) {
|
||||||
|
pickMedia(photoUploadUri!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
private val pickMediaFilePermissionLauncher =
|
||||||
private val pickMediaFile = registerForActivityResult(PickMediaFiles()) { uris ->
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||||
|
if (isGranted) {
|
||||||
|
pickMediaFileLauncher.launch(true)
|
||||||
|
} else {
|
||||||
|
Snackbar.make(
|
||||||
|
binding.activityCompose,
|
||||||
|
R.string.error_media_upload_permission,
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
).apply {
|
||||||
|
setAction(R.string.action_retry) { onMediaPick() }
|
||||||
|
// necessary so snackbar is shown over everything
|
||||||
|
view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val pickMediaFileLauncher = registerForActivityResult(PickMediaFiles()) { uris ->
|
||||||
if (viewModel.media.value.size + uris.size > maxUploadMediaNumber) {
|
if (viewModel.media.value.size + uris.size > maxUploadMediaNumber) {
|
||||||
Toast.makeText(this, resources.getQuantityString(R.plurals.error_upload_max_media_reached, maxUploadMediaNumber, maxUploadMediaNumber), Toast.LENGTH_SHORT).show()
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
resources.getQuantityString(
|
||||||
|
R.plurals.error_upload_max_media_reached,
|
||||||
|
maxUploadMediaNumber,
|
||||||
|
maxUploadMediaNumber
|
||||||
|
),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
} else {
|
} else {
|
||||||
uris.forEach { uri ->
|
uris.forEach { uri ->
|
||||||
pickMedia(uri)
|
pickMedia(uri)
|
||||||
|
@ -191,7 +212,8 @@ class ComposeActivity :
|
||||||
uriNew,
|
uriNew,
|
||||||
size,
|
size,
|
||||||
itemOld.description,
|
itemOld.description,
|
||||||
null, // Intentionally reset focus when cropping
|
// Intentionally reset focus when cropping
|
||||||
|
null,
|
||||||
itemOld
|
itemOld
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -205,6 +227,24 @@ class ComposeActivity :
|
||||||
viewModel.cropImageItemOld = null
|
viewModel.cropImageItemOld = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||||
|
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||||
|
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||||
|
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
) {
|
||||||
|
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@ -222,7 +262,11 @@ class ComposeActivity :
|
||||||
val mediaAdapter = MediaPreviewAdapter(
|
val mediaAdapter = MediaPreviewAdapter(
|
||||||
this,
|
this,
|
||||||
onAddCaption = { item ->
|
onAddCaption = { item ->
|
||||||
CaptionDialog.newInstance(item.localId, item.description, item.uri).show(supportFragmentManager, "caption_dialog")
|
CaptionDialog.newInstance(
|
||||||
|
item.localId,
|
||||||
|
item.description,
|
||||||
|
item.uri
|
||||||
|
).show(supportFragmentManager, "caption_dialog")
|
||||||
},
|
},
|
||||||
onAddFocus = { item ->
|
onAddFocus = { item ->
|
||||||
makeFocusDialog(item.focus, item.uri) { newFocus ->
|
makeFocusDialog(item.focus, item.uri) { newFocus ->
|
||||||
|
@ -240,13 +284,13 @@ class ComposeActivity :
|
||||||
|
|
||||||
/* If the composer is started up as a reply to another post, override the "starting" state
|
/* If the composer is started up as a reply to another post, override the "starting" state
|
||||||
* based on what the intent from the reply request passes. */
|
* based on what the intent from the reply request passes. */
|
||||||
val composeOptions: ComposeOptions? = IntentCompat.getParcelableExtra(intent, COMPOSE_OPTIONS_EXTRA, ComposeOptions::class.java)
|
val composeOptions: ComposeOptions? = intent.getParcelableExtraCompat(COMPOSE_OPTIONS_EXTRA)
|
||||||
viewModel.setup(composeOptions)
|
viewModel.setup(composeOptions)
|
||||||
|
|
||||||
setupButtons()
|
setupButtons()
|
||||||
subscribeToUpdates(mediaAdapter)
|
subscribeToUpdates(mediaAdapter)
|
||||||
|
|
||||||
if (accountManager.shouldDisplaySelfUsername(this)) {
|
if (accountManager.shouldDisplaySelfUsername()) {
|
||||||
binding.composeUsernameView.text = getString(
|
binding.composeUsernameView.text = getString(
|
||||||
R.string.compose_active_account_description,
|
R.string.compose_active_account_description,
|
||||||
activeAccount.fullName
|
activeAccount.fullName
|
||||||
|
@ -274,11 +318,9 @@ class ComposeActivity :
|
||||||
|
|
||||||
/* Finally, overwrite state with data from saved instance state. */
|
/* Finally, overwrite state with data from saved instance state. */
|
||||||
savedInstanceState?.let {
|
savedInstanceState?.let {
|
||||||
photoUploadUri = BundleCompat.getParcelable(it, PHOTO_UPLOAD_URI_KEY, Uri::class.java)
|
photoUploadUri = it.getParcelableCompat(PHOTO_UPLOAD_URI_KEY)
|
||||||
|
|
||||||
(it.getSerializable(VISIBILITY_KEY) as Status.Visibility).apply {
|
setStatusVisibility(it.getSerializableCompat(VISIBILITY_KEY)!!)
|
||||||
setStatusVisibility(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
it.getBoolean(CONTENT_WARNING_VISIBLE_KEY).apply {
|
it.getBoolean(CONTENT_WARNING_VISIBLE_KEY).apply {
|
||||||
viewModel.contentWarningChanged(this)
|
viewModel.contentWarningChanged(this)
|
||||||
|
@ -303,14 +345,15 @@ class ComposeActivity :
|
||||||
if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) {
|
if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) {
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Intent.ACTION_SEND -> {
|
Intent.ACTION_SEND -> {
|
||||||
IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)?.let { uri ->
|
intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)?.let { uri ->
|
||||||
pickMedia(uri)
|
pickMedia(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Intent.ACTION_SEND_MULTIPLE -> {
|
Intent.ACTION_SEND_MULTIPLE -> {
|
||||||
IntentCompat.getParcelableArrayListExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)?.forEach { uri ->
|
intent.getParcelableArrayListExtraCompat<Uri>(Intent.EXTRA_STREAM)
|
||||||
pickMedia(uri)
|
?.forEach { uri ->
|
||||||
}
|
pickMedia(uri)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -328,7 +371,13 @@ class ComposeActivity :
|
||||||
val end = binding.composeEditField.selectionEnd.coerceAtLeast(0)
|
val end = binding.composeEditField.selectionEnd.coerceAtLeast(0)
|
||||||
val left = min(start, end)
|
val left = min(start, end)
|
||||||
val right = max(start, end)
|
val right = max(start, end)
|
||||||
binding.composeEditField.text.replace(left, right, shareBody, 0, shareBody.length)
|
binding.composeEditField.text.replace(
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
shareBody,
|
||||||
|
0,
|
||||||
|
shareBody.length
|
||||||
|
)
|
||||||
// move edittext cursor to first when shareBody parsed
|
// move edittext cursor to first when shareBody parsed
|
||||||
binding.composeEditField.text.insert(0, "\n")
|
binding.composeEditField.text.insert(0, "\n")
|
||||||
binding.composeEditField.setSelection(0)
|
binding.composeEditField.setSelection(0)
|
||||||
|
@ -341,23 +390,48 @@ class ComposeActivity :
|
||||||
if (replyingStatusAuthor != null) {
|
if (replyingStatusAuthor != null) {
|
||||||
binding.composeReplyView.show()
|
binding.composeReplyView.show()
|
||||||
binding.composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor)
|
binding.composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor)
|
||||||
val arrowDownIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_down).apply { sizeDp = 12 }
|
val arrowDownIcon = IconicsDrawable(
|
||||||
|
this,
|
||||||
|
GoogleMaterial.Icon.gmd_arrow_drop_down
|
||||||
|
).apply {
|
||||||
|
sizeDp = 12
|
||||||
|
}
|
||||||
|
|
||||||
setDrawableTint(this, arrowDownIcon, android.R.attr.textColorTertiary)
|
setDrawableTint(this, arrowDownIcon, android.R.attr.textColorTertiary)
|
||||||
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null)
|
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
arrowDownIcon,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
binding.composeReplyView.setOnClickListener {
|
binding.composeReplyView.setOnClickListener {
|
||||||
TransitionManager.beginDelayedTransition(binding.composeReplyContentView.parent as ViewGroup)
|
TransitionManager.beginDelayedTransition(
|
||||||
|
binding.composeReplyContentView.parent as ViewGroup
|
||||||
|
)
|
||||||
|
|
||||||
if (binding.composeReplyContentView.isVisible) {
|
if (binding.composeReplyContentView.isVisible) {
|
||||||
binding.composeReplyContentView.hide()
|
binding.composeReplyContentView.hide()
|
||||||
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null)
|
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
arrowDownIcon,
|
||||||
|
null
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
binding.composeReplyContentView.show()
|
binding.composeReplyContentView.show()
|
||||||
val arrowUpIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_up).apply { sizeDp = 12 }
|
val arrowUpIcon = IconicsDrawable(
|
||||||
|
this,
|
||||||
|
GoogleMaterial.Icon.gmd_arrow_drop_up
|
||||||
|
).apply { sizeDp = 12 }
|
||||||
|
|
||||||
setDrawableTint(this, arrowUpIcon, android.R.attr.textColorTertiary)
|
setDrawableTint(this, arrowUpIcon, android.R.attr.textColorTertiary)
|
||||||
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowUpIcon, null)
|
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
arrowUpIcon,
|
||||||
|
null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -368,13 +442,21 @@ class ComposeActivity :
|
||||||
if (startingContentWarning != null) {
|
if (startingContentWarning != null) {
|
||||||
binding.composeContentWarningField.setText(startingContentWarning)
|
binding.composeContentWarningField.setText(startingContentWarning)
|
||||||
}
|
}
|
||||||
binding.composeContentWarningField.doOnTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() }
|
binding.composeContentWarningField.doOnTextChanged { newContentWarning, _, _, _ ->
|
||||||
|
updateVisibleCharactersLeft()
|
||||||
|
viewModel.updateContentWarning(newContentWarning?.toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupComposeField(preferences: SharedPreferences, startingText: String?) {
|
private fun setupComposeField(preferences: SharedPreferences, startingText: String?) {
|
||||||
binding.composeEditField.setOnReceiveContentListener(this)
|
binding.composeEditField.setOnReceiveContentListener(this)
|
||||||
|
|
||||||
binding.composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) }
|
binding.composeEditField.setOnKeyListener { _, keyCode, event ->
|
||||||
|
this.onKeyDown(
|
||||||
|
keyCode,
|
||||||
|
event
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
binding.composeEditField.setAdapter(
|
binding.composeEditField.setAdapter(
|
||||||
ComposeAutoCompleteAdapter(
|
ComposeAutoCompleteAdapter(
|
||||||
|
@ -390,10 +472,11 @@ class ComposeActivity :
|
||||||
binding.composeEditField.setSelection(binding.composeEditField.length())
|
binding.composeEditField.setSelection(binding.composeEditField.length())
|
||||||
|
|
||||||
val mentionColour = binding.composeEditField.linkTextColors.defaultColor
|
val mentionColour = binding.composeEditField.linkTextColors.defaultColor
|
||||||
highlightSpans(binding.composeEditField.text, mentionColour)
|
binding.composeEditField.text.highlightSpans(mentionColour, highlightFinders)
|
||||||
binding.composeEditField.doAfterTextChanged { editable ->
|
binding.composeEditField.doAfterTextChanged { editable ->
|
||||||
highlightSpans(editable!!, mentionColour)
|
editable!!.highlightSpans(mentionColour, highlightFinders)
|
||||||
updateVisibleCharactersLeft()
|
updateVisibleCharactersLeft()
|
||||||
|
viewModel.updateContent(editable.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
// work around Android platform bug -> https://issuetracker.google.com/issues/67102093
|
// work around Android platform bug -> https://issuetracker.google.com/issues/67102093
|
||||||
|
@ -419,7 +502,9 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
viewModel.showContentWarning.combine(viewModel.markMediaAsSensitive) { showContentWarning, markSensitive ->
|
viewModel.showContentWarning.combine(
|
||||||
|
viewModel.markMediaAsSensitive
|
||||||
|
) { showContentWarning, markSensitive ->
|
||||||
updateSensitiveMediaToggle(markSensitive, showContentWarning)
|
updateSensitiveMediaToggle(markSensitive, showContentWarning)
|
||||||
showContentWarning(showContentWarning)
|
showContentWarning(showContentWarning)
|
||||||
}.collect()
|
}.collect()
|
||||||
|
@ -434,7 +519,10 @@ class ComposeActivity :
|
||||||
mediaAdapter.submitList(media)
|
mediaAdapter.submitList(media)
|
||||||
|
|
||||||
binding.composeMediaPreviewBar.visible(media.isNotEmpty())
|
binding.composeMediaPreviewBar.visible(media.isNotEmpty())
|
||||||
updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value, viewModel.showContentWarning.value)
|
updateSensitiveMediaToggle(
|
||||||
|
viewModel.markMediaAsSensitive.value,
|
||||||
|
viewModel.showContentWarning.value
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -480,6 +568,12 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.closeConfirmation.collect {
|
||||||
|
updateOnBackPressedCallbackState()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupButtons() {
|
private fun setupButtons() {
|
||||||
|
@ -490,6 +584,17 @@ class ComposeActivity :
|
||||||
scheduleBehavior = BottomSheetBehavior.from(binding.composeScheduleView)
|
scheduleBehavior = BottomSheetBehavior.from(binding.composeScheduleView)
|
||||||
emojiBehavior = BottomSheetBehavior.from(binding.emojiView)
|
emojiBehavior = BottomSheetBehavior.from(binding.emojiView)
|
||||||
|
|
||||||
|
val bottomSheetCallback = object : BottomSheetCallback() {
|
||||||
|
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||||
|
updateOnBackPressedCallbackState()
|
||||||
|
}
|
||||||
|
override fun onSlide(bottomSheet: View, slideOffset: Float) { }
|
||||||
|
}
|
||||||
|
composeOptionsBehavior.addBottomSheetCallback(bottomSheetCallback)
|
||||||
|
addMediaBehavior.addBottomSheetCallback(bottomSheetCallback)
|
||||||
|
scheduleBehavior.addBottomSheetCallback(bottomSheetCallback)
|
||||||
|
emojiBehavior.addBottomSheetCallback(bottomSheetCallback)
|
||||||
|
|
||||||
enableButton(binding.composeEmojiButton, clickable = false, colorActive = false)
|
enableButton(binding.composeEmojiButton, clickable = false, colorActive = false)
|
||||||
|
|
||||||
// Setup the interface buttons.
|
// Setup the interface buttons.
|
||||||
|
@ -510,46 +615,58 @@ class ComposeActivity :
|
||||||
|
|
||||||
val textColor = MaterialColors.getColor(binding.root, android.R.attr.textColorTertiary)
|
val textColor = MaterialColors.getColor(binding.root, android.R.attr.textColorTertiary)
|
||||||
|
|
||||||
val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply { colorInt = textColor; sizeDp = 18 }
|
val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply {
|
||||||
binding.actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(cameraIcon, null, null, null)
|
colorInt = textColor
|
||||||
|
sizeDp = 18
|
||||||
|
}
|
||||||
|
binding.actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||||
|
cameraIcon,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
val imageIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).apply { colorInt = textColor; sizeDp = 18 }
|
val imageIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).apply {
|
||||||
binding.actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds(imageIcon, null, null, null)
|
colorInt = textColor
|
||||||
|
sizeDp = 18
|
||||||
|
}
|
||||||
|
binding.actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||||
|
imageIcon,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
val pollIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_poll).apply { colorInt = textColor; sizeDp = 18 }
|
val pollIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_poll).apply {
|
||||||
binding.addPollTextActionTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(pollIcon, null, null, null)
|
colorInt = textColor
|
||||||
|
sizeDp = 18
|
||||||
|
}
|
||||||
|
binding.addPollTextActionTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||||
|
pollIcon,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
binding.actionPhotoTake.visible(Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(packageManager) != null)
|
binding.actionPhotoTake.visible(
|
||||||
|
Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(packageManager) != null
|
||||||
|
)
|
||||||
|
|
||||||
binding.actionPhotoTake.setOnClickListener { initiateCameraApp() }
|
binding.actionPhotoTake.setOnClickListener { initiateCameraApp() }
|
||||||
binding.actionPhotoPick.setOnClickListener { onMediaPick() }
|
binding.actionPhotoPick.setOnClickListener { onMediaPick() }
|
||||||
binding.addPollTextActionTextView.setOnClickListener { openPollDialog() }
|
binding.addPollTextActionTextView.setOnClickListener { openPollDialog() }
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(
|
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||||
this,
|
|
||||||
object : OnBackPressedCallback(true) {
|
|
||||||
override fun handleOnBackPressed() {
|
|
||||||
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
|
||||||
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
|
||||||
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
|
||||||
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED
|
|
||||||
) {
|
|
||||||
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
||||||
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
||||||
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
||||||
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCloseButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupLanguageSpinner(initialLanguages: List<String>) {
|
private fun setupLanguageSpinner(initialLanguages: List<String>) {
|
||||||
binding.composePostLanguageButton.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
binding.composePostLanguageButton.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
|
override fun onItemSelected(
|
||||||
|
parent: AdapterView<*>,
|
||||||
|
view: View?,
|
||||||
|
position: Int,
|
||||||
|
id: Long
|
||||||
|
) {
|
||||||
viewModel.postLanguage = (parent.adapter.getItem(position) as Locale).modernLanguageCode
|
viewModel.postLanguage = (parent.adapter.getItem(position) as Locale).modernLanguageCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -592,10 +709,23 @@ class ComposeActivity :
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateOnBackPressedCallbackState() {
|
||||||
|
val confirmation = viewModel.closeConfirmation.value
|
||||||
|
onBackPressedCallback.isEnabled = confirmation != ConfirmationKind.NONE ||
|
||||||
|
composeOptionsBehavior.state != BottomSheetBehavior.STATE_HIDDEN ||
|
||||||
|
addMediaBehavior.state != BottomSheetBehavior.STATE_HIDDEN ||
|
||||||
|
emojiBehavior.state != BottomSheetBehavior.STATE_HIDDEN ||
|
||||||
|
scheduleBehavior.state != BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
}
|
||||||
|
|
||||||
private fun replaceTextAtCaret(text: CharSequence) {
|
private fun replaceTextAtCaret(text: CharSequence) {
|
||||||
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd
|
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd
|
||||||
val start = binding.composeEditField.selectionStart.coerceAtMost(binding.composeEditField.selectionEnd)
|
val start = binding.composeEditField.selectionStart.coerceAtMost(
|
||||||
val end = binding.composeEditField.selectionStart.coerceAtLeast(binding.composeEditField.selectionEnd)
|
binding.composeEditField.selectionEnd
|
||||||
|
)
|
||||||
|
val end = binding.composeEditField.selectionStart.coerceAtLeast(
|
||||||
|
binding.composeEditField.selectionEnd
|
||||||
|
)
|
||||||
val textToInsert = if (start > 0 && !binding.composeEditField.text[start - 1].isWhitespace()) {
|
val textToInsert = if (start > 0 && !binding.composeEditField.text[start - 1].isWhitespace()) {
|
||||||
" $text"
|
" $text"
|
||||||
} else {
|
} else {
|
||||||
|
@ -609,8 +739,12 @@ class ComposeActivity :
|
||||||
|
|
||||||
fun prependSelectedWordsWith(text: CharSequence) {
|
fun prependSelectedWordsWith(text: CharSequence) {
|
||||||
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd
|
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd
|
||||||
val start = binding.composeEditField.selectionStart.coerceAtMost(binding.composeEditField.selectionEnd)
|
val start = binding.composeEditField.selectionStart.coerceAtMost(
|
||||||
val end = binding.composeEditField.selectionStart.coerceAtLeast(binding.composeEditField.selectionEnd)
|
binding.composeEditField.selectionEnd
|
||||||
|
)
|
||||||
|
val end = binding.composeEditField.selectionStart.coerceAtLeast(
|
||||||
|
binding.composeEditField.selectionEnd
|
||||||
|
)
|
||||||
val editorText = binding.composeEditField.text
|
val editorText = binding.composeEditField.text
|
||||||
|
|
||||||
if (start == end) {
|
if (start == end) {
|
||||||
|
@ -678,7 +812,10 @@ class ComposeActivity :
|
||||||
this.viewModel.toggleMarkSensitive()
|
this.viewModel.toggleMarkSensitive()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSensitiveMediaToggle(markMediaSensitive: Boolean, contentWarningShown: Boolean) {
|
private fun updateSensitiveMediaToggle(
|
||||||
|
markMediaSensitive: Boolean,
|
||||||
|
contentWarningShown: Boolean
|
||||||
|
) {
|
||||||
if (viewModel.media.value.isEmpty()) {
|
if (viewModel.media.value.isEmpty()) {
|
||||||
binding.composeHideMediaButton.hide()
|
binding.composeHideMediaButton.hide()
|
||||||
binding.descriptionMissingWarningButton.hide()
|
binding.descriptionMissingWarningButton.hide()
|
||||||
|
@ -695,10 +832,13 @@ class ComposeActivity :
|
||||||
getColor(R.color.tusky_blue)
|
getColor(R.color.tusky_blue)
|
||||||
} else {
|
} else {
|
||||||
binding.composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp)
|
binding.composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp)
|
||||||
MaterialColors.getColor(binding.composeHideMediaButton, android.R.attr.textColorTertiary)
|
MaterialColors.getColor(
|
||||||
|
binding.composeHideMediaButton,
|
||||||
|
android.R.attr.textColorTertiary
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.composeHideMediaButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
binding.composeHideMediaButton.drawable.setTint(color)
|
||||||
|
|
||||||
var oneMediaWithoutDescription = false
|
var oneMediaWithoutDescription = false
|
||||||
for (media in viewModel.media.value) {
|
for (media in viewModel.media.value) {
|
||||||
|
@ -717,11 +857,14 @@ class ComposeActivity :
|
||||||
enableButton(binding.composeScheduleButton, clickable = false, colorActive = false)
|
enableButton(binding.composeScheduleButton, clickable = false, colorActive = false)
|
||||||
} else {
|
} else {
|
||||||
@ColorInt val color = if (binding.composeScheduleView.time == null) {
|
@ColorInt val color = if (binding.composeScheduleView.time == null) {
|
||||||
MaterialColors.getColor(binding.composeScheduleButton, android.R.attr.textColorTertiary)
|
MaterialColors.getColor(
|
||||||
|
binding.composeScheduleButton,
|
||||||
|
android.R.attr.textColorTertiary
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
getColor(R.color.tusky_blue)
|
getColor(R.color.tusky_blue)
|
||||||
}
|
}
|
||||||
binding.composeScheduleButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
binding.composeScheduleButton.drawable.setTint(color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -748,7 +891,11 @@ class ComposeActivity :
|
||||||
binding.composeToggleVisibilityButton.setImageResource(iconRes)
|
binding.composeToggleVisibilityButton.setImageResource(iconRes)
|
||||||
if (viewModel.editing) {
|
if (viewModel.editing) {
|
||||||
// Can't update visibility on published status
|
// Can't update visibility on published status
|
||||||
enableButton(binding.composeToggleVisibilityButton, clickable = false, colorActive = false)
|
enableButton(
|
||||||
|
binding.composeToggleVisibilityButton,
|
||||||
|
clickable = false,
|
||||||
|
colorActive = false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -785,7 +932,11 @@ class ComposeActivity :
|
||||||
private fun showEmojis() {
|
private fun showEmojis() {
|
||||||
binding.emojiView.adapter?.let {
|
binding.emojiView.adapter?.let {
|
||||||
if (it.itemCount == 0) {
|
if (it.itemCount == 0) {
|
||||||
val errorMessage = getString(R.string.error_no_custom_emojis, accountManager.activeAccount!!.domain)
|
val errorMessage =
|
||||||
|
getString(
|
||||||
|
R.string.error_no_custom_emojis,
|
||||||
|
accountManager.activeAccount!!.domain
|
||||||
|
)
|
||||||
displayTransientMessage(errorMessage)
|
displayTransientMessage(errorMessage)
|
||||||
} else {
|
} else {
|
||||||
if (emojiBehavior.state == BottomSheetBehavior.STATE_HIDDEN || emojiBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) {
|
if (emojiBehavior.state == BottomSheetBehavior.STATE_HIDDEN || emojiBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||||
|
@ -813,19 +964,15 @@ class ComposeActivity :
|
||||||
|
|
||||||
private fun onMediaPick() {
|
private fun onMediaPick() {
|
||||||
addMediaBehavior.addBottomSheetCallback(
|
addMediaBehavior.addBottomSheetCallback(
|
||||||
object : BottomSheetBehavior.BottomSheetCallback() {
|
object : BottomSheetCallback() {
|
||||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||||
// Wait until bottom sheet is not collapsed and show next screen after
|
// Wait until bottom sheet is not collapsed and show next screen after
|
||||||
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
|
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||||
addMediaBehavior.removeBottomSheetCallback(this)
|
addMediaBehavior.removeBottomSheetCallback(this)
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(this@ComposeActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
ActivityCompat.requestPermissions(
|
pickMediaFilePermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
this@ComposeActivity,
|
|
||||||
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
|
|
||||||
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
pickMediaFile.launch(true)
|
pickMediaFileLauncher.launch(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -852,9 +999,14 @@ class ComposeActivity :
|
||||||
|
|
||||||
private fun setupPollView() {
|
private fun setupPollView() {
|
||||||
val margin = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
|
val margin = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
|
||||||
val marginBottom = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom)
|
val marginBottom = resources.getDimensionPixelSize(
|
||||||
|
R.dimen.compose_media_preview_margin_bottom
|
||||||
|
)
|
||||||
|
|
||||||
val layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
val layoutParams = LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
layoutParams.setMargins(margin, margin, margin, marginBottom)
|
layoutParams.setMargins(margin, margin, margin, marginBottom)
|
||||||
binding.pollPreview.layoutParams = layoutParams
|
binding.pollPreview.layoutParams = layoutParams
|
||||||
|
|
||||||
|
@ -876,13 +1028,13 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removePoll() {
|
private fun removePoll() {
|
||||||
viewModel.poll.value = null
|
viewModel.updatePoll(null)
|
||||||
binding.pollPreview.hide()
|
binding.pollPreview.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVisibilityChanged(visibility: Status.Visibility) {
|
override fun onVisibilityChanged(visibility: Status.Visibility) {
|
||||||
composeOptionsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
composeOptionsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
viewModel.statusVisibility.value = visibility
|
viewModel.changeStatusVisibility(visibility)
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
@ -903,9 +1055,12 @@ class ComposeActivity :
|
||||||
binding.composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength)
|
binding.composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength)
|
||||||
|
|
||||||
val textColor = if (remainingLength < 0) {
|
val textColor = if (remainingLength < 0) {
|
||||||
getColor(R.color.tusky_red)
|
getColor(R.color.warning_color)
|
||||||
} else {
|
} else {
|
||||||
MaterialColors.getColor(binding.composeCharactersLeftView, android.R.attr.textColorTertiary)
|
MaterialColors.getColor(
|
||||||
|
binding.composeCharactersLeftView,
|
||||||
|
android.R.attr.textColorTertiary
|
||||||
|
)
|
||||||
}
|
}
|
||||||
binding.composeCharactersLeftView.setTextColor(textColor)
|
binding.composeCharactersLeftView.setTextColor(textColor)
|
||||||
}
|
}
|
||||||
|
@ -917,7 +1072,9 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyScheduledTime(): Boolean {
|
private fun verifyScheduledTime(): Boolean {
|
||||||
return binding.composeScheduleView.verifyScheduledTime(binding.composeScheduleView.getDateTime(viewModel.scheduledAt.value))
|
return binding.composeScheduleView.verifyScheduledTime(
|
||||||
|
binding.composeScheduleView.getDateTime(viewModel.scheduledAt.value)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSendClicked() {
|
private fun onSendClicked() {
|
||||||
|
@ -967,27 +1124,6 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
|
||||||
|
|
||||||
if (requestCode == PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) {
|
|
||||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
pickMediaFile.launch(true)
|
|
||||||
} else {
|
|
||||||
Snackbar.make(
|
|
||||||
binding.activityCompose,
|
|
||||||
R.string.error_media_upload_permission,
|
|
||||||
Snackbar.LENGTH_SHORT
|
|
||||||
).apply {
|
|
||||||
setAction(R.string.action_retry) { onMediaPick() }
|
|
||||||
// necessary so snackbar is shown over everything
|
|
||||||
view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initiateCameraApp() {
|
private fun initiateCameraApp() {
|
||||||
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
|
||||||
|
@ -1003,8 +1139,7 @@ class ComposeActivity :
|
||||||
this,
|
this,
|
||||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||||
photoFile
|
photoFile
|
||||||
)
|
).also { uri -> takePictureLauncher.launch(uri) }
|
||||||
takePicture.launch(photoUploadUri)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) {
|
private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) {
|
||||||
|
@ -1031,7 +1166,7 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
binding.addPollTextActionTextView.setTextColor(textColor)
|
binding.addPollTextActionTextView.setTextColor(textColor)
|
||||||
binding.addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN)
|
binding.addPollTextActionTextView.compoundDrawablesRelative[0].setTint(textColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun editImageInQueue(item: QueuedMedia) {
|
private fun editImageInQueue(item: QueuedMedia) {
|
||||||
|
@ -1042,14 +1177,20 @@ class ComposeActivity :
|
||||||
val tempFile = createNewImageFile(this, if (isPng) ".png" else ".jpg")
|
val tempFile = createNewImageFile(this, if (isPng) ".png" else ".jpg")
|
||||||
|
|
||||||
// "Authority" must be the same as the android:authorities string in AndroidManifest.xml
|
// "Authority" must be the same as the android:authorities string in AndroidManifest.xml
|
||||||
val uriNew = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", tempFile)
|
val uriNew = FileProvider.getUriForFile(
|
||||||
|
this,
|
||||||
|
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||||
|
tempFile
|
||||||
|
)
|
||||||
|
|
||||||
viewModel.cropImageItemOld = item
|
viewModel.cropImageItemOld = item
|
||||||
|
|
||||||
cropImage.launch(
|
cropImage.launch(
|
||||||
options(uri = item.uri) {
|
options(uri = item.uri) {
|
||||||
setOutputUri(uriNew)
|
setOutputUri(uriNew)
|
||||||
setOutputCompressFormat(if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG)
|
setOutputCompressFormat(
|
||||||
|
if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1076,7 +1217,7 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pickMedia(uri: Uri, description: String? = null) {
|
private fun pickMedia(uri: Uri, description: String? = null) {
|
||||||
var sanitizedDescription = sanitizePickMediaDescription(description)
|
val sanitizedDescription = sanitizePickMediaDescription(description)
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
viewModel.pickMedia(uri, sanitizedDescription).onFailure { throwable ->
|
viewModel.pickMedia(uri, sanitizedDescription).onFailure { throwable ->
|
||||||
|
@ -1087,7 +1228,9 @@ class ComposeActivity :
|
||||||
val formattedSize = decimalFormat.format(allowedSizeInMb)
|
val formattedSize = decimalFormat.format(allowedSizeInMb)
|
||||||
getString(R.string.error_multimedia_size_limit, formattedSize)
|
getString(R.string.error_multimedia_size_limit, formattedSize)
|
||||||
}
|
}
|
||||||
is VideoOrImageException -> getString(R.string.error_media_upload_image_or_video)
|
is VideoOrImageException -> getString(
|
||||||
|
R.string.error_media_upload_image_or_video
|
||||||
|
)
|
||||||
else -> getString(R.string.error_media_upload_opening)
|
else -> getString(R.string.error_media_upload_opening)
|
||||||
}
|
}
|
||||||
displayTransientMessage(errorString)
|
displayTransientMessage(errorString)
|
||||||
|
@ -1096,18 +1239,25 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showContentWarning(show: Boolean) {
|
private fun showContentWarning(show: Boolean) {
|
||||||
TransitionManager.beginDelayedTransition(binding.composeContentWarningBar.parent as ViewGroup)
|
TransitionManager.beginDelayedTransition(
|
||||||
|
binding.composeContentWarningBar.parent as ViewGroup
|
||||||
|
)
|
||||||
@ColorInt val color = if (show) {
|
@ColorInt val color = if (show) {
|
||||||
binding.composeContentWarningBar.show()
|
binding.composeContentWarningBar.show()
|
||||||
binding.composeContentWarningField.setSelection(binding.composeContentWarningField.text.length)
|
binding.composeContentWarningField.setSelection(
|
||||||
|
binding.composeContentWarningField.text.length
|
||||||
|
)
|
||||||
binding.composeContentWarningField.requestFocus()
|
binding.composeContentWarningField.requestFocus()
|
||||||
getColor(R.color.tusky_blue)
|
getColor(R.color.tusky_blue)
|
||||||
} else {
|
} else {
|
||||||
binding.composeContentWarningBar.hide()
|
binding.composeContentWarningBar.hide()
|
||||||
binding.composeEditField.requestFocus()
|
binding.composeEditField.requestFocus()
|
||||||
MaterialColors.getColor(binding.composeContentWarningButton, android.R.attr.textColorTertiary)
|
MaterialColors.getColor(
|
||||||
|
binding.composeContentWarningButton,
|
||||||
|
android.R.attr.textColorTertiary
|
||||||
|
)
|
||||||
}
|
}
|
||||||
binding.composeContentWarningButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
binding.composeContentWarningButton.drawable.setTint(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
@ -1140,10 +1290,10 @@ class ComposeActivity :
|
||||||
private fun handleCloseButton() {
|
private fun handleCloseButton() {
|
||||||
val contentText = binding.composeEditField.text.toString()
|
val contentText = binding.composeEditField.text.toString()
|
||||||
val contentWarning = binding.composeContentWarningField.text.toString()
|
val contentWarning = binding.composeContentWarningField.text.toString()
|
||||||
when (viewModel.handleCloseButton(contentText, contentWarning)) {
|
when (viewModel.closeConfirmation.value) {
|
||||||
ConfirmationKind.NONE -> {
|
ConfirmationKind.NONE -> {
|
||||||
viewModel.stopUploads()
|
viewModel.stopUploads()
|
||||||
finishWithoutSlideOutAnimation()
|
finish()
|
||||||
}
|
}
|
||||||
ConfirmationKind.SAVE_OR_DISCARD ->
|
ConfirmationKind.SAVE_OR_DISCARD ->
|
||||||
getSaveAsDraftOrDiscardDialog(contentText, contentWarning).show()
|
getSaveAsDraftOrDiscardDialog(contentText, contentWarning).show()
|
||||||
|
@ -1159,7 +1309,10 @@ class ComposeActivity :
|
||||||
/**
|
/**
|
||||||
* User is editing a new post, and can either save the changes as a draft or discard them.
|
* User is editing a new post, and can either save the changes as a draft or discard them.
|
||||||
*/
|
*/
|
||||||
private fun getSaveAsDraftOrDiscardDialog(contentText: String, contentWarning: String): AlertDialog.Builder {
|
private fun getSaveAsDraftOrDiscardDialog(
|
||||||
|
contentText: String,
|
||||||
|
contentWarning: String
|
||||||
|
): AlertDialog.Builder {
|
||||||
val warning = if (viewModel.media.value.isNotEmpty()) {
|
val warning = if (viewModel.media.value.isNotEmpty()) {
|
||||||
R.string.compose_save_draft_loses_media
|
R.string.compose_save_draft_loses_media
|
||||||
} else {
|
} else {
|
||||||
|
@ -1182,7 +1335,10 @@ class ComposeActivity :
|
||||||
* User is editing an existing draft, and can either update the draft with the new changes or
|
* User is editing an existing draft, and can either update the draft with the new changes or
|
||||||
* discard them.
|
* discard them.
|
||||||
*/
|
*/
|
||||||
private fun getUpdateDraftOrDiscardDialog(contentText: String, contentWarning: String): AlertDialog.Builder {
|
private fun getUpdateDraftOrDiscardDialog(
|
||||||
|
contentText: String,
|
||||||
|
contentWarning: String
|
||||||
|
): AlertDialog.Builder {
|
||||||
val warning = if (viewModel.media.value.isNotEmpty()) {
|
val warning = if (viewModel.media.value.isNotEmpty()) {
|
||||||
R.string.compose_save_draft_loses_media
|
R.string.compose_save_draft_loses_media
|
||||||
} else {
|
} else {
|
||||||
|
@ -1197,7 +1353,7 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.action_discard) { _, _ ->
|
.setNegativeButton(R.string.action_discard) { _, _ ->
|
||||||
viewModel.stopUploads()
|
viewModel.stopUploads()
|
||||||
finishWithoutSlideOutAnimation()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1213,7 +1369,7 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.action_discard) { _, _ ->
|
.setNegativeButton(R.string.action_discard) { _, _ ->
|
||||||
viewModel.stopUploads()
|
viewModel.stopUploads()
|
||||||
finishWithoutSlideOutAnimation()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1227,7 +1383,7 @@ class ComposeActivity :
|
||||||
.setPositiveButton(R.string.action_delete) { _, _ ->
|
.setPositiveButton(R.string.action_delete) { _, _ ->
|
||||||
viewModel.deleteDraft()
|
viewModel.deleteDraft()
|
||||||
viewModel.stopUploads()
|
viewModel.stopUploads()
|
||||||
finishWithoutSlideOutAnimation()
|
finish()
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.action_continue_edit) { _, _ ->
|
.setNegativeButton(R.string.action_continue_edit) { _, _ ->
|
||||||
// Do nothing, dialog will dismiss, user can continue editing
|
// Do nothing, dialog will dismiss, user can continue editing
|
||||||
|
@ -1236,7 +1392,7 @@ class ComposeActivity :
|
||||||
|
|
||||||
private fun deleteDraftAndFinish() {
|
private fun deleteDraftAndFinish() {
|
||||||
viewModel.deleteDraft()
|
viewModel.deleteDraft()
|
||||||
finishWithoutSlideOutAnimation()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveDraftAndFinish(contentText: String, contentWarning: String) {
|
private fun saveDraftAndFinish(contentText: String, contentWarning: String) {
|
||||||
|
@ -1254,7 +1410,7 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
viewModel.saveDraft(contentText, contentWarning)
|
viewModel.saveDraft(contentText, contentWarning)
|
||||||
dialog?.cancel()
|
dialog?.cancel()
|
||||||
finishWithoutSlideOutAnimation()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1286,10 +1442,15 @@ class ComposeActivity :
|
||||||
val state: State
|
val state: State
|
||||||
) {
|
) {
|
||||||
enum class Type {
|
enum class Type {
|
||||||
IMAGE, VIDEO, AUDIO;
|
IMAGE,
|
||||||
|
VIDEO,
|
||||||
|
AUDIO
|
||||||
}
|
}
|
||||||
enum class State {
|
enum class State {
|
||||||
UPLOADING, UNPROCESSED, PROCESSED, PUBLISHED
|
UPLOADING,
|
||||||
|
UNPROCESSED,
|
||||||
|
PROCESSED,
|
||||||
|
PUBLISHED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1357,7 +1518,6 @@ class ComposeActivity :
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "ComposeActivity" // logging tag
|
private const val TAG = "ComposeActivity" // logging tag
|
||||||
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
|
||||||
|
|
||||||
internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
|
internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
|
||||||
private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI"
|
private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI"
|
||||||
|
@ -1370,10 +1530,7 @@ class ComposeActivity :
|
||||||
* @return an Intent to start the ComposeActivity
|
* @return an Intent to start the ComposeActivity
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun startIntent(
|
fun startIntent(context: Context, options: ComposeOptions): Intent {
|
||||||
context: Context,
|
|
||||||
options: ComposeOptions
|
|
||||||
): Intent {
|
|
||||||
return Intent(context, ComposeActivity::class.java).apply {
|
return Intent(context, ComposeActivity::class.java).apply {
|
||||||
putExtra(COMPOSE_OPTIONS_EXTRA, options)
|
putExtra(COMPOSE_OPTIONS_EXTRA, options)
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,7 @@ class ComposeAutoCompleteAdapter(
|
||||||
return filterResults
|
return filterResults
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun publishResults(constraint: CharSequence?, results: FilterResults) {
|
override fun publishResults(constraint: CharSequence?, results: FilterResults) {
|
||||||
if (results.count > 0) {
|
if (results.count > 0) {
|
||||||
resultList = results.values as List<AutocompleteResult>
|
resultList = results.values as List<AutocompleteResult>
|
||||||
|
@ -108,7 +109,9 @@ class ComposeAutoCompleteAdapter(
|
||||||
val account = accountResult.account
|
val account = accountResult.account
|
||||||
binding.username.text = context.getString(R.string.post_username_format, account.username)
|
binding.username.text = context.getString(R.string.post_username_format, account.username)
|
||||||
binding.displayName.text = account.name.emojify(account.emojis, binding.displayName, animateEmojis)
|
binding.displayName.text = account.name.emojify(account.emojis, binding.displayName, animateEmojis)
|
||||||
val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp)
|
val avatarRadius = context.resources.getDimensionPixelSize(
|
||||||
|
R.dimen.avatar_radius_42dp
|
||||||
|
)
|
||||||
loadAvatar(
|
loadAvatar(
|
||||||
account.avatar,
|
account.avatar,
|
||||||
binding.avatar,
|
binding.avatar,
|
||||||
|
@ -143,12 +146,12 @@ class ComposeAutoCompleteAdapter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class AutocompleteResult {
|
sealed interface AutocompleteResult {
|
||||||
class AccountResult(val account: TimelineAccount) : AutocompleteResult()
|
class AccountResult(val account: TimelineAccount) : AutocompleteResult
|
||||||
|
|
||||||
class HashtagResult(val hashtag: String) : AutocompleteResult()
|
class HashtagResult(val hashtag: String) : AutocompleteResult
|
||||||
|
|
||||||
class EmojiResult(val emoji: Emoji) : AutocompleteResult()
|
class EmojiResult(val emoji: Emoji) : AutocompleteResult
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AutocompletionProvider {
|
interface AutocompletionProvider {
|
||||||
|
|
|
@ -38,20 +38,26 @@ import com.keylesspalace.tusky.service.MediaToSend
|
||||||
import com.keylesspalace.tusky.service.ServiceClient
|
import com.keylesspalace.tusky.service.ServiceClient
|
||||||
import com.keylesspalace.tusky.service.StatusToSend
|
import com.keylesspalace.tusky.service.StatusToSend
|
||||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.shareIn
|
import kotlinx.coroutines.flow.shareIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
class ComposeViewModel @Inject constructor(
|
class ComposeViewModel @Inject constructor(
|
||||||
private val api: MastodonApi,
|
private val api: MastodonApi,
|
||||||
private val accountManager: AccountManager,
|
private val accountManager: AccountManager,
|
||||||
|
@ -76,22 +82,43 @@ class ComposeViewModel @Inject constructor(
|
||||||
private var modifiedInitialState: Boolean = false
|
private var modifiedInitialState: Boolean = false
|
||||||
private var hasScheduledTimeChanged: Boolean = false
|
private var hasScheduledTimeChanged: Boolean = false
|
||||||
|
|
||||||
val instanceInfo: SharedFlow<InstanceInfo> = instanceInfoRepo::getInstanceInfo.asFlow()
|
private var currentContent: String? = ""
|
||||||
|
private var currentContentWarning: String? = ""
|
||||||
|
|
||||||
|
val instanceInfo: SharedFlow<InstanceInfo> = instanceInfoRepo::getUpdatedInstanceInfoOrFallback.asFlow()
|
||||||
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
||||||
|
|
||||||
val emoji: SharedFlow<List<Emoji>> = instanceInfoRepo::getEmojis.asFlow()
|
val emoji: SharedFlow<List<Emoji>> = instanceInfoRepo::getEmojis.asFlow()
|
||||||
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
||||||
|
|
||||||
val markMediaAsSensitive: MutableStateFlow<Boolean> =
|
private val _markMediaAsSensitive =
|
||||||
MutableStateFlow(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
|
MutableStateFlow(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
|
||||||
|
val markMediaAsSensitive: StateFlow<Boolean> = _markMediaAsSensitive.asStateFlow()
|
||||||
|
|
||||||
val statusVisibility: MutableStateFlow<Status.Visibility> = MutableStateFlow(Status.Visibility.UNKNOWN)
|
private val _statusVisibility = MutableStateFlow(Status.Visibility.UNKNOWN)
|
||||||
val showContentWarning: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
val statusVisibility: StateFlow<Status.Visibility> = _statusVisibility.asStateFlow()
|
||||||
val poll: MutableStateFlow<NewPoll?> = MutableStateFlow(null)
|
|
||||||
val scheduledAt: MutableStateFlow<String?> = MutableStateFlow(null)
|
|
||||||
|
|
||||||
val media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList())
|
private val _showContentWarning = MutableStateFlow(false)
|
||||||
val uploadError = MutableSharedFlow<Throwable>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
val showContentWarning: StateFlow<Boolean> = _showContentWarning.asStateFlow()
|
||||||
|
|
||||||
|
private val _poll = MutableStateFlow(null as NewPoll?)
|
||||||
|
val poll: StateFlow<NewPoll?> = _poll.asStateFlow()
|
||||||
|
|
||||||
|
private val _scheduledAt = MutableStateFlow(null as String?)
|
||||||
|
val scheduledAt: StateFlow<String?> = _scheduledAt.asStateFlow()
|
||||||
|
|
||||||
|
private val _media = MutableStateFlow(emptyList<QueuedMedia>())
|
||||||
|
val media: StateFlow<List<QueuedMedia>> = _media.asStateFlow()
|
||||||
|
|
||||||
|
private val _uploadError = MutableSharedFlow<Throwable>(
|
||||||
|
replay = 0,
|
||||||
|
extraBufferCapacity = 1,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||||
|
)
|
||||||
|
val uploadError: SharedFlow<Throwable> = _uploadError.asSharedFlow()
|
||||||
|
|
||||||
|
private val _closeConfirmation = MutableStateFlow(ConfirmationKind.NONE)
|
||||||
|
val closeConfirmation: StateFlow<ConfirmationKind> = _closeConfirmation.asStateFlow()
|
||||||
|
|
||||||
private lateinit var composeKind: ComposeKind
|
private lateinit var composeKind: ComposeKind
|
||||||
|
|
||||||
|
@ -100,10 +127,16 @@ class ComposeViewModel @Inject constructor(
|
||||||
|
|
||||||
private var setupComplete = false
|
private var setupComplete = false
|
||||||
|
|
||||||
suspend fun pickMedia(mediaUri: Uri, description: String? = null, focus: Attachment.Focus? = null): Result<QueuedMedia> = withContext(Dispatchers.IO) {
|
suspend fun pickMedia(
|
||||||
|
mediaUri: Uri,
|
||||||
|
description: String? = null,
|
||||||
|
focus: Attachment.Focus? = null
|
||||||
|
): Result<QueuedMedia> = withContext(
|
||||||
|
Dispatchers.IO
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
val (type, uri, size) = mediaUploader.prepareMedia(mediaUri, instanceInfo.first())
|
val (type, uri, size) = mediaUploader.prepareMedia(mediaUri, instanceInfo.first())
|
||||||
val mediaItems = media.value
|
val mediaItems = _media.value
|
||||||
if (type != QueuedMedia.Type.IMAGE &&
|
if (type != QueuedMedia.Type.IMAGE &&
|
||||||
mediaItems.isNotEmpty() &&
|
mediaItems.isNotEmpty() &&
|
||||||
mediaItems[0].type == QueuedMedia.Type.IMAGE
|
mediaItems[0].type == QueuedMedia.Type.IMAGE
|
||||||
|
@ -128,7 +161,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
): QueuedMedia {
|
): QueuedMedia {
|
||||||
var stashMediaItem: QueuedMedia? = null
|
var stashMediaItem: QueuedMedia? = null
|
||||||
|
|
||||||
media.update { mediaList ->
|
_media.update { mediaList ->
|
||||||
val mediaItem = QueuedMedia(
|
val mediaItem = QueuedMedia(
|
||||||
localId = mediaUploader.getNewLocalMediaId(),
|
localId = mediaUploader.getNewLocalMediaId(),
|
||||||
uri = uri,
|
uri = uri,
|
||||||
|
@ -149,30 +182,36 @@ class ComposeViewModel @Inject constructor(
|
||||||
mediaList + mediaItem
|
mediaList + mediaItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val mediaItem = stashMediaItem!! // stashMediaItem is always non-null and uncaptured at this point, but Kotlin doesn't know that
|
val mediaItem =
|
||||||
|
stashMediaItem!! // stashMediaItem is always non-null and uncaptured at this point, but Kotlin doesn't know that
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
mediaUploader
|
mediaUploader
|
||||||
.uploadMedia(mediaItem, instanceInfo.first())
|
.uploadMedia(mediaItem, instanceInfo.first())
|
||||||
.collect { event ->
|
.collect { event ->
|
||||||
val item = media.value.find { it.localId == mediaItem.localId }
|
val item = _media.value.find { it.localId == mediaItem.localId }
|
||||||
?: return@collect
|
?: return@collect
|
||||||
val newMediaItem = when (event) {
|
val newMediaItem = when (event) {
|
||||||
is UploadEvent.ProgressEvent ->
|
is UploadEvent.ProgressEvent ->
|
||||||
item.copy(uploadPercent = event.percentage)
|
item.copy(uploadPercent = event.percentage)
|
||||||
|
|
||||||
is UploadEvent.FinishedEvent ->
|
is UploadEvent.FinishedEvent ->
|
||||||
item.copy(
|
item.copy(
|
||||||
id = event.mediaId,
|
id = event.mediaId,
|
||||||
uploadPercent = -1,
|
uploadPercent = -1,
|
||||||
state = if (event.processed) { QueuedMedia.State.PROCESSED } else { QueuedMedia.State.UNPROCESSED }
|
state = if (event.processed) {
|
||||||
|
QueuedMedia.State.PROCESSED
|
||||||
|
} else {
|
||||||
|
QueuedMedia.State.UNPROCESSED
|
||||||
|
}
|
||||||
)
|
)
|
||||||
is UploadEvent.ErrorEvent -> {
|
is UploadEvent.ErrorEvent -> {
|
||||||
media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } }
|
_media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } }
|
||||||
uploadError.emit(event.error)
|
_uploadError.emit(event.error)
|
||||||
return@collect
|
return@collect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
media.update { mediaList ->
|
_media.update { mediaList ->
|
||||||
mediaList.map { mediaItem ->
|
mediaList.map { mediaItem ->
|
||||||
if (mediaItem.localId == newMediaItem.localId) {
|
if (mediaItem.localId == newMediaItem.localId) {
|
||||||
newMediaItem
|
newMediaItem
|
||||||
|
@ -183,11 +222,22 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateCloseConfirmation()
|
||||||
return mediaItem
|
return mediaItem
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?, focus: Attachment.Focus?) {
|
fun changeStatusVisibility(visibility: Status.Visibility) {
|
||||||
media.update { mediaList ->
|
_statusVisibility.value = visibility
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addUploadedMedia(
|
||||||
|
id: String,
|
||||||
|
type: QueuedMedia.Type,
|
||||||
|
uri: Uri,
|
||||||
|
description: String?,
|
||||||
|
focus: Attachment.Focus?
|
||||||
|
) {
|
||||||
|
_media.update { mediaList ->
|
||||||
val mediaItem = QueuedMedia(
|
val mediaItem = QueuedMedia(
|
||||||
localId = mediaUploader.getNewLocalMediaId(),
|
localId = mediaUploader.getNewLocalMediaId(),
|
||||||
uri = uri,
|
uri = uri,
|
||||||
|
@ -205,22 +255,38 @@ class ComposeViewModel @Inject constructor(
|
||||||
|
|
||||||
fun removeMediaFromQueue(item: QueuedMedia) {
|
fun removeMediaFromQueue(item: QueuedMedia) {
|
||||||
mediaUploader.cancelUploadScope(item.localId)
|
mediaUploader.cancelUploadScope(item.localId)
|
||||||
media.update { mediaList -> mediaList.filter { it.localId != item.localId } }
|
_media.update { mediaList -> mediaList.filter { it.localId != item.localId } }
|
||||||
|
updateCloseConfirmation()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleMarkSensitive() {
|
fun toggleMarkSensitive() {
|
||||||
this.markMediaAsSensitive.value = this.markMediaAsSensitive.value != true
|
this._markMediaAsSensitive.value = this._markMediaAsSensitive.value != true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleCloseButton(contentText: String?, contentWarning: String?): ConfirmationKind {
|
fun updateContent(newContent: String?) {
|
||||||
return if (didChange(contentText, contentWarning)) {
|
currentContent = newContent
|
||||||
|
updateCloseConfirmation()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateContentWarning(newContentWarning: String?) {
|
||||||
|
currentContentWarning = newContentWarning
|
||||||
|
updateCloseConfirmation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateCloseConfirmation() {
|
||||||
|
val contentWarning = if (_showContentWarning.value) {
|
||||||
|
currentContentWarning
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
this._closeConfirmation.value = if (didChange(currentContent, contentWarning)) {
|
||||||
when (composeKind) {
|
when (composeKind) {
|
||||||
ComposeKind.NEW -> if (isEmpty(contentText, contentWarning)) {
|
ComposeKind.NEW -> if (isEmpty(currentContent, contentWarning)) {
|
||||||
ConfirmationKind.NONE
|
ConfirmationKind.NONE
|
||||||
} else {
|
} else {
|
||||||
ConfirmationKind.SAVE_OR_DISCARD
|
ConfirmationKind.SAVE_OR_DISCARD
|
||||||
}
|
}
|
||||||
ComposeKind.EDIT_DRAFT -> if (isEmpty(contentText, contentWarning)) {
|
ComposeKind.EDIT_DRAFT -> if (isEmpty(currentContent, contentWarning)) {
|
||||||
ConfirmationKind.CONTINUE_EDITING_OR_DISCARD_DRAFT
|
ConfirmationKind.CONTINUE_EDITING_OR_DISCARD_DRAFT
|
||||||
} else {
|
} else {
|
||||||
ConfirmationKind.UPDATE_OR_DISCARD
|
ConfirmationKind.UPDATE_OR_DISCARD
|
||||||
|
@ -236,20 +302,21 @@ class ComposeViewModel @Inject constructor(
|
||||||
private fun didChange(content: String?, contentWarning: String?): Boolean {
|
private fun didChange(content: String?, contentWarning: String?): Boolean {
|
||||||
val textChanged = content.orEmpty() != startingText.orEmpty()
|
val textChanged = content.orEmpty() != startingText.orEmpty()
|
||||||
val contentWarningChanged = contentWarning.orEmpty() != startingContentWarning
|
val contentWarningChanged = contentWarning.orEmpty() != startingContentWarning
|
||||||
val mediaChanged = media.value.isNotEmpty()
|
val mediaChanged = _media.value.isNotEmpty()
|
||||||
val pollChanged = poll.value != null
|
val pollChanged = _poll.value != null
|
||||||
val didScheduledTimeChange = hasScheduledTimeChanged
|
val didScheduledTimeChange = hasScheduledTimeChanged
|
||||||
|
|
||||||
return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged || didScheduledTimeChange
|
return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged || didScheduledTimeChange
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isEmpty(content: String?, contentWarning: String?): Boolean {
|
private fun isEmpty(content: String?, contentWarning: String?): Boolean {
|
||||||
return !modifiedInitialState && (content.isNullOrBlank() && contentWarning.isNullOrBlank() && media.value.isEmpty() && poll.value == null)
|
return !modifiedInitialState && (content.isNullOrBlank() && contentWarning.isNullOrBlank() && _media.value.isEmpty() && _poll.value == null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun contentWarningChanged(value: Boolean) {
|
fun contentWarningChanged(value: Boolean) {
|
||||||
showContentWarning.value = value
|
_showContentWarning.value = value
|
||||||
contentWarningStateChanged = true
|
contentWarningStateChanged = true
|
||||||
|
updateCloseConfirmation()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteDraft() {
|
fun deleteDraft() {
|
||||||
|
@ -261,12 +328,12 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopUploads() {
|
fun stopUploads() {
|
||||||
mediaUploader.cancelUploadScope(*media.value.map { it.localId }.toIntArray())
|
mediaUploader.cancelUploadScope(*_media.value.map { it.localId }.toIntArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shouldShowSaveDraftDialog(): Boolean {
|
fun shouldShowSaveDraftDialog(): Boolean {
|
||||||
// if any of the media files need to be downloaded first it could take a while, so show a loading dialog
|
// if any of the media files need to be downloaded first it could take a while, so show a loading dialog
|
||||||
return media.value.any { mediaValue ->
|
return _media.value.any { mediaValue ->
|
||||||
mediaValue.uri.scheme == "https"
|
mediaValue.uri.scheme == "https"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -275,7 +342,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
val mediaUris: MutableList<String> = mutableListOf()
|
val mediaUris: MutableList<String> = mutableListOf()
|
||||||
val mediaDescriptions: MutableList<String?> = mutableListOf()
|
val mediaDescriptions: MutableList<String?> = mutableListOf()
|
||||||
val mediaFocus: MutableList<Attachment.Focus?> = mutableListOf()
|
val mediaFocus: MutableList<Attachment.Focus?> = mutableListOf()
|
||||||
for (item in media.value) {
|
for (item in _media.value) {
|
||||||
mediaUris.add(item.uri.toString())
|
mediaUris.add(item.uri.toString())
|
||||||
mediaDescriptions.add(item.description)
|
mediaDescriptions.add(item.description)
|
||||||
mediaFocus.add(item.focus)
|
mediaFocus.add(item.focus)
|
||||||
|
@ -287,15 +354,15 @@ class ComposeViewModel @Inject constructor(
|
||||||
inReplyToId = inReplyToId,
|
inReplyToId = inReplyToId,
|
||||||
content = content,
|
content = content,
|
||||||
contentWarning = contentWarning,
|
contentWarning = contentWarning,
|
||||||
sensitive = markMediaAsSensitive.value,
|
sensitive = _markMediaAsSensitive.value,
|
||||||
visibility = statusVisibility.value,
|
visibility = _statusVisibility.value,
|
||||||
mediaUris = mediaUris,
|
mediaUris = mediaUris,
|
||||||
mediaDescriptions = mediaDescriptions,
|
mediaDescriptions = mediaDescriptions,
|
||||||
mediaFocus = mediaFocus,
|
mediaFocus = mediaFocus,
|
||||||
poll = poll.value,
|
poll = _poll.value,
|
||||||
failedToSend = false,
|
failedToSend = false,
|
||||||
failedToSendAlert = false,
|
failedToSendAlert = false,
|
||||||
scheduledAt = scheduledAt.value,
|
scheduledAt = _scheduledAt.value,
|
||||||
language = postLanguage,
|
language = postLanguage,
|
||||||
statusId = originalStatusId
|
statusId = originalStatusId
|
||||||
)
|
)
|
||||||
|
@ -305,16 +372,12 @@ class ComposeViewModel @Inject constructor(
|
||||||
* Send status to the server.
|
* Send status to the server.
|
||||||
* Uses current state plus provided arguments.
|
* Uses current state plus provided arguments.
|
||||||
*/
|
*/
|
||||||
suspend fun sendStatus(
|
suspend fun sendStatus(content: String, spoilerText: String, accountId: Long) {
|
||||||
content: String,
|
|
||||||
spoilerText: String,
|
|
||||||
accountId: Long
|
|
||||||
) {
|
|
||||||
if (!scheduledTootId.isNullOrEmpty()) {
|
if (!scheduledTootId.isNullOrEmpty()) {
|
||||||
api.deleteScheduledStatus(scheduledTootId!!)
|
api.deleteScheduledStatus(scheduledTootId!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
val attachedMedia = media.value.map { item ->
|
val attachedMedia = _media.value.map { item ->
|
||||||
MediaToSend(
|
MediaToSend(
|
||||||
localId = item.localId,
|
localId = item.localId,
|
||||||
id = item.id,
|
id = item.id,
|
||||||
|
@ -327,12 +390,12 @@ class ComposeViewModel @Inject constructor(
|
||||||
val tootToSend = StatusToSend(
|
val tootToSend = StatusToSend(
|
||||||
text = content,
|
text = content,
|
||||||
warningText = spoilerText,
|
warningText = spoilerText,
|
||||||
visibility = statusVisibility.value.serverString(),
|
visibility = _statusVisibility.value.serverString,
|
||||||
sensitive = attachedMedia.isNotEmpty() && (markMediaAsSensitive.value || showContentWarning.value),
|
sensitive = attachedMedia.isNotEmpty() && (_markMediaAsSensitive.value || _showContentWarning.value),
|
||||||
media = attachedMedia,
|
media = attachedMedia,
|
||||||
scheduledAt = scheduledAt.value,
|
scheduledAt = _scheduledAt.value,
|
||||||
inReplyToId = inReplyToId,
|
inReplyToId = inReplyToId,
|
||||||
poll = poll.value,
|
poll = _poll.value,
|
||||||
replyingStatusContent = null,
|
replyingStatusContent = null,
|
||||||
replyingStatusAuthorUsername = null,
|
replyingStatusAuthorUsername = null,
|
||||||
accountId = accountId,
|
accountId = accountId,
|
||||||
|
@ -347,7 +410,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMediaItem(localId: Int, mutator: (QueuedMedia) -> QueuedMedia) {
|
private fun updateMediaItem(localId: Int, mutator: (QueuedMedia) -> QueuedMedia) {
|
||||||
media.update { mediaList ->
|
_media.update { mediaList ->
|
||||||
mediaList.map { mediaItem ->
|
mediaList.map { mediaItem ->
|
||||||
if (mediaItem.localId == localId) {
|
if (mediaItem.localId == localId) {
|
||||||
mutator(mediaItem)
|
mutator(mediaItem)
|
||||||
|
@ -371,9 +434,9 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchAutocompleteSuggestions(token: String): List<AutocompleteResult> {
|
fun searchAutocompleteSuggestions(token: String): List<AutocompleteResult> {
|
||||||
when (token[0]) {
|
return when (token[0]) {
|
||||||
'@' -> {
|
'@' -> runBlocking {
|
||||||
return api.searchAccountsSync(query = token.substring(1), limit = 10)
|
api.searchAccounts(query = token.substring(1), limit = 10)
|
||||||
.fold({ accounts ->
|
.fold({ accounts ->
|
||||||
accounts.map { AutocompleteResult.AccountResult(it) }
|
accounts.map { AutocompleteResult.AccountResult(it) }
|
||||||
}, { e ->
|
}, { e ->
|
||||||
|
@ -381,8 +444,12 @@ class ComposeViewModel @Inject constructor(
|
||||||
emptyList()
|
emptyList()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
'#' -> {
|
'#' -> runBlocking {
|
||||||
return api.searchSync(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
|
api.search(
|
||||||
|
query = token,
|
||||||
|
type = SearchType.Hashtag.apiParameter,
|
||||||
|
limit = 10
|
||||||
|
)
|
||||||
.fold({ searchResult ->
|
.fold({ searchResult ->
|
||||||
searchResult.hashtags.map { AutocompleteResult.HashtagResult(it.name) }
|
searchResult.hashtags.map { AutocompleteResult.HashtagResult(it.name) }
|
||||||
}, { e ->
|
}, { e ->
|
||||||
|
@ -390,11 +457,12 @@ class ComposeViewModel @Inject constructor(
|
||||||
emptyList()
|
emptyList()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
':' -> {
|
':' -> {
|
||||||
val emojiList = emoji.replayCache.firstOrNull() ?: return emptyList()
|
val emojiList = emoji.replayCache.firstOrNull() ?: return emptyList()
|
||||||
val incomplete = token.substring(1)
|
val incomplete = token.substring(1)
|
||||||
|
|
||||||
return emojiList.filter { emoji ->
|
emojiList.filter { emoji ->
|
||||||
emoji.shortcode.contains(incomplete, ignoreCase = true)
|
emoji.shortcode.contains(incomplete, ignoreCase = true)
|
||||||
}.sortedBy { emoji ->
|
}.sortedBy { emoji ->
|
||||||
emoji.shortcode.indexOf(incomplete, ignoreCase = true)
|
emoji.shortcode.indexOf(incomplete, ignoreCase = true)
|
||||||
|
@ -402,9 +470,10 @@ class ComposeViewModel @Inject constructor(
|
||||||
AutocompleteResult.EmojiResult(emoji)
|
AutocompleteResult.EmojiResult(emoji)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Log.w(TAG, "Unexpected autocompletion token: $token")
|
Log.w(TAG, "Unexpected autocompletion token: $token")
|
||||||
return emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -415,16 +484,17 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
composeKind = composeOptions?.kind ?: ComposeKind.NEW
|
composeKind = composeOptions?.kind ?: ComposeKind.NEW
|
||||||
|
inReplyToId = composeOptions?.inReplyToId
|
||||||
|
|
||||||
val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy
|
val activeAccount = accountManager.activeAccount!!
|
||||||
|
val preferredVisibility =
|
||||||
|
if (inReplyToId != null) activeAccount.defaultReplyPrivacy else activeAccount.defaultPostPrivacy
|
||||||
|
|
||||||
val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN
|
val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN
|
||||||
startingVisibility = Status.Visibility.byNum(
|
startingVisibility = Status.Visibility.byNum(
|
||||||
preferredVisibility.num.coerceAtLeast(replyVisibility.num)
|
preferredVisibility.num.coerceAtLeast(replyVisibility.num)
|
||||||
)
|
)
|
||||||
|
|
||||||
inReplyToId = composeOptions?.inReplyToId
|
|
||||||
|
|
||||||
modifiedInitialState = composeOptions?.modifiedInitialState == true
|
modifiedInitialState = composeOptions?.modifiedInitialState == true
|
||||||
|
|
||||||
val contentWarning = composeOptions?.contentWarning
|
val contentWarning = composeOptions?.contentWarning
|
||||||
|
@ -432,7 +502,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
startingContentWarning = contentWarning
|
startingContentWarning = contentWarning
|
||||||
}
|
}
|
||||||
if (!contentWarningStateChanged) {
|
if (!contentWarningStateChanged) {
|
||||||
showContentWarning.value = !contentWarning.isNullOrBlank()
|
_showContentWarning.value = !contentWarning.isNullOrBlank()
|
||||||
}
|
}
|
||||||
|
|
||||||
// recreate media list
|
// recreate media list
|
||||||
|
@ -460,13 +530,14 @@ class ComposeViewModel @Inject constructor(
|
||||||
scheduledTootId = composeOptions?.scheduledTootId
|
scheduledTootId = composeOptions?.scheduledTootId
|
||||||
originalStatusId = composeOptions?.statusId
|
originalStatusId = composeOptions?.statusId
|
||||||
startingText = composeOptions?.content
|
startingText = composeOptions?.content
|
||||||
|
currentContent = composeOptions?.content
|
||||||
postLanguage = composeOptions?.language
|
postLanguage = composeOptions?.language
|
||||||
|
|
||||||
val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
|
val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
|
||||||
if (tootVisibility.num != Status.Visibility.UNKNOWN.num) {
|
if (tootVisibility.num != Status.Visibility.UNKNOWN.num) {
|
||||||
startingVisibility = tootVisibility
|
startingVisibility = tootVisibility
|
||||||
}
|
}
|
||||||
statusVisibility.value = startingVisibility
|
_statusVisibility.value = startingVisibility
|
||||||
val mentionedUsernames = composeOptions?.mentionedUsernames
|
val mentionedUsernames = composeOptions?.mentionedUsernames
|
||||||
if (mentionedUsernames != null) {
|
if (mentionedUsernames != null) {
|
||||||
val builder = StringBuilder()
|
val builder = StringBuilder()
|
||||||
|
@ -478,30 +549,33 @@ class ComposeViewModel @Inject constructor(
|
||||||
startingText = builder.toString()
|
startingText = builder.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduledAt.value = composeOptions?.scheduledAt
|
_scheduledAt.value = composeOptions?.scheduledAt
|
||||||
|
|
||||||
composeOptions?.sensitive?.let { markMediaAsSensitive.value = it }
|
composeOptions?.sensitive?.let { _markMediaAsSensitive.value = it }
|
||||||
|
|
||||||
val poll = composeOptions?.poll
|
val poll = composeOptions?.poll
|
||||||
if (poll != null && composeOptions.mediaAttachments.isNullOrEmpty()) {
|
if (poll != null && composeOptions.mediaAttachments.isNullOrEmpty()) {
|
||||||
this.poll.value = poll
|
this._poll.value = poll
|
||||||
}
|
}
|
||||||
replyingStatusContent = composeOptions?.replyingStatusContent
|
replyingStatusContent = composeOptions?.replyingStatusContent
|
||||||
replyingStatusAuthor = composeOptions?.replyingStatusAuthor
|
replyingStatusAuthor = composeOptions?.replyingStatusAuthor
|
||||||
|
|
||||||
|
updateCloseConfirmation()
|
||||||
|
|
||||||
setupComplete = true
|
setupComplete = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updatePoll(newPoll: NewPoll) {
|
fun updatePoll(newPoll: NewPoll?) {
|
||||||
poll.value = newPoll
|
_poll.value = newPoll
|
||||||
|
updateCloseConfirmation()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateScheduledAt(newScheduledAt: String?) {
|
fun updateScheduledAt(newScheduledAt: String?) {
|
||||||
if (newScheduledAt != scheduledAt.value) {
|
if (newScheduledAt != _scheduledAt.value) {
|
||||||
hasScheduledTimeChanged = true
|
hasScheduledTimeChanged = true
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduledAt.value = newScheduledAt
|
_scheduledAt.value = newScheduledAt
|
||||||
}
|
}
|
||||||
|
|
||||||
val editing: Boolean
|
val editing: Boolean
|
||||||
|
|
|
@ -42,7 +42,7 @@ fun downsizeImage(
|
||||||
tempFile: File
|
tempFile: File
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val decodeBoundsInputStream = try {
|
val decodeBoundsInputStream = try {
|
||||||
contentResolver.openInputStream(uri)
|
contentResolver.openInputStream(uri) ?: return false
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -54,10 +54,10 @@ fun downsizeImage(
|
||||||
// Get EXIF data, for orientation info.
|
// Get EXIF data, for orientation info.
|
||||||
val orientation = getImageOrientation(uri, contentResolver)
|
val orientation = getImageOrientation(uri, contentResolver)
|
||||||
/* Unfortunately, there isn't a determined worst case compression ratio for image
|
/* Unfortunately, there isn't a determined worst case compression ratio for image
|
||||||
* formats. So, the only way to tell if they're too big is to compress them and
|
* formats. So, the only way to tell if they're too big is to compress them and
|
||||||
* test, and keep trying at smaller sizes. The initial estimate should be good for
|
* test, and keep trying at smaller sizes. The initial estimate should be good for
|
||||||
* many cases, so it should only iterate once, but the loop is used to be absolutely
|
* many cases, so it should only iterate once, but the loop is used to be absolutely
|
||||||
* sure it gets downsized to below the limit. */
|
* sure it gets downsized to below the limit. */
|
||||||
var scaledImageSize = 1024
|
var scaledImageSize = 1024
|
||||||
do {
|
do {
|
||||||
val outputStream = try {
|
val outputStream = try {
|
||||||
|
@ -66,7 +66,7 @@ fun downsizeImage(
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val decodeBitmapInputStream = try {
|
val decodeBitmapInputStream = try {
|
||||||
contentResolver.openInputStream(uri)
|
contentResolver.openInputStream(uri) ?: return false
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,11 +113,17 @@ class MediaPreviewAdapter(
|
||||||
private val differ = AsyncListDiffer(
|
private val differ = AsyncListDiffer(
|
||||||
this,
|
this,
|
||||||
object : DiffUtil.ItemCallback<ComposeActivity.QueuedMedia>() {
|
object : DiffUtil.ItemCallback<ComposeActivity.QueuedMedia>() {
|
||||||
override fun areItemsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
|
override fun areItemsTheSame(
|
||||||
|
oldItem: ComposeActivity.QueuedMedia,
|
||||||
|
newItem: ComposeActivity.QueuedMedia
|
||||||
|
): Boolean {
|
||||||
return oldItem.localId == newItem.localId
|
return oldItem.localId == newItem.localId
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
|
override fun areContentsTheSame(
|
||||||
|
oldItem: ComposeActivity.QueuedMedia,
|
||||||
|
newItem: ComposeActivity.QueuedMedia
|
||||||
|
): Boolean {
|
||||||
return oldItem == newItem
|
return oldItem == newItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.compose
|
package com.keylesspalace.tusky.components.compose
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
|
@ -31,12 +30,17 @@ import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||||
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo
|
||||||
import com.keylesspalace.tusky.network.MediaUploadApi
|
import com.keylesspalace.tusky.network.MediaUploadApi
|
||||||
import com.keylesspalace.tusky.network.ProgressRequestBody
|
import com.keylesspalace.tusky.network.asRequestBody
|
||||||
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
|
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
|
||||||
import com.keylesspalace.tusky.util.getImageSquarePixels
|
import com.keylesspalace.tusky.util.getImageSquarePixels
|
||||||
import com.keylesspalace.tusky.util.getMediaSize
|
import com.keylesspalace.tusky.util.getMediaSize
|
||||||
import com.keylesspalace.tusky.util.getServerErrorMessage
|
import com.keylesspalace.tusky.util.getServerErrorMessage
|
||||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
@ -53,21 +57,20 @@ import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.shareIn
|
import kotlinx.coroutines.flow.shareIn
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import okio.source
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.Date
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
sealed interface FinalUploadEvent
|
sealed interface FinalUploadEvent
|
||||||
|
|
||||||
sealed class UploadEvent {
|
sealed interface UploadEvent {
|
||||||
data class ProgressEvent(val percentage: Int) : UploadEvent()
|
data class ProgressEvent(val percentage: Int) : UploadEvent
|
||||||
data class FinishedEvent(val mediaId: String, val processed: Boolean) : UploadEvent(), FinalUploadEvent
|
data class FinishedEvent(
|
||||||
data class ErrorEvent(val error: Throwable) : UploadEvent(), FinalUploadEvent
|
val mediaId: String,
|
||||||
|
val processed: Boolean
|
||||||
|
) : UploadEvent, FinalUploadEvent
|
||||||
|
data class ErrorEvent(val error: Throwable) : UploadEvent, FinalUploadEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
data class UploadData(
|
data class UploadData(
|
||||||
|
@ -80,11 +83,7 @@ fun createNewImageFile(context: Context, suffix: String = ".jpg"): File {
|
||||||
val randomId = randomAlphanumericString(12)
|
val randomId = randomAlphanumericString(12)
|
||||||
val imageFileName = "Tusky_${randomId}_"
|
val imageFileName = "Tusky_${randomId}_"
|
||||||
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
||||||
return File.createTempFile(
|
return File.createTempFile(imageFileName, suffix, storageDir)
|
||||||
imageFileName, /* prefix */
|
|
||||||
suffix, /* suffix */
|
|
||||||
storageDir /* directory */
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PreparedMedia(val type: QueuedMedia.Type, val uri: Uri, val size: Long)
|
data class PreparedMedia(val type: QueuedMedia.Type, val uri: Uri, val size: Long)
|
||||||
|
@ -96,7 +95,7 @@ class UploadServerError(val errorMessage: String) : Exception()
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class MediaUploader @Inject constructor(
|
class MediaUploader @Inject constructor(
|
||||||
private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val mediaUploadApi: MediaUploadApi
|
private val mediaUploadApi: MediaUploadApi
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -164,22 +163,22 @@ class MediaUploader @Inject constructor(
|
||||||
|
|
||||||
val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp")
|
val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp")
|
||||||
|
|
||||||
contentResolver.openInputStream(inUri).use { input ->
|
contentResolver.openInputStream(inUri)?.source().use { input ->
|
||||||
if (input == null) {
|
if (input == null) {
|
||||||
Log.w(TAG, "Media input is null")
|
Log.w(TAG, "Media input is null")
|
||||||
uri = inUri
|
uri = inUri
|
||||||
return@use
|
return@use
|
||||||
}
|
}
|
||||||
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
|
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
|
||||||
FileOutputStream(file.absoluteFile).use { out ->
|
file.absoluteFile.sink().buffer().use { out ->
|
||||||
input.copyTo(out)
|
out.writeAll(input)
|
||||||
uri = FileProvider.getUriForFile(
|
|
||||||
context,
|
|
||||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
|
||||||
file
|
|
||||||
)
|
|
||||||
mediaSize = getMediaSize(contentResolver, uri)
|
|
||||||
}
|
}
|
||||||
|
uri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
mediaSize = getMediaSize(contentResolver, uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContentResolver.SCHEME_FILE -> {
|
ContentResolver.SCHEME_FILE -> {
|
||||||
|
@ -192,17 +191,18 @@ class MediaUploader @Inject constructor(
|
||||||
val suffix = inputFile.name.substringAfterLast('.', "tmp")
|
val suffix = inputFile.name.substringAfterLast('.', "tmp")
|
||||||
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(suffix)
|
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(suffix)
|
||||||
val file = File.createTempFile("randomTemp1", ".$suffix", context.cacheDir)
|
val file = File.createTempFile("randomTemp1", ".$suffix", context.cacheDir)
|
||||||
val input = FileInputStream(inputFile)
|
|
||||||
|
|
||||||
FileOutputStream(file.absoluteFile).use { out ->
|
inputFile.source().use { input ->
|
||||||
input.copyTo(out)
|
file.absoluteFile.sink().buffer().use { out ->
|
||||||
uri = FileProvider.getUriForFile(
|
out.writeAll(input)
|
||||||
context,
|
}
|
||||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
|
||||||
file
|
|
||||||
)
|
|
||||||
mediaSize = getMediaSize(contentResolver, uri)
|
|
||||||
}
|
}
|
||||||
|
uri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
mediaSize = getMediaSize(contentResolver, uri)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Log.w(TAG, "Unknown uri scheme $uri")
|
Log.w(TAG, "Unknown uri scheme $uri")
|
||||||
|
@ -247,7 +247,6 @@ class MediaUploader @Inject constructor(
|
||||||
|
|
||||||
private val contentResolver = context.contentResolver
|
private val contentResolver = context.contentResolver
|
||||||
|
|
||||||
@SuppressLint("Recycle") // stream is closed in ProgressRequestBody
|
|
||||||
private suspend fun upload(media: QueuedMedia): Flow<UploadEvent> {
|
private suspend fun upload(media: QueuedMedia): Flow<UploadEvent> {
|
||||||
return callbackFlow {
|
return callbackFlow {
|
||||||
var mimeType = contentResolver.getType(media.uri)
|
var mimeType = contentResolver.getType(media.uri)
|
||||||
|
@ -256,9 +255,9 @@ class MediaUploader @Inject constructor(
|
||||||
// .m4a files. See https://github.com/tuskyapp/Tusky/issues/3189 for details.
|
// .m4a files. See https://github.com/tuskyapp/Tusky/issues/3189 for details.
|
||||||
// Sniff the content of the file to determine the actual type.
|
// Sniff the content of the file to determine the actual type.
|
||||||
if (mimeType != null && (
|
if (mimeType != null && (
|
||||||
mimeType.startsWith("audio/", ignoreCase = true) ||
|
mimeType.startsWith("audio/", ignoreCase = true) ||
|
||||||
mimeType.startsWith("video/", ignoreCase = true)
|
mimeType.startsWith("video/", ignoreCase = true)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
val retriever = MediaMetadataRetriever()
|
val retriever = MediaMetadataRetriever()
|
||||||
retriever.setDataSource(context, media.uri)
|
retriever.setDataSource(context, media.uri)
|
||||||
|
@ -266,22 +265,20 @@ class MediaUploader @Inject constructor(
|
||||||
}
|
}
|
||||||
val map = MimeTypeMap.getSingleton()
|
val map = MimeTypeMap.getSingleton()
|
||||||
val fileExtension = map.getExtensionFromMimeType(mimeType)
|
val fileExtension = map.getExtensionFromMimeType(mimeType)
|
||||||
val filename = "%s_%s_%s.%s".format(
|
val filename = "%s_%d_%s.%s".format(
|
||||||
context.getString(R.string.app_name),
|
context.getString(R.string.app_name),
|
||||||
Date().time.toString(),
|
System.currentTimeMillis(),
|
||||||
randomAlphanumericString(10),
|
randomAlphanumericString(10),
|
||||||
fileExtension
|
fileExtension
|
||||||
)
|
)
|
||||||
|
|
||||||
val stream = contentResolver.openInputStream(media.uri)
|
|
||||||
|
|
||||||
if (mimeType == null) mimeType = "multipart/form-data"
|
if (mimeType == null) mimeType = "multipart/form-data"
|
||||||
|
|
||||||
var lastProgress = -1
|
var lastProgress = -1
|
||||||
val fileBody = ProgressRequestBody(
|
val fileBody = media.uri.asRequestBody(
|
||||||
stream!!,
|
contentResolver,
|
||||||
media.mediaSize,
|
requireNotNull(mimeType.toMediaTypeOrNull()) { "Invalid Content Type" },
|
||||||
mimeType.toMediaTypeOrNull()!!
|
media.mediaSize
|
||||||
) { percentage ->
|
) { percentage ->
|
||||||
if (percentage != lastProgress) {
|
if (percentage != lastProgress) {
|
||||||
trySend(UploadEvent.ProgressEvent(percentage))
|
trySend(UploadEvent.ProgressEvent(percentage))
|
||||||
|
|
|
@ -60,7 +60,9 @@ fun showAddPollDialog(
|
||||||
binding.pollChoices.adapter = adapter
|
binding.pollChoices.adapter = adapter
|
||||||
|
|
||||||
var durations = context.resources.getIntArray(R.array.poll_duration_values).toList()
|
var durations = context.resources.getIntArray(R.array.poll_duration_values).toList()
|
||||||
val durationLabels = context.resources.getStringArray(R.array.poll_duration_names).filterIndexed { index, _ -> durations[index] in minDuration..maxDuration }
|
val durationLabels = context.resources.getStringArray(
|
||||||
|
R.array.poll_duration_names
|
||||||
|
).filterIndexed { index, _ -> durations[index] in minDuration..maxDuration }
|
||||||
binding.pollDurationSpinner.adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, durationLabels).apply {
|
binding.pollDurationSpinner.adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, durationLabels).apply {
|
||||||
setDropDownViewResource(androidx.appcompat.R.layout.support_simple_spinner_dropdown_item)
|
setDropDownViewResource(androidx.appcompat.R.layout.support_simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
|
@ -75,8 +77,8 @@ fun showAddPollDialog(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val DAY_SECONDS = 60 * 60 * 24
|
val secondsInADay = 60 * 60 * 24
|
||||||
val desiredDuration = poll?.expiresIn ?: DAY_SECONDS
|
val desiredDuration = poll?.expiresIn ?: secondsInADay
|
||||||
val pollDurationId = durations.indexOfLast {
|
val pollDurationId = durations.indexOfLast {
|
||||||
it <= desiredDuration
|
it <= desiredDuration
|
||||||
}
|
}
|
||||||
|
@ -105,5 +107,7 @@ fun showAddPollDialog(
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|
||||||
// make the dialog focusable so the keyboard does not stay behind it
|
// make the dialog focusable so the keyboard does not stay behind it
|
||||||
dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
|
dialog.window?.clearFlags(
|
||||||
|
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,8 +41,15 @@ class AddPollOptionsAdapter(
|
||||||
notifyItemInserted(options.size - 1)
|
notifyItemInserted(options.size - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAddPollOptionBinding> {
|
override fun onCreateViewHolder(
|
||||||
val binding = ItemAddPollOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): BindingHolder<ItemAddPollOptionBinding> {
|
||||||
|
val binding = ItemAddPollOptionBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
val holder = BindingHolder(binding)
|
val holder = BindingHolder(binding)
|
||||||
binding.optionEditText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength))
|
binding.optionEditText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength))
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,6 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.core.os.BundleCompat
|
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
@ -34,6 +33,7 @@ import com.bumptech.glide.request.target.CustomTarget
|
||||||
import com.bumptech.glide.request.transition.Transition
|
import com.bumptech.glide.request.transition.Transition
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.DialogImageDescriptionBinding
|
import com.keylesspalace.tusky.databinding.DialogImageDescriptionBinding
|
||||||
|
import com.keylesspalace.tusky.util.getParcelableCompat
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ class CaptionDialog : DialogFragment() {
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
) = inflater.inflate(R.layout.dialog_image_description, container, false)
|
): View = inflater.inflate(R.layout.dialog_image_description, container, false)
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
val imageView = binding.imageDescriptionView
|
val imageView = binding.imageDescriptionView
|
||||||
|
@ -82,7 +82,7 @@ class CaptionDialog : DialogFragment() {
|
||||||
|
|
||||||
isCancelable = true
|
isCancelable = true
|
||||||
|
|
||||||
val previewUri = BundleCompat.getParcelable(requireArguments(), PREVIEW_URI_ARG, Uri::class.java) ?: error("Preview Uri is null")
|
val previewUri = arguments?.getParcelableCompat<Uri>(PREVIEW_URI_ARG) ?: error("Preview Uri is null")
|
||||||
|
|
||||||
// Load the image and manually set it into the ImageView because it doesn't have a fixed size.
|
// Load the image and manually set it into the ImageView because it doesn't have a fixed size.
|
||||||
Glide.with(this)
|
Glide.with(this)
|
||||||
|
@ -133,17 +133,14 @@ class CaptionDialog : DialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance(
|
fun newInstance(localId: Int, existingDescription: String?, previewUri: Uri) =
|
||||||
localId: Int,
|
CaptionDialog().apply {
|
||||||
existingDescription: String?,
|
arguments = bundleOf(
|
||||||
previewUri: Uri
|
LOCAL_ID_ARG to localId,
|
||||||
) = CaptionDialog().apply {
|
EXISTING_DESCRIPTION_ARG to existingDescription,
|
||||||
arguments = bundleOf(
|
PREVIEW_URI_ARG to previewUri
|
||||||
LOCAL_ID_ARG to localId,
|
)
|
||||||
EXISTING_DESCRIPTION_ARG to existingDescription,
|
}
|
||||||
PREVIEW_URI_ARG to previewUri
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val DESCRIPTION_KEY = "description"
|
private const val DESCRIPTION_KEY = "description"
|
||||||
private const val EXISTING_DESCRIPTION_ARG = "existing_description"
|
private const val EXISTING_DESCRIPTION_ARG = "existing_description"
|
||||||
|
|
|
@ -49,11 +49,22 @@ fun <T> T.makeFocusDialog(
|
||||||
.load(previewUri)
|
.load(previewUri)
|
||||||
.downsample(DownsampleStrategy.CENTER_INSIDE)
|
.downsample(DownsampleStrategy.CENTER_INSIDE)
|
||||||
.listener(object : RequestListener<Drawable> {
|
.listener(object : RequestListener<Drawable> {
|
||||||
override fun onLoadFailed(p0: GlideException?, p1: Any?, p2: Target<Drawable?>, p3: Boolean): Boolean {
|
override fun onLoadFailed(
|
||||||
|
p0: GlideException?,
|
||||||
|
p1: Any?,
|
||||||
|
p2: Target<Drawable?>,
|
||||||
|
p3: Boolean
|
||||||
|
): Boolean {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResourceReady(resource: Drawable, model: Any, target: Target<Drawable?>?, dataSource: DataSource, isFirstResource: Boolean): Boolean {
|
override fun onResourceReady(
|
||||||
|
resource: Drawable,
|
||||||
|
model: Any,
|
||||||
|
target: Target<Drawable?>?,
|
||||||
|
dataSource: DataSource,
|
||||||
|
isFirstResource: Boolean
|
||||||
|
): Boolean {
|
||||||
val width = resource.intrinsicWidth
|
val width = resource.intrinsicWidth
|
||||||
val height = resource.intrinsicHeight
|
val height = resource.intrinsicHeight
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,10 @@ import android.widget.RadioGroup
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
|
||||||
class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : RadioGroup(context, attrs) {
|
class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : RadioGroup(
|
||||||
|
context,
|
||||||
|
attrs
|
||||||
|
) {
|
||||||
|
|
||||||
var listener: ComposeOptionsListener? = null
|
var listener: ComposeOptionsListener? = null
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,8 @@ import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.google.android.material.datepicker.CalendarConstraints
|
import com.google.android.material.datepicker.CalendarConstraints
|
||||||
import com.google.android.material.datepicker.DateValidatorPointForward
|
import com.google.android.material.datepicker.DateValidatorPointForward
|
||||||
import com.google.android.material.datepicker.MaterialDatePicker
|
import com.google.android.material.datepicker.MaterialDatePicker
|
||||||
|
@ -89,10 +89,10 @@ class ComposeScheduleView
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setEditIcons() {
|
private fun setEditIcons() {
|
||||||
val icon = ContextCompat.getDrawable(context, R.drawable.ic_create_24dp) ?: return
|
val icon = AppCompatResources.getDrawable(context, R.drawable.ic_create_24dp) ?: return
|
||||||
val size = binding.scheduledDateTime.lineHeight
|
val size = binding.scheduledDateTime.lineHeight
|
||||||
icon.setBounds(0, 0, size, size)
|
icon.setBounds(0, 0, size, size)
|
||||||
binding.scheduledDateTime.setCompoundDrawables(null, null, icon, null)
|
binding.scheduledDateTime.setCompoundDrawablesRelative(null, null, icon, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setResetOnClickListener(listener: OnClickListener?) {
|
fun setResetOnClickListener(listener: OnClickListener?) {
|
||||||
|
@ -223,7 +223,8 @@ class ComposeScheduleView
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
var MINIMUM_SCHEDULED_SECONDS = 330 // Minimum is 5 minutes, pad 30 seconds for posting
|
// Minimum is 5 minutes, pad 30 seconds for posting
|
||||||
|
private const val MINIMUM_SCHEDULED_SECONDS = 330
|
||||||
fun calendar(): Calendar = Calendar.getInstance(TimeZone.getDefault())
|
fun calendar(): Calendar = Calendar.getInstance(TimeZone.getDefault())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,9 @@ class FocusIndicatorView
|
||||||
return offset.toFloat() + ((value + 1.0f) / 2.0f) * innerLimit.toFloat() // From range -1..1
|
return offset.toFloat() + ((value + 1.0f) / 2.0f) * innerLimit.toFloat() // From range -1..1
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility") // Android Studio wants us to implement PerformClick for accessibility, but that unfortunately cannot be made meaningful for this widget.
|
@SuppressLint(
|
||||||
|
"ClickableViewAccessibility"
|
||||||
|
) // Android Studio wants us to implement PerformClick for accessibility, but that unfortunately cannot be made meaningful for this widget.
|
||||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
||||||
return false
|
return false
|
||||||
|
@ -105,14 +107,20 @@ class FocusIndicatorView
|
||||||
val imageSize = this.imageSize
|
val imageSize = this.imageSize
|
||||||
val focus = this.focus
|
val focus = this.focus
|
||||||
|
|
||||||
if (imageSize != null && focus != null) {
|
if (imageSize != null && focus?.x != null && focus.y != null) {
|
||||||
val x = axisFromFocus(focus.x, imageSize.x, this.width)
|
val x = axisFromFocus(focus.x, imageSize.x, this.width)
|
||||||
val y = axisFromFocus(-focus.y, imageSize.y, this.height)
|
val y = axisFromFocus(-focus.y, imageSize.y, this.height)
|
||||||
val circleRadius = getCircleRadius()
|
val circleRadius = getCircleRadius()
|
||||||
|
|
||||||
curtainPath.reset() // Draw a flood fill with a hole cut out of it
|
curtainPath.reset() // Draw a flood fill with a hole cut out of it
|
||||||
curtainPath.fillType = Path.FillType.WINDING
|
curtainPath.fillType = Path.FillType.WINDING
|
||||||
curtainPath.addRect(0.0f, 0.0f, this.width.toFloat(), this.height.toFloat(), Path.Direction.CW)
|
curtainPath.addRect(
|
||||||
|
0.0f,
|
||||||
|
0.0f,
|
||||||
|
this.width.toFloat(),
|
||||||
|
this.height.toFloat(),
|
||||||
|
Path.Direction.CW
|
||||||
|
)
|
||||||
curtainPath.addCircle(x, y, circleRadius, Path.Direction.CCW)
|
curtainPath.addCircle(x, y, circleRadius, Path.Direction.CCW)
|
||||||
canvas.drawPath(curtainPath, curtainPaint)
|
canvas.drawPath(curtainPath, curtainPaint)
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,10 @@ class TootButton
|
||||||
Status.Visibility.PRIVATE,
|
Status.Visibility.PRIVATE,
|
||||||
Status.Visibility.DIRECT -> {
|
Status.Visibility.DIRECT -> {
|
||||||
setText(R.string.action_send)
|
setText(R.string.action_send)
|
||||||
IconicsDrawable(context, GoogleMaterial.Icon.gmd_lock).apply { sizeDp = 18; colorInt = Color.WHITE }
|
IconicsDrawable(context, GoogleMaterial.Icon.gmd_lock).apply {
|
||||||
|
sizeDp = 18
|
||||||
|
colorInt = Color.WHITE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
null
|
null
|
||||||
|
|
|
@ -38,7 +38,9 @@ class ConversationAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder {
|
||||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false)
|
val view = LayoutInflater.from(
|
||||||
|
parent.context
|
||||||
|
).inflate(R.layout.item_conversation, parent, false)
|
||||||
return ConversationViewHolder(view, statusDisplayOptions, listener)
|
return ConversationViewHolder(view, statusDisplayOptions, listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,15 +60,24 @@ class ConversationAdapter(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback<ConversationViewData>() {
|
val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback<ConversationViewData>() {
|
||||||
override fun areItemsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean {
|
override fun areItemsTheSame(
|
||||||
|
oldItem: ConversationViewData,
|
||||||
|
newItem: ConversationViewData
|
||||||
|
): Boolean {
|
||||||
return oldItem.id == newItem.id
|
return oldItem.id == newItem.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean {
|
override fun areContentsTheSame(
|
||||||
|
oldItem: ConversationViewData,
|
||||||
|
newItem: ConversationViewData
|
||||||
|
): Boolean {
|
||||||
return false // Items are different always. It allows to refresh timestamp on every view holder update
|
return false // Items are different always. It allows to refresh timestamp on every view holder update
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: ConversationViewData, newItem: ConversationViewData): Any? {
|
override fun getChangePayload(
|
||||||
|
oldItem: ConversationViewData,
|
||||||
|
newItem: ConversationViewData
|
||||||
|
): Any? {
|
||||||
return if (oldItem == newItem) {
|
return if (oldItem == newItem) {
|
||||||
// If items are equal - update timestamp only
|
// If items are equal - update timestamp only
|
||||||
listOf(StatusBaseViewHolder.Key.KEY_CREATED)
|
listOf(StatusBaseViewHolder.Key.KEY_CREATED)
|
||||||
|
|
|
@ -27,6 +27,7 @@ import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@Entity(primaryKeys = ["id", "accountId"])
|
@Entity(primaryKeys = ["id", "accountId"])
|
||||||
|
@ -50,6 +51,7 @@ data class ConversationEntity(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class ConversationAccountEntity(
|
data class ConversationAccountEntity(
|
||||||
val id: String,
|
val id: String,
|
||||||
val localUsername: String,
|
val localUsername: String,
|
||||||
|
@ -124,14 +126,14 @@ data class ConversationStatusEntity(
|
||||||
visibility = Status.Visibility.DIRECT,
|
visibility = Status.Visibility.DIRECT,
|
||||||
attachments = attachments,
|
attachments = attachments,
|
||||||
mentions = mentions,
|
mentions = mentions,
|
||||||
tags = tags,
|
tags = tags.orEmpty(),
|
||||||
application = null,
|
application = null,
|
||||||
pinned = false,
|
pinned = false,
|
||||||
muted = muted,
|
muted = muted,
|
||||||
poll = poll,
|
poll = poll,
|
||||||
card = null,
|
card = null,
|
||||||
language = language,
|
language = language,
|
||||||
filtered = null
|
filtered = emptyList()
|
||||||
),
|
),
|
||||||
isExpanded = expanded,
|
isExpanded = expanded,
|
||||||
isShowingContent = showingHiddenContent,
|
isShowingContent = showingHiddenContent,
|
||||||
|
@ -140,21 +142,16 @@ data class ConversationStatusEntity(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TimelineAccount.toEntity() =
|
fun TimelineAccount.toEntity() = ConversationAccountEntity(
|
||||||
ConversationAccountEntity(
|
id = id,
|
||||||
id = id,
|
localUsername = localUsername,
|
||||||
localUsername = localUsername,
|
username = username,
|
||||||
username = username,
|
displayName = name,
|
||||||
displayName = name,
|
avatar = avatar,
|
||||||
avatar = avatar,
|
emojis = emojis
|
||||||
emojis = emojis.orEmpty()
|
)
|
||||||
)
|
|
||||||
|
|
||||||
fun Status.toEntity(
|
fun Status.toEntity(expanded: Boolean, contentShowing: Boolean, contentCollapsed: Boolean) =
|
||||||
expanded: Boolean,
|
|
||||||
contentShowing: Boolean,
|
|
||||||
contentCollapsed: Boolean
|
|
||||||
) =
|
|
||||||
ConversationStatusEntity(
|
ConversationStatusEntity(
|
||||||
id = id,
|
id = id,
|
||||||
url = url,
|
url = url,
|
||||||
|
@ -177,7 +174,7 @@ fun Status.toEntity(
|
||||||
showingHiddenContent = contentShowing,
|
showingHiddenContent = contentShowing,
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
collapsed = contentCollapsed,
|
collapsed = contentCollapsed,
|
||||||
muted = muted ?: false,
|
muted = muted,
|
||||||
poll = poll,
|
poll = poll,
|
||||||
language = language
|
language = language
|
||||||
)
|
)
|
||||||
|
@ -188,16 +185,15 @@ fun Conversation.toEntity(
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
contentShowing: Boolean,
|
contentShowing: Boolean,
|
||||||
contentCollapsed: Boolean
|
contentCollapsed: Boolean
|
||||||
) =
|
) = ConversationEntity(
|
||||||
ConversationEntity(
|
accountId = accountId,
|
||||||
accountId = accountId,
|
id = id,
|
||||||
id = id,
|
order = order,
|
||||||
order = order,
|
accounts = accounts.map { it.toEntity() },
|
||||||
accounts = accounts.map { it.toEntity() },
|
unread = unread,
|
||||||
unread = unread,
|
lastStatus = lastStatus!!.toEntity(
|
||||||
lastStatus = lastStatus!!.toEntity(
|
expanded = expanded,
|
||||||
expanded = expanded,
|
contentShowing = contentShowing,
|
||||||
contentShowing = contentShowing,
|
contentCollapsed = contentCollapsed
|
||||||
contentCollapsed = contentCollapsed
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
|
@ -27,7 +27,10 @@ class ConversationLoadStateAdapter(
|
||||||
private val retryCallback: () -> Unit
|
private val retryCallback: () -> Unit
|
||||||
) : LoadStateAdapter<BindingHolder<ItemNetworkStateBinding>>() {
|
) : LoadStateAdapter<BindingHolder<ItemNetworkStateBinding>>() {
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BindingHolder<ItemNetworkStateBinding>, loadState: LoadState) {
|
override fun onBindViewHolder(
|
||||||
|
holder: BindingHolder<ItemNetworkStateBinding>,
|
||||||
|
loadState: LoadState
|
||||||
|
) {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
binding.progressBar.visible(loadState == LoadState.Loading)
|
binding.progressBar.visible(loadState == LoadState.Loading)
|
||||||
binding.retryButton.visible(loadState is LoadState.Error)
|
binding.retryButton.visible(loadState is LoadState.Error)
|
||||||
|
@ -47,7 +50,11 @@ class ConversationLoadStateAdapter(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
loadState: LoadState
|
loadState: LoadState
|
||||||
): BindingHolder<ItemNetworkStateBinding> {
|
): BindingHolder<ItemNetworkStateBinding> {
|
||||||
val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding = ItemNetworkStateBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
return BindingHolder(binding)
|
return BindingHolder(binding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ data class ConversationViewData(
|
||||||
accountId: Long,
|
accountId: Long,
|
||||||
favourited: Boolean = lastStatus.status.favourited,
|
favourited: Boolean = lastStatus.status.favourited,
|
||||||
bookmarked: Boolean = lastStatus.status.bookmarked,
|
bookmarked: Boolean = lastStatus.status.bookmarked,
|
||||||
muted: Boolean = lastStatus.status.muted ?: false,
|
muted: Boolean = lastStatus.status.muted,
|
||||||
poll: Poll? = lastStatus.status.poll,
|
poll: Poll? = lastStatus.status.poll,
|
||||||
expanded: Boolean = lastStatus.isExpanded,
|
expanded: Boolean = lastStatus.isExpanded,
|
||||||
collapsed: Boolean = lastStatus.isCollapsed,
|
collapsed: Boolean = lastStatus.isCollapsed,
|
||||||
|
@ -57,7 +57,7 @@ data class ConversationViewData(
|
||||||
fun StatusViewData.Concrete.toConversationStatusEntity(
|
fun StatusViewData.Concrete.toConversationStatusEntity(
|
||||||
favourited: Boolean = status.favourited,
|
favourited: Boolean = status.favourited,
|
||||||
bookmarked: Boolean = status.bookmarked,
|
bookmarked: Boolean = status.bookmarked,
|
||||||
muted: Boolean = status.muted ?: false,
|
muted: Boolean = status.muted,
|
||||||
poll: Poll? = status.poll,
|
poll: Poll? = status.poll,
|
||||||
expanded: Boolean = isExpanded,
|
expanded: Boolean = isExpanded,
|
||||||
collapsed: Boolean = isCollapsed,
|
collapsed: Boolean = isCollapsed,
|
||||||
|
|
|
@ -80,7 +80,11 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||||
|
|
||||||
setupCollapsedState(statusViewData.isCollapsible(), statusViewData.isCollapsed(), statusViewData.isExpanded(), status.getSpoilerText(), listener);
|
setupCollapsedState(statusViewData.isCollapsible(), statusViewData.isCollapsed(), statusViewData.isExpanded(), status.getSpoilerText(), listener);
|
||||||
|
|
||||||
setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions);
|
String displayName = account.getDisplayName();
|
||||||
|
if (displayName == null) {
|
||||||
|
displayName = "";
|
||||||
|
}
|
||||||
|
setDisplayName(displayName, account.getEmojis(), statusDisplayOptions);
|
||||||
setUsername(account.getUsername());
|
setUsername(account.getUsername());
|
||||||
setMetaData(statusViewData, statusDisplayOptions, listener);
|
setMetaData(statusViewData, statusDisplayOptions, listener);
|
||||||
setIsReply(status.getInReplyToId() != null);
|
setIsReply(status.getInReplyToId() != null);
|
||||||
|
@ -92,7 +96,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||||
setMediaPreviews(attachments, sensitive, listener, statusViewData.isShowingContent(),
|
setMediaPreviews(attachments, sensitive, listener, statusViewData.isShowingContent(),
|
||||||
statusDisplayOptions.useBlurhash());
|
statusDisplayOptions.useBlurhash());
|
||||||
|
|
||||||
if (attachments.size() == 0) {
|
if (attachments.isEmpty()) {
|
||||||
hideSensitiveMediaWarning();
|
hideSensitiveMediaWarning();
|
||||||
}
|
}
|
||||||
// Hide the unused label.
|
// Hide the unused label.
|
||||||
|
|
|
@ -15,13 +15,12 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.conversation
|
package com.keylesspalace.tusky.components.conversation
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
|
@ -30,7 +29,6 @@ import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -45,10 +43,7 @@ import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||||
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
|
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
|
||||||
import com.keylesspalace.tusky.fragment.SFragment
|
import com.keylesspalace.tusky.fragment.SFragment
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
|
||||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
|
@ -63,43 +58,36 @@ import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
import com.mikepenz.iconics.utils.sizeDp
|
import com.mikepenz.iconics.utils.sizeDp
|
||||||
import kotlinx.coroutines.delay
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
import kotlin.time.toDuration
|
import kotlin.time.toDuration
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class ConversationsFragment :
|
class ConversationsFragment :
|
||||||
SFragment(),
|
SFragment(R.layout.fragment_timeline),
|
||||||
StatusActionListener,
|
StatusActionListener,
|
||||||
Injectable,
|
|
||||||
ReselectableFragment,
|
ReselectableFragment,
|
||||||
MenuProvider {
|
MenuProvider {
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var viewModelFactory: ViewModelFactory
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var eventHub: EventHub
|
lateinit var eventHub: EventHub
|
||||||
|
|
||||||
private val viewModel: ConversationsViewModel by viewModels { viewModelFactory }
|
@Inject
|
||||||
|
lateinit var preferences: SharedPreferences
|
||||||
|
|
||||||
|
private val viewModel: ConversationsViewModel by viewModels()
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentTimelineBinding::bind)
|
private val binding by viewBinding(FragmentTimelineBinding::bind)
|
||||||
|
|
||||||
private lateinit var adapter: ConversationAdapter
|
private var adapter: ConversationAdapter? = null
|
||||||
|
|
||||||
private var hideFab = false
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
||||||
return inflater.inflate(R.layout.fragment_timeline, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||||
|
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(view.context)
|
|
||||||
|
|
||||||
val statusDisplayOptions = StatusDisplayOptions(
|
val statusDisplayOptions = StatusDisplayOptions(
|
||||||
animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
|
animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
|
||||||
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
|
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
|
||||||
|
@ -116,9 +104,10 @@ class ConversationsFragment :
|
||||||
openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler
|
openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler
|
||||||
)
|
)
|
||||||
|
|
||||||
adapter = ConversationAdapter(statusDisplayOptions, this)
|
val adapter = ConversationAdapter(statusDisplayOptions, this)
|
||||||
|
this.adapter = adapter
|
||||||
|
|
||||||
setupRecyclerView()
|
setupRecyclerView(adapter)
|
||||||
|
|
||||||
initSwipeToRefresh()
|
initSwipeToRefresh()
|
||||||
|
|
||||||
|
@ -131,8 +120,12 @@ class ConversationsFragment :
|
||||||
binding.progressBar.hide()
|
binding.progressBar.hide()
|
||||||
|
|
||||||
if (loadState.isAnyLoading()) {
|
if (loadState.isAnyLoading()) {
|
||||||
lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
eventHub.dispatch(ConversationsLoadingEvent(accountManager.activeAccount?.accountId ?: ""))
|
eventHub.dispatch(
|
||||||
|
ConversationsLoadingEvent(
|
||||||
|
accountManager.activeAccount?.accountId ?: ""
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,14 +134,22 @@ class ConversationsFragment :
|
||||||
is LoadState.NotLoading -> {
|
is LoadState.NotLoading -> {
|
||||||
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
|
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
|
||||||
binding.statusView.show()
|
binding.statusView.show()
|
||||||
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null)
|
binding.statusView.setup(
|
||||||
|
R.drawable.elephant_friend_empty,
|
||||||
|
R.string.message_empty,
|
||||||
|
null
|
||||||
|
)
|
||||||
binding.statusView.showHelp(R.string.help_empty_conversations)
|
binding.statusView.showHelp(R.string.help_empty_conversations)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is LoadState.Error -> {
|
is LoadState.Error -> {
|
||||||
binding.statusView.show()
|
binding.statusView.show()
|
||||||
binding.statusView.setup((loadState.refresh as LoadState.Error).error) { refreshContent() }
|
binding.statusView.setup(
|
||||||
|
(loadState.refresh as LoadState.Error).error
|
||||||
|
) { refreshContent() }
|
||||||
}
|
}
|
||||||
|
|
||||||
is LoadState.Loading -> {
|
is LoadState.Loading -> {
|
||||||
binding.progressBar.show()
|
binding.progressBar.show()
|
||||||
}
|
}
|
||||||
|
@ -158,7 +159,8 @@ class ConversationsFragment :
|
||||||
|
|
||||||
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||||
if (positionStart == 0 && adapter.itemCount != itemCount) {
|
val firstPos = (binding.recyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
|
||||||
|
if (firstPos == 0 && positionStart == 0 && adapter.itemCount != itemCount) {
|
||||||
binding.recyclerView.post {
|
binding.recyclerView.post {
|
||||||
if (getView() != null) {
|
if (getView() != null) {
|
||||||
binding.recyclerView.scrollBy(0, Utils.dpToPx(requireContext(), -30))
|
binding.recyclerView.scrollBy(0, Utils.dpToPx(requireContext(), -30))
|
||||||
|
@ -168,24 +170,6 @@ class ConversationsFragment :
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
hideFab = preferences.getBoolean(PrefKeys.FAB_HIDE, false)
|
|
||||||
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
|
||||||
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
|
||||||
val composeButton = (activity as ActionButtonActivity).actionButton
|
|
||||||
if (composeButton != null) {
|
|
||||||
if (hideFab) {
|
|
||||||
if (dy > 0 && composeButton.isShown) {
|
|
||||||
composeButton.hide() // hides the button if we're scrolling down
|
|
||||||
} else if (dy < 0 && !composeButton.isShown) {
|
|
||||||
composeButton.show() // shows it if we are scrolling up
|
|
||||||
}
|
|
||||||
} else if (!composeButton.isShown) {
|
|
||||||
composeButton.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
viewModel.conversationFlow.collectLatest { pagingData ->
|
viewModel.conversationFlow.collectLatest { pagingData ->
|
||||||
adapter.submitData(pagingData)
|
adapter.submitData(pagingData)
|
||||||
|
@ -206,15 +190,21 @@ class ConversationsFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
eventHub.events.collect { event ->
|
eventHub.events.collect { event ->
|
||||||
if (event is PreferenceChangedEvent) {
|
if (event is PreferenceChangedEvent) {
|
||||||
onPreferenceChanged(event.preferenceKey)
|
onPreferenceChanged(adapter, event.preferenceKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
// Clear the adapter to prevent leaking the View
|
||||||
|
adapter = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
menuInflater.inflate(R.menu.fragment_conversations, menu)
|
menuInflater.inflate(R.menu.fragment_conversations, menu)
|
||||||
menu.findItem(R.id.action_refresh)?.apply {
|
menu.findItem(R.id.action_refresh)?.apply {
|
||||||
|
@ -232,23 +222,27 @@ class ConversationsFragment :
|
||||||
refreshContent()
|
refreshContent()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupRecyclerView() {
|
private fun setupRecyclerView(adapter: ConversationAdapter) {
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(context)
|
binding.recyclerView.layoutManager = LinearLayoutManager(context)
|
||||||
|
|
||||||
binding.recyclerView.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
binding.recyclerView.addItemDecoration(
|
||||||
|
DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
|
||||||
|
)
|
||||||
|
|
||||||
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
|
||||||
binding.recyclerView.adapter = adapter.withLoadStateFooter(ConversationLoadStateAdapter(adapter::retry))
|
binding.recyclerView.adapter =
|
||||||
|
adapter.withLoadStateFooter(ConversationLoadStateAdapter(adapter::retry))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshContent() {
|
private fun refreshContent() {
|
||||||
adapter.refresh()
|
adapter?.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initSwipeToRefresh() {
|
private fun initSwipeToRefresh() {
|
||||||
|
@ -261,24 +255,26 @@ class ConversationsFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFavourite(favourite: Boolean, position: Int) {
|
override fun onFavourite(favourite: Boolean, position: Int) {
|
||||||
adapter.peek(position)?.let { conversation ->
|
adapter?.peek(position)?.let { conversation ->
|
||||||
viewModel.favourite(favourite, conversation)
|
viewModel.favourite(favourite, conversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBookmark(favourite: Boolean, position: Int) {
|
override fun onBookmark(favourite: Boolean, position: Int) {
|
||||||
adapter.peek(position)?.let { conversation ->
|
adapter?.peek(position)?.let { conversation ->
|
||||||
viewModel.bookmark(favourite, conversation)
|
viewModel.bookmark(favourite, conversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val onMoreTranslate: ((translate: Boolean, position: Int) -> Unit)? = null
|
||||||
|
|
||||||
override fun onMore(view: View, position: Int) {
|
override fun onMore(view: View, position: Int) {
|
||||||
adapter.peek(position)?.let { conversation ->
|
adapter?.peek(position)?.let { conversation ->
|
||||||
|
|
||||||
val popup = PopupMenu(requireContext(), view)
|
val popup = PopupMenu(requireContext(), view)
|
||||||
popup.inflate(R.menu.conversation_more)
|
popup.inflate(R.menu.conversation_more)
|
||||||
|
|
||||||
if (conversation.lastStatus.status.muted == true) {
|
if (conversation.lastStatus.status.muted) {
|
||||||
popup.menu.removeItem(R.id.status_mute_conversation)
|
popup.menu.removeItem(R.id.status_mute_conversation)
|
||||||
} else {
|
} else {
|
||||||
popup.menu.removeItem(R.id.status_unmute_conversation)
|
popup.menu.removeItem(R.id.status_unmute_conversation)
|
||||||
|
@ -297,13 +293,17 @@ class ConversationsFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
||||||
adapter.peek(position)?.let { conversation ->
|
adapter?.peek(position)?.let { conversation ->
|
||||||
viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.status), view)
|
viewMedia(
|
||||||
|
attachmentIndex,
|
||||||
|
AttachmentViewData.list(conversation.lastStatus),
|
||||||
|
view
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewThread(position: Int) {
|
override fun onViewThread(position: Int) {
|
||||||
adapter.peek(position)?.let { conversation ->
|
adapter?.peek(position)?.let { conversation ->
|
||||||
viewThread(conversation.lastStatus.id, conversation.lastStatus.status.url)
|
viewThread(conversation.lastStatus.id, conversation.lastStatus.status.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -313,13 +313,13 @@ class ConversationsFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
||||||
adapter.peek(position)?.let { conversation ->
|
adapter?.peek(position)?.let { conversation ->
|
||||||
viewModel.expandHiddenStatus(expanded, conversation)
|
viewModel.expandHiddenStatus(expanded, conversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
||||||
adapter.peek(position)?.let { conversation ->
|
adapter?.peek(position)?.let { conversation ->
|
||||||
viewModel.showContent(isShowing, conversation)
|
viewModel.showContent(isShowing, conversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -329,7 +329,7 @@ class ConversationsFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
||||||
adapter.peek(position)?.let { conversation ->
|
adapter?.peek(position)?.let { conversation ->
|
||||||
viewModel.collapseLongStatus(isCollapsed, conversation)
|
viewModel.collapseLongStatus(isCollapsed, conversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -349,13 +349,13 @@ class ConversationsFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReply(position: Int) {
|
override fun onReply(position: Int) {
|
||||||
adapter.peek(position)?.let { conversation ->
|
adapter?.peek(position)?.let { conversation ->
|
||||||
reply(conversation.lastStatus.status)
|
reply(conversation.lastStatus.status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
||||||
adapter.peek(position)?.let { conversation ->
|
adapter?.peek(position)?.let { conversation ->
|
||||||
viewModel.voteInPoll(choices, conversation)
|
viewModel.voteInPoll(choices, conversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -364,12 +364,16 @@ class ConversationsFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReselect() {
|
override fun onReselect() {
|
||||||
if (isAdded) {
|
if (view != null) {
|
||||||
binding.recyclerView.layoutManager?.scrollToPosition(0)
|
binding.recyclerView.layoutManager?.scrollToPosition(0)
|
||||||
binding.recyclerView.stopScroll()
|
binding.recyclerView.stopScroll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onUntranslate(position: Int) {
|
||||||
|
// not needed
|
||||||
|
}
|
||||||
|
|
||||||
private fun deleteConversation(conversation: ConversationViewData) {
|
private fun deleteConversation(conversation: ConversationViewData) {
|
||||||
AlertDialog.Builder(requireContext())
|
AlertDialog.Builder(requireContext())
|
||||||
.setMessage(R.string.dialog_delete_conversation_warning)
|
.setMessage(R.string.dialog_delete_conversation_warning)
|
||||||
|
@ -380,12 +384,8 @@ class ConversationsFragment :
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPreferenceChanged(key: String) {
|
private fun onPreferenceChanged(adapter: ConversationAdapter, key: String) {
|
||||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
|
||||||
when (key) {
|
when (key) {
|
||||||
PrefKeys.FAB_HIDE -> {
|
|
||||||
hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false)
|
|
||||||
}
|
|
||||||
PrefKeys.MEDIA_PREVIEW_ENABLED -> {
|
PrefKeys.MEDIA_PREVIEW_ENABLED -> {
|
||||||
val enabled = accountManager.activeAccount!!.mediaPreviewEnabled
|
val enabled = accountManager.activeAccount!!.mediaPreviewEnabled
|
||||||
val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled
|
val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled
|
||||||
|
|
|
@ -38,7 +38,10 @@ class ConversationsRemoteMediator(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val conversationsResponse = api.getConversations(maxId = nextKey, limit = state.config.pageSize)
|
val conversationsResponse = api.getConversations(
|
||||||
|
maxId = nextKey,
|
||||||
|
limit = state.config.pageSize
|
||||||
|
)
|
||||||
|
|
||||||
val conversations = conversationsResponse.body()
|
val conversations = conversationsResponse.body()
|
||||||
if (!conversationsResponse.isSuccessful || conversations == null) {
|
if (!conversationsResponse.isSuccessful || conversations == null) {
|
||||||
|
|
|
@ -29,10 +29,12 @@ import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.usecase.TimelineCases
|
import com.keylesspalace.tusky.usecase.TimelineCases
|
||||||
import com.keylesspalace.tusky.util.EmptyPagingSource
|
import com.keylesspalace.tusky.util.EmptyPagingSource
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
class ConversationsViewModel @Inject constructor(
|
class ConversationsViewModel @Inject constructor(
|
||||||
private val timelineCases: TimelineCases,
|
private val timelineCases: TimelineCases,
|
||||||
private val database: AppDatabase,
|
private val database: AppDatabase,
|
||||||
|
@ -42,7 +44,9 @@ class ConversationsViewModel @Inject constructor(
|
||||||
|
|
||||||
@OptIn(ExperimentalPagingApi::class)
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
val conversationFlow = Pager(
|
val conversationFlow = Pager(
|
||||||
config = PagingConfig(pageSize = 30),
|
config = PagingConfig(
|
||||||
|
pageSize = 30
|
||||||
|
),
|
||||||
remoteMediator = ConversationsRemoteMediator(api, database, accountManager),
|
remoteMediator = ConversationsRemoteMediator(api, database, accountManager),
|
||||||
pagingSourceFactory = {
|
pagingSourceFactory = {
|
||||||
val activeAccount = accountManager.activeAccount
|
val activeAccount = accountManager.activeAccount
|
||||||
|
@ -91,7 +95,11 @@ class ConversationsViewModel @Inject constructor(
|
||||||
|
|
||||||
fun voteInPoll(choices: List<Int>, conversation: ConversationViewData) {
|
fun voteInPoll(choices: List<Int>, conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.status.poll?.id!!, choices)
|
timelineCases.voteInPoll(
|
||||||
|
conversation.lastStatus.id,
|
||||||
|
conversation.lastStatus.status.poll?.id!!,
|
||||||
|
choices
|
||||||
|
)
|
||||||
.fold({ poll ->
|
.fold({ poll ->
|
||||||
val newConversation = conversation.toEntity(
|
val newConversation = conversation.toEntity(
|
||||||
accountId = accountManager.activeAccount!!.id,
|
accountId = accountManager.activeAccount!!.id,
|
||||||
|
@ -155,12 +163,12 @@ class ConversationsViewModel @Inject constructor(
|
||||||
try {
|
try {
|
||||||
timelineCases.muteConversation(
|
timelineCases.muteConversation(
|
||||||
conversation.lastStatus.id,
|
conversation.lastStatus.id,
|
||||||
!(conversation.lastStatus.status.muted ?: false)
|
!conversation.lastStatus.status.muted
|
||||||
)
|
)
|
||||||
|
|
||||||
val newConversation = conversation.toEntity(
|
val newConversation = conversation.toEntity(
|
||||||
accountId = accountManager.activeAccount!!.id,
|
accountId = accountManager.activeAccount!!.id,
|
||||||
muted = !(conversation.lastStatus.status.muted ?: false)
|
muted = !conversation.lastStatus.status.muted
|
||||||
)
|
)
|
||||||
|
|
||||||
database.conversationDao().insert(newConversation)
|
database.conversationDao().insert(newConversation)
|
||||||
|
|
|
@ -4,14 +4,10 @@ import android.os.Bundle
|
||||||
import com.keylesspalace.tusky.BaseActivity
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ActivityAccountListBinding
|
import com.keylesspalace.tusky.databinding.ActivityAccountListBinding
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dagger.android.HasAndroidInjector
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class DomainBlocksActivity : BaseActivity(), HasAndroidInjector {
|
@AndroidEntryPoint
|
||||||
|
class DomainBlocksActivity : BaseActivity() {
|
||||||
@Inject
|
|
||||||
lateinit var androidInjector: DispatchingAndroidInjector<Any>
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -30,6 +26,4 @@ class DomainBlocksActivity : BaseActivity(), HasAndroidInjector {
|
||||||
.replace(R.id.fragment_container, DomainBlocksFragment())
|
.replace(R.id.fragment_container, DomainBlocksFragment())
|
||||||
.commit()
|
.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun androidInjector() = androidInjector
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,15 @@ class DomainBlocksAdapter(
|
||||||
private val onUnmute: (String) -> Unit
|
private val onUnmute: (String) -> Unit
|
||||||
) : PagingDataAdapter<String, BindingHolder<ItemBlockedDomainBinding>>(STRING_COMPARATOR) {
|
) : PagingDataAdapter<String, BindingHolder<ItemBlockedDomainBinding>>(STRING_COMPARATOR) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemBlockedDomainBinding> {
|
override fun onCreateViewHolder(
|
||||||
val binding = ItemBlockedDomainBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): BindingHolder<ItemBlockedDomainBinding> {
|
||||||
|
val binding = ItemBlockedDomainBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
return BindingHolder(binding)
|
return BindingHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,30 +12,28 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.FragmentDomainBlocksBinding
|
import com.keylesspalace.tusky.databinding.FragmentDomainBlocksBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.util.visible
|
import com.keylesspalace.tusky.util.visible
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class DomainBlocksFragment : Fragment(R.layout.fragment_domain_blocks), Injectable {
|
@AndroidEntryPoint
|
||||||
|
class DomainBlocksFragment : Fragment(R.layout.fragment_domain_blocks) {
|
||||||
@Inject
|
|
||||||
lateinit var viewModelFactory: ViewModelFactory
|
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentDomainBlocksBinding::bind)
|
private val binding by viewBinding(FragmentDomainBlocksBinding::bind)
|
||||||
|
|
||||||
private val viewModel: DomainBlocksViewModel by viewModels { viewModelFactory }
|
private val viewModel: DomainBlocksViewModel by viewModels()
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
val adapter = DomainBlocksAdapter(viewModel::unblock)
|
val adapter = DomainBlocksAdapter(viewModel::unblock)
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
binding.recyclerView.addItemDecoration(
|
||||||
|
DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)
|
||||||
|
)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(view.context)
|
binding.recyclerView.layoutManager = LinearLayoutManager(view.context)
|
||||||
|
|
||||||
|
@ -45,14 +43,16 @@ class DomainBlocksFragment : Fragment(R.layout.fragment_domain_blocks), Injectab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
viewModel.domainPager.collectLatest { pagingData ->
|
viewModel.domainPager.collectLatest { pagingData ->
|
||||||
adapter.submitData(pagingData)
|
adapter.submitData(pagingData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter.addLoadStateListener { loadState ->
|
adapter.addLoadStateListener { loadState ->
|
||||||
binding.progressBar.visible(loadState.refresh == LoadState.Loading && adapter.itemCount == 0)
|
binding.progressBar.visible(
|
||||||
|
loadState.refresh == LoadState.Loading && adapter.itemCount == 0
|
||||||
|
)
|
||||||
|
|
||||||
if (loadState.refresh is LoadState.Error) {
|
if (loadState.refresh is LoadState.Error) {
|
||||||
binding.recyclerView.hide()
|
binding.recyclerView.hide()
|
||||||
|
|
|
@ -39,7 +39,10 @@ class DomainBlocksRepository @Inject constructor(
|
||||||
|
|
||||||
@OptIn(ExperimentalPagingApi::class)
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
val domainPager = Pager(
|
val domainPager = Pager(
|
||||||
config = PagingConfig(pageSize = PAGE_SIZE, initialLoadSize = PAGE_SIZE),
|
config = PagingConfig(
|
||||||
|
pageSize = PAGE_SIZE,
|
||||||
|
initialLoadSize = PAGE_SIZE
|
||||||
|
),
|
||||||
remoteMediator = DomainBlocksRemoteMediator(api, this),
|
remoteMediator = DomainBlocksRemoteMediator(api, this),
|
||||||
pagingSourceFactory = factory
|
pagingSourceFactory = factory
|
||||||
).flow
|
).flow
|
||||||
|
|
|
@ -8,22 +8,27 @@ import androidx.paging.cachedIn
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import at.connyduck.calladapter.networkresult.onFailure
|
import at.connyduck.calladapter.networkresult.onFailure
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
class DomainBlocksViewModel @Inject constructor(
|
class DomainBlocksViewModel @Inject constructor(
|
||||||
private val repo: DomainBlocksRepository
|
private val repo: DomainBlocksRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val domainPager = repo.domainPager.cachedIn(viewModelScope)
|
val domainPager = repo.domainPager.cachedIn(viewModelScope)
|
||||||
|
|
||||||
val uiEvents = MutableSharedFlow<SnackbarEvent>()
|
private val _uiEvents = MutableSharedFlow<SnackbarEvent>()
|
||||||
|
val uiEvents: SharedFlow<SnackbarEvent> = _uiEvents.asSharedFlow()
|
||||||
|
|
||||||
fun block(domain: String) {
|
fun block(domain: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repo.block(domain).onFailure { e ->
|
repo.block(domain).onFailure { e ->
|
||||||
uiEvents.emit(
|
_uiEvents.emit(
|
||||||
SnackbarEvent(
|
SnackbarEvent(
|
||||||
message = R.string.error_blocking_domain,
|
message = R.string.error_blocking_domain,
|
||||||
domain = domain,
|
domain = domain,
|
||||||
|
@ -39,7 +44,7 @@ class DomainBlocksViewModel @Inject constructor(
|
||||||
fun unblock(domain: String) {
|
fun unblock(domain: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repo.unblock(domain).fold({
|
repo.unblock(domain).fold({
|
||||||
uiEvents.emit(
|
_uiEvents.emit(
|
||||||
SnackbarEvent(
|
SnackbarEvent(
|
||||||
message = R.string.confirmation_domain_unmuted,
|
message = R.string.confirmation_domain_unmuted,
|
||||||
domain = domain,
|
domain = domain,
|
||||||
|
@ -49,7 +54,7 @@ class DomainBlocksViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}, { e ->
|
}, { e ->
|
||||||
uiEvents.emit(
|
_uiEvents.emit(
|
||||||
SnackbarEvent(
|
SnackbarEvent(
|
||||||
message = R.string.error_unblocking_domain,
|
message = R.string.error_unblocking_domain,
|
||||||
domain = domain,
|
domain = domain,
|
||||||
|
|
|
@ -23,27 +23,28 @@ import androidx.core.content.FileProvider
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.keylesspalace.tusky.BuildConfig
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.db.DraftAttachment
|
import com.keylesspalace.tusky.db.entity.DraftAttachment
|
||||||
import com.keylesspalace.tusky.db.DraftEntity
|
import com.keylesspalace.tusky.db.entity.DraftEntity
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.entity.NewPoll
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.util.copyToFile
|
import com.keylesspalace.tusky.util.copyToFile
|
||||||
import kotlinx.coroutines.Dispatchers
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okio.buffer
|
|
||||||
import okio.sink
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
|
||||||
class DraftHelper @Inject constructor(
|
class DraftHelper @Inject constructor(
|
||||||
val context: Context,
|
@ApplicationContext val context: Context,
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
db: AppDatabase
|
db: AppDatabase
|
||||||
) {
|
) {
|
||||||
|
@ -101,16 +102,17 @@ class DraftHelper @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val attachments: MutableList<DraftAttachment> = mutableListOf()
|
val attachments: List<DraftAttachment> = buildList(mediaUris.size) {
|
||||||
for (i in mediaUris.indices) {
|
for (i in mediaUris.indices) {
|
||||||
attachments.add(
|
add(
|
||||||
DraftAttachment(
|
DraftAttachment(
|
||||||
uriString = uris[i].toString(),
|
uriString = uris[i].toString(),
|
||||||
description = mediaDescriptions[i],
|
description = mediaDescriptions[i],
|
||||||
focus = mediaFocus[i],
|
focus = mediaFocus[i],
|
||||||
type = types[i]
|
type = types[i]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val draft = DraftEntity(
|
val draft = DraftEntity(
|
||||||
|
@ -186,10 +188,8 @@ class DraftHelper @Inject constructor(
|
||||||
|
|
||||||
val response = okHttpClient.newCall(request).execute()
|
val response = okHttpClient.newCall(request).execute()
|
||||||
|
|
||||||
val sink = file.sink().buffer()
|
file.sink().buffer().use { output ->
|
||||||
|
response.body?.source()?.use { input ->
|
||||||
response.body?.source()?.use { input ->
|
|
||||||
sink.use { output ->
|
|
||||||
output.writeAll(input)
|
output.writeAll(input)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -200,6 +200,10 @@ class DraftHelper @Inject constructor(
|
||||||
} else {
|
} else {
|
||||||
this.copyToFile(contentResolver, file)
|
this.copyToFile(contentResolver, file)
|
||||||
}
|
}
|
||||||
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file)
|
return FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.db.DraftAttachment
|
import com.keylesspalace.tusky.db.entity.DraftAttachment
|
||||||
import com.keylesspalace.tusky.view.MediaPreviewImageView
|
import com.keylesspalace.tusky.view.MediaPreviewImageView
|
||||||
|
|
||||||
class DraftMediaAdapter(
|
class DraftMediaAdapter(
|
||||||
|
@ -35,7 +35,10 @@ class DraftMediaAdapter(
|
||||||
return oldItem == newItem
|
return oldItem == newItem
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
|
override fun areContentsTheSame(
|
||||||
|
oldItem: DraftAttachment,
|
||||||
|
newItem: DraftAttachment
|
||||||
|
): Boolean {
|
||||||
return oldItem == newItem
|
return oldItem == newItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,7 +78,9 @@ class DraftMediaAdapter(
|
||||||
RecyclerView.ViewHolder(imageView) {
|
RecyclerView.ViewHolder(imageView) {
|
||||||
init {
|
init {
|
||||||
val thumbnailViewSize =
|
val thumbnailViewSize =
|
||||||
imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
|
imageView.context.resources.getDimensionPixelSize(
|
||||||
|
R.dimen.compose_media_preview_size
|
||||||
|
)
|
||||||
val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
|
val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
|
||||||
val margin = itemView.context.resources
|
val margin = itemView.context.resources
|
||||||
.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
|
.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
|
||||||
|
|
|
@ -32,25 +32,23 @@ import com.keylesspalace.tusky.BaseActivity
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.databinding.ActivityDraftsBinding
|
import com.keylesspalace.tusky.databinding.ActivityDraftsBinding
|
||||||
import com.keylesspalace.tusky.db.DraftEntity
|
|
||||||
import com.keylesspalace.tusky.db.DraftsAlert
|
import com.keylesspalace.tusky.db.DraftsAlert
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.db.entity.DraftEntity
|
||||||
import com.keylesspalace.tusky.util.isHttpNotFound
|
import com.keylesspalace.tusky.util.isHttpNotFound
|
||||||
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||||
import com.keylesspalace.tusky.util.visible
|
import com.keylesspalace.tusky.util.visible
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class DraftsActivity : BaseActivity(), DraftActionListener {
|
class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var viewModelFactory: ViewModelFactory
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var draftsAlert: DraftsAlert
|
lateinit var draftsAlert: DraftsAlert
|
||||||
|
|
||||||
private val viewModel: DraftsViewModel by viewModels { viewModelFactory }
|
private val viewModel: DraftsViewModel by viewModels()
|
||||||
|
|
||||||
private lateinit var binding: ActivityDraftsBinding
|
private lateinit var binding: ActivityDraftsBinding
|
||||||
private lateinit var bottomSheet: BottomSheetBehavior<LinearLayout>
|
private lateinit var bottomSheet: BottomSheetBehavior<LinearLayout>
|
||||||
|
@ -74,7 +72,9 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
|
|
||||||
binding.draftsRecyclerView.adapter = adapter
|
binding.draftsRecyclerView.adapter = adapter
|
||||||
binding.draftsRecyclerView.layoutManager = LinearLayoutManager(this)
|
binding.draftsRecyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
binding.draftsRecyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
|
binding.draftsRecyclerView.addItemDecoration(
|
||||||
|
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
|
||||||
|
)
|
||||||
|
|
||||||
bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root)
|
bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root)
|
||||||
|
|
||||||
|
@ -134,10 +134,18 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
if (throwable.isHttpNotFound()) {
|
if (throwable.isHttpNotFound()) {
|
||||||
// the original status to which a reply was drafted has been deleted
|
// the original status to which a reply was drafted has been deleted
|
||||||
// let's open the ComposeActivity without reply information
|
// let's open the ComposeActivity without reply information
|
||||||
Toast.makeText(context, getString(R.string.drafts_post_reply_removed), Toast.LENGTH_LONG).show()
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
getString(R.string.drafts_post_reply_removed),
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
openDraftWithoutReply(draft)
|
openDraftWithoutReply(draft)
|
||||||
} else {
|
} else {
|
||||||
Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT)
|
Snackbar.make(
|
||||||
|
binding.root,
|
||||||
|
getString(R.string.drafts_failed_loading_reply),
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.databinding.ItemDraftBinding
|
import com.keylesspalace.tusky.databinding.ItemDraftBinding
|
||||||
import com.keylesspalace.tusky.db.DraftEntity
|
import com.keylesspalace.tusky.db.entity.DraftEntity
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
|
@ -47,7 +47,10 @@ class DraftsAdapter(
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemDraftBinding> {
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): BindingHolder<ItemDraftBinding> {
|
||||||
val binding = ItemDraftBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding = ItemDraftBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
|
||||||
val viewHolder = BindingHolder(binding)
|
val viewHolder = BindingHolder(binding)
|
||||||
|
@ -77,7 +80,9 @@ class DraftsAdapter(
|
||||||
holder.binding.content.text = draft.content
|
holder.binding.content.text = draft.content
|
||||||
|
|
||||||
holder.binding.draftMediaPreview.visible(draft.attachments.isNotEmpty())
|
holder.binding.draftMediaPreview.visible(draft.attachments.isNotEmpty())
|
||||||
(holder.binding.draftMediaPreview.adapter as DraftMediaAdapter).submitList(draft.attachments)
|
(holder.binding.draftMediaPreview.adapter as DraftMediaAdapter).submitList(
|
||||||
|
draft.attachments
|
||||||
|
)
|
||||||
|
|
||||||
if (draft.poll != null) {
|
if (draft.poll != null) {
|
||||||
holder.binding.draftPoll.show()
|
holder.binding.draftPoll.show()
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue